Skip to content

Commit 6a3d17f

Browse files
Merge pull request #1489 from datajoint/retire/strict-provenance
Retire strict_provenance runtime guardrail (#1488)
2 parents 66bb490 + a4a4477 commit 6a3d17f

8 files changed

Lines changed: 8 additions & 608 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ DataJoint is a framework for scientific data pipelines based on the **Relational
55
- **Tables represent workflow steps** — Each table is a step in your pipeline
66
- **Foreign keys encode dependencies** — Parent tables must be populated before child tables
77
- **Computations are declarative** — Define *what* to compute; DataJoint handles *when*
8-
- **Results are immutable** — Full provenance and reproducibility
8+
- **Results are immutable** — Full lineage and reproducibility
99

1010
**Documentation:** https://docs.datajoint.com
1111

src/datajoint/autopopulate.py

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,11 @@ def upstream(self):
111111
(or ``FreeTable``, when indexed by a string) for any ancestor of
112112
``self``.
113113
114-
Reading via ``self.upstream`` is the provenance-safe pattern: the
115-
framework guarantees the restriction matches the current ``key``,
116-
and indexing a non-ancestor table raises ``DataJointError``. See
117-
:doc:`reference/specs/provenance` for the contract.
114+
Reading via ``self.upstream`` is the recommended pattern for the
115+
make() reproducibility contract: the framework guarantees the
116+
restriction matches the current ``key``, and indexing a non-ancestor
117+
table raises ``DataJointError``. See
118+
:doc:`reference/specs/autopopulate` for the contract.
118119
119120
Raises
120121
------
@@ -671,34 +672,6 @@ def _populate1(
671672

672673
self._upstream = Diagram.trace(self & dict(key))
673674

674-
# If strict_provenance is on, push the active-make context so the
675-
# runtime gates in expression.cursor / table.insert can check this
676-
# make()'s reads and writes. The context is popped in the finally
677-
# block below.
678-
strict_token = None
679-
if self.connection._config.get("strict_provenance", False):
680-
from .provenance import push_strict_make_context
681-
from .user_tables import Part
682-
683-
allowed_tables = set(self._upstream._cascade_restrictions.keys()) | {self.full_table_name}
684-
# Add Part tables of self to the allowed set. Use class __dict__
685-
# (not dir/getattr) to avoid triggering descriptors like the
686-
# _JobsDescriptor that lazy-declares the ~~ job table.
687-
for cls in type(self).__mro__:
688-
for attr_name, attr in cls.__dict__.items():
689-
if attr_name.startswith("_"):
690-
continue
691-
if isinstance(attr, type) and issubclass(attr, Part):
692-
# Instantiate to get full_table_name resolved against
693-
# this schema. The Part class is already attached via
694-
# @schema decoration of the master.
695-
try:
696-
part_ftn = attr().full_table_name
697-
allowed_tables.add(part_ftn)
698-
except Exception:
699-
pass
700-
strict_token = push_strict_make_context(self, frozenset(allowed_tables), dict(key))
701-
702675
try:
703676
if not is_generator:
704677
make(dict(key), **(make_kwargs or {}))
@@ -760,11 +733,6 @@ def _populate1(
760733
# access raises a clear error rather than silently using a
761734
# stale trace from the previous make() call.
762735
self._upstream = None
763-
# Pop the strict-make context, if any.
764-
if strict_token is not None:
765-
from .provenance import pop_strict_make_context
766-
767-
pop_strict_make_context(strict_token)
768736

769737
def progress(self, *restrictions: Any, display: bool = False) -> tuple[int, int]:
770738
"""

src/datajoint/expression.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,12 +1242,6 @@ def cursor(self, as_dict=False):
12421242
cursor
12431243
Database query cursor.
12441244
"""
1245-
# Strict-provenance read gate. No-op outside make() or when the
1246-
# config flag is off. See src/datajoint/provenance.py.
1247-
from .provenance import assert_read_allowed
1248-
1249-
assert_read_allowed(self)
1250-
12511245
sql = self.make_sql()
12521246
logger.debug(sql)
12531247
return self.connection.query(sql, as_dict=as_dict)

src/datajoint/provenance.py

Lines changed: 0 additions & 206 deletions
This file was deleted.

src/datajoint/settings.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@
6969
"database.database_prefix": "DJ_DATABASE_PREFIX",
7070
"database.create_tables": "DJ_CREATE_TABLES",
7171
"loglevel": "DJ_LOG_LEVEL",
72-
"strict_provenance": "DJ_STRICT_PROVENANCE",
7372
"display.diagram_direction": "DJ_DIAGRAM_DIRECTION",
7473
}
7574

@@ -362,16 +361,6 @@ class Config(BaseSettings):
362361
"*New in 2.2.3.*",
363362
)
364363

365-
strict_provenance: bool = Field(
366-
default=False,
367-
validation_alias="DJ_STRICT_PROVENANCE",
368-
description="If True, enforces the upstream-only convention inside make(): "
369-
"reads must go through self.upstream[Ancestor], writes must target self "
370-
"or self's Part tables with primary keys consistent with the current key. "
371-
"Off by default; opt-in for deployments that need runtime provenance "
372-
"guarantees backing downstream lineage / CDC tooling. *New in 2.3.*",
373-
)
374-
375364
# Cache path for query results
376365
query_cache: Path | None = None
377366

src/datajoint/table.py

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -834,23 +834,10 @@ def insert(
834834
" To override, set keyword argument allow_direct_insert=True."
835835
)
836836

837-
# Strict-provenance write gate (target check only). No-op outside make()
838-
# or when the config flag is off. Deliberately does NOT touch `rows` —
839-
# the per-row key-consistency check happens in `_insert_rows` as rows are
840-
# materialized, so a one-shot iterable (generator) is not consumed here.
841-
# See src/datajoint/provenance.py.
842-
from .provenance import assert_write_allowed
843-
844-
assert_write_allowed(self)
845-
846837
if inspect.isclass(rows) and issubclass(rows, QueryExpression):
847838
rows = rows() # instantiate if a class
848839
if isinstance(rows, QueryExpression):
849840
# insert from select - chunk_size not applicable.
850-
# Note: this INSERT ... SELECT runs entirely server-side, so under
851-
# strict_provenance the per-row key-consistency check does not apply
852-
# (row values are never materialized client-side). The target check
853-
# in assert_write_allowed above still governs which table is written.
854841
if chunk_size is not None:
855842
raise DataJointError("chunk_size is not supported for QueryExpression inserts")
856843
if not ignore_extra_fields:
@@ -905,17 +892,7 @@ def _insert_rows(self, rows, replace, skip_duplicates, ignore_extra_fields):
905892
"""
906893
# collects the field list from first row (passed by reference)
907894
field_list = []
908-
# Strict-provenance per-row key check runs here, as each row is
909-
# materialized — no-op outside make()/when the flag is off. Placing it in
910-
# this single materialization point (reached by both the chunked and
911-
# single-batch paths) avoids consuming the caller's `rows` iterable early.
912-
from .provenance import assert_row_key_allowed
913-
914-
def _make_row(row):
915-
assert_row_key_allowed(row)
916-
return self.__make_row_to_insert(row, field_list, ignore_extra_fields)
917-
918-
rows = list(_make_row(row) for row in rows)
895+
rows = list(self.__make_row_to_insert(row, field_list, ignore_extra_fields) for row in rows)
919896
if rows:
920897
try:
921898
# Handle empty field_list (all-defaults insert)

tests/integration/test_autopopulate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ class Greeting(dj.Computed):
381381
"""
382382

383383
def make(self, key):
384-
# Provenance-safe read: self.upstream pre-restricted to current key
384+
# Upstream read: self.upstream pre-restricted to current key
385385
name = self.upstream[Subject].fetch1("name")
386386
self.insert1({**key, "greeting": f"Hello, {name}!"})
387387

0 commit comments

Comments
 (0)