diff --git a/lamindb/models/sqlrecord.py b/lamindb/models/sqlrecord.py index df98e6651..e41cbb502 100644 --- a/lamindb/models/sqlrecord.py +++ b/lamindb/models/sqlrecord.py @@ -257,8 +257,9 @@ def single_space(self) -> bool | Space: Can only be set if the `SQLRecord` class inherits from `HasType` and `.is_type` is `True`. - This only affects updates to objects that will start to throw an error upon `INSERT` or `UPDATE` - if their space does not match the enforced space. Existing objects are not affected. + This affects creates and updates: new objects without an explicit `space` default to the + enforced space, and `INSERT` / `UPDATE` throw if their space does not match. Existing + objects are not rewritten when the setting changes. The enforced space can be configured in 2 ways: @@ -341,6 +342,28 @@ def single_space(self, value: bool | Space) -> None: self._sqlrecord._aux = None +def resolve_space_from_single_space_policy(type_record: HasType) -> Space | None: + """Return the write space enforced by a type's `_aux["ss"]`, if any. + + Mirrors the frontend single-space policy used for default write-space selection: + + - `_aux.ss = 1` or `"1"` → the type's own owning space + - `_aux.ss = ""` → that exact space + - missing / unset → ``None`` (caller keeps context / settings / ``all`` fallback) + """ + aux = getattr(type_record, "_aux", None) + if not aux: + return None + ss = aux.get("ss") + if ss is None: + return None + if ss == 1 or ss == "1": + return getattr(type_record, "space", None) + if isinstance(ss, str) and ss: + return Space.get(ss) + return None + + def deferred_attribute__repr__(self): return f"FieldAttr({self.field.model.__name__}.{self.field.name})" @@ -1185,7 +1208,9 @@ def resolve_fk_or_id(field_name: str) -> bool: from lamindb import context as run_context # Precedence is uniform across SQLRecord children: - # explicit `_id` or `` (mutually exclusive) > context/settings fallback. + # explicit `_id` or `` (mutually exclusive) + # > type `_aux.ss` single-space policy + # > context/settings fallback. has_explicit_space = resolve_fk_or_id("space") if run_context.space is not None: current_space = run_context.space @@ -1195,7 +1220,15 @@ def resolve_fk_or_id(field_name: str) -> bool: current_space = None if not has_explicit_space: - if current_space is not None: + policy_space = None + type_record = kwargs.get("type") + if isinstance(type_record, HasType): + policy_space = resolve_space_from_single_space_policy( + type_record + ) + if policy_space is not None: + kwargs["space"] = policy_space + elif current_space is not None: kwargs["space"] = current_space elif kwargs.get("space") is None: kwargs.pop("space", None) diff --git a/tests/pydata/test_record_basics.py b/tests/pydata/test_record_basics.py index 3f6d656c3..de79f9627 100644 --- a/tests/pydata/test_record_basics.py +++ b/tests/pydata/test_record_basics.py @@ -744,6 +744,12 @@ def test_single_space_enforces_type_space_or_specific_space(): assert constrained_type._aux is not None assert constrained_type._aux.get("ss") == 1 + # Without an explicit space, default to the type's owning space. + auto_type_space_record = ln.Record( + name="auto_type_space_record", type=constrained_type + ).save() + assert auto_type_space_record.space_id == constrained_type.space_id + constrained_type.settings.single_space = restricted_space constrained_type.save() constrained_type.refresh_from_db() @@ -753,6 +759,12 @@ def test_single_space_enforces_type_space_or_specific_space(): assert constrained_type._aux is not None assert constrained_type._aux.get("ss") == restricted_space.uid + # Exact-space policy also defaults when space is omitted. + auto_exact_space_record = ln.Record( + name="auto_exact_space_record", type=constrained_type + ).save() + assert auto_exact_space_record.space_id == restricted_space.id + valid_record = ln.Record( name="same_space_record", type=constrained_type, space=restricted_space ).save() @@ -763,7 +775,11 @@ def test_single_space_enforces_type_space_or_specific_space(): assert "record space must match locked type space" in error.exconly() with pytest.raises(InternalError) as error: - ln.Record(name="different_space_record", type=constrained_type).save() + ln.Record( + name="different_space_record", + type=constrained_type, + space=ln.Space.get(1), + ).save() assert "record space must match locked type space" in error.exconly() constrained_type.settings.single_space = True @@ -775,7 +791,9 @@ def test_single_space_enforces_type_space_or_specific_space(): with pytest.raises(InternalError) as error: ln.Record( - name="different_space_record_type_space_only", type=constrained_type + name="different_space_record_type_space_only", + type=constrained_type, + space=ln.Space.get(1), ).save() assert "record space must match locked type space" in error.exconly() @@ -792,6 +810,8 @@ def test_single_space_enforces_type_space_or_specific_space(): unconstrained_record.delete(permanent=True) unconstrained_record_2.delete(permanent=True) + auto_type_space_record.delete(permanent=True) + auto_exact_space_record.delete(permanent=True) valid_record.delete(permanent=True) constrained_type.delete(permanent=True) restricted_space.delete(permanent=True)