Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 37 additions & 4 deletions lamindb/models/sqlrecord.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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 = "<space uid>"` → 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})"

Expand Down Expand Up @@ -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 `<fk>_id` or `<fk>` (mutually exclusive) > context/settings fallback.
# explicit `<fk>_id` or `<fk>` (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
Expand All @@ -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)
Expand Down
24 changes: 22 additions & 2 deletions tests/pydata/test_record_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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()

Expand All @@ -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)
Expand Down
Loading