@@ -444,37 +444,40 @@ def add_entity(
444444 else :
445445 assert entity .raw_id is not None
446446 store_key = entity .raw_id
447- with self .store .transaction ():
448- previous = self .store .get (store_key )
449- if (
450- previous
451- and not self .allow_entity_updates
452- and previous .content != entity .content
453- ):
454- return GtsAddEntityResult (
455- ok = False ,
456- error = f"Entity '{ store_key } ' is already registered with different content" ,
457- is_type_schema = entity .is_schema ,
458- conflict = True ,
459- )
460- self .store .register (entity )
447+ previous = self .store .get_committed (store_key )
448+ if (
449+ previous
450+ and not self .allow_entity_updates
451+ and previous .content != entity .content
452+ ):
453+ return GtsAddEntityResult (
454+ ok = False ,
455+ error = f"Entity '{ store_key } ' is already registered with different content" ,
456+ is_type_schema = entity .is_schema ,
457+ conflict = True ,
458+ )
461459
462- try :
463- if entity .is_schema :
464- self .store .validate_schema_basic (store_key )
465- if validate :
466- self .store .validate_schema (store_key , gts_ref_validation )
467- elif validate :
468- self .store .validate_instance (store_key , gts_ref_validation )
469- except Exception as e : # noqa: BLE001 - converted to a result object at API boundary
470- self .store .unregister (store_key )
471- if previous :
472- self .store .register (previous )
473- return GtsAddEntityResult (
474- ok = False ,
475- error = f"Validation failed: { e !s} " ,
476- is_type_schema = entity .is_schema ,
477- )
460+ # Stage the entity (invisible to public reads) and validate it before
461+ # publishing. A failure discards the staged copy, so a reader never
462+ # observes an entity that has not passed validation, and the committed
463+ # state (any prior version under this id) is never touched. No lock is
464+ # held across validation, so concurrent reads are not blocked.
465+ self .store .stage (entity )
466+ try :
467+ if entity .is_schema :
468+ self .store .validate_schema_basic (store_key )
469+ if validate :
470+ self .store .validate_schema (store_key , gts_ref_validation )
471+ elif validate :
472+ self .store .validate_instance (store_key , gts_ref_validation )
473+ except Exception as e : # noqa: BLE001 - converted to a result object at API boundary
474+ self .store .discard (store_key )
475+ return GtsAddEntityResult (
476+ ok = False ,
477+ error = f"Validation failed: { e !s} " ,
478+ is_type_schema = entity .is_schema ,
479+ )
480+ self .store .commit (store_key )
478481
479482 # Return gts_id if available, otherwise raw_id
480483 entity_id = entity .gts_id .id if entity .gts_id else (entity .raw_id or "" )
@@ -494,39 +497,33 @@ def add_entities(
494497 ok = all (r .ok for r in results )
495498 return GtsAddEntitiesResult (ok = ok , results = results )
496499
497- def add_schemas (
500+ def _validate_staged (
498501 self ,
499- schemas : builtins .list [dict [str , Any ]],
500- validate : bool = False ,
501- gts_ref_validation : GtsRefValidationMode = GtsRefValidationMode .ANY_VALID ,
502- ) -> GtsAddSchemasResult :
503- """Register a batch of GTS Type Schemas.
504-
505- Each entry's GTS Type Identifier is derived from its embedded ``$id``;
506- the aggregate ``ok`` is ``True`` only when every entry registered.
507- ``validate`` / ``gts_ref_validation`` apply to every entry exactly as
508- they do on ``POST /entities``.
509- """
510- results = [
511- self .add_schema (schema , validate = validate , gts_ref_validation = gts_ref_validation )
512- for schema in schemas
513- ]
514- ok = all (r .ok for r in results )
515- return GtsAddSchemasResult (ok = ok , results = results )
516-
517- def add_schema (
518- self ,
519- schema : dict [str , Any ],
520- validate : bool = False ,
521- gts_ref_validation : GtsRefValidationMode = GtsRefValidationMode .ANY_VALID ,
522- ) -> GtsAddSchemaResult :
523- """Register a single GTS Type Schema, deriving its type_id from ``$id``.
524-
525- The embedded ``$schema`` / ``$id`` presence checks are batch-specific;
526- the actual registration and (when requested) validation reuse the
527- single-entity :meth:`add_entity` path, so each entry honors ``validate``
528- / ``gts_ref_validation`` exactly like a ``POST /entities`` call.
529- """
502+ entity : GtsEntity ,
503+ store_key : str ,
504+ validate : bool ,
505+ gts_ref_validation : GtsRefValidationMode ,
506+ ) -> str | None :
507+ """Validate a staged entity against the current (staged + committed)
508+ set, returning an error message on failure or ``None`` on success. The
509+ caller stages before and commits/discards after."""
510+ try :
511+ if entity .is_schema :
512+ self .store .validate_schema_basic (store_key )
513+ if validate :
514+ self .store .validate_schema (store_key , gts_ref_validation )
515+ elif validate :
516+ self .store .validate_instance (store_key , gts_ref_validation )
517+ except Exception as e : # noqa: BLE001 - converted to a result object at API boundary
518+ return f"Validation failed: { e !s} "
519+ return None
520+
521+ def _prepare_type_schema (
522+ self , schema : dict [str , Any ]
523+ ) -> tuple [str , GtsEntity , str ] | GtsAddSchemaResult :
524+ """Run the batch-specific $schema/$id checks and build the entity. On
525+ success returns ``(type_id, entity, store_key)``; on failure returns the
526+ per-item error result. Does not stage or register anything."""
530527 if not isinstance (schema , dict ):
531528 return GtsAddSchemaResult (
532529 ok = False ,
@@ -558,18 +555,112 @@ def add_schema(
558555 type_id = type_id ,
559556 error = f"Invalid GTS Type Schema $id: { embedded_id } " ,
560557 )
558+ entity = GtsEntity (content = schema , cfg = self .cfg )
559+ if not entity .is_schema or not entity .gts_id :
560+ return GtsAddSchemaResult (
561+ ok = False , type_id = type_id , error = "Unable to detect GTS ID in schema"
562+ )
563+ store_key = entity .gts_id .id
564+ previous = self .store .get_committed (store_key )
565+ if (
566+ previous
567+ and not self .allow_entity_updates
568+ and previous .content != entity .content
569+ ):
570+ return GtsAddSchemaResult (
571+ ok = False ,
572+ type_id = type_id ,
573+ error = f"Entity '{ store_key } ' is already registered with different content" ,
574+ conflict = True ,
575+ )
576+ return (type_id , entity , store_key )
561577
562- result = self .add_entity (
563- schema , validate = validate , gts_ref_validation = gts_ref_validation
564- )
565- if result .ok :
566- return GtsAddSchemaResult (ok = True , type_id = type_id )
567- return GtsAddSchemaResult (
568- ok = False ,
569- type_id = type_id ,
570- error = result .error ,
571- conflict = result .conflict ,
572- )
578+ def add_schemas (
579+ self ,
580+ schemas : builtins .list [dict [str , Any ]],
581+ validate : bool = False ,
582+ gts_ref_validation : GtsRefValidationMode = GtsRefValidationMode .ANY_VALID ,
583+ ) -> GtsAddSchemasResult :
584+ """Register a batch of GTS Type Schemas.
585+
586+ Each entry's GTS Type Identifier is derived from its embedded ``$id``;
587+ the aggregate ``ok`` is ``True`` only when every entry registered.
588+ ``validate`` / ``gts_ref_validation`` apply to every entry exactly as
589+ they do on ``POST /entities``.
590+
591+ With ``validate`` the batch runs in two phases so the outcome is
592+ order-independent and nothing invalid is ever published: every
593+ structurally-valid entry is staged first (invisible to public reads),
594+ then each is validated against the fully-staged set - so an entry can
595+ resolve intra-batch references/ancestors regardless of position - and
596+ finally the entries that passed are committed while the rest are
597+ discarded.
598+ """
599+ gts_ref_validation = _normalize_gts_ref_validation (gts_ref_validation )
600+ if not validate :
601+ results = [
602+ self .add_schema (schema , validate = False , gts_ref_validation = gts_ref_validation )
603+ for schema in schemas
604+ ]
605+ return GtsAddSchemasResult (ok = all (r .ok for r in results ), results = results )
606+
607+ results : list [GtsAddSchemaResult | None ] = [None ] * len (schemas )
608+ # Phase 1: stage every structurally-valid entry.
609+ staged : list [tuple [int , str , GtsEntity , str ]] = []
610+ for index , schema in enumerate (schemas ):
611+ prepared = self ._prepare_type_schema (schema )
612+ if isinstance (prepared , GtsAddSchemaResult ):
613+ results [index ] = prepared
614+ continue
615+ type_id , entity , store_key = prepared
616+ self .store .stage (entity )
617+ staged .append ((index , type_id , entity , store_key ))
618+
619+ # Phase 2: validate every staged entry against the fully-staged set.
620+ verdicts : list [tuple [int , str , str , str | None ]] = []
621+ for index , type_id , entity , store_key in staged :
622+ error = self ._validate_staged (entity , store_key , True , gts_ref_validation )
623+ verdicts .append ((index , type_id , store_key , error ))
624+
625+ # Phase 3: publish the entries that passed, discard the ones that failed.
626+ for index , type_id , store_key , error in verdicts :
627+ if error is None :
628+ self .store .commit (store_key )
629+ results [index ] = GtsAddSchemaResult (ok = True , type_id = type_id )
630+ else :
631+ self .store .discard (store_key )
632+ results [index ] = GtsAddSchemaResult (
633+ ok = False , type_id = type_id , error = error
634+ )
635+
636+ final = [r for r in results if r is not None ]
637+ return GtsAddSchemasResult (ok = all (r .ok for r in final ), results = final )
638+
639+ def add_schema (
640+ self ,
641+ schema : dict [str , Any ],
642+ validate : bool = False ,
643+ gts_ref_validation : GtsRefValidationMode = GtsRefValidationMode .ANY_VALID ,
644+ ) -> GtsAddSchemaResult :
645+ """Register a single GTS Type Schema, deriving its type_id from ``$id``.
646+
647+ Stages the entity (invisible to public reads), validates it, then
648+ commits on success or discards on failure, so each entry honors
649+ ``validate`` / ``gts_ref_validation`` exactly like a ``POST /entities``
650+ call and an invalid schema is never observable.
651+ """
652+ gts_ref_validation = _normalize_gts_ref_validation (gts_ref_validation )
653+ prepared = self ._prepare_type_schema (schema )
654+ if isinstance (prepared , GtsAddSchemaResult ):
655+ return prepared
656+ type_id , entity , store_key = prepared
657+ self .store .stage (entity )
658+ error = self ._validate_staged (entity , store_key , validate , gts_ref_validation )
659+ if error is not None :
660+ self .store .discard (store_key )
661+ return GtsAddSchemaResult (ok = False , type_id = type_id , error = error )
662+ self .store .commit (store_key )
663+ return GtsAddSchemaResult (ok = True , type_id = type_id )
573664
574665 def validate_id (self , gts_id : str ) -> GtsIdValidationResult :
575666 # Check if it's a wildcard pattern (contains *)
@@ -855,7 +946,8 @@ def get_entity(self, gts_id: str) -> GtsGetEntityResult:
855946 GtsGetEntityResult with entity details or error
856947 """
857948 try :
858- entity = self .store .get (gts_id )
949+ # Public read: never expose a staged (not-yet-committed) entity.
950+ entity = self .store .get_committed (gts_id )
859951 if not entity :
860952 return GtsGetEntityResult (
861953 ok = False , error = f"Entity '{ gts_id } ' not found"
0 commit comments