diff --git a/.github/workflows/example-gate.yml b/.github/workflows/example-gate.yml
index 032f7bd..b501732 100644
--- a/.github/workflows/example-gate.yml
+++ b/.github/workflows/example-gate.yml
@@ -9,9 +9,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+ - name: Install example verification tools
+ run: sudo apt-get update && sudo apt-get install -y ripgrep
- uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: pip
- - run: pip install -e . aiosqlite
+ - run: pip install -e . aiosqlite pytest pytest-asyncio
- run: ./scripts/verify-examples.sh
diff --git a/README.md b/README.md
index a92bfc4..2f65adb 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ configure those separately. This setting does not erase older plaintext files.
Restrict access and retention when using plaintext diagnostics, then unset the
variable and restart processes when troubleshooting is complete.
-TeaQL Python SDK is a runtime engine and toolkit for building data-driven business applications. It provides seamless integration with the TeaQL ecosystem, fully aligned with the `teaql-rs` baseline.
+TeaQL Python SDK is a runtime engine and toolkit for building data-driven business applications. It uses `teaql-rs` as the cross-language design baseline; verified coverage and remaining gaps are tracked in [conformance](https://github.com/teaql/teaql-conformance).
## Recommended Agent Harness
@@ -41,8 +41,8 @@ and evidence-based verification as the generator and runtimes evolve.
## 2. Tests Performed
-After rigorous AST semantic analysis and manual verification, this SDK has successfully passed the following tests:
-* ✅ **100% API Signature and Logic Parity**: Scanned with Tree-Sitter and implemented all internal methods and logic to match the Rust baseline.
+The SDK includes tests in the following areas. These suites do not establish complete Rust parity or acceptance of every external provider:
+* **Cross-language contracts**: Shared fixtures and integration tests verify individual portable behaviors; unresolved coverage stays explicit in conformance.
* ✅ **`teaql.core` Core Tests**: Extensively tested attribute extraction, safe nullability checks, and relationship building for `Value`, `GraphNode`, `Entity`, `Mutation`, `Query`, `Expr`, and `SafeExpression`.
* ✅ **`teaql.runtime` Runtime Tests**: Verified the context propagation of `UserContext` and the complete lifecycle hooking for `record_sql_log` and `record_metadata_log`.
* ✅ **`teaql.sql` / `teaql.data_service` Tests**: Tested the SQL AST compilation engine and the underlying command dispatch mechanism.
@@ -92,6 +92,81 @@ orders = await client.execute_query(FederalQuery(
await client.aclose()
```
+## Trace Chain Local Source Status
+
+Query and Mutation Requests own their required non-blank intent independently
+of logging. Physical SQL paths use the Rust canonical algorithm, with twelve
+byte-identical frozen vectors, owned query origins and qualified relation frames.
+Native SQLite tests observe three real relation levels without inserting expected
+frames, including a deepest-query failure. A successful write and a failed
+authoritative readback keep separate canonical paths and statement outcomes;
+the failed transaction rolls back.
+
+Parent query diagnostics include declared descendant binding provenance before
+safe projection. The existing mask algorithm, expanded SQL and execution values
+are unchanged; provenance is not stored on Context or exposed in log entries.
+This additional compilation has not been performance benchmarked.
+
+Graph saves now use an explicit invocation-owned `GraphMutationSession` and an
+immutable persistent parent scope. The original Context's data service is never
+swapped; child saves receive the session and their parent scope explicitly.
+Unannotated children inherit, local reasons append once, and deletion creates its
+scope before execution. The typed ledger can store an owned complete replacement
+chain. Five shared graph fixtures verify fifteen per-entity expectations; these
+helper tests are separate from generated traversal evidence.
+
+[The generated Trace Chain example](examples/trace-chain/) drives six mutations
+through actual Q/E/save APIs and SQLite, observing request lineage, SQL metadata
+and safe audits after commit. It also verifies three query relation levels,
+concurrent independent saves in one Context, write/readback failure rollback,
+and missing root intent before transaction access. Its verifier executes twice
+on one SQLite file without cleanup and checks all generated-library hashes.
+All ten example groups and the source suite pass twice locally (424 tests and
+eight explicit external-provider skips per source run).
+
+SQL transactions queue owned audit snapshots until commit, including explicit
+transactions and automatic batches. Rollback discards them. A completion failure
+is reported with `GraphCommittedError.committed == True`; all remaining audit
+deliveries and graph cleanup callbacks are still attempted, with no postcommit
+rollback. Reentrant independent root saves fail instead of silently joining.
+The private generated save implementation has changed; old generated libraries
+must be regenerated, while public `audit_as(...).save(context)` stays unchanged.
+
+The aggregate follow-up adds runtime-owned query projection snapshots and generated
+`query_projection(alias)` / `has_query_projection(alias)` accessors. Missing aliases
+raise `KeyError`; zero and null are present values. Projections are owned snapshots,
+not writable model fields, live calculations or part of a mutation payload. The
+generated example covers root/nested counts, original Trace Chain ancestry,
+masked descendant intent, logging on/off, provider failure recovery and saving
+only a modeled field. An alias named `save` cannot replace the save method.
+Regenerate libraries to acquire this projection API; related aggregate streaming
+remains unsupported and rejects before provider I/O.
+
+Relation attachment retains original scalar keys in invocation-local runtime
+state. It does not infer membership from hydrated objects: a forward reference
+filtered to `None` must not remove its child from a separately loaded parent
+list, and a later sibling can still load using the same FK. Keys are not added
+to returned records or mutation payloads. Native SQLite cases exercise these
+boundaries with related counts, nested ancestry and logging both on and off.
+
+The reusable [generated Facet example](examples/facet-trace/) exercises 20
+root/nested/loaded-relation scenarios twice on one retained SQLite database.
+It checks actual counts and returned metadata, original root/ancestor routes,
+future-binding masking, logging on/off, and independent next-query state. The
+all-examples gate now includes it and fingerprints the generated library.
+
+This is local-source/generated-consumer evidence, not complete Trace Chain.
+Prepared-batch grouping, generated ledger overrides, complete entry-point and
+inherited mutation privacy coverage, successful readback diagnostic alignment,
+live PostgreSQL/MySQL graph acceptance, and immutable internal Registry replay
+remain open. No released-package parity is claimed.
+
+```bash
+PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python -m pytest -q
+PYTHONDONTWRITEBYTECODE=1 bash scripts/verify-examples.sh
+# Repeat both commands without source changes.
+```
+
## Security Foundation Status
Python currently provides governed local SQL execution and a **TFP client**;
diff --git a/examples/conformance/Q.py b/examples/conformance/Q.py
index 0071098..8eecdba 100644
--- a/examples/conformance/Q.py
+++ b/examples/conformance/Q.py
@@ -19,3 +19,4 @@ def work_items() -> WorkItemRequest:
def work_items_minimal() -> WorkItemRequest:
return WorkItemRequest(minimal=True)
+
diff --git a/examples/conformance/app/masking_lifecycle.py b/examples/conformance/app/masking_lifecycle.py
index 1b15783..9abd71d 100644
--- a/examples/conformance/app/masking_lifecycle.py
+++ b/examples/conformance/app/masking_lifecycle.py
@@ -46,11 +46,11 @@ def capture(entry):
entries.append(entry)
sink.write(entry)
context.set_diagnostic_sql_log_sink(SimpleNamespace(write=capture))
- async def insert(entity_id, target=None):
+ async def insert(entity_id, target=None, request_context=None):
command = (InsertCommand('MaskCustomer').value('id', entity_id).value('version', 1)
.value('display_name', 'Riverside'))
command.trace_chain = [TraceNode(comment='what: seed Riverside lifecycle fixture')]
- return await (target or service).mutate(context, MutationRequest(command))
+ return await (target or service).mutate(request_context or context, MutationRequest(command, comment='what: seed Riverside lifecycle fixture'))
try:
for entity_id in [1, 2, 3]:
await insert(entity_id)
@@ -58,7 +58,7 @@ async def insert(entity_id, target=None):
output.clear()
request = QueryRequest(SelectQuery('MaskCustomer')
.filter(Expr.eq('display_name', 'Riverside')).limit(3)
- ).comment('what: inspect customers').purpose('why: verify stream lifecycle')
+ , _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior').comment('what: inspect customers').purpose('why: verify stream lifecycle')
# async-for break alone does not promise immediate generator close
# in Python. aclosing makes ownership explicit and deterministic.
async with aclosing(service.query_stream(context, request, 1)) as stream:
@@ -100,17 +100,17 @@ async def insert(entity_id, target=None):
context.insert_resource('dataService', service)
entries.clear()
- async def partial_graph():
- target = context.require_resource('dataService')
- await insert(30, target)
- await insert(777, target)
- await insert(31, target)
+ async def partial_graph(graph):
+ await insert(30, graph.transaction, graph.context)
+ await insert(777, graph.transaction, graph.context)
+ await insert(31, graph.transaction, graph.context)
try:
- await context.execute_graph_save(partial_graph)
+ await context.execute_graph_save(partial_graph, comment='what: runtime regression fixture')
raise AssertionError('partial graph unexpectedly committed')
except TransportError:
pass
- assert [entry.execution_outcome for entry in entries] == ['success','success','success']
+ assert [entry.execution_outcome for entry in entries] == ['success','success','success','success']
+ assert [entry.trace_path[-1].name for entry in entries] == ['insert','select','insert','select']
assert entries[-1].result_count == 0
assert not await service.transport.fetch_all_sql(CompiledQuery(
'SELECT id FROM mask_customer_data WHERE id IN (30,31,777)', []))
@@ -120,11 +120,11 @@ async def partial_graph():
command = (InsertCommand('MaskChild').value('id', 1).value('version', 1).value('parent_id', 1))
command.trace_chain = [TraceNode(comment='what: seed child for relation verification')]
- await service.mutate(context, MutationRequest(command))
+ await service.mutate(context, MutationRequest(command, comment='what: runtime regression fixture'))
graph_request = QueryRequest(SelectQuery('MaskCustomer').project('id')
.filter(Expr.eq('id', 1)).and_filter(Expr.eq('display_name', 'Riverside'))
.relation_query('children', SelectQuery('MaskChild').project('id').limit(2)).limit(1)
- ).comment('what: load Riverside graph').purpose('why: verify inherited relation masking')
+ , _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior').comment('what: load Riverside graph').purpose('why: verify inherited relation masking')
entries.clear()
await service.transport.execute_sql(CompiledQuery(
'ALTER TABLE mask_child_data RENAME TO mask_child_unavailable', []))
diff --git a/examples/conformance/models/platform.py b/examples/conformance/models/platform.py
index 24f3e1b..84ba4e5 100644
--- a/examples/conformance/models/platform.py
+++ b/examples/conformance/models/platform.py
@@ -1,8 +1,11 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
+
class Platform:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -31,15 +34,18 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.name = kwargs.get("name")
+
self.version = kwargs.get("version")
+
self._work_item_list = kwargs.get("work_item_list", [])
if "work_item_list" in kwargs or kwargs.get("id") is None:
self._loaded_fields.add("work_item_list")
if self._work_item_list:
from models.work_item import WorkItem
self._work_item_list = [
- item if isinstance(item, WorkItem) else WorkItem(**item)
+ item if isinstance(item, WorkItem) else WorkItem(_entity_root=self._entity_root, **item)
for item in self._work_item_list
]
self._ledger_id = getattr(self, "id", None)
@@ -50,13 +56,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("Platform", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
for child in self._work_item_list:
child._teaql_attach_root(root)
@@ -67,26 +76,71 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("Platform"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._work_item_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "name" in self._loaded_fields:
payload["name"] = Value.Text(self.name)
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -94,38 +148,43 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("Platform", payload)
elif action == "Update":
- cmd = UpdateCommand("Platform", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("Platform", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("Platform", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("Platform", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "name" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
for index, child in enumerate(self._work_item_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "platform", self)
- child._loaded_fields.add("platform")
- child._entity_root.set(child._teaql_entity_key(), "platform", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "platform", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_platform(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("work_item_list").index(index)
raise CheckException([
@@ -133,17 +192,19 @@ def _teaql_preflight_graph(self, context):
for v in error.violations
]) from error
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -182,6 +243,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -194,40 +256,45 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
- cascade_relations.append(("work_item_list", self._work_item_list, "update_platform"))
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("work_item_list", self._work_item_list, "update_platform", "platform"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/conformance/models/work_item.py b/examples/conformance/models/work_item.py
index 2ffd6b3..cec96ce 100644
--- a/examples/conformance/models/work_item.py
+++ b/examples/conformance/models/work_item.py
@@ -1,9 +1,12 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
from models.platform import Platform
+
class WorkItem:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -36,10 +39,15 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.title = kwargs.get("title")
+
self.description = kwargs.get("description")
+
self.platform = kwargs.get("platform")
+
self.version = kwargs.get("version")
+
if isinstance(self.platform, dict):
self.platform = Platform(**self.platform)
self._ledger_id = getattr(self, "id", None)
@@ -50,13 +58,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("WorkItem", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
return self
def mark_for_deletion(self):
@@ -65,30 +76,78 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("WorkItem"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "title" in self._loaded_fields:
payload["title"] = Value.Text(self.title)
+
if "description" in self._loaded_fields:
payload["description"] = Value.Text(self.description)
+
if "platform" in self._loaded_fields:
- payload["platform"] = Value.Object(self.platform)
+ reference = self.platform
+ reference_id = getattr(reference, "id", reference)
+ payload["platform"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -96,46 +155,54 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("WorkItem", payload)
elif action == "Update":
- cmd = UpdateCommand("WorkItem", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("WorkItem", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("WorkItem", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("WorkItem", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "title" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("title"), message="Mutation requires a fully loaded entity")])
- if "description" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("description"), message="Mutation requires a fully loaded entity")])
- if "platform" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("platform"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "title" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("title"), message="Mutation requires a fully loaded entity")])
+ if "description" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("description"), message="Mutation requires a fully loaded entity")])
+ if "platform" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("platform"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -186,6 +253,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -198,39 +266,44 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/conformance/pyproject.toml b/examples/conformance/pyproject.toml
index 565edd8..d8e7349 100644
--- a/examples/conformance/pyproject.toml
+++ b/examples/conformance/pyproject.toml
@@ -2,7 +2,7 @@
name = "runtime-example-conformance-service-lib"
version = "1.0.0"
description = "Generated python library"
-dependencies = ["teaql==0.2.7", "aiosqlite>=0.22.1"]
+dependencies = ["teaql==0.2.9", "aiosqlite>=0.22.1"]
[tool.setuptools]
py-modules = ["Q", "E"]
diff --git a/examples/conformance/requests/platform_request.py b/examples/conformance/requests/platform_request.py
index 2609c6e..ea0f3a3 100644
--- a/examples/conformance/requests/platform_request.py
+++ b/examples/conformance/requests/platform_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -352,8 +353,7 @@ def comment(self, c: str):
def new_entity(self, context) -> Platform:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("Platform", Platform())
if not isinstance(entity, Platform):
raise TypeError("entity initializer returned an incompatible Platform")
@@ -361,10 +361,9 @@ def new_entity(self, context) -> Platform:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -372,53 +371,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[Platform]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (Platform(_entity_root=query_root, **row) for row in result.rows),
+ (Platform(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[Platform]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(Platform(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(Platform(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutablePlatformRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield Platform(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield Platform(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/conformance/requests/work_item_request.py b/examples/conformance/requests/work_item_request.py
index d3ed1ad..0728807 100644
--- a/examples/conformance/requests/work_item_request.py
+++ b/examples/conformance/requests/work_item_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -442,8 +443,7 @@ def comment(self, c: str):
def new_entity(self, context) -> WorkItem:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("WorkItem", WorkItem())
if not isinstance(entity, WorkItem):
raise TypeError("entity initializer returned an incompatible WorkItem")
@@ -451,10 +451,9 @@ def new_entity(self, context) -> WorkItem:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -462,53 +461,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[WorkItem]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (WorkItem(_entity_root=query_root, **row) for row in result.rows),
+ (WorkItem(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[WorkItem]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(WorkItem(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(WorkItem(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableWorkItemRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield WorkItem(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield WorkItem(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/facet-trace/README.md b/examples/facet-trace/README.md
new file mode 100644
index 0000000..b2d6d0e
--- /dev/null
+++ b/examples/facet-trace/README.md
@@ -0,0 +1,33 @@
+# Generated Facet Trace Chain acceptance
+
+Run `bash examples/facet-trace/verify.sh` from the runtime repository. The script
+uses local `src`, retains a fresh SQLite database and logs, executes twice without
+cleanup, and verifies that the 13 generated library files remain unchanged.
+It is included in `scripts/verify-examples.sh`.
+
+This three-object School model isolates `TC-SQL-09`: root Facets, nested Facets,
+and nested Facets inside an already-loaded reverse relation. Twenty scenarios
+exercise include-all/matched-only, empty parents/results, limit-one display with
+full matching counts, and SQL logging on/off. Generated Q/E and audited mutation
+APIs seed and traverse the graph. Real database reads, count SQL, safe diagnostics,
+request immutability, raw policy intent, future-binding masking, and independent
+next-request `NotLoaded` semantics are asserted. `FACET_OBSERVED` records actual
+counts/results and diagnostic routes, not just a success marker. The transport
+and Context metadata boundary are observed separately: every SELECT retains
+its full original root/ancestor path and raw request-owned intent even with
+logging disabled, safe diagnostics emit nothing when disabled, and query
+execution performs no writes. Expected frames are never injected into runtime
+requests by the fixture.
+
+`lib/` is read-only generated output retained from producer commit
+`c7407c93c186fa9f0e2ff9599952f333a5963020` (Python issue #43 / producer #251).
+Do not inspect generated source for API discovery. For new operations, use
+model-aware `python-assist-query/school.school_type` or other action/entity/field
+Assist against `model.xml`; report `MISSING_ASSIST` rather than guessing.
+
+To test a downloaded artifact, copy this example into a separate workspace and
+invoke `main.py` with `PYTHONPATH` containing only that workspace's `lib`, the
+artifact's interpreter, and an explicit `TEAQL_FACET_TRACE_DB`. Verify actual
+runtime module paths and package hashes. Do not change this local-source gate
+to silently consume an installed release. This fixture does not prove all Trace
+Chain cases, external database support, or public-release parity.
diff --git a/examples/facet-trace/lib/E.py b/examples/facet-trace/lib/E.py
new file mode 100644
index 0000000..a9acbec
--- /dev/null
+++ b/examples/facet-trace/lib/E.py
@@ -0,0 +1,198 @@
+class TeaQLNotLoadedError(RuntimeError):
+ def __init__(self, root, access_path, break_point):
+ self.root = root
+ self.access_path = access_path
+ self.break_point = break_point
+ super().__init__(
+ f"TeaQLNotLoadedError: root={root} access_path={access_path} "
+ f"break_point={break_point} suggested_fix=select_{break_point}(...)"
+ )
+
+
+class ValueExpression:
+ def __init__(self, value=None, error=None):
+ self._value = value
+ self._error = error
+
+ def eval(self):
+ if self._error is not None:
+ raise self._error
+ return self._value
+
+ def or_if_null(self, fallback):
+ value = self.eval()
+ return fallback if value is None else value
+
+
+class EntityExpression:
+ def __init__(self, value, root=None, path="", error=None):
+ self._value = value
+ self._root = root or f"{type(value).__name__ if value is not None else 'Entity'}(null)"
+ self._path = path
+ self._error = error
+
+ def eval(self):
+ if self._error is not None:
+ raise self._error
+ return self._value
+
+ def _path_for(self, field):
+ return f"{self._path}.{field}" if self._path else field
+
+ def _not_loaded(self, field):
+ path = self._path_for(field)
+ return TeaQLNotLoadedError(self._root, path, field)
+
+ def _scalar(self, field, relation_id=False):
+ if self._error is not None:
+ return ValueExpression(error=self._error)
+ if self._value is None:
+ return ValueExpression(None)
+ if field not in getattr(self._value, "_loaded_fields", set()):
+ return ValueExpression(error=self._not_loaded(field))
+ value = getattr(self._value, field)
+ if relation_id and value is not None and not isinstance(value, (int, str)):
+ value = getattr(value, "id", None)
+ return ValueExpression(value)
+
+ def _relation(self, field, expression_type):
+ path = self._path_for(field)
+ if self._error is not None:
+ return expression_type(None, self._root, path, self._error)
+ if self._value is None:
+ return expression_type(None, self._root, path)
+ if field not in getattr(self._value, "_loaded_fields", set()):
+ return expression_type(None, self._root, path, self._not_loaded(field))
+ value = getattr(self._value, field)
+ if value is not None and isinstance(value, (int, str)):
+ return expression_type(None, self._root, path, self._not_loaded(field))
+ return expression_type(value, self._root, path)
+
+
+class ListExpression:
+ def __init__(self, values, root, path, item_expression, error=None):
+ self._values = values
+ self._root = root
+ self._path = path
+ self._item_expression = item_expression
+ self._error = error
+
+ def size(self):
+ return ValueExpression(error=self._error) if self._error else ValueExpression(len(self._values))
+
+ def first(self):
+ return self.get(0)
+
+ def get(self, index):
+ path = f"{self._path}.get({index})"
+ if self._error is not None:
+ return self._item_expression(None, self._root, path, self._error)
+ value = self._values[index] if 0 <= index < len(self._values) else None
+ return self._item_expression(value, self._root, path)
+
+
+class PlatformExpression(EntityExpression):
+ def id(self):
+ return self._scalar("id")
+ def name(self):
+ return self._scalar("name")
+ def base_url(self):
+ return self._scalar("baseUrl")
+ def create_time(self):
+ return self._scalar("createTime")
+ def update_time(self):
+ return self._scalar("updateTime")
+ def version(self):
+ return self._scalar("version")
+ def school_type_list(self):
+ path = self._path_for("school_type_list")
+ if self._error is not None:
+ return ListExpression([], self._root, path, SchoolTypeExpression, self._error)
+ if self._value is None:
+ return ListExpression([], self._root, path, SchoolTypeExpression)
+ if "school_type_list" not in getattr(self._value, "_loaded_fields", set()):
+ return ListExpression([], self._root, path, SchoolTypeExpression, self._not_loaded("school_type_list"))
+ return ListExpression(getattr(self._value, "_school_type_list"), self._root, path, SchoolTypeExpression)
+ def school_list(self):
+ path = self._path_for("school_list")
+ if self._error is not None:
+ return ListExpression([], self._root, path, SchoolExpression, self._error)
+ if self._value is None:
+ return ListExpression([], self._root, path, SchoolExpression)
+ if "school_list" not in getattr(self._value, "_loaded_fields", set()):
+ return ListExpression([], self._root, path, SchoolExpression, self._not_loaded("school_list"))
+ return ListExpression(getattr(self._value, "_school_list"), self._root, path, SchoolExpression)
+ pass
+
+class SchoolTypeExpression(EntityExpression):
+ def id(self):
+ return self._scalar("id")
+ def name(self):
+ return self._scalar("name")
+ def code(self):
+ return self._scalar("code")
+ def display_order(self):
+ return self._scalar("displayOrder")
+ def version(self):
+ return self._scalar("version")
+ def platform_id(self):
+ return self._scalar("platform", relation_id=True)
+
+ def platform(self):
+ return self._relation("platform", PlatformExpression)
+ def school_list(self):
+ path = self._path_for("school_list")
+ if self._error is not None:
+ return ListExpression([], self._root, path, SchoolExpression, self._error)
+ if self._value is None:
+ return ListExpression([], self._root, path, SchoolExpression)
+ if "school_list" not in getattr(self._value, "_loaded_fields", set()):
+ return ListExpression([], self._root, path, SchoolExpression, self._not_loaded("school_list"))
+ return ListExpression(getattr(self._value, "_school_list"), self._root, path, SchoolExpression)
+ pass
+
+class SchoolExpression(EntityExpression):
+ def id(self):
+ return self._scalar("id")
+ def name(self):
+ return self._scalar("name")
+ def address(self):
+ return self._scalar("address")
+ def established_date(self):
+ return self._scalar("establishedDate")
+ def student_capacity(self):
+ return self._scalar("studentCapacity")
+ def active(self):
+ return self._scalar("active")
+ def create_time(self):
+ return self._scalar("createTime")
+ def update_time(self):
+ return self._scalar("updateTime")
+ def version(self):
+ return self._scalar("version")
+ def platform_id(self):
+ return self._scalar("platform", relation_id=True)
+
+ def platform(self):
+ return self._relation("platform", PlatformExpression)
+ def school_type_id(self):
+ return self._scalar("schoolType", relation_id=True)
+
+ def school_type(self):
+ return self._relation("schoolType", SchoolTypeExpression)
+ pass
+
+class E:
+ @staticmethod
+ def platform(value):
+ entity_id = getattr(value, "id", None)
+ return PlatformExpression(value, "Platform(id={})".format(entity_id))
+ @staticmethod
+ def school_type(value):
+ entity_id = getattr(value, "id", None)
+ return SchoolTypeExpression(value, "SchoolType(id={})".format(entity_id))
+ @staticmethod
+ def school(value):
+ entity_id = getattr(value, "id", None)
+ return SchoolExpression(value, "School(id={})".format(entity_id))
+ pass
\ No newline at end of file
diff --git a/examples/facet-trace/lib/Q.py b/examples/facet-trace/lib/Q.py
new file mode 100644
index 0000000..fa189cd
--- /dev/null
+++ b/examples/facet-trace/lib/Q.py
@@ -0,0 +1,31 @@
+# Generated by teaql-code-gen
+from requests.platform_request import PlatformRequest
+from requests.school_type_request import SchoolTypeRequest
+from requests.school_request import SchoolRequest
+
+class Q:
+ @staticmethod
+ def platforms() -> PlatformRequest:
+ return PlatformRequest(minimal=False)
+
+ @staticmethod
+ def platforms_minimal() -> PlatformRequest:
+ return PlatformRequest(minimal=True)
+
+ @staticmethod
+ def school_types() -> SchoolTypeRequest:
+ return SchoolTypeRequest(minimal=False)
+
+ @staticmethod
+ def school_types_minimal() -> SchoolTypeRequest:
+ return SchoolTypeRequest(minimal=True)
+
+ @staticmethod
+ def schools() -> SchoolRequest:
+ return SchoolRequest(minimal=False)
+
+ @staticmethod
+ def schools_minimal() -> SchoolRequest:
+ return SchoolRequest(minimal=True)
+
+
diff --git a/examples/facet-trace/lib/models/__init__.py b/examples/facet-trace/lib/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/facet-trace/lib/models/platform.py b/examples/facet-trace/lib/models/platform.py
new file mode 100644
index 0000000..bc1d72d
--- /dev/null
+++ b/examples/facet-trace/lib/models/platform.py
@@ -0,0 +1,435 @@
+from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot, _QueryProjectionSnapshot
+from teaql.core.list import SmartList
+from teaql.core import MutationIntent
+from teaql.core.value import Value
+from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
+import itertools
+
+
+class Platform:
+ _teaql_temporary_ids = itertools.count(1)
+ @classmethod
+ def refer(cls, entity_id):
+ return cls(id=entity_id)
+
+ @classmethod
+ def _teaql_new_with_fixed_id(cls, entity_id):
+ """Generated bootstrap capability; application code must not call it."""
+ return cls(id=entity_id)._teaql_force_create()
+
+ def _teaql_force_create(self):
+ self._action = "Create"
+ self._entity_root.mark_as_new(self._teaql_entity_key())
+ return self
+
+ def __init__(self, **kwargs):
+ self._entity_root = kwargs.pop("_entity_root", None) or EntityRoot()
+ if "id" in kwargs and "id" not in kwargs:
+ kwargs["id"] = kwargs.pop("id")
+ if "name" in kwargs and "name" not in kwargs:
+ kwargs["name"] = kwargs.pop("name")
+ if "base_url" in kwargs and "baseUrl" not in kwargs:
+ kwargs["baseUrl"] = kwargs.pop("base_url")
+ if "create_time" in kwargs and "createTime" not in kwargs:
+ kwargs["createTime"] = kwargs.pop("create_time")
+ if "update_time" in kwargs and "updateTime" not in kwargs:
+ kwargs["updateTime"] = kwargs.pop("update_time")
+ if "version" in kwargs and "version" not in kwargs:
+ kwargs["version"] = kwargs.pop("version")
+ self._teaql_query_projections = _QueryProjectionSnapshot(kwargs, (
+ "id", "id",
+ "name", "name",
+ "baseUrl", "base_url",
+ "createTime", "create_time",
+ "updateTime", "update_time",
+ "version", "version",
+ "school_type_list",
+ "school_list",
+ ))
+ self._action = "Update" if kwargs.get("id") else "Create"
+ self._comment = None
+ self._loaded_fields = set(kwargs.keys())
+ self.id = kwargs.get("id")
+
+ self.name = kwargs.get("name")
+
+ self.baseUrl = kwargs.get("baseUrl")
+
+ self.createTime = kwargs.get("createTime")
+
+ self.updateTime = kwargs.get("updateTime")
+
+ self.version = kwargs.get("version")
+
+ self._school_type_list = kwargs.get("school_type_list", [])
+ if "school_type_list" in kwargs or kwargs.get("id") is None:
+ self._loaded_fields.add("school_type_list")
+ self._school_list = kwargs.get("school_list", [])
+ if "school_list" in kwargs or kwargs.get("id") is None:
+ self._loaded_fields.add("school_list")
+ if self._school_type_list:
+ from models.school_type import SchoolType
+ source = self._school_type_list
+ hydrated = [
+ item if isinstance(item, SchoolType) else SchoolType(_entity_root=self._entity_root, **item)
+ for item in source
+ ]
+ self._school_type_list = (SmartList(hydrated, facets=source.facets, total_count=source.total_count)
+ if isinstance(source, SmartList) else hydrated)
+ if self._school_list:
+ from models.school import School
+ source = self._school_list
+ hydrated = [
+ item if isinstance(item, School) else School(_entity_root=self._entity_root, **item)
+ for item in source
+ ]
+ self._school_list = (SmartList(hydrated, facets=source.facets, total_count=source.total_count)
+ if isinstance(source, SmartList) else hydrated)
+ self._ledger_id = getattr(self, "id", None)
+ if self._ledger_id is None:
+ self._ledger_id = -next(self._teaql_temporary_ids)
+ key = self._teaql_entity_key()
+ if self._action == "Create":
+ self._entity_root.mark_as_new(key)
+ elif getattr(self, "version", None) is not None:
+ self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
+
+ def query_projection(self, alias):
+ """Read a query-only alias; raises KeyError when it was not returned."""
+ return self._teaql_query_projections.get(alias)
+
+ def has_query_projection(self, alias):
+ return self._teaql_query_projections.contains(alias)
+
+ def _teaql_entity_key(self):
+ return EntityKey("Platform", self._ledger_id)
+
+ def _teaql_attach_root(self, root):
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
+ self._entity_root = root
+ for child in self._school_type_list:
+ child._teaql_attach_root(root)
+ for child in self._school_list:
+ child._teaql_attach_root(root)
+ return self
+ def mark_for_deletion(self):
+ self._action = "Delete"
+ self._entity_root.mark_as_deleted(self._teaql_entity_key())
+ return self
+
+ def audit_as(self, comment: str):
+ MutationIntent(comment)
+ self._comment = comment
+ return self
+
+ async def save(self, context):
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
+
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
+
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("Platform"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._school_type_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+ for child in self._school_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
+ payload = {}
+ if "id" in self._loaded_fields:
+ payload["id"] = Value.I64(self.id)
+
+ if "name" in self._loaded_fields:
+ payload["name"] = Value.Text(self.name)
+
+ if "baseUrl" in self._loaded_fields:
+ payload["base_url"] = Value.Text(self.baseUrl)
+
+ if "createTime" in self._loaded_fields:
+ payload["create_time"] = Value.DateTime(self.createTime)
+
+ if "updateTime" in self._loaded_fields:
+ payload["update_time"] = Value.DateTime(self.updateTime)
+
+ if "version" in self._loaded_fields:
+ payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
+ action = self._action
+ if action == "Update":
+ ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
+ payload = {field: value for field, value in ledger.items() if field not in ("id", "version")}
+ if action == "Create":
+ cmd = InsertCommand("Platform", payload)
+ elif action == "Update":
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("Platform", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ for key, value in payload.items():
+ if key not in ("id", "version"): cmd.value(key, value)
+ else:
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("Platform", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
+ return action, cmd
+
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "baseUrl" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("base_url"), message="Mutation requires a fully loaded entity")])
+ if "createTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
+ if "updateTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+ for index, child in enumerate(self._school_type_list):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, "platform", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_platform(self)
+ try:
+ child._teaql_preflight_graph(graph)
+ except CheckException as error:
+ prefix = ObjectLocation().property("school_type_list").index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+ for index, child in enumerate(self._school_list):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, "platform", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_platform(self)
+ try:
+ child._teaql_preflight_graph(graph)
+ except CheckException as error:
+ prefix = ObjectLocation().property("school_list").index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
+
+ self._teaql_attach_root(self._entity_root)
+ action, cmd = self._teaql_build_command()
+
+
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
+
+ try:
+ context.check_and_fix_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+ context.mark_mutation_checked(cmd)
+ service = context.require_resource("dataService")
+ result = await service.mutate(context, req)
+ persisted = result.persisted_record
+ if persisted is None:
+ raise RuntimeError(
+ "Mutation provider did not return authoritative persisted state for Platform"
+ )
+ rollback_payload = {field: getattr(self, field, None) for field in self._loaded_fields | {"id", "version"}}
+ rollback_ledger_id = self._ledger_id
+ rollback_action = self._action
+ rollback_loaded_fields = set(self._loaded_fields)
+ old_key = self._teaql_entity_key()
+ if "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ elif "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ if "name" in persisted:
+ self.name = persisted["name"]
+ self._loaded_fields.add("name")
+ elif "name" in persisted:
+ self.name = persisted["name"]
+ self._loaded_fields.add("name")
+ if "base_url" in persisted:
+ self.baseUrl = persisted["base_url"]
+ self._loaded_fields.add("baseUrl")
+ elif "baseUrl" in persisted:
+ self.baseUrl = persisted["baseUrl"]
+ self._loaded_fields.add("baseUrl")
+ if "create_time" in persisted:
+ self.createTime = persisted["create_time"]
+ self._loaded_fields.add("createTime")
+ elif "createTime" in persisted:
+ self.createTime = persisted["createTime"]
+ self._loaded_fields.add("createTime")
+ if "update_time" in persisted:
+ self.updateTime = persisted["update_time"]
+ self._loaded_fields.add("updateTime")
+ elif "updateTime" in persisted:
+ self.updateTime = persisted["updateTime"]
+ self._loaded_fields.add("updateTime")
+ if "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ elif "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
+ self._ledger_id = getattr(self, "id", self._ledger_id)
+ new_key = self._teaql_entity_key()
+ if old_key != new_key:
+ self._entity_root.rekey(old_key, new_key)
+ def rollback_entity():
+ for field, value in rollback_payload.items():
+ setattr(self, field, value)
+ self._ledger_id = rollback_ledger_id
+ self._action = rollback_action
+ self._loaded_fields = rollback_loaded_fields
+ if old_key != new_key:
+ self._entity_root.rekey(new_key, old_key)
+ graph.after_rollback(rollback_entity)
+ if action != "Delete":
+ self._action = "Update"
+
+ if action != "Delete":
+ await self._teaql_save_children(graph, scope)
+ def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
+ self._entity_root.clear_entity(new_key)
+ if getattr(self, "version", None) is not None:
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
+ return self
+
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("school_type_list", self._school_type_list, "update_platform", "platform"))
+ cascade_relations.append(("school_list", self._school_list, "update_platform", "platform"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
+ def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
+ self.id = value
+ self._loaded_fields.add("id")
+ self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
+ return self
+
+ def update_name(self, value):
+ self.name = value
+ self._loaded_fields.add("name")
+ self._entity_root.set(self._teaql_entity_key(), "name", Value.from_any(value))
+ return self
+
+ def update_base_url(self, value):
+ self.baseUrl = value
+ self._loaded_fields.add("baseUrl")
+ self._entity_root.set(self._teaql_entity_key(), "base_url", Value.from_any(value))
+ return self
+
+ def update_create_time(self, value):
+ self.createTime = value
+ self._loaded_fields.add("createTime")
+ self._entity_root.set(self._teaql_entity_key(), "create_time", Value.from_any(value))
+ return self
+
+ def update_update_time(self, value):
+ self.updateTime = value
+ self._loaded_fields.add("updateTime")
+ self._entity_root.set(self._teaql_entity_key(), "update_time", Value.from_any(value))
+ return self
+
+ def update_version(self, value):
+ self.version = value
+ self._loaded_fields.add("version")
+ self._entity_root.set(self._teaql_entity_key(), "version", Value.from_any(value))
+ return self
+ def school_type_list(self) -> list:
+ self._loaded_fields.add("school_type_list")
+ return self._school_type_list
+
+ def school_list(self) -> list:
+ self._loaded_fields.add("school_list")
+ return self._school_list
diff --git a/examples/facet-trace/lib/models/school.py b/examples/facet-trace/lib/models/school.py
new file mode 100644
index 0000000..a2785bb
--- /dev/null
+++ b/examples/facet-trace/lib/models/school.py
@@ -0,0 +1,493 @@
+from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot, _QueryProjectionSnapshot
+from teaql.core.list import SmartList
+from teaql.core import MutationIntent
+from teaql.core.value import Value
+from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
+import itertools
+from models.platform import Platform
+from models.school_type import SchoolType
+
+
+class School:
+ _teaql_temporary_ids = itertools.count(1)
+ @classmethod
+ def refer(cls, entity_id):
+ return cls(id=entity_id)
+
+ @classmethod
+ def _teaql_new_with_fixed_id(cls, entity_id):
+ """Generated bootstrap capability; application code must not call it."""
+ return cls(id=entity_id)._teaql_force_create()
+
+ def _teaql_force_create(self):
+ self._action = "Create"
+ self._entity_root.mark_as_new(self._teaql_entity_key())
+ return self
+
+ def __init__(self, **kwargs):
+ self._entity_root = kwargs.pop("_entity_root", None) or EntityRoot()
+ if "id" in kwargs and "id" not in kwargs:
+ kwargs["id"] = kwargs.pop("id")
+ if "platform" in kwargs and "platform" not in kwargs:
+ kwargs["platform"] = kwargs.pop("platform")
+ if "school_type" in kwargs and "schoolType" not in kwargs:
+ kwargs["schoolType"] = kwargs.pop("school_type")
+ if "name" in kwargs and "name" not in kwargs:
+ kwargs["name"] = kwargs.pop("name")
+ if "address" in kwargs and "address" not in kwargs:
+ kwargs["address"] = kwargs.pop("address")
+ if "established_date" in kwargs and "establishedDate" not in kwargs:
+ kwargs["establishedDate"] = kwargs.pop("established_date")
+ if "student_capacity" in kwargs and "studentCapacity" not in kwargs:
+ kwargs["studentCapacity"] = kwargs.pop("student_capacity")
+ if "active" in kwargs and "active" not in kwargs:
+ kwargs["active"] = kwargs.pop("active")
+ if "create_time" in kwargs and "createTime" not in kwargs:
+ kwargs["createTime"] = kwargs.pop("create_time")
+ if "update_time" in kwargs and "updateTime" not in kwargs:
+ kwargs["updateTime"] = kwargs.pop("update_time")
+ if "version" in kwargs and "version" not in kwargs:
+ kwargs["version"] = kwargs.pop("version")
+ self._teaql_query_projections = _QueryProjectionSnapshot(kwargs, (
+ "id", "id",
+ "platform", "platform",
+ "schoolType", "school_type",
+ "name", "name",
+ "address", "address",
+ "establishedDate", "established_date",
+ "studentCapacity", "student_capacity",
+ "active", "active",
+ "createTime", "create_time",
+ "updateTime", "update_time",
+ "version", "version",
+ ))
+ self._action = "Update" if kwargs.get("id") else "Create"
+ self._comment = None
+ self._loaded_fields = set(kwargs.keys())
+ self.id = kwargs.get("id")
+
+ self.platform = kwargs.get("platform")
+
+ self.schoolType = kwargs.get("schoolType")
+
+ self.name = kwargs.get("name")
+
+ self.address = kwargs.get("address")
+
+ self.establishedDate = kwargs.get("establishedDate")
+
+ self.studentCapacity = kwargs.get("studentCapacity")
+
+ self.active = kwargs.get("active")
+
+ self.createTime = kwargs.get("createTime")
+
+ self.updateTime = kwargs.get("updateTime")
+
+ self.version = kwargs.get("version")
+
+ if isinstance(self.platform, dict):
+ self.platform = Platform(**self.platform)
+ if isinstance(self.schoolType, dict):
+ self.schoolType = SchoolType(**self.schoolType)
+ self._ledger_id = getattr(self, "id", None)
+ if self._ledger_id is None:
+ self._ledger_id = -next(self._teaql_temporary_ids)
+ key = self._teaql_entity_key()
+ if self._action == "Create":
+ self._entity_root.mark_as_new(key)
+ elif getattr(self, "version", None) is not None:
+ self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
+
+ def query_projection(self, alias):
+ """Read a query-only alias; raises KeyError when it was not returned."""
+ return self._teaql_query_projections.get(alias)
+
+ def has_query_projection(self, alias):
+ return self._teaql_query_projections.contains(alias)
+
+ def _teaql_entity_key(self):
+ return EntityKey("School", self._ledger_id)
+
+ def _teaql_attach_root(self, root):
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
+ self._entity_root = root
+ return self
+ def mark_for_deletion(self):
+ self._action = "Delete"
+ self._entity_root.mark_as_deleted(self._teaql_entity_key())
+ return self
+
+ def audit_as(self, comment: str):
+ MutationIntent(comment)
+ self._comment = comment
+ return self
+
+ async def save(self, context):
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
+
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
+
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("School"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
+ payload = {}
+ if "id" in self._loaded_fields:
+ payload["id"] = Value.I64(self.id)
+
+ if "platform" in self._loaded_fields:
+ reference = self.platform
+ reference_id = getattr(reference, "id", reference)
+ payload["platform"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
+ if "schoolType" in self._loaded_fields:
+ reference = self.schoolType
+ reference_id = getattr(reference, "id", reference)
+ payload["school_type"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
+ if "name" in self._loaded_fields:
+ payload["name"] = Value.Text(self.name)
+
+ if "address" in self._loaded_fields:
+ payload["address"] = Value.Text(self.address)
+
+ if "establishedDate" in self._loaded_fields:
+ payload["established_date"] = Value.Date(self.establishedDate)
+
+ if "studentCapacity" in self._loaded_fields:
+ payload["student_capacity"] = Value.I64(self.studentCapacity)
+
+ if "active" in self._loaded_fields:
+ payload["active"] = Value.Bool(self.active)
+
+ if "createTime" in self._loaded_fields:
+ payload["create_time"] = Value.DateTime(self.createTime)
+
+ if "updateTime" in self._loaded_fields:
+ payload["update_time"] = Value.DateTime(self.updateTime)
+
+ if "version" in self._loaded_fields:
+ payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
+ action = self._action
+ if action == "Update":
+ ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
+ payload = {field: value for field, value in ledger.items() if field not in ("id", "version")}
+ if action == "Create":
+ cmd = InsertCommand("School", payload)
+ elif action == "Update":
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("School", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ for key, value in payload.items():
+ if key not in ("id", "version"): cmd.value(key, value)
+ else:
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("School", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
+ return action, cmd
+
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "platform" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("platform"), message="Mutation requires a fully loaded entity")])
+ if "schoolType" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("school_type"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "address" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("address"), message="Mutation requires a fully loaded entity")])
+ if "establishedDate" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("established_date"), message="Mutation requires a fully loaded entity")])
+ if "studentCapacity" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("student_capacity"), message="Mutation requires a fully loaded entity")])
+ if "active" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("active"), message="Mutation requires a fully loaded entity")])
+ if "createTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
+ if "updateTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
+
+ self._teaql_attach_root(self._entity_root)
+ action, cmd = self._teaql_build_command()
+
+
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
+
+ try:
+ context.check_and_fix_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+ context.mark_mutation_checked(cmd)
+ service = context.require_resource("dataService")
+ result = await service.mutate(context, req)
+ persisted = result.persisted_record
+ if persisted is None:
+ raise RuntimeError(
+ "Mutation provider did not return authoritative persisted state for School"
+ )
+ rollback_payload = {field: getattr(self, field, None) for field in self._loaded_fields | {"id", "version"}}
+ rollback_ledger_id = self._ledger_id
+ rollback_action = self._action
+ rollback_loaded_fields = set(self._loaded_fields)
+ old_key = self._teaql_entity_key()
+ if "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ elif "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ if "platform" in persisted:
+ self.platform = persisted["platform"]
+ self._loaded_fields.add("platform")
+ elif "platform" in persisted:
+ self.platform = persisted["platform"]
+ self._loaded_fields.add("platform")
+ if "school_type" in persisted:
+ self.schoolType = persisted["school_type"]
+ self._loaded_fields.add("schoolType")
+ elif "schoolType" in persisted:
+ self.schoolType = persisted["schoolType"]
+ self._loaded_fields.add("schoolType")
+ if "name" in persisted:
+ self.name = persisted["name"]
+ self._loaded_fields.add("name")
+ elif "name" in persisted:
+ self.name = persisted["name"]
+ self._loaded_fields.add("name")
+ if "address" in persisted:
+ self.address = persisted["address"]
+ self._loaded_fields.add("address")
+ elif "address" in persisted:
+ self.address = persisted["address"]
+ self._loaded_fields.add("address")
+ if "established_date" in persisted:
+ self.establishedDate = persisted["established_date"]
+ self._loaded_fields.add("establishedDate")
+ elif "establishedDate" in persisted:
+ self.establishedDate = persisted["establishedDate"]
+ self._loaded_fields.add("establishedDate")
+ if "student_capacity" in persisted:
+ self.studentCapacity = persisted["student_capacity"]
+ self._loaded_fields.add("studentCapacity")
+ elif "studentCapacity" in persisted:
+ self.studentCapacity = persisted["studentCapacity"]
+ self._loaded_fields.add("studentCapacity")
+ if "active" in persisted:
+ self.active = persisted["active"]
+ self._loaded_fields.add("active")
+ elif "active" in persisted:
+ self.active = persisted["active"]
+ self._loaded_fields.add("active")
+ if "create_time" in persisted:
+ self.createTime = persisted["create_time"]
+ self._loaded_fields.add("createTime")
+ elif "createTime" in persisted:
+ self.createTime = persisted["createTime"]
+ self._loaded_fields.add("createTime")
+ if "update_time" in persisted:
+ self.updateTime = persisted["update_time"]
+ self._loaded_fields.add("updateTime")
+ elif "updateTime" in persisted:
+ self.updateTime = persisted["updateTime"]
+ self._loaded_fields.add("updateTime")
+ if "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ elif "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
+ self._ledger_id = getattr(self, "id", self._ledger_id)
+ new_key = self._teaql_entity_key()
+ if old_key != new_key:
+ self._entity_root.rekey(old_key, new_key)
+ def rollback_entity():
+ for field, value in rollback_payload.items():
+ setattr(self, field, value)
+ self._ledger_id = rollback_ledger_id
+ self._action = rollback_action
+ self._loaded_fields = rollback_loaded_fields
+ if old_key != new_key:
+ self._entity_root.rekey(new_key, old_key)
+ graph.after_rollback(rollback_entity)
+ if action != "Delete":
+ self._action = "Update"
+
+ if action != "Delete":
+ await self._teaql_save_children(graph, scope)
+ def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
+ self._entity_root.clear_entity(new_key)
+ if getattr(self, "version", None) is not None:
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
+ return self
+
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
+ def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
+ self.id = value
+ self._loaded_fields.add("id")
+ self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
+ return self
+
+ def update_name(self, value):
+ self.name = value
+ self._loaded_fields.add("name")
+ self._entity_root.set(self._teaql_entity_key(), "name", Value.from_any(value))
+ return self
+
+ def update_address(self, value):
+ self.address = value
+ self._loaded_fields.add("address")
+ self._entity_root.set(self._teaql_entity_key(), "address", Value.from_any(value))
+ return self
+
+ def update_established_date(self, value):
+ self.establishedDate = value
+ self._loaded_fields.add("establishedDate")
+ self._entity_root.set(self._teaql_entity_key(), "established_date", Value.from_any(value))
+ return self
+
+ def update_student_capacity(self, value):
+ self.studentCapacity = value
+ self._loaded_fields.add("studentCapacity")
+ self._entity_root.set(self._teaql_entity_key(), "student_capacity", Value.from_any(value))
+ return self
+
+ def update_active(self, value):
+ self.active = value
+ self._loaded_fields.add("active")
+ self._entity_root.set(self._teaql_entity_key(), "active", Value.from_any(value))
+ return self
+
+ def update_create_time(self, value):
+ self.createTime = value
+ self._loaded_fields.add("createTime")
+ self._entity_root.set(self._teaql_entity_key(), "create_time", Value.from_any(value))
+ return self
+
+ def update_update_time(self, value):
+ self.updateTime = value
+ self._loaded_fields.add("updateTime")
+ self._entity_root.set(self._teaql_entity_key(), "update_time", Value.from_any(value))
+ return self
+
+ def update_version(self, value):
+ self.version = value
+ self._loaded_fields.add("version")
+ self._entity_root.set(self._teaql_entity_key(), "version", Value.from_any(value))
+ return self
+ def update_platform(self, value):
+ self.platform = getattr(value, "id", value) if value else None
+ self._loaded_fields.add("platform")
+ self._entity_root.set(self._teaql_entity_key(), "platform", Value.from_any(self.platform))
+ return self
+
+
+ def update_school_type(self, value):
+ self.schoolType = getattr(value, "id", value) if value else None
+ self._loaded_fields.add("schoolType")
+ self._entity_root.set(self._teaql_entity_key(), "school_type", Value.from_any(self.schoolType))
+ return self
+ def update_school_type_to_primary(self):
+ self.schoolType = 1001
+ self._loaded_fields.add("schoolType")
+ self._entity_root.set(self._teaql_entity_key(), "school_type", Value.from_any(self.schoolType))
+ return self
diff --git a/examples/facet-trace/lib/models/school_type.py b/examples/facet-trace/lib/models/school_type.py
new file mode 100644
index 0000000..01cfe88
--- /dev/null
+++ b/examples/facet-trace/lib/models/school_type.py
@@ -0,0 +1,406 @@
+from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot, _QueryProjectionSnapshot
+from teaql.core.list import SmartList
+from teaql.core import MutationIntent
+from teaql.core.value import Value
+from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
+import itertools
+from models.platform import Platform
+
+
+class SchoolType:
+ _teaql_temporary_ids = itertools.count(1)
+ @classmethod
+ def refer(cls, entity_id):
+ return cls(id=entity_id)
+
+ @classmethod
+ def _teaql_new_with_fixed_id(cls, entity_id):
+ """Generated bootstrap capability; application code must not call it."""
+ return cls(id=entity_id)._teaql_force_create()
+
+ def _teaql_force_create(self):
+ self._action = "Create"
+ self._entity_root.mark_as_new(self._teaql_entity_key())
+ return self
+
+ def __init__(self, **kwargs):
+ self._entity_root = kwargs.pop("_entity_root", None) or EntityRoot()
+ if "platform" in kwargs and "platform" not in kwargs:
+ kwargs["platform"] = kwargs.pop("platform")
+ if "id" in kwargs and "id" not in kwargs:
+ kwargs["id"] = kwargs.pop("id")
+ if "name" in kwargs and "name" not in kwargs:
+ kwargs["name"] = kwargs.pop("name")
+ if "code" in kwargs and "code" not in kwargs:
+ kwargs["code"] = kwargs.pop("code")
+ if "display_order" in kwargs and "displayOrder" not in kwargs:
+ kwargs["displayOrder"] = kwargs.pop("display_order")
+ if "version" in kwargs and "version" not in kwargs:
+ kwargs["version"] = kwargs.pop("version")
+ self._teaql_query_projections = _QueryProjectionSnapshot(kwargs, (
+ "platform", "platform",
+ "id", "id",
+ "name", "name",
+ "code", "code",
+ "displayOrder", "display_order",
+ "version", "version",
+ "school_list",
+ ))
+ self._action = "Update" if kwargs.get("id") else "Create"
+ self._comment = None
+ self._loaded_fields = set(kwargs.keys())
+ self.platform = kwargs.get("platform")
+
+ self.id = kwargs.get("id")
+
+ self.name = kwargs.get("name")
+
+ self.code = kwargs.get("code")
+
+ self.displayOrder = kwargs.get("displayOrder")
+
+ self.version = kwargs.get("version")
+
+ if isinstance(self.platform, dict):
+ self.platform = Platform(**self.platform)
+ self._school_list = kwargs.get("school_list", [])
+ if "school_list" in kwargs or kwargs.get("id") is None:
+ self._loaded_fields.add("school_list")
+ if self._school_list:
+ from models.school import School
+ source = self._school_list
+ hydrated = [
+ item if isinstance(item, School) else School(_entity_root=self._entity_root, **item)
+ for item in source
+ ]
+ self._school_list = (SmartList(hydrated, facets=source.facets, total_count=source.total_count)
+ if isinstance(source, SmartList) else hydrated)
+ self._ledger_id = getattr(self, "id", None)
+ if self._ledger_id is None:
+ self._ledger_id = -next(self._teaql_temporary_ids)
+ key = self._teaql_entity_key()
+ if self._action == "Create":
+ self._entity_root.mark_as_new(key)
+ elif getattr(self, "version", None) is not None:
+ self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
+
+ def query_projection(self, alias):
+ """Read a query-only alias; raises KeyError when it was not returned."""
+ return self._teaql_query_projections.get(alias)
+
+ def has_query_projection(self, alias):
+ return self._teaql_query_projections.contains(alias)
+
+ def _teaql_entity_key(self):
+ return EntityKey("SchoolType", self._ledger_id)
+
+ def _teaql_attach_root(self, root):
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
+ self._entity_root = root
+ for child in self._school_list:
+ child._teaql_attach_root(root)
+ return self
+ def mark_for_deletion(self):
+ self._action = "Delete"
+ self._entity_root.mark_as_deleted(self._teaql_entity_key())
+ return self
+
+ def audit_as(self, comment: str):
+ MutationIntent(comment)
+ self._comment = comment
+ return self
+
+ async def save(self, context):
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
+
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
+
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("SchoolType"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._school_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
+ payload = {}
+ if "platform" in self._loaded_fields:
+ reference = self.platform
+ reference_id = getattr(reference, "id", reference)
+ payload["platform"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
+ if "id" in self._loaded_fields:
+ payload["id"] = Value.I64(self.id)
+
+ if "name" in self._loaded_fields:
+ payload["name"] = Value.Text(self.name)
+
+ if "code" in self._loaded_fields:
+ payload["code"] = Value.Text(self.code)
+
+ if "displayOrder" in self._loaded_fields:
+ payload["display_order"] = Value.Decimal(self.displayOrder)
+
+ if "version" in self._loaded_fields:
+ payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
+ action = self._action
+ if action == "Update":
+ ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
+ payload = {field: value for field, value in ledger.items() if field not in ("id", "version")}
+ if action == "Create":
+ cmd = InsertCommand("SchoolType", payload)
+ elif action == "Update":
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("SchoolType", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ for key, value in payload.items():
+ if key not in ("id", "version"): cmd.value(key, value)
+ else:
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("SchoolType", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
+ return action, cmd
+
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "platform" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("platform"), message="Mutation requires a fully loaded entity")])
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "code" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("code"), message="Mutation requires a fully loaded entity")])
+ if "displayOrder" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("display_order"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+ for index, child in enumerate(self._school_list):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, "schoolType", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_school_type(self)
+ try:
+ child._teaql_preflight_graph(graph)
+ except CheckException as error:
+ prefix = ObjectLocation().property("school_list").index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
+
+ self._teaql_attach_root(self._entity_root)
+ action, cmd = self._teaql_build_command()
+
+
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
+
+ try:
+ context.check_and_fix_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+ context.mark_mutation_checked(cmd)
+ service = context.require_resource("dataService")
+ result = await service.mutate(context, req)
+ persisted = result.persisted_record
+ if persisted is None:
+ raise RuntimeError(
+ "Mutation provider did not return authoritative persisted state for SchoolType"
+ )
+ rollback_payload = {field: getattr(self, field, None) for field in self._loaded_fields | {"id", "version"}}
+ rollback_ledger_id = self._ledger_id
+ rollback_action = self._action
+ rollback_loaded_fields = set(self._loaded_fields)
+ old_key = self._teaql_entity_key()
+ if "platform" in persisted:
+ self.platform = persisted["platform"]
+ self._loaded_fields.add("platform")
+ elif "platform" in persisted:
+ self.platform = persisted["platform"]
+ self._loaded_fields.add("platform")
+ if "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ elif "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ if "name" in persisted:
+ self.name = persisted["name"]
+ self._loaded_fields.add("name")
+ elif "name" in persisted:
+ self.name = persisted["name"]
+ self._loaded_fields.add("name")
+ if "code" in persisted:
+ self.code = persisted["code"]
+ self._loaded_fields.add("code")
+ elif "code" in persisted:
+ self.code = persisted["code"]
+ self._loaded_fields.add("code")
+ if "display_order" in persisted:
+ self.displayOrder = persisted["display_order"]
+ self._loaded_fields.add("displayOrder")
+ elif "displayOrder" in persisted:
+ self.displayOrder = persisted["displayOrder"]
+ self._loaded_fields.add("displayOrder")
+ if "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ elif "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
+ self._ledger_id = getattr(self, "id", self._ledger_id)
+ new_key = self._teaql_entity_key()
+ if old_key != new_key:
+ self._entity_root.rekey(old_key, new_key)
+ def rollback_entity():
+ for field, value in rollback_payload.items():
+ setattr(self, field, value)
+ self._ledger_id = rollback_ledger_id
+ self._action = rollback_action
+ self._loaded_fields = rollback_loaded_fields
+ if old_key != new_key:
+ self._entity_root.rekey(new_key, old_key)
+ graph.after_rollback(rollback_entity)
+ if action != "Delete":
+ self._action = "Update"
+
+ if action != "Delete":
+ await self._teaql_save_children(graph, scope)
+ def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
+ self._entity_root.clear_entity(new_key)
+ if getattr(self, "version", None) is not None:
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
+ return self
+
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("school_list", self._school_list, "update_school_type", "schoolType"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
+ def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
+ self.id = value
+ self._loaded_fields.add("id")
+ self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
+ return self
+
+ def update_name(self, value):
+ self.name = value
+ self._loaded_fields.add("name")
+ self._entity_root.set(self._teaql_entity_key(), "name", Value.from_any(value))
+ return self
+
+ def update_code(self, value):
+ self.code = value
+ self._loaded_fields.add("code")
+ self._entity_root.set(self._teaql_entity_key(), "code", Value.from_any(value))
+ return self
+
+ def update_display_order(self, value):
+ self.displayOrder = value
+ self._loaded_fields.add("displayOrder")
+ self._entity_root.set(self._teaql_entity_key(), "display_order", Value.from_any(value))
+ return self
+
+ def update_version(self, value):
+ self.version = value
+ self._loaded_fields.add("version")
+ self._entity_root.set(self._teaql_entity_key(), "version", Value.from_any(value))
+ return self
+ def update_platform(self, value):
+ self.platform = getattr(value, "id", value) if value else None
+ self._loaded_fields.add("platform")
+ self._entity_root.set(self._teaql_entity_key(), "platform", Value.from_any(self.platform))
+ return self
+
+ def school_list(self) -> list:
+ self._loaded_fields.add("school_list")
+ return self._school_list
diff --git a/examples/facet-trace/lib/pyproject.toml b/examples/facet-trace/lib/pyproject.toml
new file mode 100644
index 0000000..062f286
--- /dev/null
+++ b/examples/facet-trace/lib/pyproject.toml
@@ -0,0 +1,16 @@
+[project]
+name = "school-management-service-lib"
+version = "1.0.0"
+description = "Generated python library"
+dependencies = ["teaql==0.2.9", "aiosqlite>=0.22.1"]
+
+[tool.setuptools]
+py-modules = ["Q", "E"]
+
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["models*", "requests*"]
+
+[build-system]
+requires = ["setuptools>=42"]
+build-backend = "setuptools.build_meta"
\ No newline at end of file
diff --git a/examples/facet-trace/lib/requests/__init__.py b/examples/facet-trace/lib/requests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/facet-trace/lib/requests/platform_request.py b/examples/facet-trace/lib/requests/platform_request.py
new file mode 100644
index 0000000..d07370d
--- /dev/null
+++ b/examples/facet-trace/lib/requests/platform_request.py
@@ -0,0 +1,858 @@
+from teaql.core.query import RelationAggregate, SelectQuery
+from teaql.core.list import SmartList, TeaQLPage
+from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
+from teaql.core.expr import (
+ begin_with, between, column, contain, end_with, eq, gt, gte,
+ in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
+ not_contain, not_end_with, not_in_list, not_in_subquery, value,
+ sound_like,
+)
+from models.platform import Platform
+from typing import Protocol
+
+class QuerySelection(Protocol):
+ query: SelectQuery
+
+class PlatformRequest:
+ def __init__(self, minimal=False):
+ self.query = SelectQuery("Platform")
+ self._purpose = None
+ self._comment = None
+ self.query.and_filter(gte("version", 1))
+ if minimal:
+ self.select_id()
+ self.select_version()
+ else:
+ self.select_self_fields()
+
+ def comment(self, c: str):
+ self.query.comment(c)
+ self._comment = c
+ return self
+
+ def purpose(self, p: str):
+ self.query.purpose(p)
+ self._purpose = p
+ return ExecutablePlatformRequest(self)
+
+ def optimize_for_continuous_page_fetch(self):
+ self.query.optimize_for_continuous_page_fetch()
+ return self
+
+ def optimize_for_continuous_page_fetch_with(self, namespace: str, ttl_seconds: int):
+ self.query.optimize_for_continuous_page_fetch_with(namespace, ttl_seconds)
+ return self
+
+ def optimize_pagination_with_id_set(self):
+ self.query.optimize_pagination_with_id_set()
+ return self
+
+ def optimize_pagination_with_id_set_config(self, namespace: str, ttl_seconds: int, max_ids: int):
+ self.query.optimize_pagination_with_id_set_config(namespace, ttl_seconds, max_ids)
+ return self
+
+ def top_n_probe_parent_threshold(self, threshold: int):
+ self.query.top_n_probe_parent_threshold(threshold)
+ return self
+
+ def limit(self, n: int):
+ self.query.limit(n)
+ return self
+
+ def offset(self, n: int):
+ self.query.offset(n)
+ return self
+
+ def with_deleted_rows(self):
+ self.query.with_deleted_rows()
+ return self
+
+ def deleted_rows_only(self):
+ self.query.deleted_rows_only()
+ return self
+
+ def select_self_fields(self):
+ self.query.project("id", "name", "base_url", "create_time", "update_time", "version")
+ return self
+
+ def select_id(self):
+ self.query.project("id")
+ return self
+
+ def select_name(self):
+ self.query.project("name")
+ return self
+
+ def select_base_url(self):
+ self.query.project("base_url")
+ return self
+
+ def select_create_time(self):
+ self.query.project("create_time")
+ return self
+
+ def select_update_time(self):
+ self.query.project("update_time")
+ return self
+
+ def select_version(self):
+ self.query.project("version")
+ return self
+
+
+ def with_id_is(self, val):
+ self.query.and_filter(eq("id", val))
+ return self
+
+ def with_id_is_not(self, val):
+ self.query.and_filter(ne("id", val))
+ return self
+
+ def with_id_in(self, *vals):
+ self.query.and_filter(in_list("id", list(vals)))
+ return self
+
+ def with_id_not_in(self, *vals):
+ self.query.and_filter(not_in_list("id", list(vals)))
+ return self
+
+ def with_id_greater_than(self, val):
+ self.query.and_filter(gt("id", val))
+ return self
+
+ def with_id_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("id", val))
+ return self
+
+ def with_id_less_than(self, val):
+ self.query.and_filter(lt("id", val))
+ return self
+
+ def with_id_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("id", val))
+ return self
+
+ def with_id_between(self, lower, upper):
+ self.query.and_filter(between(column("id"), value(lower), value(upper)))
+ return self
+
+ def with_id_is_known(self):
+ self.query.and_filter(is_not_null(column("id")))
+ return self
+
+ def with_id_is_unknown(self):
+ self.query.and_filter(is_null(column("id")))
+ return self
+
+ def with_name_containing(self, val: str):
+ self.query.and_filter(contain("name", val))
+ return self
+
+ def with_name_not_containing(self, val: str):
+ self.query.and_filter(not_contain("name", val))
+ return self
+
+ def with_name_starting_with(self, val: str):
+ self.query.and_filter(begin_with("name", val))
+ return self
+
+ def with_name_not_starting_with(self, val: str):
+ self.query.and_filter(not_begin_with("name", val))
+ return self
+
+ def with_name_ending_with(self, val: str):
+ self.query.and_filter(end_with("name", val))
+ return self
+
+ def with_name_not_ending_with(self, val: str):
+ self.query.and_filter(not_end_with("name", val))
+ return self
+
+ def with_name_sounding_like(self, val: str):
+ self.query.and_filter(sound_like("name", val))
+ return self
+
+ def with_name_is(self, val: str):
+ self.query.and_filter(eq("name", val))
+ return self
+ def with_name_is_not(self, val):
+ self.query.and_filter(ne("name", val))
+ return self
+
+ def with_name_in(self, *vals):
+ self.query.and_filter(in_list("name", list(vals)))
+ return self
+
+ def with_name_not_in(self, *vals):
+ self.query.and_filter(not_in_list("name", list(vals)))
+ return self
+
+ def with_name_greater_than(self, val):
+ self.query.and_filter(gt("name", val))
+ return self
+
+ def with_name_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("name", val))
+ return self
+
+ def with_name_less_than(self, val):
+ self.query.and_filter(lt("name", val))
+ return self
+
+ def with_name_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("name", val))
+ return self
+
+ def with_name_between(self, lower, upper):
+ self.query.and_filter(between(column("name"), value(lower), value(upper)))
+ return self
+
+ def with_name_is_known(self):
+ self.query.and_filter(is_not_null(column("name")))
+ return self
+
+ def with_name_is_unknown(self):
+ self.query.and_filter(is_null(column("name")))
+ return self
+
+ def with_base_url_containing(self, val: str):
+ self.query.and_filter(contain("base_url", val))
+ return self
+
+ def with_base_url_not_containing(self, val: str):
+ self.query.and_filter(not_contain("base_url", val))
+ return self
+
+ def with_base_url_starting_with(self, val: str):
+ self.query.and_filter(begin_with("base_url", val))
+ return self
+
+ def with_base_url_not_starting_with(self, val: str):
+ self.query.and_filter(not_begin_with("base_url", val))
+ return self
+
+ def with_base_url_ending_with(self, val: str):
+ self.query.and_filter(end_with("base_url", val))
+ return self
+
+ def with_base_url_not_ending_with(self, val: str):
+ self.query.and_filter(not_end_with("base_url", val))
+ return self
+
+ def with_base_url_sounding_like(self, val: str):
+ self.query.and_filter(sound_like("base_url", val))
+ return self
+
+ def with_base_url_is(self, val: str):
+ self.query.and_filter(eq("base_url", val))
+ return self
+ def with_base_url_is_not(self, val):
+ self.query.and_filter(ne("base_url", val))
+ return self
+
+ def with_base_url_in(self, *vals):
+ self.query.and_filter(in_list("base_url", list(vals)))
+ return self
+
+ def with_base_url_not_in(self, *vals):
+ self.query.and_filter(not_in_list("base_url", list(vals)))
+ return self
+
+ def with_base_url_greater_than(self, val):
+ self.query.and_filter(gt("base_url", val))
+ return self
+
+ def with_base_url_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("base_url", val))
+ return self
+
+ def with_base_url_less_than(self, val):
+ self.query.and_filter(lt("base_url", val))
+ return self
+
+ def with_base_url_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("base_url", val))
+ return self
+
+ def with_base_url_between(self, lower, upper):
+ self.query.and_filter(between(column("base_url"), value(lower), value(upper)))
+ return self
+
+ def with_base_url_is_known(self):
+ self.query.and_filter(is_not_null(column("base_url")))
+ return self
+
+ def with_base_url_is_unknown(self):
+ self.query.and_filter(is_null(column("base_url")))
+ return self
+
+ def with_create_time_is(self, val):
+ self.query.and_filter(eq("create_time", val))
+ return self
+
+ def with_create_time_is_not(self, val):
+ self.query.and_filter(ne("create_time", val))
+ return self
+
+ def with_create_time_in(self, *vals):
+ self.query.and_filter(in_list("create_time", list(vals)))
+ return self
+
+ def with_create_time_not_in(self, *vals):
+ self.query.and_filter(not_in_list("create_time", list(vals)))
+ return self
+
+ def with_create_time_greater_than(self, val):
+ self.query.and_filter(gt("create_time", val))
+ return self
+
+ def with_create_time_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("create_time", val))
+ return self
+
+ def with_create_time_less_than(self, val):
+ self.query.and_filter(lt("create_time", val))
+ return self
+
+ def with_create_time_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("create_time", val))
+ return self
+
+ def with_create_time_between(self, lower, upper):
+ self.query.and_filter(between(column("create_time"), value(lower), value(upper)))
+ return self
+
+ def with_create_time_is_known(self):
+ self.query.and_filter(is_not_null(column("create_time")))
+ return self
+
+ def with_create_time_is_unknown(self):
+ self.query.and_filter(is_null(column("create_time")))
+ return self
+
+ def with_update_time_is(self, val):
+ self.query.and_filter(eq("update_time", val))
+ return self
+
+ def with_update_time_is_not(self, val):
+ self.query.and_filter(ne("update_time", val))
+ return self
+
+ def with_update_time_in(self, *vals):
+ self.query.and_filter(in_list("update_time", list(vals)))
+ return self
+
+ def with_update_time_not_in(self, *vals):
+ self.query.and_filter(not_in_list("update_time", list(vals)))
+ return self
+
+ def with_update_time_greater_than(self, val):
+ self.query.and_filter(gt("update_time", val))
+ return self
+
+ def with_update_time_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("update_time", val))
+ return self
+
+ def with_update_time_less_than(self, val):
+ self.query.and_filter(lt("update_time", val))
+ return self
+
+ def with_update_time_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("update_time", val))
+ return self
+
+ def with_update_time_between(self, lower, upper):
+ self.query.and_filter(between(column("update_time"), value(lower), value(upper)))
+ return self
+
+ def with_update_time_is_known(self):
+ self.query.and_filter(is_not_null(column("update_time")))
+ return self
+
+ def with_update_time_is_unknown(self):
+ self.query.and_filter(is_null(column("update_time")))
+ return self
+
+ def with_version_is(self, val):
+ self.query.and_filter(eq("version", val))
+ return self
+
+ def with_version_is_not(self, val):
+ self.query.and_filter(ne("version", val))
+ return self
+
+ def with_version_in(self, *vals):
+ self.query.and_filter(in_list("version", list(vals)))
+ return self
+
+ def with_version_not_in(self, *vals):
+ self.query.and_filter(not_in_list("version", list(vals)))
+ return self
+
+ def with_version_greater_than(self, val):
+ self.query.and_filter(gt("version", val))
+ return self
+
+ def with_version_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("version", val))
+ return self
+
+ def with_version_less_than(self, val):
+ self.query.and_filter(lt("version", val))
+ return self
+
+ def with_version_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("version", val))
+ return self
+
+ def with_version_between(self, lower, upper):
+ self.query.and_filter(between(column("version"), value(lower), value(upper)))
+ return self
+
+ def with_version_is_known(self):
+ self.query.and_filter(is_not_null(column("version")))
+ return self
+
+ def with_version_is_unknown(self):
+ self.query.and_filter(is_null(column("version")))
+ return self
+
+ def order_by_id_ascending(self):
+ self.query.order_by("id", "asc")
+ return self
+
+ def order_by_id_descending(self):
+ self.query.order_by("id", "desc")
+ return self
+
+ def order_by_name_ascending(self):
+ self.query.order_by("name", "asc")
+ return self
+
+ def order_by_name_descending(self):
+ self.query.order_by("name", "desc")
+ return self
+
+ def order_by_base_url_ascending(self):
+ self.query.order_by("base_url", "asc")
+ return self
+
+ def order_by_base_url_descending(self):
+ self.query.order_by("base_url", "desc")
+ return self
+
+ def order_by_create_time_ascending(self):
+ self.query.order_by("create_time", "asc")
+ return self
+
+ def order_by_create_time_descending(self):
+ self.query.order_by("create_time", "desc")
+ return self
+
+ def order_by_update_time_ascending(self):
+ self.query.order_by("update_time", "asc")
+ return self
+
+ def order_by_update_time_descending(self):
+ self.query.order_by("update_time", "desc")
+ return self
+
+ def order_by_version_ascending(self):
+ self.query.order_by("version", "asc")
+ return self
+
+ def order_by_version_descending(self):
+ self.query.order_by("version", "desc")
+ return self
+
+
+ def count(self):
+ self.query.count_field("id", "count")
+ return self
+
+ def count_as(self, ret_name: str):
+ self.query.count_field("id", ret_name)
+ return self
+
+ def group_by_id(self):
+ self.query.group_by("id")
+ return self
+
+ def group_by_id_as(self, ret_name: str):
+ self.query.group_by("id")
+ return self
+ def group_by_name(self):
+ self.query.group_by("name")
+ return self
+
+ def group_by_name_as(self, ret_name: str):
+ self.query.group_by("name")
+ return self
+ def group_by_base_url(self):
+ self.query.group_by("base_url")
+ return self
+
+ def group_by_base_url_as(self, ret_name: str):
+ self.query.group_by("base_url")
+ return self
+ def group_by_create_time(self):
+ self.query.group_by("create_time")
+ return self
+
+ def group_by_create_time_as(self, ret_name: str):
+ self.query.group_by("create_time")
+ return self
+ def group_by_update_time(self):
+ self.query.group_by("update_time")
+ return self
+
+ def group_by_update_time_as(self, ret_name: str):
+ self.query.group_by("update_time")
+ return self
+ def group_by_version(self):
+ self.query.group_by("version")
+ return self
+
+ def group_by_version_as(self, ret_name: str):
+ self.query.group_by("version")
+ return self
+ def select_school_type_list(self):
+ from requests.school_type_request import SchoolTypeRequest
+ return self.select_school_type_list_with(SchoolTypeRequest())
+
+ def select_school_type_list_with(self, child_request):
+ self.query.relation_query("school_type_list", child_request.query)
+ return self
+ def select_school_list(self):
+ from requests.school_request import SchoolRequest
+ return self.select_school_list_with(SchoolRequest())
+
+ def select_school_list_with(self, child_request):
+ self.query.relation_query("school_list", child_request.query)
+ return self
+ def have_school_types(self):
+ from requests.school_type_request import SchoolTypeRequest
+ return self.with_school_type_list_matching(SchoolTypeRequest())
+
+ def have_no_school_types(self):
+ from requests.school_type_request import SchoolTypeRequest
+ return self.without_school_type_list_matching(SchoolTypeRequest())
+
+ def with_school_type_list_matching(self, child_request):
+ child_request.query.projection = ["platform"]
+ self.query.and_filter(in_subquery(column("id"), "SchoolType", child_request.query))
+ return self
+
+ def without_school_type_list_matching(self, child_request):
+ child_request.query.projection = ["platform"]
+ self.query.and_filter(not_in_subquery(column("id"), "SchoolType", child_request.query))
+ return self
+ def have_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.with_school_list_matching(SchoolRequest())
+
+ def have_no_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.without_school_list_matching(SchoolRequest())
+
+ def with_school_list_matching(self, child_request):
+ child_request.query.projection = ["platform"]
+ self.query.and_filter(in_subquery(column("id"), "School", child_request.query))
+ return self
+
+ def without_school_list_matching(self, child_request):
+ child_request.query.projection = ["platform"]
+ self.query.and_filter(not_in_subquery(column("id"), "School", child_request.query))
+ return self
+ def count_school_types(self):
+ return self.count_school_types_as("count_school_types")
+
+ def count_school_types_as(self, alias: str):
+ from requests.school_type_request import SchoolTypeRequest
+ return self.count_school_types_with(alias, SchoolTypeRequest())
+
+ def count_school_types_with(self, alias: str, child_request):
+ child_request.query.count_field("id", alias)
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_type_list", alias, child_request.query, True)
+ )
+ return self
+
+ def min_display_order_of_school_types(self):
+ from requests.school_type_request import SchoolTypeRequest
+ return self.min_display_order_of_school_types_as(
+ "min_display_order_of_school_types", SchoolTypeRequest())
+
+ def min_display_order_of_school_types_as(self, alias: str, child_request):
+ child_request.query.min("display_order", "min_display_order")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_type_list", alias, child_request.query, True)
+ )
+ return self
+ def max_display_order_of_school_types(self):
+ from requests.school_type_request import SchoolTypeRequest
+ return self.max_display_order_of_school_types_as(
+ "max_display_order_of_school_types", SchoolTypeRequest())
+
+ def max_display_order_of_school_types_as(self, alias: str, child_request):
+ child_request.query.max("display_order", "max_display_order")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_type_list", alias, child_request.query, True)
+ )
+ return self
+ def sum_display_order_of_school_types(self):
+ from requests.school_type_request import SchoolTypeRequest
+ return self.sum_display_order_of_school_types_as(
+ "sum_display_order_of_school_types", SchoolTypeRequest())
+
+ def sum_display_order_of_school_types_as(self, alias: str, child_request):
+ child_request.query.sum("display_order", "sum_display_order")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_type_list", alias, child_request.query, True)
+ )
+ return self
+ def avg_display_order_of_school_types(self):
+ from requests.school_type_request import SchoolTypeRequest
+ return self.avg_display_order_of_school_types_as(
+ "avg_display_order_of_school_types", SchoolTypeRequest())
+
+ def avg_display_order_of_school_types_as(self, alias: str, child_request):
+ child_request.query.avg("display_order", "avg_display_order")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_type_list", alias, child_request.query, True)
+ )
+ return self
+ def standardDeviation_display_order_of_school_types(self):
+ from requests.school_type_request import SchoolTypeRequest
+ return self.standardDeviation_display_order_of_school_types_as(
+ "standardDeviation_display_order_of_school_types", SchoolTypeRequest())
+
+ def standardDeviation_display_order_of_school_types_as(self, alias: str, child_request):
+ child_request.query.standardDeviation("display_order", "standardDeviation_display_order")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_type_list", alias, child_request.query, True)
+ )
+ return self
+ def squareRootOfPopulationStandardDeviation_display_order_of_school_types(self):
+ from requests.school_type_request import SchoolTypeRequest
+ return self.squareRootOfPopulationStandardDeviation_display_order_of_school_types_as(
+ "squareRootOfPopulationStandardDeviation_display_order_of_school_types", SchoolTypeRequest())
+
+ def squareRootOfPopulationStandardDeviation_display_order_of_school_types_as(self, alias: str, child_request):
+ child_request.query.squareRootOfPopulationStandardDeviation("display_order", "squareRootOfPopulationStandardDeviation_display_order")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_type_list", alias, child_request.query, True)
+ )
+ return self
+ def sampleVariance_display_order_of_school_types(self):
+ from requests.school_type_request import SchoolTypeRequest
+ return self.sampleVariance_display_order_of_school_types_as(
+ "sampleVariance_display_order_of_school_types", SchoolTypeRequest())
+
+ def sampleVariance_display_order_of_school_types_as(self, alias: str, child_request):
+ child_request.query.sampleVariance("display_order", "sampleVariance_display_order")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_type_list", alias, child_request.query, True)
+ )
+ return self
+ def samplePopulationVariance_display_order_of_school_types(self):
+ from requests.school_type_request import SchoolTypeRequest
+ return self.samplePopulationVariance_display_order_of_school_types_as(
+ "samplePopulationVariance_display_order_of_school_types", SchoolTypeRequest())
+
+ def samplePopulationVariance_display_order_of_school_types_as(self, alias: str, child_request):
+ child_request.query.samplePopulationVariance("display_order", "samplePopulationVariance_display_order")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_type_list", alias, child_request.query, True)
+ )
+ return self
+ def count_schools(self):
+ return self.count_schools_as("count_schools")
+
+ def count_schools_as(self, alias: str):
+ from requests.school_request import SchoolRequest
+ return self.count_schools_with(alias, SchoolRequest())
+
+ def count_schools_with(self, alias: str, child_request):
+ child_request.query.count_field("id", alias)
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+
+ def min_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.min_student_capacity_of_schools_as(
+ "min_student_capacity_of_schools", SchoolRequest())
+
+ def min_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.min("student_capacity", "min_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def max_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.max_student_capacity_of_schools_as(
+ "max_student_capacity_of_schools", SchoolRequest())
+
+ def max_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.max("student_capacity", "max_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def sum_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.sum_student_capacity_of_schools_as(
+ "sum_student_capacity_of_schools", SchoolRequest())
+
+ def sum_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.sum("student_capacity", "sum_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def avg_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.avg_student_capacity_of_schools_as(
+ "avg_student_capacity_of_schools", SchoolRequest())
+
+ def avg_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.avg("student_capacity", "avg_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def standardDeviation_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.standardDeviation_student_capacity_of_schools_as(
+ "standardDeviation_student_capacity_of_schools", SchoolRequest())
+
+ def standardDeviation_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.standardDeviation("student_capacity", "standardDeviation_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def squareRootOfPopulationStandardDeviation_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.squareRootOfPopulationStandardDeviation_student_capacity_of_schools_as(
+ "squareRootOfPopulationStandardDeviation_student_capacity_of_schools", SchoolRequest())
+
+ def squareRootOfPopulationStandardDeviation_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.squareRootOfPopulationStandardDeviation("student_capacity", "squareRootOfPopulationStandardDeviation_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def sampleVariance_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.sampleVariance_student_capacity_of_schools_as(
+ "sampleVariance_student_capacity_of_schools", SchoolRequest())
+
+ def sampleVariance_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.sampleVariance("student_capacity", "sampleVariance_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def samplePopulationVariance_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.samplePopulationVariance_student_capacity_of_schools_as(
+ "samplePopulationVariance_student_capacity_of_schools", SchoolRequest())
+
+ def samplePopulationVariance_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.samplePopulationVariance("student_capacity", "samplePopulationVariance_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+
+class ExecutablePlatformRequest:
+ def __init__(self, request):
+ self._request = request
+
+ def comment(self, c: str):
+ self._request.comment(c)
+ return self
+
+ def new_entity(self, context) -> Platform:
+ request = self._request
+ QueryIntent(request._comment, request._purpose)
+ entity = context.initialize_entity("Platform", Platform())
+ if not isinstance(entity, Platform):
+ raise TypeError("entity initializer returned an incompatible Platform")
+ return entity
+
+ async def execute_for_result(self, context):
+ self = self._request
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ return await service.query(context, req)
+
+ async def execute_for_rows(self, context):
+ return (await self.execute_for_result(context)).rows
+
+ async def execute_for_list(self, context) -> SmartList[Platform]:
+ result = await self.execute_for_result(context)
+ return SmartList(
+ (Platform(**row) for row in result.rows),
+ facets=result.facets)
+
+ async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[Platform]:
+ request = self._request
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
+ service = context.require_resource("dataService")
+ alias = "__teaql_total"
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
+ retained_count, accuracy = context.id_set_count()
+ if accuracy == "EXACT":
+ total_count = retained_count
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ row_result = await service.query(context, authorized)
+ data = SmartList(Platform(**row) for row in row_result.rows)
+ return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
+
+ async def execute_for_one(self, context):
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutablePlatformRequest(request).execute_for_list(context)
+ return entities[0] if entities else None
+
+ def execute_for_stream(self, context, chunk_size: int = 1000):
+ """Yield entity chunks lazily from the provider cursor."""
+ request = self._request
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ if not hasattr(service, "query_stream"):
+ raise RuntimeError("dataService does not implement query_stream")
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield Platform(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/facet-trace/lib/requests/school_request.py b/examples/facet-trace/lib/requests/school_request.py
new file mode 100644
index 0000000..3a20d9c
--- /dev/null
+++ b/examples/facet-trace/lib/requests/school_request.py
@@ -0,0 +1,927 @@
+from teaql.core.query import RelationAggregate, SelectQuery
+from teaql.core.list import SmartList, TeaQLPage
+from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
+from teaql.core.expr import (
+ begin_with, between, column, contain, end_with, eq, gt, gte,
+ in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
+ not_contain, not_end_with, not_in_list, not_in_subquery, value,
+ sound_like,
+)
+from models.school import School
+from typing import Protocol
+
+class QuerySelection(Protocol):
+ query: SelectQuery
+
+class SchoolRequest:
+ def __init__(self, minimal=False):
+ self.query = SelectQuery("School")
+ self._purpose = None
+ self._comment = None
+ self.query.and_filter(gte("version", 1))
+ if minimal:
+ self.select_id()
+ self.select_version()
+ else:
+ self.select_self_fields()
+
+ def comment(self, c: str):
+ self.query.comment(c)
+ self._comment = c
+ return self
+
+ def purpose(self, p: str):
+ self.query.purpose(p)
+ self._purpose = p
+ return ExecutableSchoolRequest(self)
+
+ def optimize_for_continuous_page_fetch(self):
+ self.query.optimize_for_continuous_page_fetch()
+ return self
+
+ def optimize_for_continuous_page_fetch_with(self, namespace: str, ttl_seconds: int):
+ self.query.optimize_for_continuous_page_fetch_with(namespace, ttl_seconds)
+ return self
+
+ def optimize_pagination_with_id_set(self):
+ self.query.optimize_pagination_with_id_set()
+ return self
+
+ def optimize_pagination_with_id_set_config(self, namespace: str, ttl_seconds: int, max_ids: int):
+ self.query.optimize_pagination_with_id_set_config(namespace, ttl_seconds, max_ids)
+ return self
+
+ def top_n_probe_parent_threshold(self, threshold: int):
+ self.query.top_n_probe_parent_threshold(threshold)
+ return self
+
+ def limit(self, n: int):
+ self.query.limit(n)
+ return self
+
+ def offset(self, n: int):
+ self.query.offset(n)
+ return self
+
+ def with_deleted_rows(self):
+ self.query.with_deleted_rows()
+ return self
+
+ def deleted_rows_only(self):
+ self.query.deleted_rows_only()
+ return self
+
+ def select_self_fields(self):
+ self.query.project("id", "platform", "school_type", "name", "address", "established_date", "student_capacity", "active", "create_time", "update_time", "version")
+ return self
+
+ def select_id(self):
+ self.query.project("id")
+ return self
+
+
+
+ def select_name(self):
+ self.query.project("name")
+ return self
+
+ def select_address(self):
+ self.query.project("address")
+ return self
+
+ def select_established_date(self):
+ self.query.project("established_date")
+ return self
+
+ def select_student_capacity(self):
+ self.query.project("student_capacity")
+ return self
+
+ def select_active(self):
+ self.query.project("active")
+ return self
+
+ def select_create_time(self):
+ self.query.project("create_time")
+ return self
+
+ def select_update_time(self):
+ self.query.project("update_time")
+ return self
+
+ def select_version(self):
+ self.query.project("version")
+ return self
+
+ def select_platform_with(self, child_request):
+ self.query.project("platform")
+ self.query.relation_query("platform", child_request.query)
+ return self
+ def select_school_type_with(self, child_request):
+ self.query.project("school_type")
+ self.query.relation_query("school_type", child_request.query)
+ return self
+ def with_platform_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(in_subquery(column("platform"), "Platform", child_request.query))
+ return self
+
+ def without_platform_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(not_in_subquery(column("platform"), "Platform", child_request.query))
+ return self
+
+ def have_platform(self):
+ self.query.and_filter(is_not_null(column("platform")))
+ return self
+
+ def have_no_platform(self):
+ self.query.and_filter(is_null(column("platform")))
+ return self
+ def with_school_type_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(in_subquery(column("school_type"), "SchoolType", child_request.query))
+ return self
+
+ def without_school_type_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(not_in_subquery(column("school_type"), "SchoolType", child_request.query))
+ return self
+
+ def have_school_type(self):
+ self.query.and_filter(is_not_null(column("school_type")))
+ return self
+
+ def have_no_school_type(self):
+ self.query.and_filter(is_null(column("school_type")))
+ return self
+
+ def with_id_is(self, val):
+ self.query.and_filter(eq("id", val))
+ return self
+
+ def with_id_is_not(self, val):
+ self.query.and_filter(ne("id", val))
+ return self
+
+ def with_id_in(self, *vals):
+ self.query.and_filter(in_list("id", list(vals)))
+ return self
+
+ def with_id_not_in(self, *vals):
+ self.query.and_filter(not_in_list("id", list(vals)))
+ return self
+
+ def with_id_greater_than(self, val):
+ self.query.and_filter(gt("id", val))
+ return self
+
+ def with_id_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("id", val))
+ return self
+
+ def with_id_less_than(self, val):
+ self.query.and_filter(lt("id", val))
+ return self
+
+ def with_id_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("id", val))
+ return self
+
+ def with_id_between(self, lower, upper):
+ self.query.and_filter(between(column("id"), value(lower), value(upper)))
+ return self
+
+ def with_id_is_known(self):
+ self.query.and_filter(is_not_null(column("id")))
+ return self
+
+ def with_id_is_unknown(self):
+ self.query.and_filter(is_null(column("id")))
+ return self
+
+ def filter_by_platform(self, val):
+ self.query.and_filter(eq("platform", val))
+ return self
+
+ def filter_by_school_type(self, val):
+ self.query.and_filter(eq("school_type", val))
+ return self
+ def with_school_type_is_primary(self):
+ self.query.and_filter(eq("school_type", 1001))
+ return self
+
+ def with_name_containing(self, val: str):
+ self.query.and_filter(contain("name", val))
+ return self
+
+ def with_name_not_containing(self, val: str):
+ self.query.and_filter(not_contain("name", val))
+ return self
+
+ def with_name_starting_with(self, val: str):
+ self.query.and_filter(begin_with("name", val))
+ return self
+
+ def with_name_not_starting_with(self, val: str):
+ self.query.and_filter(not_begin_with("name", val))
+ return self
+
+ def with_name_ending_with(self, val: str):
+ self.query.and_filter(end_with("name", val))
+ return self
+
+ def with_name_not_ending_with(self, val: str):
+ self.query.and_filter(not_end_with("name", val))
+ return self
+
+ def with_name_sounding_like(self, val: str):
+ self.query.and_filter(sound_like("name", val))
+ return self
+
+ def with_name_is(self, val: str):
+ self.query.and_filter(eq("name", val))
+ return self
+ def with_name_is_not(self, val):
+ self.query.and_filter(ne("name", val))
+ return self
+
+ def with_name_in(self, *vals):
+ self.query.and_filter(in_list("name", list(vals)))
+ return self
+
+ def with_name_not_in(self, *vals):
+ self.query.and_filter(not_in_list("name", list(vals)))
+ return self
+
+ def with_name_greater_than(self, val):
+ self.query.and_filter(gt("name", val))
+ return self
+
+ def with_name_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("name", val))
+ return self
+
+ def with_name_less_than(self, val):
+ self.query.and_filter(lt("name", val))
+ return self
+
+ def with_name_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("name", val))
+ return self
+
+ def with_name_between(self, lower, upper):
+ self.query.and_filter(between(column("name"), value(lower), value(upper)))
+ return self
+
+ def with_name_is_known(self):
+ self.query.and_filter(is_not_null(column("name")))
+ return self
+
+ def with_name_is_unknown(self):
+ self.query.and_filter(is_null(column("name")))
+ return self
+
+ def with_address_containing(self, val: str):
+ self.query.and_filter(contain("address", val))
+ return self
+
+ def with_address_not_containing(self, val: str):
+ self.query.and_filter(not_contain("address", val))
+ return self
+
+ def with_address_starting_with(self, val: str):
+ self.query.and_filter(begin_with("address", val))
+ return self
+
+ def with_address_not_starting_with(self, val: str):
+ self.query.and_filter(not_begin_with("address", val))
+ return self
+
+ def with_address_ending_with(self, val: str):
+ self.query.and_filter(end_with("address", val))
+ return self
+
+ def with_address_not_ending_with(self, val: str):
+ self.query.and_filter(not_end_with("address", val))
+ return self
+
+ def with_address_sounding_like(self, val: str):
+ self.query.and_filter(sound_like("address", val))
+ return self
+
+ def with_address_is(self, val: str):
+ self.query.and_filter(eq("address", val))
+ return self
+ def with_address_is_not(self, val):
+ self.query.and_filter(ne("address", val))
+ return self
+
+ def with_address_in(self, *vals):
+ self.query.and_filter(in_list("address", list(vals)))
+ return self
+
+ def with_address_not_in(self, *vals):
+ self.query.and_filter(not_in_list("address", list(vals)))
+ return self
+
+ def with_address_greater_than(self, val):
+ self.query.and_filter(gt("address", val))
+ return self
+
+ def with_address_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("address", val))
+ return self
+
+ def with_address_less_than(self, val):
+ self.query.and_filter(lt("address", val))
+ return self
+
+ def with_address_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("address", val))
+ return self
+
+ def with_address_between(self, lower, upper):
+ self.query.and_filter(between(column("address"), value(lower), value(upper)))
+ return self
+
+ def with_address_is_known(self):
+ self.query.and_filter(is_not_null(column("address")))
+ return self
+
+ def with_address_is_unknown(self):
+ self.query.and_filter(is_null(column("address")))
+ return self
+
+ def with_established_date_is(self, val):
+ self.query.and_filter(eq("established_date", val))
+ return self
+
+ def with_established_date_is_not(self, val):
+ self.query.and_filter(ne("established_date", val))
+ return self
+
+ def with_established_date_in(self, *vals):
+ self.query.and_filter(in_list("established_date", list(vals)))
+ return self
+
+ def with_established_date_not_in(self, *vals):
+ self.query.and_filter(not_in_list("established_date", list(vals)))
+ return self
+
+ def with_established_date_greater_than(self, val):
+ self.query.and_filter(gt("established_date", val))
+ return self
+
+ def with_established_date_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("established_date", val))
+ return self
+
+ def with_established_date_less_than(self, val):
+ self.query.and_filter(lt("established_date", val))
+ return self
+
+ def with_established_date_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("established_date", val))
+ return self
+
+ def with_established_date_between(self, lower, upper):
+ self.query.and_filter(between(column("established_date"), value(lower), value(upper)))
+ return self
+
+ def with_established_date_is_known(self):
+ self.query.and_filter(is_not_null(column("established_date")))
+ return self
+
+ def with_established_date_is_unknown(self):
+ self.query.and_filter(is_null(column("established_date")))
+ return self
+
+ def with_student_capacity_is(self, val):
+ self.query.and_filter(eq("student_capacity", val))
+ return self
+
+ def with_student_capacity_is_not(self, val):
+ self.query.and_filter(ne("student_capacity", val))
+ return self
+
+ def with_student_capacity_in(self, *vals):
+ self.query.and_filter(in_list("student_capacity", list(vals)))
+ return self
+
+ def with_student_capacity_not_in(self, *vals):
+ self.query.and_filter(not_in_list("student_capacity", list(vals)))
+ return self
+
+ def with_student_capacity_greater_than(self, val):
+ self.query.and_filter(gt("student_capacity", val))
+ return self
+
+ def with_student_capacity_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("student_capacity", val))
+ return self
+
+ def with_student_capacity_less_than(self, val):
+ self.query.and_filter(lt("student_capacity", val))
+ return self
+
+ def with_student_capacity_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("student_capacity", val))
+ return self
+
+ def with_student_capacity_between(self, lower, upper):
+ self.query.and_filter(between(column("student_capacity"), value(lower), value(upper)))
+ return self
+
+ def with_student_capacity_is_known(self):
+ self.query.and_filter(is_not_null(column("student_capacity")))
+ return self
+
+ def with_student_capacity_is_unknown(self):
+ self.query.and_filter(is_null(column("student_capacity")))
+ return self
+
+ def which_are_active(self):
+ self.query.and_filter(eq("active", True))
+ return self
+
+ def which_are_not_active(self):
+ self.query.and_filter(eq("active", False))
+ return self
+ def with_active_is_not(self, val):
+ self.query.and_filter(ne("active", val))
+ return self
+
+ def with_active_in(self, *vals):
+ self.query.and_filter(in_list("active", list(vals)))
+ return self
+
+ def with_active_not_in(self, *vals):
+ self.query.and_filter(not_in_list("active", list(vals)))
+ return self
+
+ def with_active_greater_than(self, val):
+ self.query.and_filter(gt("active", val))
+ return self
+
+ def with_active_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("active", val))
+ return self
+
+ def with_active_less_than(self, val):
+ self.query.and_filter(lt("active", val))
+ return self
+
+ def with_active_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("active", val))
+ return self
+
+ def with_active_between(self, lower, upper):
+ self.query.and_filter(between(column("active"), value(lower), value(upper)))
+ return self
+
+ def with_active_is_known(self):
+ self.query.and_filter(is_not_null(column("active")))
+ return self
+
+ def with_active_is_unknown(self):
+ self.query.and_filter(is_null(column("active")))
+ return self
+
+ def with_create_time_is(self, val):
+ self.query.and_filter(eq("create_time", val))
+ return self
+
+ def with_create_time_is_not(self, val):
+ self.query.and_filter(ne("create_time", val))
+ return self
+
+ def with_create_time_in(self, *vals):
+ self.query.and_filter(in_list("create_time", list(vals)))
+ return self
+
+ def with_create_time_not_in(self, *vals):
+ self.query.and_filter(not_in_list("create_time", list(vals)))
+ return self
+
+ def with_create_time_greater_than(self, val):
+ self.query.and_filter(gt("create_time", val))
+ return self
+
+ def with_create_time_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("create_time", val))
+ return self
+
+ def with_create_time_less_than(self, val):
+ self.query.and_filter(lt("create_time", val))
+ return self
+
+ def with_create_time_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("create_time", val))
+ return self
+
+ def with_create_time_between(self, lower, upper):
+ self.query.and_filter(between(column("create_time"), value(lower), value(upper)))
+ return self
+
+ def with_create_time_is_known(self):
+ self.query.and_filter(is_not_null(column("create_time")))
+ return self
+
+ def with_create_time_is_unknown(self):
+ self.query.and_filter(is_null(column("create_time")))
+ return self
+
+ def with_update_time_is(self, val):
+ self.query.and_filter(eq("update_time", val))
+ return self
+
+ def with_update_time_is_not(self, val):
+ self.query.and_filter(ne("update_time", val))
+ return self
+
+ def with_update_time_in(self, *vals):
+ self.query.and_filter(in_list("update_time", list(vals)))
+ return self
+
+ def with_update_time_not_in(self, *vals):
+ self.query.and_filter(not_in_list("update_time", list(vals)))
+ return self
+
+ def with_update_time_greater_than(self, val):
+ self.query.and_filter(gt("update_time", val))
+ return self
+
+ def with_update_time_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("update_time", val))
+ return self
+
+ def with_update_time_less_than(self, val):
+ self.query.and_filter(lt("update_time", val))
+ return self
+
+ def with_update_time_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("update_time", val))
+ return self
+
+ def with_update_time_between(self, lower, upper):
+ self.query.and_filter(between(column("update_time"), value(lower), value(upper)))
+ return self
+
+ def with_update_time_is_known(self):
+ self.query.and_filter(is_not_null(column("update_time")))
+ return self
+
+ def with_update_time_is_unknown(self):
+ self.query.and_filter(is_null(column("update_time")))
+ return self
+
+ def with_version_is(self, val):
+ self.query.and_filter(eq("version", val))
+ return self
+
+ def with_version_is_not(self, val):
+ self.query.and_filter(ne("version", val))
+ return self
+
+ def with_version_in(self, *vals):
+ self.query.and_filter(in_list("version", list(vals)))
+ return self
+
+ def with_version_not_in(self, *vals):
+ self.query.and_filter(not_in_list("version", list(vals)))
+ return self
+
+ def with_version_greater_than(self, val):
+ self.query.and_filter(gt("version", val))
+ return self
+
+ def with_version_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("version", val))
+ return self
+
+ def with_version_less_than(self, val):
+ self.query.and_filter(lt("version", val))
+ return self
+
+ def with_version_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("version", val))
+ return self
+
+ def with_version_between(self, lower, upper):
+ self.query.and_filter(between(column("version"), value(lower), value(upper)))
+ return self
+
+ def with_version_is_known(self):
+ self.query.and_filter(is_not_null(column("version")))
+ return self
+
+ def with_version_is_unknown(self):
+ self.query.and_filter(is_null(column("version")))
+ return self
+
+ def order_by_id_ascending(self):
+ self.query.order_by("id", "asc")
+ return self
+
+ def order_by_id_descending(self):
+ self.query.order_by("id", "desc")
+ return self
+
+
+
+ def order_by_name_ascending(self):
+ self.query.order_by("name", "asc")
+ return self
+
+ def order_by_name_descending(self):
+ self.query.order_by("name", "desc")
+ return self
+
+ def order_by_address_ascending(self):
+ self.query.order_by("address", "asc")
+ return self
+
+ def order_by_address_descending(self):
+ self.query.order_by("address", "desc")
+ return self
+
+ def order_by_established_date_ascending(self):
+ self.query.order_by("established_date", "asc")
+ return self
+
+ def order_by_established_date_descending(self):
+ self.query.order_by("established_date", "desc")
+ return self
+
+ def order_by_student_capacity_ascending(self):
+ self.query.order_by("student_capacity", "asc")
+ return self
+
+ def order_by_student_capacity_descending(self):
+ self.query.order_by("student_capacity", "desc")
+ return self
+
+ def order_by_active_ascending(self):
+ self.query.order_by("active", "asc")
+ return self
+
+ def order_by_active_descending(self):
+ self.query.order_by("active", "desc")
+ return self
+
+ def order_by_create_time_ascending(self):
+ self.query.order_by("create_time", "asc")
+ return self
+
+ def order_by_create_time_descending(self):
+ self.query.order_by("create_time", "desc")
+ return self
+
+ def order_by_update_time_ascending(self):
+ self.query.order_by("update_time", "asc")
+ return self
+
+ def order_by_update_time_descending(self):
+ self.query.order_by("update_time", "desc")
+ return self
+
+ def order_by_version_ascending(self):
+ self.query.order_by("version", "asc")
+ return self
+
+ def order_by_version_descending(self):
+ self.query.order_by("version", "desc")
+ return self
+
+
+ def count(self):
+ self.query.count_field("id", "count")
+ return self
+
+ def count_as(self, ret_name: str):
+ self.query.count_field("id", ret_name)
+ return self
+
+ def min_student_capacity(self):
+ return self.min_student_capacity_as("minOfStudentCapacity")
+
+ def min_student_capacity_as(self, ret_name: str):
+ self.query.min("student_capacity", ret_name)
+ return self
+ def max_student_capacity(self):
+ return self.max_student_capacity_as("maxOfStudentCapacity")
+
+ def max_student_capacity_as(self, ret_name: str):
+ self.query.max("student_capacity", ret_name)
+ return self
+ def sum_student_capacity(self):
+ return self.sum_student_capacity_as("sumOfStudentCapacity")
+
+ def sum_student_capacity_as(self, ret_name: str):
+ self.query.sum("student_capacity", ret_name)
+ return self
+ def avg_student_capacity(self):
+ return self.avg_student_capacity_as("avgOfStudentCapacity")
+
+ def avg_student_capacity_as(self, ret_name: str):
+ self.query.avg("student_capacity", ret_name)
+ return self
+ def standardDeviation_student_capacity(self):
+ return self.standardDeviation_student_capacity_as("standardDeviationOfStudentCapacity")
+
+ def standardDeviation_student_capacity_as(self, ret_name: str):
+ self.query.standardDeviation("student_capacity", ret_name)
+ return self
+ def squareRootOfPopulationStandardDeviation_student_capacity(self):
+ return self.squareRootOfPopulationStandardDeviation_student_capacity_as("squareRootOfPopulationStandardDeviationOfStudentCapacity")
+
+ def squareRootOfPopulationStandardDeviation_student_capacity_as(self, ret_name: str):
+ self.query.squareRootOfPopulationStandardDeviation("student_capacity", ret_name)
+ return self
+ def sampleVariance_student_capacity(self):
+ return self.sampleVariance_student_capacity_as("sampleVarianceOfStudentCapacity")
+
+ def sampleVariance_student_capacity_as(self, ret_name: str):
+ self.query.sampleVariance("student_capacity", ret_name)
+ return self
+ def samplePopulationVariance_student_capacity(self):
+ return self.samplePopulationVariance_student_capacity_as("samplePopulationVarianceOfStudentCapacity")
+
+ def samplePopulationVariance_student_capacity_as(self, ret_name: str):
+ self.query.samplePopulationVariance("student_capacity", ret_name)
+ return self
+ def group_by_id(self):
+ self.query.group_by("id")
+ return self
+
+ def group_by_id_as(self, ret_name: str):
+ self.query.group_by("id")
+ return self
+ def group_by_platform(self):
+ self.query.group_by("platform")
+ return self
+
+ def group_by_platform_as(self, ret_name: str):
+ self.query.group_by("platform")
+ return self
+ def group_by_school_type(self):
+ self.query.group_by("school_type")
+ return self
+
+ def group_by_school_type_as(self, ret_name: str):
+ self.query.group_by("school_type")
+ return self
+ def group_by_name(self):
+ self.query.group_by("name")
+ return self
+
+ def group_by_name_as(self, ret_name: str):
+ self.query.group_by("name")
+ return self
+ def group_by_address(self):
+ self.query.group_by("address")
+ return self
+
+ def group_by_address_as(self, ret_name: str):
+ self.query.group_by("address")
+ return self
+ def group_by_established_date(self):
+ self.query.group_by("established_date")
+ return self
+
+ def group_by_established_date_as(self, ret_name: str):
+ self.query.group_by("established_date")
+ return self
+ def group_by_student_capacity(self):
+ self.query.group_by("student_capacity")
+ return self
+
+ def group_by_student_capacity_as(self, ret_name: str):
+ self.query.group_by("student_capacity")
+ return self
+ def group_by_active(self):
+ self.query.group_by("active")
+ return self
+
+ def group_by_active_as(self, ret_name: str):
+ self.query.group_by("active")
+ return self
+ def group_by_create_time(self):
+ self.query.group_by("create_time")
+ return self
+
+ def group_by_create_time_as(self, ret_name: str):
+ self.query.group_by("create_time")
+ return self
+ def group_by_update_time(self):
+ self.query.group_by("update_time")
+ return self
+
+ def group_by_update_time_as(self, ret_name: str):
+ self.query.group_by("update_time")
+ return self
+ def group_by_version(self):
+ self.query.group_by("version")
+ return self
+
+ def group_by_version_as(self, ret_name: str):
+ self.query.group_by("version")
+ return self
+ def facet_by_platform_as(self, name: str, request: QuerySelection,
+ include_all_facets: bool = True):
+ self.query.facet_by(name, "platform", request.query, include_all_facets)
+ return self
+
+ def facet_by_school_type_as(self, name: str, request: QuerySelection,
+ include_all_facets: bool = True):
+ self.query.facet_by(name, "school_type", request.query, include_all_facets)
+ return self
+
+
+class ExecutableSchoolRequest:
+ def __init__(self, request):
+ self._request = request
+
+ def comment(self, c: str):
+ self._request.comment(c)
+ return self
+
+ def new_entity(self, context) -> School:
+ request = self._request
+ QueryIntent(request._comment, request._purpose)
+ entity = context.initialize_entity("School", School())
+ if not isinstance(entity, School):
+ raise TypeError("entity initializer returned an incompatible School")
+ return entity
+
+ async def execute_for_result(self, context):
+ self = self._request
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ return await service.query(context, req)
+
+ async def execute_for_rows(self, context):
+ return (await self.execute_for_result(context)).rows
+
+ async def execute_for_list(self, context) -> SmartList[School]:
+ result = await self.execute_for_result(context)
+ return SmartList(
+ (School(**row) for row in result.rows),
+ facets=result.facets)
+
+ async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[School]:
+ request = self._request
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
+ service = context.require_resource("dataService")
+ alias = "__teaql_total"
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
+ retained_count, accuracy = context.id_set_count()
+ if accuracy == "EXACT":
+ total_count = retained_count
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ row_result = await service.query(context, authorized)
+ data = SmartList(School(**row) for row in row_result.rows)
+ return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
+
+ async def execute_for_one(self, context):
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableSchoolRequest(request).execute_for_list(context)
+ return entities[0] if entities else None
+
+ def execute_for_stream(self, context, chunk_size: int = 1000):
+ """Yield entity chunks lazily from the provider cursor."""
+ request = self._request
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ if not hasattr(service, "query_stream"):
+ raise RuntimeError("dataService does not implement query_stream")
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield School(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/facet-trace/lib/requests/school_type_request.py b/examples/facet-trace/lib/requests/school_type_request.py
new file mode 100644
index 0000000..605dca8
--- /dev/null
+++ b/examples/facet-trace/lib/requests/school_type_request.py
@@ -0,0 +1,754 @@
+from teaql.core.query import RelationAggregate, SelectQuery
+from teaql.core.list import SmartList, TeaQLPage
+from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
+from teaql.core.expr import (
+ begin_with, between, column, contain, end_with, eq, gt, gte,
+ in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
+ not_contain, not_end_with, not_in_list, not_in_subquery, value,
+ sound_like,
+)
+from models.school_type import SchoolType
+from typing import Protocol
+
+class QuerySelection(Protocol):
+ query: SelectQuery
+
+class SchoolTypeRequest:
+ def __init__(self, minimal=False):
+ self.query = SelectQuery("SchoolType")
+ self._purpose = None
+ self._comment = None
+ self.query.and_filter(gte("version", 1))
+ if minimal:
+ self.select_id()
+ self.select_version()
+ else:
+ self.select_self_fields()
+
+ def comment(self, c: str):
+ self.query.comment(c)
+ self._comment = c
+ return self
+
+ def purpose(self, p: str):
+ self.query.purpose(p)
+ self._purpose = p
+ return ExecutableSchoolTypeRequest(self)
+
+ def optimize_for_continuous_page_fetch(self):
+ self.query.optimize_for_continuous_page_fetch()
+ return self
+
+ def optimize_for_continuous_page_fetch_with(self, namespace: str, ttl_seconds: int):
+ self.query.optimize_for_continuous_page_fetch_with(namespace, ttl_seconds)
+ return self
+
+ def optimize_pagination_with_id_set(self):
+ self.query.optimize_pagination_with_id_set()
+ return self
+
+ def optimize_pagination_with_id_set_config(self, namespace: str, ttl_seconds: int, max_ids: int):
+ self.query.optimize_pagination_with_id_set_config(namespace, ttl_seconds, max_ids)
+ return self
+
+ def top_n_probe_parent_threshold(self, threshold: int):
+ self.query.top_n_probe_parent_threshold(threshold)
+ return self
+
+ def limit(self, n: int):
+ self.query.limit(n)
+ return self
+
+ def offset(self, n: int):
+ self.query.offset(n)
+ return self
+
+ def with_deleted_rows(self):
+ self.query.with_deleted_rows()
+ return self
+
+ def deleted_rows_only(self):
+ self.query.deleted_rows_only()
+ return self
+
+ def select_self_fields(self):
+ self.query.project("platform", "id", "name", "code", "display_order", "version")
+ return self
+
+ def select_id(self):
+ self.query.project("id")
+ return self
+
+ def select_name(self):
+ self.query.project("name")
+ return self
+
+ def select_code(self):
+ self.query.project("code")
+ return self
+
+ def select_display_order(self):
+ self.query.project("display_order")
+ return self
+
+ def select_version(self):
+ self.query.project("version")
+ return self
+
+ def select_platform_with(self, child_request):
+ self.query.project("platform")
+ self.query.relation_query("platform", child_request.query)
+ return self
+ def with_platform_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(in_subquery(column("platform"), "Platform", child_request.query))
+ return self
+
+ def without_platform_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(not_in_subquery(column("platform"), "Platform", child_request.query))
+ return self
+
+ def have_platform(self):
+ self.query.and_filter(is_not_null(column("platform")))
+ return self
+
+ def have_no_platform(self):
+ self.query.and_filter(is_null(column("platform")))
+ return self
+
+ def filter_by_platform(self, val):
+ self.query.and_filter(eq("platform", val))
+ return self
+
+ def with_id_is(self, val):
+ self.query.and_filter(eq("id", val))
+ return self
+
+ def with_id_is_not(self, val):
+ self.query.and_filter(ne("id", val))
+ return self
+
+ def with_id_in(self, *vals):
+ self.query.and_filter(in_list("id", list(vals)))
+ return self
+
+ def with_id_not_in(self, *vals):
+ self.query.and_filter(not_in_list("id", list(vals)))
+ return self
+
+ def with_id_greater_than(self, val):
+ self.query.and_filter(gt("id", val))
+ return self
+
+ def with_id_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("id", val))
+ return self
+
+ def with_id_less_than(self, val):
+ self.query.and_filter(lt("id", val))
+ return self
+
+ def with_id_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("id", val))
+ return self
+
+ def with_id_between(self, lower, upper):
+ self.query.and_filter(between(column("id"), value(lower), value(upper)))
+ return self
+
+ def with_id_is_known(self):
+ self.query.and_filter(is_not_null(column("id")))
+ return self
+
+ def with_id_is_unknown(self):
+ self.query.and_filter(is_null(column("id")))
+ return self
+
+ def with_name_containing(self, val: str):
+ self.query.and_filter(contain("name", val))
+ return self
+
+ def with_name_not_containing(self, val: str):
+ self.query.and_filter(not_contain("name", val))
+ return self
+
+ def with_name_starting_with(self, val: str):
+ self.query.and_filter(begin_with("name", val))
+ return self
+
+ def with_name_not_starting_with(self, val: str):
+ self.query.and_filter(not_begin_with("name", val))
+ return self
+
+ def with_name_ending_with(self, val: str):
+ self.query.and_filter(end_with("name", val))
+ return self
+
+ def with_name_not_ending_with(self, val: str):
+ self.query.and_filter(not_end_with("name", val))
+ return self
+
+ def with_name_sounding_like(self, val: str):
+ self.query.and_filter(sound_like("name", val))
+ return self
+
+ def with_name_is(self, val: str):
+ self.query.and_filter(eq("name", val))
+ return self
+ def with_name_is_not(self, val):
+ self.query.and_filter(ne("name", val))
+ return self
+
+ def with_name_in(self, *vals):
+ self.query.and_filter(in_list("name", list(vals)))
+ return self
+
+ def with_name_not_in(self, *vals):
+ self.query.and_filter(not_in_list("name", list(vals)))
+ return self
+
+ def with_name_greater_than(self, val):
+ self.query.and_filter(gt("name", val))
+ return self
+
+ def with_name_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("name", val))
+ return self
+
+ def with_name_less_than(self, val):
+ self.query.and_filter(lt("name", val))
+ return self
+
+ def with_name_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("name", val))
+ return self
+
+ def with_name_between(self, lower, upper):
+ self.query.and_filter(between(column("name"), value(lower), value(upper)))
+ return self
+
+ def with_name_is_known(self):
+ self.query.and_filter(is_not_null(column("name")))
+ return self
+
+ def with_name_is_unknown(self):
+ self.query.and_filter(is_null(column("name")))
+ return self
+
+ def with_code_containing(self, val: str):
+ self.query.and_filter(contain("code", val))
+ return self
+
+ def with_code_not_containing(self, val: str):
+ self.query.and_filter(not_contain("code", val))
+ return self
+
+ def with_code_starting_with(self, val: str):
+ self.query.and_filter(begin_with("code", val))
+ return self
+
+ def with_code_not_starting_with(self, val: str):
+ self.query.and_filter(not_begin_with("code", val))
+ return self
+
+ def with_code_ending_with(self, val: str):
+ self.query.and_filter(end_with("code", val))
+ return self
+
+ def with_code_not_ending_with(self, val: str):
+ self.query.and_filter(not_end_with("code", val))
+ return self
+
+ def with_code_sounding_like(self, val: str):
+ self.query.and_filter(sound_like("code", val))
+ return self
+
+ def with_code_is(self, val: str):
+ self.query.and_filter(eq("code", val))
+ return self
+ def with_code_is_not(self, val):
+ self.query.and_filter(ne("code", val))
+ return self
+
+ def with_code_in(self, *vals):
+ self.query.and_filter(in_list("code", list(vals)))
+ return self
+
+ def with_code_not_in(self, *vals):
+ self.query.and_filter(not_in_list("code", list(vals)))
+ return self
+
+ def with_code_greater_than(self, val):
+ self.query.and_filter(gt("code", val))
+ return self
+
+ def with_code_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("code", val))
+ return self
+
+ def with_code_less_than(self, val):
+ self.query.and_filter(lt("code", val))
+ return self
+
+ def with_code_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("code", val))
+ return self
+
+ def with_code_between(self, lower, upper):
+ self.query.and_filter(between(column("code"), value(lower), value(upper)))
+ return self
+
+ def with_code_is_known(self):
+ self.query.and_filter(is_not_null(column("code")))
+ return self
+
+ def with_code_is_unknown(self):
+ self.query.and_filter(is_null(column("code")))
+ return self
+
+ def with_display_order_is(self, val):
+ self.query.and_filter(eq("display_order", val))
+ return self
+
+ def with_display_order_is_not(self, val):
+ self.query.and_filter(ne("display_order", val))
+ return self
+
+ def with_display_order_in(self, *vals):
+ self.query.and_filter(in_list("display_order", list(vals)))
+ return self
+
+ def with_display_order_not_in(self, *vals):
+ self.query.and_filter(not_in_list("display_order", list(vals)))
+ return self
+
+ def with_display_order_greater_than(self, val):
+ self.query.and_filter(gt("display_order", val))
+ return self
+
+ def with_display_order_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("display_order", val))
+ return self
+
+ def with_display_order_less_than(self, val):
+ self.query.and_filter(lt("display_order", val))
+ return self
+
+ def with_display_order_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("display_order", val))
+ return self
+
+ def with_display_order_between(self, lower, upper):
+ self.query.and_filter(between(column("display_order"), value(lower), value(upper)))
+ return self
+
+ def with_display_order_is_known(self):
+ self.query.and_filter(is_not_null(column("display_order")))
+ return self
+
+ def with_display_order_is_unknown(self):
+ self.query.and_filter(is_null(column("display_order")))
+ return self
+
+ def with_version_is(self, val):
+ self.query.and_filter(eq("version", val))
+ return self
+
+ def with_version_is_not(self, val):
+ self.query.and_filter(ne("version", val))
+ return self
+
+ def with_version_in(self, *vals):
+ self.query.and_filter(in_list("version", list(vals)))
+ return self
+
+ def with_version_not_in(self, *vals):
+ self.query.and_filter(not_in_list("version", list(vals)))
+ return self
+
+ def with_version_greater_than(self, val):
+ self.query.and_filter(gt("version", val))
+ return self
+
+ def with_version_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("version", val))
+ return self
+
+ def with_version_less_than(self, val):
+ self.query.and_filter(lt("version", val))
+ return self
+
+ def with_version_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("version", val))
+ return self
+
+ def with_version_between(self, lower, upper):
+ self.query.and_filter(between(column("version"), value(lower), value(upper)))
+ return self
+
+ def with_version_is_known(self):
+ self.query.and_filter(is_not_null(column("version")))
+ return self
+
+ def with_version_is_unknown(self):
+ self.query.and_filter(is_null(column("version")))
+ return self
+
+ def order_by_id_ascending(self):
+ self.query.order_by("id", "asc")
+ return self
+
+ def order_by_id_descending(self):
+ self.query.order_by("id", "desc")
+ return self
+
+ def order_by_name_ascending(self):
+ self.query.order_by("name", "asc")
+ return self
+
+ def order_by_name_descending(self):
+ self.query.order_by("name", "desc")
+ return self
+
+ def order_by_code_ascending(self):
+ self.query.order_by("code", "asc")
+ return self
+
+ def order_by_code_descending(self):
+ self.query.order_by("code", "desc")
+ return self
+
+ def order_by_display_order_ascending(self):
+ self.query.order_by("display_order", "asc")
+ return self
+
+ def order_by_display_order_descending(self):
+ self.query.order_by("display_order", "desc")
+ return self
+
+ def order_by_version_ascending(self):
+ self.query.order_by("version", "asc")
+ return self
+
+ def order_by_version_descending(self):
+ self.query.order_by("version", "desc")
+ return self
+
+
+ def count(self):
+ self.query.count_field("id", "count")
+ return self
+
+ def count_as(self, ret_name: str):
+ self.query.count_field("id", ret_name)
+ return self
+
+ def min_display_order(self):
+ return self.min_display_order_as("minOfDisplayOrder")
+
+ def min_display_order_as(self, ret_name: str):
+ self.query.min("display_order", ret_name)
+ return self
+ def max_display_order(self):
+ return self.max_display_order_as("maxOfDisplayOrder")
+
+ def max_display_order_as(self, ret_name: str):
+ self.query.max("display_order", ret_name)
+ return self
+ def sum_display_order(self):
+ return self.sum_display_order_as("sumOfDisplayOrder")
+
+ def sum_display_order_as(self, ret_name: str):
+ self.query.sum("display_order", ret_name)
+ return self
+ def avg_display_order(self):
+ return self.avg_display_order_as("avgOfDisplayOrder")
+
+ def avg_display_order_as(self, ret_name: str):
+ self.query.avg("display_order", ret_name)
+ return self
+ def standardDeviation_display_order(self):
+ return self.standardDeviation_display_order_as("standardDeviationOfDisplayOrder")
+
+ def standardDeviation_display_order_as(self, ret_name: str):
+ self.query.standardDeviation("display_order", ret_name)
+ return self
+ def squareRootOfPopulationStandardDeviation_display_order(self):
+ return self.squareRootOfPopulationStandardDeviation_display_order_as("squareRootOfPopulationStandardDeviationOfDisplayOrder")
+
+ def squareRootOfPopulationStandardDeviation_display_order_as(self, ret_name: str):
+ self.query.squareRootOfPopulationStandardDeviation("display_order", ret_name)
+ return self
+ def sampleVariance_display_order(self):
+ return self.sampleVariance_display_order_as("sampleVarianceOfDisplayOrder")
+
+ def sampleVariance_display_order_as(self, ret_name: str):
+ self.query.sampleVariance("display_order", ret_name)
+ return self
+ def samplePopulationVariance_display_order(self):
+ return self.samplePopulationVariance_display_order_as("samplePopulationVarianceOfDisplayOrder")
+
+ def samplePopulationVariance_display_order_as(self, ret_name: str):
+ self.query.samplePopulationVariance("display_order", ret_name)
+ return self
+ def group_by_platform(self):
+ self.query.group_by("platform")
+ return self
+
+ def group_by_platform_as(self, ret_name: str):
+ self.query.group_by("platform")
+ return self
+ def group_by_id(self):
+ self.query.group_by("id")
+ return self
+
+ def group_by_id_as(self, ret_name: str):
+ self.query.group_by("id")
+ return self
+ def group_by_name(self):
+ self.query.group_by("name")
+ return self
+
+ def group_by_name_as(self, ret_name: str):
+ self.query.group_by("name")
+ return self
+ def group_by_code(self):
+ self.query.group_by("code")
+ return self
+
+ def group_by_code_as(self, ret_name: str):
+ self.query.group_by("code")
+ return self
+ def group_by_display_order(self):
+ self.query.group_by("display_order")
+ return self
+
+ def group_by_display_order_as(self, ret_name: str):
+ self.query.group_by("display_order")
+ return self
+ def group_by_version(self):
+ self.query.group_by("version")
+ return self
+
+ def group_by_version_as(self, ret_name: str):
+ self.query.group_by("version")
+ return self
+ def select_school_list(self):
+ from requests.school_request import SchoolRequest
+ return self.select_school_list_with(SchoolRequest())
+
+ def select_school_list_with(self, child_request):
+ self.query.relation_query("school_list", child_request.query)
+ return self
+ def have_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.with_school_list_matching(SchoolRequest())
+
+ def have_no_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.without_school_list_matching(SchoolRequest())
+
+ def with_school_list_matching(self, child_request):
+ child_request.query.projection = ["school_type"]
+ self.query.and_filter(in_subquery(column("id"), "School", child_request.query))
+ return self
+
+ def without_school_list_matching(self, child_request):
+ child_request.query.projection = ["school_type"]
+ self.query.and_filter(not_in_subquery(column("id"), "School", child_request.query))
+ return self
+ def count_schools(self):
+ return self.count_schools_as("count_schools")
+
+ def count_schools_as(self, alias: str):
+ from requests.school_request import SchoolRequest
+ return self.count_schools_with(alias, SchoolRequest())
+
+ def count_schools_with(self, alias: str, child_request):
+ child_request.query.count_field("id", alias)
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+
+ def min_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.min_student_capacity_of_schools_as(
+ "min_student_capacity_of_schools", SchoolRequest())
+
+ def min_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.min("student_capacity", "min_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def max_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.max_student_capacity_of_schools_as(
+ "max_student_capacity_of_schools", SchoolRequest())
+
+ def max_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.max("student_capacity", "max_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def sum_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.sum_student_capacity_of_schools_as(
+ "sum_student_capacity_of_schools", SchoolRequest())
+
+ def sum_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.sum("student_capacity", "sum_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def avg_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.avg_student_capacity_of_schools_as(
+ "avg_student_capacity_of_schools", SchoolRequest())
+
+ def avg_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.avg("student_capacity", "avg_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def standardDeviation_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.standardDeviation_student_capacity_of_schools_as(
+ "standardDeviation_student_capacity_of_schools", SchoolRequest())
+
+ def standardDeviation_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.standardDeviation("student_capacity", "standardDeviation_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def squareRootOfPopulationStandardDeviation_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.squareRootOfPopulationStandardDeviation_student_capacity_of_schools_as(
+ "squareRootOfPopulationStandardDeviation_student_capacity_of_schools", SchoolRequest())
+
+ def squareRootOfPopulationStandardDeviation_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.squareRootOfPopulationStandardDeviation("student_capacity", "squareRootOfPopulationStandardDeviation_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def sampleVariance_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.sampleVariance_student_capacity_of_schools_as(
+ "sampleVariance_student_capacity_of_schools", SchoolRequest())
+
+ def sampleVariance_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.sampleVariance("student_capacity", "sampleVariance_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def samplePopulationVariance_student_capacity_of_schools(self):
+ from requests.school_request import SchoolRequest
+ return self.samplePopulationVariance_student_capacity_of_schools_as(
+ "samplePopulationVariance_student_capacity_of_schools", SchoolRequest())
+
+ def samplePopulationVariance_student_capacity_of_schools_as(self, alias: str, child_request):
+ child_request.query.samplePopulationVariance("student_capacity", "samplePopulationVariance_student_capacity")
+ self.query.relation_aggregates.append(
+ RelationAggregate("school_list", alias, child_request.query, True)
+ )
+ return self
+ def facet_by_platform_as(self, name: str, request: QuerySelection,
+ include_all_facets: bool = True):
+ self.query.facet_by(name, "platform", request.query, include_all_facets)
+ return self
+
+
+class ExecutableSchoolTypeRequest:
+ def __init__(self, request):
+ self._request = request
+
+ def comment(self, c: str):
+ self._request.comment(c)
+ return self
+
+ def new_entity(self, context) -> SchoolType:
+ request = self._request
+ QueryIntent(request._comment, request._purpose)
+ entity = context.initialize_entity("SchoolType", SchoolType())
+ if not isinstance(entity, SchoolType):
+ raise TypeError("entity initializer returned an incompatible SchoolType")
+ return entity
+
+ async def execute_for_result(self, context):
+ self = self._request
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ return await service.query(context, req)
+
+ async def execute_for_rows(self, context):
+ return (await self.execute_for_result(context)).rows
+
+ async def execute_for_list(self, context) -> SmartList[SchoolType]:
+ result = await self.execute_for_result(context)
+ return SmartList(
+ (SchoolType(**row) for row in result.rows),
+ facets=result.facets)
+
+ async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[SchoolType]:
+ request = self._request
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
+ service = context.require_resource("dataService")
+ alias = "__teaql_total"
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
+ retained_count, accuracy = context.id_set_count()
+ if accuracy == "EXACT":
+ total_count = retained_count
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ row_result = await service.query(context, authorized)
+ data = SmartList(SchoolType(**row) for row in row_result.rows)
+ return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
+
+ async def execute_for_one(self, context):
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableSchoolTypeRequest(request).execute_for_list(context)
+ return entities[0] if entities else None
+
+ def execute_for_stream(self, context, chunk_size: int = 1000):
+ """Yield entity chunks lazily from the provider cursor."""
+ request = self._request
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ if not hasattr(service, "query_stream"):
+ raise RuntimeError("dataService does not implement query_stream")
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield SchoolType(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/facet-trace/lib/runtime_module.py b/examples/facet-trace/lib/runtime_module.py
new file mode 100644
index 0000000..4e8d108
--- /dev/null
+++ b/examples/facet-trace/lib/runtime_module.py
@@ -0,0 +1,274 @@
+import asyncio
+from datetime import datetime, timezone
+from teaql.runtime import CheckResult, ContextEntityRef, JsonFieldNamingProfile, ObjectLocation, RuntimeModule, create_wire_entity_metadata
+from teaql.core.meta import EntityDescriptor, PropertyDescriptor, RelationDescriptor
+from teaql.core.value import DataType
+from Q import Q
+from teaql.core.value import Value
+try:
+ from teaql.core.graph import GraphNode
+except ImportError:
+ class GraphNode:
+ def __init__(self, entity):
+ self.entity, self.fields = entity, {}
+ def set(self, field, value):
+ self.fields[field] = value
+ return self
+from models.platform import Platform
+from models.school_type import SchoolType
+from models.school import School
+
+def _teaql_is_null(value):
+ return value.is_null() if hasattr(value, "is_null") else value is None
+
+def _teaql_raw(value):
+ return value.val if hasattr(value, "val") else value
+
+def _teaql_entity_id(value):
+ value = _teaql_raw(value)
+ if hasattr(value, "id"):
+ return value.id
+ if isinstance(value, dict):
+ return value.get("id")
+ return value
+
+class _PlatformChecker:
+ def check_and_fix(self, context, record, location, results):
+ operation = context.get_resource("fix_operation")
+ now = context.get_resource("fix_time")
+ if operation == "insert" and ("create_time" not in record or _teaql_is_null(record["create_time"])):
+ record["create_time"] = Value.from_any(now)
+ context.record_fix_evidence("Platform", "create_time", "clock", "graphClock")
+
+ if operation == "insert" and ("update_time" not in record or _teaql_is_null(record["update_time"])):
+ record["update_time"] = Value.from_any(now)
+ context.record_fix_evidence("Platform", "update_time", "clock", "graphClock")
+ if operation == "insert" or operation == "update":
+ record["update_time"] = Value.from_any(now)
+ context.record_fix_evidence("Platform", "update_time", "clock", "graphClock")
+
+
+ if (operation == "insert" and "name" not in record) or ("name" in record and _teaql_is_null(record["name"])):
+ results.append(CheckResult("required", ObjectLocation().property("name")))
+ if "name" in record and _teaql_raw(record["name"]) is not None and len(_teaql_raw(record["name"])) > 100:
+ results.append(CheckResult("max_length", ObjectLocation().property("name"), _teaql_raw(record["name"]), 100))
+
+ if (operation == "insert" and "base_url" not in record) or ("base_url" in record and _teaql_is_null(record["base_url"])):
+ results.append(CheckResult("required", ObjectLocation().property("base_url")))
+ if "base_url" in record and _teaql_raw(record["base_url"]) is not None and len(_teaql_raw(record["base_url"])) > 100:
+ results.append(CheckResult("max_length", ObjectLocation().property("base_url"), _teaql_raw(record["base_url"]), 100))
+
+ if (operation == "insert" and "create_time" not in record) or ("create_time" in record and _teaql_is_null(record["create_time"])):
+ results.append(CheckResult("required", ObjectLocation().property("create_time")))
+
+ if (operation == "insert" and "update_time" not in record) or ("update_time" in record and _teaql_is_null(record["update_time"])):
+ results.append(CheckResult("required", ObjectLocation().property("update_time")))
+
+
+
+class _SchoolTypeChecker:
+ def check_and_fix(self, context, record, location, results):
+ operation = context.get_resource("fix_operation")
+ now = context.get_resource("fix_time")
+ if (operation == "insert" and "platform" not in record) or ("platform" in record and _teaql_is_null(record["platform"])):
+ results.append(CheckResult("required", ObjectLocation().property("platform")))
+
+
+ if (operation == "insert" and "name" not in record) or ("name" in record and _teaql_is_null(record["name"])):
+ results.append(CheckResult("required", ObjectLocation().property("name")))
+ if "name" in record and _teaql_raw(record["name"]) is not None and len(_teaql_raw(record["name"])) > 100:
+ results.append(CheckResult("max_length", ObjectLocation().property("name"), _teaql_raw(record["name"]), 100))
+
+ if (operation == "insert" and "code" not in record) or ("code" in record and _teaql_is_null(record["code"])):
+ results.append(CheckResult("required", ObjectLocation().property("code")))
+ if "code" in record and _teaql_raw(record["code"]) is not None and len(_teaql_raw(record["code"])) > 100:
+ results.append(CheckResult("max_length", ObjectLocation().property("code"), _teaql_raw(record["code"]), 100))
+
+ if (operation == "insert" and "display_order" not in record) or ("display_order" in record and _teaql_is_null(record["display_order"])):
+ results.append(CheckResult("required", ObjectLocation().property("display_order")))
+
+
+
+class _SchoolChecker:
+ def check_and_fix(self, context, record, location, results):
+ operation = context.get_resource("fix_operation")
+ now = context.get_resource("fix_time")
+ if operation == "insert" and ("create_time" not in record or _teaql_is_null(record["create_time"])):
+ record["create_time"] = Value.from_any(now)
+ context.record_fix_evidence("School", "create_time", "clock", "graphClock")
+
+ if operation == "insert" and ("update_time" not in record or _teaql_is_null(record["update_time"])):
+ record["update_time"] = Value.from_any(now)
+ context.record_fix_evidence("School", "update_time", "clock", "graphClock")
+ if operation == "insert" or operation == "update":
+ record["update_time"] = Value.from_any(now)
+ context.record_fix_evidence("School", "update_time", "clock", "graphClock")
+
+
+ if (operation == "insert" and "platform" not in record) or ("platform" in record and _teaql_is_null(record["platform"])):
+ results.append(CheckResult("required", ObjectLocation().property("platform")))
+
+ if (operation == "insert" and "school_type" not in record) or ("school_type" in record and _teaql_is_null(record["school_type"])):
+ results.append(CheckResult("required", ObjectLocation().property("school_type")))
+
+ if (operation == "insert" and "name" not in record) or ("name" in record and _teaql_is_null(record["name"])):
+ results.append(CheckResult("required", ObjectLocation().property("name")))
+ if "name" in record and _teaql_raw(record["name"]) is not None and len(_teaql_raw(record["name"])) > 100:
+ results.append(CheckResult("max_length", ObjectLocation().property("name"), _teaql_raw(record["name"]), 100))
+
+ if (operation == "insert" and "address" not in record) or ("address" in record and _teaql_is_null(record["address"])):
+ results.append(CheckResult("required", ObjectLocation().property("address")))
+ if "address" in record and _teaql_raw(record["address"]) is not None and len(_teaql_raw(record["address"])) > 100:
+ results.append(CheckResult("max_length", ObjectLocation().property("address"), _teaql_raw(record["address"]), 100))
+
+ if (operation == "insert" and "established_date" not in record) or ("established_date" in record and _teaql_is_null(record["established_date"])):
+ results.append(CheckResult("required", ObjectLocation().property("established_date")))
+
+ if (operation == "insert" and "student_capacity" not in record) or ("student_capacity" in record and _teaql_is_null(record["student_capacity"])):
+ results.append(CheckResult("required", ObjectLocation().property("student_capacity")))
+
+ if (operation == "insert" and "active" not in record) or ("active" in record and _teaql_is_null(record["active"])):
+ results.append(CheckResult("required", ObjectLocation().property("active")))
+
+ if (operation == "insert" and "create_time" not in record) or ("create_time" in record and _teaql_is_null(record["create_time"])):
+ results.append(CheckResult("required", ObjectLocation().property("create_time")))
+
+ if (operation == "insert" and "update_time" not in record) or ("update_time" in record and _teaql_is_null(record["update_time"])):
+ results.append(CheckResult("required", ObjectLocation().property("update_time")))
+
+
+
+_Platform_DESCRIPTOR = (EntityDescriptor("Platform")
+ .audit_mask_fields(["name"])
+ .table_name("platform_data").property(PropertyDescriptor("id", DataType.I64).column_name("id").log_policy("plain").is_id().required()).property(PropertyDescriptor("name", DataType.Text).column_name("name").log_policy("plain").required()).property(PropertyDescriptor("base_url", DataType.Text).column_name("base_url").log_policy("plain").required()).property(PropertyDescriptor("create_time", DataType.Timestamp).column_name("create_time").log_policy("plain").required()).property(PropertyDescriptor("update_time", DataType.Timestamp).column_name("update_time").log_policy("plain").required()).property(PropertyDescriptor("version", DataType.I64).column_name("version").log_policy("plain").is_version().required()).relation(RelationDescriptor("school_type_list", "SchoolType").local("id").foreign("platform").many()).relation(RelationDescriptor("school_list", "School").local("id").foreign("platform").many())
+)
+
+_SchoolType_DESCRIPTOR = (EntityDescriptor("SchoolType")
+ .audit_mask_fields([])
+ .table_name("school_type_data").property(PropertyDescriptor("platform", DataType.I64).column_name("platform").log_policy("plain").required()).property(PropertyDescriptor("id", DataType.I64).column_name("id").log_policy("plain").is_id().required()).property(PropertyDescriptor("name", DataType.Text).column_name("name").log_policy("plain").required()).property(PropertyDescriptor("code", DataType.Text).column_name("code").log_policy("plain").required()).property(PropertyDescriptor("display_order", DataType.Decimal).column_name("display_order").log_policy("plain").required()).property(PropertyDescriptor("version", DataType.I64).column_name("version").log_policy("plain").is_version().required()).relation(RelationDescriptor("platform", "Platform").local("platform").foreign("id")).relation(RelationDescriptor("school_list", "School").local("id").foreign("school_type").many())
+)
+
+_School_DESCRIPTOR = (EntityDescriptor("School")
+ .audit_mask_fields(["name"])
+ .table_name("school_data").property(PropertyDescriptor("id", DataType.I64).column_name("id").log_policy("plain").is_id().required()).property(PropertyDescriptor("platform", DataType.I64).column_name("platform").log_policy("plain").required()).property(PropertyDescriptor("school_type", DataType.I64).column_name("school_type").log_policy("plain").required()).property(PropertyDescriptor("name", DataType.Text).column_name("name").log_policy("plain").required()).property(PropertyDescriptor("address", DataType.Text).column_name("address").log_policy("plain").required()).property(PropertyDescriptor("established_date", DataType.Date).column_name("established_date").log_policy("plain").required()).property(PropertyDescriptor("student_capacity", DataType.I64).column_name("student_capacity").log_policy("plain").required()).property(PropertyDescriptor("active", DataType.Bool).column_name("active").log_policy("plain").required()).property(PropertyDescriptor("create_time", DataType.Timestamp).column_name("create_time").log_policy("plain").required()).property(PropertyDescriptor("update_time", DataType.Timestamp).column_name("update_time").log_policy("plain").required()).property(PropertyDescriptor("version", DataType.I64).column_name("version").log_policy("plain").is_version().required()).relation(RelationDescriptor("platform", "Platform").local("platform").foreign("id")).relation(RelationDescriptor("school_type", "SchoolType").local("school_type").foreign("id"))
+)
+
+async def _ensure_generated_bootstrap_once(context):
+ previous_actor = context.user_identifier() if hasattr(context, 'user_identifier') else None
+ previous_category = context.get_resource('bootstrapCategory')
+ if hasattr(context, 'set_user_identifier'):
+ context.set_user_identifier('teaql-generated-bootstrap')
+ context.insert_resource('bootstrapCategory', 'runtime-bootstrap')
+ try:
+ platform_1 = await (Q.platforms().with_id_is(1).comment('what: locate generated bootstrap entity').purpose('why: idempotent runtime bootstrap').execute_for_one(context))
+ if platform_1 is None:
+ platform_1 = Platform._teaql_new_with_fixed_id(1)
+ platform_1.update_name("Campus Learning Platform")
+ platform_1.update_base_url("https://campus.example.com")
+ try:
+ await platform_1.audit_as('create model root Platform(1)').save(context)
+ except Exception as _teaql_create_error:
+ for _teaql_attempt in range(5):
+ platform_1 = await (Q.platforms().with_id_is(1).comment('what: recover concurrent bootstrap').purpose('why: make generated bootstrap idempotent').execute_for_one(context))
+ if platform_1 is not None:
+ break
+ if _teaql_attempt < 4:
+ await asyncio.sleep((_teaql_attempt + 1) * 0.01)
+ if platform_1 is None:
+ raise _teaql_create_error
+ context.with_active_root(ContextEntityRef("Platform", 1))
+ school_type_1001 = await (Q.school_types().with_id_is(1001).comment('what: locate generated bootstrap entity').purpose('why: idempotent runtime bootstrap').execute_for_one(context))
+ if school_type_1001 is None:
+ school_type_1001 = SchoolType._teaql_new_with_fixed_id(1001)
+ school_type_1001.update_platform(Platform.refer(1))
+ school_type_1001.update_name("Primary")
+ school_type_1001.update_code("PRIMARY")
+ school_type_1001.update_display_order(1)
+ try:
+ await school_type_1001.audit_as('create model constant SchoolType(1001)').save(context)
+ except Exception as _teaql_create_error:
+ for _teaql_attempt in range(5):
+ school_type_1001 = await (Q.school_types().with_id_is(1001).comment('what: recover concurrent bootstrap').purpose('why: make generated bootstrap idempotent').execute_for_one(context))
+ if school_type_1001 is not None:
+ break
+ if _teaql_attempt < 4:
+ await asyncio.sleep((_teaql_attempt + 1) * 0.01)
+ if school_type_1001 is None:
+ raise _teaql_create_error
+ _teaql_changed = False
+ if school_type_1001.platform != 1:
+ school_type_1001.update_platform(Platform.refer(1))
+ _teaql_changed = True
+ if school_type_1001.name != "Primary":
+ school_type_1001.update_name("Primary")
+ _teaql_changed = True
+ if school_type_1001.code != "PRIMARY":
+ school_type_1001.update_code("PRIMARY")
+ _teaql_changed = True
+ if school_type_1001.displayOrder != 1:
+ school_type_1001.update_display_order(1)
+ _teaql_changed = True
+ if _teaql_changed:
+ await school_type_1001.audit_as('reconcile model constant SchoolType(1001)').save(context)
+ school_type_1002 = await (Q.school_types().with_id_is(1002).comment('what: locate generated bootstrap entity').purpose('why: idempotent runtime bootstrap').execute_for_one(context))
+ if school_type_1002 is None:
+ school_type_1002 = SchoolType._teaql_new_with_fixed_id(1002)
+ school_type_1002.update_platform(Platform.refer(1))
+ school_type_1002.update_name("Secondary")
+ school_type_1002.update_code("SECONDARY")
+ school_type_1002.update_display_order(2)
+ try:
+ await school_type_1002.audit_as('create model constant SchoolType(1002)').save(context)
+ except Exception as _teaql_create_error:
+ for _teaql_attempt in range(5):
+ school_type_1002 = await (Q.school_types().with_id_is(1002).comment('what: recover concurrent bootstrap').purpose('why: make generated bootstrap idempotent').execute_for_one(context))
+ if school_type_1002 is not None:
+ break
+ if _teaql_attempt < 4:
+ await asyncio.sleep((_teaql_attempt + 1) * 0.01)
+ if school_type_1002 is None:
+ raise _teaql_create_error
+ _teaql_changed = False
+ if school_type_1002.platform != 1:
+ school_type_1002.update_platform(Platform.refer(1))
+ _teaql_changed = True
+ if school_type_1002.name != "Secondary":
+ school_type_1002.update_name("Secondary")
+ _teaql_changed = True
+ if school_type_1002.code != "SECONDARY":
+ school_type_1002.update_code("SECONDARY")
+ _teaql_changed = True
+ if school_type_1002.displayOrder != 2:
+ school_type_1002.update_display_order(2)
+ _teaql_changed = True
+ if _teaql_changed:
+ await school_type_1002.audit_as('reconcile model constant SchoolType(1002)').save(context)
+ finally:
+ if hasattr(context, 'set_user_identifier'):
+ context.set_user_identifier(previous_actor)
+ context.insert_resource('bootstrapCategory', previous_category)
+
+async def _ensure_generated_bootstrap(context):
+ for _teaql_attempt in range(5):
+ try:
+ await _ensure_generated_bootstrap_once(context)
+ return
+ except Exception:
+ if _teaql_attempt == 4:
+ raise
+ await asyncio.sleep((_teaql_attempt + 1) * 0.01)
+
+
+# Passive generated manifest. Call ensure_schema() separately and explicitly.
+GENERATED_RUNTIME_MODULE = (RuntimeModule().entity(Platform)
+ .schema_entity(_Platform_DESCRIPTOR)
+ .checker("Platform", _PlatformChecker())
+ .wire_metadata("Platform", create_wire_entity_metadata("Platform", ["id", "name", "base_url", "create_time", "update_time", "version"], JsonFieldNamingProfile.CAMEL_CASE, {"id": ["id"], "name": ["name"], "base_url": ["base_url"], "create_time": ["create_time"], "update_time": ["update_time"], "version": ["version"]})).entity(SchoolType)
+ .schema_entity(_SchoolType_DESCRIPTOR)
+ .checker("SchoolType", _SchoolTypeChecker())
+ .wire_metadata("SchoolType", create_wire_entity_metadata("SchoolType", ["platform", "id", "name", "code", "display_order", "version"], JsonFieldNamingProfile.CAMEL_CASE, {"platform": ["platform"], "id": ["id"], "name": ["name"], "code": ["code"], "display_order": ["display_order"], "version": ["version"]})).entity(School)
+ .schema_entity(_School_DESCRIPTOR)
+ .checker("School", _SchoolChecker())
+ .wire_metadata("School", create_wire_entity_metadata("School", ["id", "platform", "school_type", "name", "address", "established_date", "student_capacity", "active", "create_time", "update_time", "version"], JsonFieldNamingProfile.CAMEL_CASE, {"id": ["id"], "platform": ["platform"], "school_type": ["school_type"], "name": ["name"], "address": ["address"], "established_date": ["established_date"], "student_capacity": ["student_capacity"], "active": ["active"], "create_time": ["create_time"], "update_time": ["update_time"], "version": ["version"]}))
+ .generated_bootstrap(_ensure_generated_bootstrap)
+)
\ No newline at end of file
diff --git a/examples/facet-trace/lib/teaql-i18n.json b/examples/facet-trace/lib/teaql-i18n.json
new file mode 100644
index 0000000..21b3f9b
--- /dev/null
+++ b/examples/facet-trace/lib/teaql-i18n.json
@@ -0,0 +1,122 @@
+{
+ "schema": "teaql.i18n/v1",
+ "defaultLocale": "en",
+ "locales": {
+ "de": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "ko": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "pt": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "zh-TW": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "fil": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "en": {
+ "vocabulary": {
+ "property.platform.name": "Name",
+ "property.platform.createTime": "Create Time",
+ "property.platform.updateTime": "Update Time",
+ "property.school.platform": "Platform",
+ "property.school.id": "Id",
+ "property.schoolType.version": "Version",
+ "property.school.establishedDate": "Established Date",
+ "property.schoolType.code": "Code",
+ "property.school.studentCapacity": "Student Capacity",
+ "property.school.active": "Active",
+ "property.school.schoolType": "School Type",
+ "entity.platform": "Platform",
+ "property.platform.id": "Id",
+ "property.schoolType.displayOrder": "Display Order",
+ "property.schoolType.id": "Id",
+ "entity.school": "School",
+ "property.school.name": "Name",
+ "property.school.createTime": "Create Time",
+ "property.school.updateTime": "Update Time",
+ "entity.schoolType": "School Type",
+ "property.platform.baseUrl": "Base Url",
+ "property.platform.version": "Version",
+ "property.schoolType.platform": "Platform",
+ "property.school.version": "Version",
+ "property.school.address": "Address",
+ "property.schoolType.name": "Name"
+ },
+ "messages": {
+ }
+ },
+ "fr": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "zh-CN": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "es": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "ar": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "vi": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "th": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "uk": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "ja": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "id": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/facet-trace/main.py b/examples/facet-trace/main.py
new file mode 100644
index 0000000..c04f59a
--- /dev/null
+++ b/examples/facet-trace/main.py
@@ -0,0 +1,192 @@
+import asyncio
+import copy
+import json
+import os
+from types import SimpleNamespace
+from Q import Q
+from E import E, TeaQLNotLoadedError
+from models.school import School
+from runtime_module import GENERATED_RUNTIME_MODULE
+from teaql.core.list import SmartList
+from teaql.provider.sqlite import create_sqlite_service
+from teaql.runtime import UserContext
+
+PRIVATE = 'FACET-PRIVATE-SCHOOL'
+FUTURE = 'Campus Learning Platform'
+PURPOSE = 'verify generated Facet ownership'
+
+def school_facets(include_all, nested):
+ types = Q.school_types().order_by_id_ascending().limit(10).count_as('school_count')
+ if nested:
+ types.facet_by_platform_as('platforms', Q.platforms().with_name_is(FUTURE)
+ .order_by_id_ascending().limit(10).count_as('type_count'), include_all)
+ return (Q.schools().with_name_in(PRIVATE, PRIVATE + '-B')
+ .order_by_id_ascending().limit(1)
+ .facet_by_school_type_as('types', types, include_all))
+
+def assert_facets(rows, include_all, count, nested):
+ assert isinstance(rows, SmartList), 'loaded relation must remain SmartList'
+ types = rows.facet('types')
+ assert isinstance(types, SmartList), 'requested empty Facet must remain present'
+ pairs = [(row['id'], row['school_count']) for row in types]
+ assert all(type(row['school_count']) is int for row in types), 'missing count is not zero'
+ expected = [(1001, count), (1002, 0)] if include_all else ([(1001, count)] if count else [])
+ assert pairs == expected, (pairs, expected)
+ observation = {'types': pairs}
+ if nested:
+ platforms = types.facet('platforms')
+ assert isinstance(platforms, SmartList), 'nested Facet was dropped'
+ expected = [(1, 2)] if include_all else ([(1, 1)] if count else [])
+ actual = [(row['id'], row['type_count']) for row in platforms]
+ assert actual == expected, ('nested membership', actual, expected)
+ observation['platforms'] = actual
+ return observation
+
+def log_row(entry):
+ return {'comment': entry.comment, 'purpose': entry.purpose,
+ 'outcome': entry.execution_outcome, 'sql': entry.sql,
+ 'debug_sql': entry.debug_sql, 'params': [getattr(value, 'val', value) for value in entry.params],
+ 'path': [(n.kind, n.name, n.comment) for n in entry.trace_path]}
+
+def assert_observed(context, diagnostics, reads, raw, writes, loaded, nested, logging, comment):
+ child = [('school_list', 'SchoolType.school_list')] if loaded else []
+ first = child + [('school_type', 'School.school_type')]
+ second = first + [('platform', 'SchoolType.platform')]
+ branch = [child, first] + ([first, second] if nested else [])
+ routes = [[], child, *branch, *branch] if loaded else [[], [], first] + ([first, second] if nested else [])
+ assert len(reads) == len(routes), ('physical SQL count', len(reads), routes)
+ assert sum('COUNT(' in sql.upper() for sql, _ in reads) == (4 if loaded else 2 if nested else 1)
+ assert not writes, 'Facet reads must not write query metadata or entities'
+ assert len(raw) == len(reads), 'logging-off must retain actual execution metadata'
+ root = 'SchoolType' if loaded else 'School'
+ for entry, route, (sql, _) in zip(raw, routes, reads):
+ assert entry['sql'] == sql
+ assert entry['outcome'] == 'success'
+ assert entry['purpose'] == PURPOSE
+ assert entry['comment'] == comment, 'raw metadata must retain original request-owned intent'
+ assert entry['path'] == [('operation', root, 'query'), ('request', root, ''),
+ *[('relation', name, detail) for name, detail in route],
+ ('provider', 'sqlite', ''), ('sql', 'select', '')]
+ entries = context.sql_logs()
+ assert len(entries) == len(diagnostics) == (len(reads) if logging else 0)
+ if not logging:
+ return
+ for entry, route in zip(entries, routes):
+ assert entry.execution_outcome == 'success'
+ assert entry.purpose == PURPOSE
+ assert entry.comment.startswith('load ')
+ assert [(n.kind, n.name, n.comment) for n in entry.trace_path] == [
+ ('operation', root, 'query'), ('request', root, ''),
+ *[('relation', name, detail) for name, detail in route],
+ ('provider', 'sqlite', ''), ('sql', 'select', '')]
+ safe = json.dumps([log_row(entry) for entry in entries + diagnostics])
+ assert PRIVATE not in safe, 'private root binding leaked'
+ if nested:
+ assert FUTURE not in safe, 'future-only Facet binding leaked before first root SQL'
+
+async def main():
+ service = create_sqlite_service(os.environ['TEAQL_FACET_TRACE_DB'])
+ context = UserContext.new().install(GENERATED_RUNTIME_MODULE).insert_resource('dataService', service)
+ diagnostics, reads, policies, raw, writes = [], [], [], [], []
+ context.set_diagnostic_sql_log_sink(SimpleNamespace(write=diagnostics.append))
+ await context.ensure_schema()
+ for suffix in ('', '-B'):
+ name = PRIVATE + suffix
+ existing = await (Q.schools().with_name_is(name).limit(1).comment('find retained fixture')
+ .purpose('idempotent seed').execute_for_list(context))
+ if not existing:
+ school = Q.schools().comment('initialize fixture').purpose('idempotent seed').new_entity(context)
+ (school.update_platform(1).update_school_type(1001).update_name(name)
+ .update_address('12 River Road').update_established_date('1995-09-01')
+ .update_student_capacity(800).update_active(True))
+ await school.audit_as('seed generated Facet acceptance').save(context)
+ original_fetch = service.transport.fetch_all_sql
+ async def capture(compiled):
+ reads.append((compiled.sql, [value.val for value in compiled.params]))
+ return await original_fetch(compiled)
+ service.transport.fetch_all_sql = capture
+ original_execute = service.transport.execute_sql
+ async def capture_write(compiled):
+ writes.append(compiled.sql)
+ return await original_execute(compiled)
+ service.transport.execute_sql = capture_write
+ original_record = context._record_metadata_log
+ def capture_metadata(metadata, *args, **kwargs):
+ # Observe runtime-produced frames without injecting expected lineage.
+ raw.append({'comment': metadata.comment, 'purpose': metadata.purpose,
+ 'outcome': metadata.execution_outcome, 'sql': metadata.parameterized_sql,
+ 'path': [(n.kind, n.name, n.comment) for n in metadata.trace_chain]})
+ return original_record(metadata, *args, **kwargs)
+ context._record_metadata_log = capture_metadata
+ def policy(query):
+ policies.append(query.comment_text)
+ return query
+ context.with_request_policy(policy)
+ failures, cases = [], 0
+ for mode in ('root', 'nested', 'loaded'):
+ for include_all in (True, False):
+ for logging in (True, False):
+ for empty in ((False,) if mode == 'loaded' else (False, True)):
+ label = (mode, include_all, logging, empty)
+ context.enable_all_sql_log() if logging else context.disable_sql_log()
+ context.clear_sql_logs(); diagnostics.clear(); reads.clear(); policies.clear(); raw.clear(); writes.clear()
+ nested = mode != 'root'
+ request = school_facets(include_all, nested)
+ if empty: request.with_name_is('absent')
+ if mode == 'loaded':
+ request.top_n_probe_parent_threshold(0)
+ request = (Q.school_types().order_by_id_ascending().limit(10)
+ .select_school_list_with(request))
+ comment = 'load ' + PRIVATE + (' via ' + FUTURE if nested else '')
+ executable = request.comment(comment).purpose(PURPOSE)
+ before = copy.deepcopy(request.query)
+ try:
+ rows = await executable.execute_for_list(context)
+ assert request.query == before, 'execution mutated caller query'
+ assert policies[0] == comment, 'safe projection changed policy intent'
+ assert_observed(context, diagnostics, reads, raw, writes, mode == 'loaded', nested, logging, comment)
+ if mode == 'loaded':
+ assert [E.school_type(parent).id().eval() for parent in rows] == [1001, 1002]
+ results = []
+ for parent in rows:
+ own_count = 2 if E.school_type(parent).id().eval() == 1001 else 0
+ assert E.school_type(parent).school_list().size().eval() == int(bool(own_count))
+ children = parent.school_list()
+ if children:
+ assert isinstance(children[0], School)
+ assert E.school(children[0]).name().eval() == PRIVATE
+ results.append({'parent_id': E.school_type(parent).id().eval(),
+ 'visible_rows': len(children),
+ 'facets': assert_facets(children, include_all, own_count, True)})
+ else:
+ assert len(rows) == (0 if empty else 1)
+ if rows:
+ assert isinstance(rows[0], School)
+ assert E.school(rows[0]).name().eval() == PRIVATE
+ results = [{'visible_rows': len(rows),
+ 'facets': assert_facets(rows, include_all, 0 if empty else 2, nested)}]
+ assert any(PRIVATE in params for _, params in reads), 'real root bind was changed'
+ if nested:
+ assert any(FUTURE in params for _, params in reads), 'real future bind was changed'
+ print('FACET_OBSERVED ' + json.dumps({'case': label, 'calls': len(reads),
+ 'counts': sum('COUNT(' in sql.upper() for sql, _ in reads),
+ 'results': results,
+ 'raw': raw, 'writes': writes,
+ 'sql': [log_row(entry) for entry in context.sql_logs()]}))
+ cases += 1
+ except Exception as error:
+ failures.append((label, type(error).__name__, str(error)))
+ print('FACET_FAILURE', failures[-1])
+ context.enable_all_sql_log(); context.clear_sql_logs(); diagnostics.clear(); reads.clear()
+ independent = await (Q.school_types().with_id_is(1001).limit(1)
+ .comment('independent ' + PRIVATE).purpose('no inherited Facet redaction').execute_for_list(context))
+ assert context.sql_logs()[0].comment == 'independent ' + PRIVATE
+ try: E.school_type(independent[0]).school_list().size().eval()
+ except TeaQLNotLoadedError: pass
+ else: raise AssertionError('unselected relation stopped being NotLoaded')
+ await service.close()
+ assert not failures, failures
+ assert cases == 20, cases
+ print('PASS: Python generated Facets; 20 root/nested/loaded/empty/includeAll/logging scenarios; Q/E/save; retained SQLite')
+
+asyncio.run(main())
diff --git a/examples/facet-trace/model.xml b/examples/facet-trace/model.xml
new file mode 100644
index 0000000..f114b40
--- /dev/null
+++ b/examples/facet-trace/model.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+ <_value id="1001" name="Primary" code="PRIMARY" display_order="1"/>
+ <_value id="1002" name="Secondary" code="SECONDARY" display_order="2"/>
+
+
+
+
diff --git a/examples/facet-trace/verify.sh b/examples/facet-trace/verify.sh
new file mode 100644
index 0000000..90d9d7f
--- /dev/null
+++ b/examples/facet-trace/verify.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+set -euo pipefail
+example="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+repo="$(cd "$example/../.." && pwd)"
+facet_run_directory="$(mktemp -d -t teaql-python-facet.XXXXXX)"
+export TEAQL_FACET_TRACE_DB="$facet_run_directory/school.sqlite"
+export PYTHONDONTWRITEBYTECODE=1
+# Runtime examples always exercise this repository, never an installed release.
+export PYTHONPATH="$example/lib:$repo/src"
+unset TEAQL_ALLOW_SENSITIVE_PLAINTEXT_LOGS
+(cd "$example/lib" && rg --files --hidden -g '!**/__pycache__/**' | LC_ALL=C sort | xargs -d '\n' sha256sum) > "$facet_run_directory/library-before.sha256"
+for round in first second; do
+ log="$facet_run_directory/$round.log"
+ if ! timeout --kill-after=5s 60s python -B "$example/main.py" > "$log" 2>&1; then
+ tail -80 "$log" >&2
+ echo "FAIL: generated Facet $round; retained evidence $facet_run_directory" >&2
+ exit 1
+ fi
+ [[ "$(rg -c '^FACET_OBSERVED ' "$log")" == 20 ]]
+ rg -Fxq 'PASS: Python generated Facets; 20 root/nested/loaded/empty/includeAll/logging scenarios; Q/E/save; retained SQLite' "$log"
+ echo "PASS: generated Facet $round, 20 scenarios; same database $TEAQL_FACET_TRACE_DB; log $log"
+done
+(cd "$example/lib" && sha256sum --check "$facet_run_directory/library-before.sha256")
+echo "PASS: generated Facet library unchanged; retained evidence $facet_run_directory"
diff --git a/examples/mutation-policy/main.py b/examples/mutation-policy/main.py
index 008ed06..e31380a 100644
--- a/examples/mutation-policy/main.py
+++ b/examples/mutation-policy/main.py
@@ -127,14 +127,15 @@ def context_for(provider, audit):
async def save(context, *commands):
- async def graph():
- transaction = context.require_resource("dataService")
+ async def graph(session):
+ context = session.context
+ transaction = session.transaction
for command in commands:
context.preflight_mutation(command)
for command in commands:
- await transaction.mutate(context, MutationRequest(command))
+ await transaction.mutate(context, MutationRequest(command, comment='what: runtime regression fixture'))
- await context.execute_graph_save(graph)
+ await context.execute_graph_save(graph, comment='what: runtime regression fixture')
async def main():
diff --git a/examples/order-management/python-lib-core/Q.py b/examples/order-management/python-lib-core/Q.py
index ad178bf..22a4514 100644
--- a/examples/order-management/python-lib-core/Q.py
+++ b/examples/order-management/python-lib-core/Q.py
@@ -64,3 +64,4 @@ def order_search_presets() -> OrderSearchPresetRequest:
def order_search_presets_minimal() -> OrderSearchPresetRequest:
return OrderSearchPresetRequest(minimal=True)
+
diff --git a/examples/order-management/python-lib-core/models/commerce_platform.py b/examples/order-management/python-lib-core/models/commerce_platform.py
index a0b2a83..6ac5876 100644
--- a/examples/order-management/python-lib-core/models/commerce_platform.py
+++ b/examples/order-management/python-lib-core/models/commerce_platform.py
@@ -1,8 +1,11 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
+
class CommercePlatform:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -35,10 +38,15 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.name = kwargs.get("name")
+
self.createTime = kwargs.get("createTime")
+
self.updateTime = kwargs.get("updateTime")
+
self.version = kwargs.get("version")
+
self._customer_list = kwargs.get("customer_list", [])
if "customer_list" in kwargs or kwargs.get("id") is None:
self._loaded_fields.add("customer_list")
@@ -60,37 +68,37 @@ def __init__(self, **kwargs):
if self._customer_list:
from models.customer import Customer
self._customer_list = [
- item if isinstance(item, Customer) else Customer(**item)
+ item if isinstance(item, Customer) else Customer(_entity_root=self._entity_root, **item)
for item in self._customer_list
]
if self._order_status_list:
from models.order_status import OrderStatus
self._order_status_list = [
- item if isinstance(item, OrderStatus) else OrderStatus(**item)
+ item if isinstance(item, OrderStatus) else OrderStatus(_entity_root=self._entity_root, **item)
for item in self._order_status_list
]
if self._customer_order_list:
from models.customer_order import CustomerOrder
self._customer_order_list = [
- item if isinstance(item, CustomerOrder) else CustomerOrder(**item)
+ item if isinstance(item, CustomerOrder) else CustomerOrder(_entity_root=self._entity_root, **item)
for item in self._customer_order_list
]
if self._product_list:
from models.product import Product
self._product_list = [
- item if isinstance(item, Product) else Product(**item)
+ item if isinstance(item, Product) else Product(_entity_root=self._entity_root, **item)
for item in self._product_list
]
if self._order_line_list:
from models.order_line import OrderLine
self._order_line_list = [
- item if isinstance(item, OrderLine) else OrderLine(**item)
+ item if isinstance(item, OrderLine) else OrderLine(_entity_root=self._entity_root, **item)
for item in self._order_line_list
]
if self._order_search_preset_list:
from models.order_search_preset import OrderSearchPreset
self._order_search_preset_list = [
- item if isinstance(item, OrderSearchPreset) else OrderSearchPreset(**item)
+ item if isinstance(item, OrderSearchPreset) else OrderSearchPreset(_entity_root=self._entity_root, **item)
for item in self._order_search_preset_list
]
self._ledger_id = getattr(self, "id", None)
@@ -101,13 +109,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("CommercePlatform", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
for child in self._customer_list:
child._teaql_attach_root(root)
@@ -128,30 +139,92 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("CommercePlatform"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._customer_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+ for child in self._order_status_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+ for child in self._customer_order_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+ for child in self._product_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+ for child in self._order_line_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+ for child in self._order_search_preset_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "name" in self._loaded_fields:
payload["name"] = Value.Text(self.name)
+
if "createTime" in self._loaded_fields:
payload["create_time"] = Value.DateTime(self.createTime)
+
if "updateTime" in self._loaded_fields:
payload["update_time"] = Value.DateTime(self.updateTime)
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -159,42 +232,47 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("CommercePlatform", payload)
elif action == "Update":
- cmd = UpdateCommand("CommercePlatform", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("CommercePlatform", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("CommercePlatform", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("CommercePlatform", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "name" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
- if "createTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
- if "updateTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "createTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
+ if "updateTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
for index, child in enumerate(self._customer_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "commercePlatform", self)
- child._loaded_fields.add("commercePlatform")
- child._entity_root.set(child._teaql_entity_key(), "commerce_platform", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "commercePlatform", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_commerce_platform(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("customer_list").index(index)
raise CheckException([
@@ -203,12 +281,11 @@ def _teaql_preflight_graph(self, context):
]) from error
for index, child in enumerate(self._order_status_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "commercePlatform", self)
- child._loaded_fields.add("commercePlatform")
- child._entity_root.set(child._teaql_entity_key(), "commerce_platform", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "commercePlatform", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_commerce_platform(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("order_status_list").index(index)
raise CheckException([
@@ -217,12 +294,11 @@ def _teaql_preflight_graph(self, context):
]) from error
for index, child in enumerate(self._customer_order_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "commercePlatform", self)
- child._loaded_fields.add("commercePlatform")
- child._entity_root.set(child._teaql_entity_key(), "commerce_platform", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "commercePlatform", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_commerce_platform(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("customer_order_list").index(index)
raise CheckException([
@@ -231,12 +307,11 @@ def _teaql_preflight_graph(self, context):
]) from error
for index, child in enumerate(self._product_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "commercePlatform", self)
- child._loaded_fields.add("commercePlatform")
- child._entity_root.set(child._teaql_entity_key(), "commerce_platform", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "commercePlatform", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_commerce_platform(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("product_list").index(index)
raise CheckException([
@@ -245,12 +320,11 @@ def _teaql_preflight_graph(self, context):
]) from error
for index, child in enumerate(self._order_line_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "commercePlatform", self)
- child._loaded_fields.add("commercePlatform")
- child._entity_root.set(child._teaql_entity_key(), "commerce_platform", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "commercePlatform", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_commerce_platform(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("order_line_list").index(index)
raise CheckException([
@@ -259,12 +333,11 @@ def _teaql_preflight_graph(self, context):
]) from error
for index, child in enumerate(self._order_search_preset_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "commercePlatform", self)
- child._loaded_fields.add("commercePlatform")
- child._entity_root.set(child._teaql_entity_key(), "commerce_platform", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "commercePlatform", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_commerce_platform(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("order_search_preset_list").index(index)
raise CheckException([
@@ -272,17 +345,19 @@ def _teaql_preflight_graph(self, context):
for v in error.violations
]) from error
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -333,6 +408,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -345,45 +421,50 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
- cascade_relations.append(("customer_list", self._customer_list, "update_commerce_platform"))
- cascade_relations.append(("order_status_list", self._order_status_list, "update_commerce_platform"))
- cascade_relations.append(("customer_order_list", self._customer_order_list, "update_commerce_platform"))
- cascade_relations.append(("product_list", self._product_list, "update_commerce_platform"))
- cascade_relations.append(("order_line_list", self._order_line_list, "update_commerce_platform"))
- cascade_relations.append(("order_search_preset_list", self._order_search_preset_list, "update_commerce_platform"))
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("customer_list", self._customer_list, "update_commerce_platform", "commercePlatform"))
+ cascade_relations.append(("order_status_list", self._order_status_list, "update_commerce_platform", "commercePlatform"))
+ cascade_relations.append(("customer_order_list", self._customer_order_list, "update_commerce_platform", "commercePlatform"))
+ cascade_relations.append(("product_list", self._product_list, "update_commerce_platform", "commercePlatform"))
+ cascade_relations.append(("order_line_list", self._order_line_list, "update_commerce_platform", "commercePlatform"))
+ cascade_relations.append(("order_search_preset_list", self._order_search_preset_list, "update_commerce_platform", "commercePlatform"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/order-management/python-lib-core/models/customer.py b/examples/order-management/python-lib-core/models/customer.py
index c8cd7ab..ff0b9c5 100644
--- a/examples/order-management/python-lib-core/models/customer.py
+++ b/examples/order-management/python-lib-core/models/customer.py
@@ -1,9 +1,12 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
from models.commerce_platform import CommercePlatform
+
class Customer:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -40,12 +43,19 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.name = kwargs.get("name")
+
self.email = kwargs.get("email")
+
self.commercePlatform = kwargs.get("commercePlatform")
+
self.createTime = kwargs.get("createTime")
+
self.updateTime = kwargs.get("updateTime")
+
self.version = kwargs.get("version")
+
if isinstance(self.commercePlatform, dict):
self.commercePlatform = CommercePlatform(**self.commercePlatform)
self._customer_order_list = kwargs.get("customer_order_list", [])
@@ -54,7 +64,7 @@ def __init__(self, **kwargs):
if self._customer_order_list:
from models.customer_order import CustomerOrder
self._customer_order_list = [
- item if isinstance(item, CustomerOrder) else CustomerOrder(**item)
+ item if isinstance(item, CustomerOrder) else CustomerOrder(_entity_root=self._entity_root, **item)
for item in self._customer_order_list
]
self._ledger_id = getattr(self, "id", None)
@@ -65,13 +75,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("Customer", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
for child in self._customer_order_list:
child._teaql_attach_root(root)
@@ -82,34 +95,87 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("Customer"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._customer_order_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "name" in self._loaded_fields:
payload["name"] = Value.Text(self.name)
+
if "email" in self._loaded_fields:
payload["email"] = Value.Text(self.email)
+
if "commercePlatform" in self._loaded_fields:
- payload["commerce_platform"] = Value.Object(self.commercePlatform)
+ reference = self.commercePlatform
+ reference_id = getattr(reference, "id", reference)
+ payload["commerce_platform"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "createTime" in self._loaded_fields:
payload["create_time"] = Value.DateTime(self.createTime)
+
if "updateTime" in self._loaded_fields:
payload["update_time"] = Value.DateTime(self.updateTime)
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -117,46 +183,51 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("Customer", payload)
elif action == "Update":
- cmd = UpdateCommand("Customer", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("Customer", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("Customer", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("Customer", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "name" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
- if "email" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("email"), message="Mutation requires a fully loaded entity")])
- if "commercePlatform" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("commerce_platform"), message="Mutation requires a fully loaded entity")])
- if "createTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
- if "updateTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "email" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("email"), message="Mutation requires a fully loaded entity")])
+ if "commercePlatform" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("commerce_platform"), message="Mutation requires a fully loaded entity")])
+ if "createTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
+ if "updateTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
for index, child in enumerate(self._customer_order_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "customer", self)
- child._loaded_fields.add("customer")
- child._entity_root.set(child._teaql_entity_key(), "customer", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "customer", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_customer(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("customer_order_list").index(index)
raise CheckException([
@@ -164,17 +235,19 @@ def _teaql_preflight_graph(self, context):
for v in error.violations
]) from error
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -237,6 +310,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -249,40 +323,45 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
- cascade_relations.append(("customer_order_list", self._customer_order_list, "update_customer"))
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("customer_order_list", self._customer_order_list, "update_customer", "customer"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/order-management/python-lib-core/models/customer_order.py b/examples/order-management/python-lib-core/models/customer_order.py
index 7eb8c5c..c188d64 100644
--- a/examples/order-management/python-lib-core/models/customer_order.py
+++ b/examples/order-management/python-lib-core/models/customer_order.py
@@ -1,4 +1,6 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
@@ -6,6 +8,7 @@
from models.customer import Customer
from models.commerce_platform import CommercePlatform
+
class CustomerOrder:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -48,15 +51,25 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.orderNumber = kwargs.get("orderNumber")
+
self.orderDate = kwargs.get("orderDate")
+
self.totalAmount = kwargs.get("totalAmount")
+
self.status = kwargs.get("status")
+
self.customer = kwargs.get("customer")
+
self.commercePlatform = kwargs.get("commercePlatform")
+
self.createTime = kwargs.get("createTime")
+
self.updateTime = kwargs.get("updateTime")
+
self.version = kwargs.get("version")
+
if isinstance(self.status, dict):
self.status = OrderStatus(**self.status)
if isinstance(self.customer, dict):
@@ -69,7 +82,7 @@ def __init__(self, **kwargs):
if self._order_line_list:
from models.order_line import OrderLine
self._order_line_list = [
- item if isinstance(item, OrderLine) else OrderLine(**item)
+ item if isinstance(item, OrderLine) else OrderLine(_entity_root=self._entity_root, **item)
for item in self._order_line_list
]
self._ledger_id = getattr(self, "id", None)
@@ -80,13 +93,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("CustomerOrder", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
for child in self._order_line_list:
child._teaql_attach_root(root)
@@ -97,40 +113,104 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("CustomerOrder"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._order_line_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "orderNumber" in self._loaded_fields:
payload["order_number"] = Value.Text(self.orderNumber)
+
if "orderDate" in self._loaded_fields:
payload["order_date"] = Value.Date(self.orderDate)
+
if "totalAmount" in self._loaded_fields:
payload["total_amount"] = Value.Decimal(self.totalAmount)
+
if "status" in self._loaded_fields:
- payload["status"] = Value.Object(self.status)
+ reference = self.status
+ reference_id = getattr(reference, "id", reference)
+ payload["status"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "customer" in self._loaded_fields:
- payload["customer"] = Value.Object(self.customer)
+ reference = self.customer
+ reference_id = getattr(reference, "id", reference)
+ payload["customer"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "commercePlatform" in self._loaded_fields:
- payload["commerce_platform"] = Value.Object(self.commercePlatform)
+ reference = self.commercePlatform
+ reference_id = getattr(reference, "id", reference)
+ payload["commerce_platform"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "createTime" in self._loaded_fields:
payload["create_time"] = Value.DateTime(self.createTime)
+
if "updateTime" in self._loaded_fields:
payload["update_time"] = Value.DateTime(self.updateTime)
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -138,52 +218,57 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("CustomerOrder", payload)
elif action == "Update":
- cmd = UpdateCommand("CustomerOrder", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("CustomerOrder", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("CustomerOrder", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("CustomerOrder", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "orderNumber" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("order_number"), message="Mutation requires a fully loaded entity")])
- if "orderDate" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("order_date"), message="Mutation requires a fully loaded entity")])
- if "totalAmount" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("total_amount"), message="Mutation requires a fully loaded entity")])
- if "status" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("status"), message="Mutation requires a fully loaded entity")])
- if "customer" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("customer"), message="Mutation requires a fully loaded entity")])
- if "commercePlatform" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("commerce_platform"), message="Mutation requires a fully loaded entity")])
- if "createTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
- if "updateTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "orderNumber" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("order_number"), message="Mutation requires a fully loaded entity")])
+ if "orderDate" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("order_date"), message="Mutation requires a fully loaded entity")])
+ if "totalAmount" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("total_amount"), message="Mutation requires a fully loaded entity")])
+ if "status" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("status"), message="Mutation requires a fully loaded entity")])
+ if "customer" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("customer"), message="Mutation requires a fully loaded entity")])
+ if "commercePlatform" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("commerce_platform"), message="Mutation requires a fully loaded entity")])
+ if "createTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
+ if "updateTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
for index, child in enumerate(self._order_line_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "customerOrder", self)
- child._loaded_fields.add("customerOrder")
- child._entity_root.set(child._teaql_entity_key(), "customer_order", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "customerOrder", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_customer_order(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("order_line_list").index(index)
raise CheckException([
@@ -191,17 +276,19 @@ def _teaql_preflight_graph(self, context):
for v in error.violations
]) from error
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -282,6 +369,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -294,40 +382,45 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
- cascade_relations.append(("order_line_list", self._order_line_list, "update_customer_order"))
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("order_line_list", self._order_line_list, "update_customer_order", "customerOrder"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/order-management/python-lib-core/models/order_line.py b/examples/order-management/python-lib-core/models/order_line.py
index d754e9a..5abf5d0 100644
--- a/examples/order-management/python-lib-core/models/order_line.py
+++ b/examples/order-management/python-lib-core/models/order_line.py
@@ -1,4 +1,6 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
@@ -6,6 +8,7 @@
from models.product import Product
from models.commerce_platform import CommercePlatform
+
class OrderLine:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -46,14 +49,23 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.customerOrder = kwargs.get("customerOrder")
+
self.product = kwargs.get("product")
+
self.productName = kwargs.get("productName")
+
self.sku = kwargs.get("sku")
+
self.quantity = kwargs.get("quantity")
+
self.commercePlatform = kwargs.get("commercePlatform")
+
self.createTime = kwargs.get("createTime")
+
self.version = kwargs.get("version")
+
if isinstance(self.customerOrder, dict):
self.customerOrder = CustomerOrder(**self.customerOrder)
if isinstance(self.product, dict):
@@ -68,13 +80,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("OrderLine", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
return self
def mark_for_deletion(self):
@@ -83,38 +98,98 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("OrderLine"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "customerOrder" in self._loaded_fields:
- payload["customer_order"] = Value.Object(self.customerOrder)
+ reference = self.customerOrder
+ reference_id = getattr(reference, "id", reference)
+ payload["customer_order"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "product" in self._loaded_fields:
- payload["product"] = Value.Object(self.product)
+ reference = self.product
+ reference_id = getattr(reference, "id", reference)
+ payload["product"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "productName" in self._loaded_fields:
payload["product_name"] = Value.Text(self.productName)
+
if "sku" in self._loaded_fields:
payload["sku"] = Value.Text(self.sku)
+
if "quantity" in self._loaded_fields:
payload["quantity"] = Value.I64(self.quantity)
+
if "commercePlatform" in self._loaded_fields:
- payload["commerce_platform"] = Value.Object(self.commercePlatform)
+ reference = self.commercePlatform
+ reference_id = getattr(reference, "id", reference)
+ payload["commerce_platform"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "createTime" in self._loaded_fields:
payload["create_time"] = Value.DateTime(self.createTime)
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -122,54 +197,62 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("OrderLine", payload)
elif action == "Update":
- cmd = UpdateCommand("OrderLine", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("OrderLine", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("OrderLine", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("OrderLine", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "customerOrder" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("customer_order"), message="Mutation requires a fully loaded entity")])
- if "product" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("product"), message="Mutation requires a fully loaded entity")])
- if "productName" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("product_name"), message="Mutation requires a fully loaded entity")])
- if "sku" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("sku"), message="Mutation requires a fully loaded entity")])
- if "quantity" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("quantity"), message="Mutation requires a fully loaded entity")])
- if "commercePlatform" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("commerce_platform"), message="Mutation requires a fully loaded entity")])
- if "createTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "customerOrder" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("customer_order"), message="Mutation requires a fully loaded entity")])
+ if "product" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("product"), message="Mutation requires a fully loaded entity")])
+ if "productName" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("product_name"), message="Mutation requires a fully loaded entity")])
+ if "sku" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("sku"), message="Mutation requires a fully loaded entity")])
+ if "quantity" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("quantity"), message="Mutation requires a fully loaded entity")])
+ if "commercePlatform" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("commerce_platform"), message="Mutation requires a fully loaded entity")])
+ if "createTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -244,6 +327,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -256,39 +340,44 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/order-management/python-lib-core/models/order_search_preset.py b/examples/order-management/python-lib-core/models/order_search_preset.py
index 7b774cc..418f3c3 100644
--- a/examples/order-management/python-lib-core/models/order_search_preset.py
+++ b/examples/order-management/python-lib-core/models/order_search_preset.py
@@ -1,9 +1,12 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
from models.commerce_platform import CommercePlatform
+
class OrderSearchPreset:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -44,14 +47,23 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.name = kwargs.get("name")
+
self.filterJson = kwargs.get("filterJson")
+
self.requestId = kwargs.get("requestId")
+
self.ownerUserId = kwargs.get("ownerUserId")
+
self.commercePlatform = kwargs.get("commercePlatform")
+
self.createTime = kwargs.get("createTime")
+
self.updateTime = kwargs.get("updateTime")
+
self.version = kwargs.get("version")
+
if isinstance(self.commercePlatform, dict):
self.commercePlatform = CommercePlatform(**self.commercePlatform)
self._ledger_id = getattr(self, "id", None)
@@ -62,13 +74,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("OrderSearchPreset", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
return self
def mark_for_deletion(self):
@@ -77,38 +92,90 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("OrderSearchPreset"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "name" in self._loaded_fields:
payload["name"] = Value.Text(self.name)
+
if "filterJson" in self._loaded_fields:
payload["filter_json"] = Value.Text(self.filterJson)
+
if "requestId" in self._loaded_fields:
payload["request_id"] = Value.Text(self.requestId)
+
if "ownerUserId" in self._loaded_fields:
payload["owner_user_id"] = Value.Text(self.ownerUserId)
+
if "commercePlatform" in self._loaded_fields:
- payload["commerce_platform"] = Value.Object(self.commercePlatform)
+ reference = self.commercePlatform
+ reference_id = getattr(reference, "id", reference)
+ payload["commerce_platform"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "createTime" in self._loaded_fields:
payload["create_time"] = Value.DateTime(self.createTime)
+
if "updateTime" in self._loaded_fields:
payload["update_time"] = Value.DateTime(self.updateTime)
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -116,54 +183,62 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("OrderSearchPreset", payload)
elif action == "Update":
- cmd = UpdateCommand("OrderSearchPreset", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("OrderSearchPreset", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("OrderSearchPreset", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("OrderSearchPreset", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "name" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
- if "filterJson" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("filter_json"), message="Mutation requires a fully loaded entity")])
- if "requestId" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("request_id"), message="Mutation requires a fully loaded entity")])
- if "ownerUserId" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("owner_user_id"), message="Mutation requires a fully loaded entity")])
- if "commercePlatform" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("commerce_platform"), message="Mutation requires a fully loaded entity")])
- if "createTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
- if "updateTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "filterJson" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("filter_json"), message="Mutation requires a fully loaded entity")])
+ if "requestId" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("request_id"), message="Mutation requires a fully loaded entity")])
+ if "ownerUserId" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("owner_user_id"), message="Mutation requires a fully loaded entity")])
+ if "commercePlatform" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("commerce_platform"), message="Mutation requires a fully loaded entity")])
+ if "createTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
+ if "updateTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -238,6 +313,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -250,39 +326,44 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/order-management/python-lib-core/models/order_status.py b/examples/order-management/python-lib-core/models/order_status.py
index d0eb8df..39eb96f 100644
--- a/examples/order-management/python-lib-core/models/order_status.py
+++ b/examples/order-management/python-lib-core/models/order_status.py
@@ -1,9 +1,12 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
from models.commerce_platform import CommercePlatform
+
class OrderStatus:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -40,12 +43,19 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.name = kwargs.get("name")
+
self.code = kwargs.get("code")
+
self.color = kwargs.get("color")
+
self.displayOrder = kwargs.get("displayOrder")
+
self.commercePlatform = kwargs.get("commercePlatform")
+
self.version = kwargs.get("version")
+
if isinstance(self.commercePlatform, dict):
self.commercePlatform = CommercePlatform(**self.commercePlatform)
self._customer_order_list = kwargs.get("customer_order_list", [])
@@ -54,7 +64,7 @@ def __init__(self, **kwargs):
if self._customer_order_list:
from models.customer_order import CustomerOrder
self._customer_order_list = [
- item if isinstance(item, CustomerOrder) else CustomerOrder(**item)
+ item if isinstance(item, CustomerOrder) else CustomerOrder(_entity_root=self._entity_root, **item)
for item in self._customer_order_list
]
self._ledger_id = getattr(self, "id", None)
@@ -65,13 +75,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("OrderStatus", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
for child in self._customer_order_list:
child._teaql_attach_root(root)
@@ -82,34 +95,87 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("OrderStatus"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._customer_order_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "name" in self._loaded_fields:
payload["name"] = Value.Text(self.name)
+
if "code" in self._loaded_fields:
payload["code"] = Value.Text(self.code)
+
if "color" in self._loaded_fields:
payload["color"] = Value.Text(self.color)
+
if "displayOrder" in self._loaded_fields:
payload["display_order"] = Value.Decimal(self.displayOrder)
+
if "commercePlatform" in self._loaded_fields:
- payload["commerce_platform"] = Value.Object(self.commercePlatform)
+ reference = self.commercePlatform
+ reference_id = getattr(reference, "id", reference)
+ payload["commerce_platform"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -117,46 +183,51 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("OrderStatus", payload)
elif action == "Update":
- cmd = UpdateCommand("OrderStatus", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("OrderStatus", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("OrderStatus", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("OrderStatus", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "name" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
- if "code" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("code"), message="Mutation requires a fully loaded entity")])
- if "color" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("color"), message="Mutation requires a fully loaded entity")])
- if "displayOrder" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("display_order"), message="Mutation requires a fully loaded entity")])
- if "commercePlatform" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("commerce_platform"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "code" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("code"), message="Mutation requires a fully loaded entity")])
+ if "color" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("color"), message="Mutation requires a fully loaded entity")])
+ if "displayOrder" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("display_order"), message="Mutation requires a fully loaded entity")])
+ if "commercePlatform" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("commerce_platform"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
for index, child in enumerate(self._customer_order_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "status", self)
- child._loaded_fields.add("status")
- child._entity_root.set(child._teaql_entity_key(), "status", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "status", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_status(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("customer_order_list").index(index)
raise CheckException([
@@ -164,17 +235,19 @@ def _teaql_preflight_graph(self, context):
for v in error.violations
]) from error
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -237,6 +310,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -249,40 +323,45 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
- cascade_relations.append(("customer_order_list", self._customer_order_list, "update_status"))
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("customer_order_list", self._customer_order_list, "update_status", "status"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/order-management/python-lib-core/models/product.py b/examples/order-management/python-lib-core/models/product.py
index 6718e79..ea563c3 100644
--- a/examples/order-management/python-lib-core/models/product.py
+++ b/examples/order-management/python-lib-core/models/product.py
@@ -1,9 +1,12 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
from models.commerce_platform import CommercePlatform
+
class Product:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -42,13 +45,21 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.name = kwargs.get("name")
+
self.sku = kwargs.get("sku")
+
self.imageUrl = kwargs.get("imageUrl")
+
self.commercePlatform = kwargs.get("commercePlatform")
+
self.createTime = kwargs.get("createTime")
+
self.updateTime = kwargs.get("updateTime")
+
self.version = kwargs.get("version")
+
if isinstance(self.commercePlatform, dict):
self.commercePlatform = CommercePlatform(**self.commercePlatform)
self._order_line_list = kwargs.get("order_line_list", [])
@@ -57,7 +68,7 @@ def __init__(self, **kwargs):
if self._order_line_list:
from models.order_line import OrderLine
self._order_line_list = [
- item if isinstance(item, OrderLine) else OrderLine(**item)
+ item if isinstance(item, OrderLine) else OrderLine(_entity_root=self._entity_root, **item)
for item in self._order_line_list
]
self._ledger_id = getattr(self, "id", None)
@@ -68,13 +79,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("Product", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
for child in self._order_line_list:
child._teaql_attach_root(root)
@@ -85,36 +99,90 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("Product"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._order_line_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "name" in self._loaded_fields:
payload["name"] = Value.Text(self.name)
+
if "sku" in self._loaded_fields:
payload["sku"] = Value.Text(self.sku)
+
if "imageUrl" in self._loaded_fields:
payload["image_url"] = Value.Text(self.imageUrl)
+
if "commercePlatform" in self._loaded_fields:
- payload["commerce_platform"] = Value.Object(self.commercePlatform)
+ reference = self.commercePlatform
+ reference_id = getattr(reference, "id", reference)
+ payload["commerce_platform"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "createTime" in self._loaded_fields:
payload["create_time"] = Value.DateTime(self.createTime)
+
if "updateTime" in self._loaded_fields:
payload["update_time"] = Value.DateTime(self.updateTime)
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -122,48 +190,53 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("Product", payload)
elif action == "Update":
- cmd = UpdateCommand("Product", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("Product", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("Product", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("Product", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "name" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
- if "sku" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("sku"), message="Mutation requires a fully loaded entity")])
- if "imageUrl" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("image_url"), message="Mutation requires a fully loaded entity")])
- if "commercePlatform" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("commerce_platform"), message="Mutation requires a fully loaded entity")])
- if "createTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
- if "updateTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "sku" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("sku"), message="Mutation requires a fully loaded entity")])
+ if "imageUrl" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("image_url"), message="Mutation requires a fully loaded entity")])
+ if "commercePlatform" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("commerce_platform"), message="Mutation requires a fully loaded entity")])
+ if "createTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
+ if "updateTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
for index, child in enumerate(self._order_line_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "product", self)
- child._loaded_fields.add("product")
- child._entity_root.set(child._teaql_entity_key(), "product", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "product", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_product(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("order_line_list").index(index)
raise CheckException([
@@ -171,17 +244,19 @@ def _teaql_preflight_graph(self, context):
for v in error.violations
]) from error
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -250,6 +325,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -262,40 +338,45 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
- cascade_relations.append(("order_line_list", self._order_line_list, "update_product"))
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("order_line_list", self._order_line_list, "update_product", "product"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/order-management/python-lib-core/pyproject.toml b/examples/order-management/python-lib-core/pyproject.toml
index d25c271..6e49444 100644
--- a/examples/order-management/python-lib-core/pyproject.toml
+++ b/examples/order-management/python-lib-core/pyproject.toml
@@ -2,7 +2,7 @@
name = "order-management-service-lib"
version = "1.0.0"
description = "Generated python library"
-dependencies = ["teaql==0.2.7", "aiosqlite>=0.22.1"]
+dependencies = ["teaql==0.2.9", "aiosqlite>=0.22.1"]
[tool.setuptools]
py-modules = ["Q", "E"]
diff --git a/examples/order-management/python-lib-core/requests/commerce_platform_request.py b/examples/order-management/python-lib-core/requests/commerce_platform_request.py
index 12fd607..633177d 100644
--- a/examples/order-management/python-lib-core/requests/commerce_platform_request.py
+++ b/examples/order-management/python-lib-core/requests/commerce_platform_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -934,8 +935,7 @@ def comment(self, c: str):
def new_entity(self, context) -> CommercePlatform:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("CommercePlatform", CommercePlatform())
if not isinstance(entity, CommercePlatform):
raise TypeError("entity initializer returned an incompatible CommercePlatform")
@@ -943,10 +943,9 @@ def new_entity(self, context) -> CommercePlatform:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -954,53 +953,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[CommercePlatform]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (CommercePlatform(_entity_root=query_root, **row) for row in result.rows),
+ (CommercePlatform(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[CommercePlatform]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(CommercePlatform(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(CommercePlatform(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableCommercePlatformRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield CommercePlatform(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield CommercePlatform(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/order-management/python-lib-core/requests/customer_order_request.py b/examples/order-management/python-lib-core/requests/customer_order_request.py
index 8dcc698..3d1c568 100644
--- a/examples/order-management/python-lib-core/requests/customer_order_request.py
+++ b/examples/order-management/python-lib-core/requests/customer_order_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -862,8 +863,7 @@ def comment(self, c: str):
def new_entity(self, context) -> CustomerOrder:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("CustomerOrder", CustomerOrder())
if not isinstance(entity, CustomerOrder):
raise TypeError("entity initializer returned an incompatible CustomerOrder")
@@ -871,10 +871,9 @@ def new_entity(self, context) -> CustomerOrder:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -882,53 +881,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[CustomerOrder]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (CustomerOrder(_entity_root=query_root, **row) for row in result.rows),
+ (CustomerOrder(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[CustomerOrder]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(CustomerOrder(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(CustomerOrder(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableCustomerOrderRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield CustomerOrder(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield CustomerOrder(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/order-management/python-lib-core/requests/customer_request.py b/examples/order-management/python-lib-core/requests/customer_request.py
index 868c902..500630a 100644
--- a/examples/order-management/python-lib-core/requests/customer_request.py
+++ b/examples/order-management/python-lib-core/requests/customer_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -694,8 +695,7 @@ def comment(self, c: str):
def new_entity(self, context) -> Customer:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("Customer", Customer())
if not isinstance(entity, Customer):
raise TypeError("entity initializer returned an incompatible Customer")
@@ -703,10 +703,9 @@ def new_entity(self, context) -> Customer:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -714,53 +713,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[Customer]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (Customer(_entity_root=query_root, **row) for row in result.rows),
+ (Customer(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[Customer]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(Customer(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(Customer(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableCustomerRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield Customer(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield Customer(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/order-management/python-lib-core/requests/order_line_request.py b/examples/order-management/python-lib-core/requests/order_line_request.py
index 135cfdb..a357d5c 100644
--- a/examples/order-management/python-lib-core/requests/order_line_request.py
+++ b/examples/order-management/python-lib-core/requests/order_line_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -694,8 +695,7 @@ def comment(self, c: str):
def new_entity(self, context) -> OrderLine:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("OrderLine", OrderLine())
if not isinstance(entity, OrderLine):
raise TypeError("entity initializer returned an incompatible OrderLine")
@@ -703,10 +703,9 @@ def new_entity(self, context) -> OrderLine:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -714,53 +713,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[OrderLine]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (OrderLine(_entity_root=query_root, **row) for row in result.rows),
+ (OrderLine(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[OrderLine]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(OrderLine(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(OrderLine(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableOrderLineRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield OrderLine(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield OrderLine(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/order-management/python-lib-core/requests/order_search_preset_request.py b/examples/order-management/python-lib-core/requests/order_search_preset_request.py
index f41b3e6..85ddaca 100644
--- a/examples/order-management/python-lib-core/requests/order_search_preset_request.py
+++ b/examples/order-management/python-lib-core/requests/order_search_preset_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -748,8 +749,7 @@ def comment(self, c: str):
def new_entity(self, context) -> OrderSearchPreset:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("OrderSearchPreset", OrderSearchPreset())
if not isinstance(entity, OrderSearchPreset):
raise TypeError("entity initializer returned an incompatible OrderSearchPreset")
@@ -757,10 +757,9 @@ def new_entity(self, context) -> OrderSearchPreset:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -768,53 +767,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[OrderSearchPreset]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (OrderSearchPreset(_entity_root=query_root, **row) for row in result.rows),
+ (OrderSearchPreset(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[OrderSearchPreset]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(OrderSearchPreset(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(OrderSearchPreset(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableOrderSearchPresetRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield OrderSearchPreset(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield OrderSearchPreset(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/order-management/python-lib-core/requests/order_status_request.py b/examples/order-management/python-lib-core/requests/order_status_request.py
index 27a10d6..cc1a2ce 100644
--- a/examples/order-management/python-lib-core/requests/order_status_request.py
+++ b/examples/order-management/python-lib-core/requests/order_status_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -769,8 +770,7 @@ def comment(self, c: str):
def new_entity(self, context) -> OrderStatus:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("OrderStatus", OrderStatus())
if not isinstance(entity, OrderStatus):
raise TypeError("entity initializer returned an incompatible OrderStatus")
@@ -778,10 +778,9 @@ def new_entity(self, context) -> OrderStatus:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -789,53 +788,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[OrderStatus]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (OrderStatus(_entity_root=query_root, **row) for row in result.rows),
+ (OrderStatus(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[OrderStatus]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(OrderStatus(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(OrderStatus(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableOrderStatusRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield OrderStatus(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield OrderStatus(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/order-management/python-lib-core/requests/product_request.py b/examples/order-management/python-lib-core/requests/product_request.py
index 5bbbad0..6223f4a 100644
--- a/examples/order-management/python-lib-core/requests/product_request.py
+++ b/examples/order-management/python-lib-core/requests/product_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -784,8 +785,7 @@ def comment(self, c: str):
def new_entity(self, context) -> Product:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("Product", Product())
if not isinstance(entity, Product):
raise TypeError("entity initializer returned an incompatible Product")
@@ -793,10 +793,9 @@ def new_entity(self, context) -> Product:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -804,53 +803,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[Product]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (Product(_entity_root=query_root, **row) for row in result.rows),
+ (Product(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[Product]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(Product(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(Product(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableProductRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield Product(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield Product(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/query-policy/main.py b/examples/query-policy/main.py
index dcc8569..e047395 100644
--- a/examples/query-policy/main.py
+++ b/examples/query-policy/main.py
@@ -3,7 +3,7 @@
shared = SelectQuery("SchoolType").project("id")
-original = SelectQuery("School").project("id")
+original = SelectQuery("School").project("id").comment('govern school graph').purpose('verify query policy')
original.relations.append(RelationLoad("schoolType", shared))
original.facets.append(FacetRequest("types", "schoolType", shared))
calls = []
@@ -27,7 +27,7 @@ def tenant_policy(query):
assert "tenant_id" not in shared.projection
try:
- context.prepare_query(SelectQuery("ForbiddenReport"))
+ context.prepare_query(SelectQuery("ForbiddenReport").comment('attempt forbidden report').purpose('verify denial'))
except PermissionError as error:
assert str(error) == "query policy denied ForbiddenReport"
else:
diff --git a/examples/school-management/Q.py b/examples/school-management/Q.py
index 02e5cba..fa189cd 100644
--- a/examples/school-management/Q.py
+++ b/examples/school-management/Q.py
@@ -28,3 +28,4 @@ def schools() -> SchoolRequest:
def schools_minimal() -> SchoolRequest:
return SchoolRequest(minimal=True)
+
diff --git a/examples/school-management/README.md b/examples/school-management/README.md
index d74a9d9..89bf8a6 100644
--- a/examples/school-management/README.md
+++ b/examples/school-management/README.md
@@ -18,3 +18,15 @@ Expected result:
```text
PASS Python School Management: idempotent bootstrap, multi-word hydration, and forward relations
```
+
+## Generated bootstrap Trace Chain gate
+
+`bash scripts/verify-school-bootstrap-example.sh` (from the runtime root) runs
+the actual generated School bootstrap twice per retained SQLite file, with SQL
+logs on/off. No trace nodes are injected. It checks request intent, canonical
+physical paths, matching typed mutation/audit lineage, caller identity
+restoration, no-op reseeding and an audited constant edit/reconciliation. An
+independent read-only connection must see the expected version at audit
+delivery. Generated-library hashes must stay unchanged. The script uses local
+runtime source and disables bytecode writes; all evidence and databases remain
+available at the printed path.
diff --git a/examples/school-management/app/bootstrap_trace.py b/examples/school-management/app/bootstrap_trace.py
new file mode 100644
index 0000000..1ba362c
--- /dev/null
+++ b/examples/school-management/app/bootstrap_trace.py
@@ -0,0 +1,114 @@
+"""Generated bootstrap acceptance; observe outputs, never inject trace frames."""
+import asyncio
+import os
+from pathlib import Path
+import sqlite3
+
+from Q import Q
+from runtime_module import GENERATED_RUNTIME_MODULE
+from teaql.data_service import SQLiteTeaQLClient
+from teaql.runtime import UserContext
+from teaql.runtime.context import SqlLogOptions
+
+
+def identity(node):
+ return node.name or node.entity_type, node.entity_id
+
+
+class Evidence:
+ def __init__(self, database):
+ self.database, self.events, self.sql = database, [], []
+ self.versions = {('Platform', 1): 1, ('SchoolType', 1001): 1, ('SchoolType', 1002): 1}
+
+ def write(self, entry):
+ self.sql.append(entry)
+
+ def on_safe_event(self, context, event):
+ key = event.entity, getattr(event.entity_id, 'val', event.entity_id)
+ table = {'Platform': 'platform_data', 'SchoolType': 'school_type_data'}[event.entity]
+ # Read-only, independent connection: audit must follow durable commit.
+ with sqlite3.connect(self.database.as_uri() + '?mode=ro', uri=True, timeout=1) as connection:
+ row = connection.execute(f'SELECT version FROM {table} WHERE id = ?', (key[1],)).fetchone()
+ assert row == (self.versions[key],), (key, row, self.versions[key])
+ self.events.append(event)
+
+ def clear(self, context):
+ self.sql.clear()
+ self.events.clear()
+ context.clear_sql_logs()
+
+ def verify(self, writes, reads, logging):
+ assert len(self.events) == writes, self.events
+ audits = {}
+ for event in self.events:
+ assert event.actor == 'teaql-generated-bootstrap', event.actor
+ assert event.category == 'runtime-bootstrap', event.category
+ assert len(event.trace_chain) == 1, event.trace_chain
+ node = event.trace_chain[0]
+ assert node.kind == 'auditReason' and node.comment.strip(), node
+ key = event.entity, getattr(event.entity_id, 'val', event.entity_id)
+ assert identity(node) == key, (node, key)
+ audits[key] = node
+ if not logging:
+ assert self.sql == [], self.sql
+ return
+ mutations = [entry for entry in self.sql if entry.operation.name.lower() != 'select']
+ assert len(mutations) == writes, mutations
+ assert len(self.sql) - writes == reads, len(self.sql)
+ for entry in self.sql:
+ select = entry.operation.name.lower() == 'select'
+ assert [node.kind for node in entry.trace_path] == [
+ 'operation', 'request' if select else 'entity', 'provider', 'sql'], entry.trace_path
+ if select:
+ assert entry.comment and entry.purpose, entry
+ else:
+ assert len(entry.mutation_lineage) == 1, entry
+ node = entry.mutation_lineage[0]
+ assert node == audits[identity(node)], (node, audits)
+ assert entry.audit_reason == node.comment, (entry.audit_reason, node)
+
+
+async def main():
+ database = Path(os.environ['TEAQL_SCHOOL_BOOTSTRAP_DB']).resolve()
+ logging = os.environ['TEAQL_SCHOOL_BOOTSTRAP_LOGGING'] == 'on'
+ evidence = Evidence(database)
+ client = SQLiteTeaQLClient(str(database))
+ context = (UserContext.new().install(GENERATED_RUNTIME_MODULE)
+ .insert_resource('dataService', client).with_user_identifier('school-example-user')
+ .with_app_audit_event_sink(evidence))
+ context.set_diagnostic_sql_log_sink(evidence)
+ context.with_sql_log_options(SqlLogOptions.all() if logging else SqlLogOptions.disabled())
+ await context.ensure_schema()
+ fresh = len(evidence.events) == 3
+ evidence.verify(3 if fresh else 0, 6 if fresh else 3, logging)
+ assert context.user_identifier() == 'school-example-user'
+ evidence.clear(context)
+ await context.ensure_schema()
+ evidence.verify(0, 3, logging)
+ primary = await (Q.school_types().with_id_is(1001).limit(1)
+ .comment('load Primary for audited drift').purpose('verify bootstrap reconciliation')
+ .execute_for_one(context))
+ original_version = primary.version
+ evidence.versions[('SchoolType', 1001)] = original_version + 1
+ primary.update_name('Drifted Primary')
+ await primary.audit_as('simulate constant drift').save(context)
+ assert len(evidence.events) == 1
+ assert evidence.events[0].actor == 'school-example-user'
+ assert evidence.events[0].category != 'runtime-bootstrap'
+ evidence.clear(context)
+ evidence.versions[('SchoolType', 1001)] = original_version + 2
+ await context.ensure_schema()
+ evidence.verify(1, 4, logging)
+ restored = await (Q.school_types().with_id_is(1001).limit(1)
+ .comment('verify corrected constant').purpose('verify audited bootstrap persistence')
+ .execute_for_one(context))
+ assert restored.name == 'Primary' and restored.version == original_version + 2
+ evidence.clear(context)
+ await context.ensure_schema()
+ evidence.verify(0, 3, logging)
+ assert context.user_identifier() == 'school-example-user'
+ print(f'PASS Python generated bootstrap trace logging={logging} fresh={fresh} originalVersion={original_version}')
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/examples/school-management/app/main.py b/examples/school-management/app/main.py
index 33bd0bb..10cdedb 100644
--- a/examples/school-management/app/main.py
+++ b/examples/school-management/app/main.py
@@ -8,12 +8,60 @@
sys.path.insert(0, str(ROOT))
from Q import Q
+from E import E
from models.platform import Platform
from models.school_type import SchoolType
from runtime_module import GENERATED_RUNTIME_MODULE
from teaql.data_service import SQLiteTeaQLClient
from teaql.runtime import UserContext
from teaql.core.dynamic_search import normalize_dynamic_search
+from teaql.core import RequestIntentError
+from teaql.runtime.context import SqlLogOptions
+
+
+async def verify_request_intent(context, client):
+ calls = []
+ context.with_sql_log_options(SqlLogOptions.disabled())
+ context.with_request_policy(lambda _: calls.append('policy'))
+ original_allocator = client.next_id
+ async def counted_allocator(entity):
+ calls.append('id allocator')
+ return await original_allocator(entity)
+ client.next_id = counted_allocator
+ try:
+ executions = [
+ (Q.schools().purpose('render schools').execute_for_list(context), 'REQUEST_COMMENT_REQUIRED'),
+ (Q.schools().comment('load schools').purpose('\u0085').execute_for_rows(context), 'QUERY_PURPOSE_REQUIRED'),
+ (Q.schools().purpose('render page').execute_for_page(context, 0, 10), 'REQUEST_COMMENT_REQUIRED'),
+ ]
+ for execution, code in executions:
+ try:
+ await execution
+ raise AssertionError('missing generated request gate')
+ except RequestIntentError as error:
+ assert error.code == code
+ try:
+ async for _ in Q.schools().purpose('stream schools').execute_for_stream(context):
+ raise AssertionError('stream must not open')
+ except RequestIntentError as error:
+ assert error.code == 'REQUEST_COMMENT_REQUIRED'
+ entity = Q.schools().comment('prepare unsaved school').purpose('verify save gate').new_entity(context)
+ for reason in (None, '\u0085'):
+ try:
+ if reason is not None:
+ entity.audit_as(reason)
+ await entity.save(context)
+ raise AssertionError('missing generated mutation gate')
+ except RequestIntentError as error:
+ assert error.code == 'REQUEST_COMMENT_REQUIRED'
+ assert calls == [], calls
+ assert entity.id is None
+ finally:
+ client.next_id = original_allocator
+ context.clear_request_policy()
+ context.with_sql_log_options(SqlLogOptions.all())
+ assert len(await Q.schools().comment('verify rejected writes').purpose('check empty table').execute_for_list(context)) == 0
+ print('PASS Python generated request intent: list/rows/page/stream/save reject before policy and ID allocation with logs off')
async def verify_dynamic_search(context):
@@ -59,6 +107,7 @@ async def main() -> None:
.insert_resource("dataService", client))
await context.ensure_schema()
await context.ensure_schema()
+ await verify_request_intent(context, client)
platform = await (Q.platforms().with_id_is(1)
.comment("Load the generated domain root")
.purpose("Verify idempotent schema bootstrap").execute_for_one(context))
@@ -121,6 +170,8 @@ async def main() -> None:
assert loaded.platform.baseUrl == "https://campus.example.com"
assert loaded.schoolType.code == "PRIMARY"
assert loaded.schoolType.displayOrder == 1
+ assert E.school(loaded).school_type().code().eval() == 'PRIMARY'
+ assert E.school(loaded).platform().name().eval() == 'Deployment Campus'
query_cases = [
("string equality", Q.schools().with_name_is("Riverside Primary School"), 1),
diff --git a/examples/school-management/models/platform.py b/examples/school-management/models/platform.py
index a2878ec..bc3dc18 100644
--- a/examples/school-management/models/platform.py
+++ b/examples/school-management/models/platform.py
@@ -1,8 +1,11 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
+
class Platform:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -37,11 +40,17 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.name = kwargs.get("name")
+
self.baseUrl = kwargs.get("baseUrl")
+
self.createTime = kwargs.get("createTime")
+
self.updateTime = kwargs.get("updateTime")
+
self.version = kwargs.get("version")
+
self._school_type_list = kwargs.get("school_type_list", [])
if "school_type_list" in kwargs or kwargs.get("id") is None:
self._loaded_fields.add("school_type_list")
@@ -51,13 +60,13 @@ def __init__(self, **kwargs):
if self._school_type_list:
from models.school_type import SchoolType
self._school_type_list = [
- item if isinstance(item, SchoolType) else SchoolType(**item)
+ item if isinstance(item, SchoolType) else SchoolType(_entity_root=self._entity_root, **item)
for item in self._school_type_list
]
if self._school_list:
from models.school import School
self._school_list = [
- item if isinstance(item, School) else School(**item)
+ item if isinstance(item, School) else School(_entity_root=self._entity_root, **item)
for item in self._school_list
]
self._ledger_id = getattr(self, "id", None)
@@ -68,13 +77,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("Platform", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
for child in self._school_type_list:
child._teaql_attach_root(root)
@@ -87,32 +99,83 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("Platform"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._school_type_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+ for child in self._school_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "name" in self._loaded_fields:
payload["name"] = Value.Text(self.name)
+
if "baseUrl" in self._loaded_fields:
payload["base_url"] = Value.Text(self.baseUrl)
+
if "createTime" in self._loaded_fields:
payload["create_time"] = Value.DateTime(self.createTime)
+
if "updateTime" in self._loaded_fields:
payload["update_time"] = Value.DateTime(self.updateTime)
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -120,44 +183,49 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("Platform", payload)
elif action == "Update":
- cmd = UpdateCommand("Platform", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("Platform", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("Platform", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("Platform", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "name" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
- if "baseUrl" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("base_url"), message="Mutation requires a fully loaded entity")])
- if "createTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
- if "updateTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "baseUrl" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("base_url"), message="Mutation requires a fully loaded entity")])
+ if "createTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
+ if "updateTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
for index, child in enumerate(self._school_type_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "platform", self)
- child._loaded_fields.add("platform")
- child._entity_root.set(child._teaql_entity_key(), "platform", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "platform", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_platform(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("school_type_list").index(index)
raise CheckException([
@@ -166,12 +234,11 @@ def _teaql_preflight_graph(self, context):
]) from error
for index, child in enumerate(self._school_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "platform", self)
- child._loaded_fields.add("platform")
- child._entity_root.set(child._teaql_entity_key(), "platform", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "platform", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_platform(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("school_list").index(index)
raise CheckException([
@@ -179,17 +246,19 @@ def _teaql_preflight_graph(self, context):
for v in error.violations
]) from error
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -246,6 +315,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -258,41 +328,46 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
- cascade_relations.append(("school_type_list", self._school_type_list, "update_platform"))
- cascade_relations.append(("school_list", self._school_list, "update_platform"))
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("school_type_list", self._school_type_list, "update_platform", "platform"))
+ cascade_relations.append(("school_list", self._school_list, "update_platform", "platform"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/school-management/models/school.py b/examples/school-management/models/school.py
index 43d2345..514a34c 100644
--- a/examples/school-management/models/school.py
+++ b/examples/school-management/models/school.py
@@ -1,10 +1,13 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
from models.platform import Platform
from models.school_type import SchoolType
+
class School:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -49,16 +52,27 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.platform = kwargs.get("platform")
+
self.schoolType = kwargs.get("schoolType")
+
self.name = kwargs.get("name")
+
self.address = kwargs.get("address")
+
self.establishedDate = kwargs.get("establishedDate")
+
self.studentCapacity = kwargs.get("studentCapacity")
+
self.active = kwargs.get("active")
+
self.createTime = kwargs.get("createTime")
+
self.updateTime = kwargs.get("updateTime")
+
self.version = kwargs.get("version")
+
if isinstance(self.platform, dict):
self.platform = Platform(**self.platform)
if isinstance(self.schoolType, dict):
@@ -71,13 +85,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("School", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
return self
def mark_for_deletion(self):
@@ -86,42 +103,100 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("School"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "platform" in self._loaded_fields:
- payload["platform"] = Value.Object(self.platform)
+ reference = self.platform
+ reference_id = getattr(reference, "id", reference)
+ payload["platform"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "schoolType" in self._loaded_fields:
- payload["school_type"] = Value.Object(self.schoolType)
+ reference = self.schoolType
+ reference_id = getattr(reference, "id", reference)
+ payload["school_type"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "name" in self._loaded_fields:
payload["name"] = Value.Text(self.name)
+
if "address" in self._loaded_fields:
payload["address"] = Value.Text(self.address)
+
if "establishedDate" in self._loaded_fields:
payload["established_date"] = Value.Date(self.establishedDate)
+
if "studentCapacity" in self._loaded_fields:
payload["student_capacity"] = Value.I64(self.studentCapacity)
+
if "active" in self._loaded_fields:
payload["active"] = Value.Bool(self.active)
+
if "createTime" in self._loaded_fields:
payload["create_time"] = Value.DateTime(self.createTime)
+
if "updateTime" in self._loaded_fields:
payload["update_time"] = Value.DateTime(self.updateTime)
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -129,58 +204,66 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("School", payload)
elif action == "Update":
- cmd = UpdateCommand("School", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("School", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("School", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("School", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "platform" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("platform"), message="Mutation requires a fully loaded entity")])
- if "schoolType" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("school_type"), message="Mutation requires a fully loaded entity")])
- if "name" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
- if "address" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("address"), message="Mutation requires a fully loaded entity")])
- if "establishedDate" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("established_date"), message="Mutation requires a fully loaded entity")])
- if "studentCapacity" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("student_capacity"), message="Mutation requires a fully loaded entity")])
- if "active" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("active"), message="Mutation requires a fully loaded entity")])
- if "createTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
- if "updateTime" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "platform" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("platform"), message="Mutation requires a fully loaded entity")])
+ if "schoolType" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("school_type"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "address" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("address"), message="Mutation requires a fully loaded entity")])
+ if "establishedDate" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("established_date"), message="Mutation requires a fully loaded entity")])
+ if "studentCapacity" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("student_capacity"), message="Mutation requires a fully loaded entity")])
+ if "active" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("active"), message="Mutation requires a fully loaded entity")])
+ if "createTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("create_time"), message="Mutation requires a fully loaded entity")])
+ if "updateTime" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("update_time"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -267,6 +350,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -279,39 +363,44 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/school-management/models/school_type.py b/examples/school-management/models/school_type.py
index 418f288..b2ecef5 100644
--- a/examples/school-management/models/school_type.py
+++ b/examples/school-management/models/school_type.py
@@ -1,9 +1,12 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
from models.platform import Platform
+
class SchoolType:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -38,11 +41,17 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.platform = kwargs.get("platform")
+
self.id = kwargs.get("id")
+
self.name = kwargs.get("name")
+
self.code = kwargs.get("code")
+
self.displayOrder = kwargs.get("displayOrder")
+
self.version = kwargs.get("version")
+
if isinstance(self.platform, dict):
self.platform = Platform(**self.platform)
self._school_list = kwargs.get("school_list", [])
@@ -51,7 +60,7 @@ def __init__(self, **kwargs):
if self._school_list:
from models.school import School
self._school_list = [
- item if isinstance(item, School) else School(**item)
+ item if isinstance(item, School) else School(_entity_root=self._entity_root, **item)
for item in self._school_list
]
self._ledger_id = getattr(self, "id", None)
@@ -62,13 +71,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("SchoolType", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
for child in self._school_list:
child._teaql_attach_root(root)
@@ -79,32 +91,84 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("SchoolType"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._school_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "platform" in self._loaded_fields:
- payload["platform"] = Value.Object(self.platform)
+ reference = self.platform
+ reference_id = getattr(reference, "id", reference)
+ payload["platform"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "name" in self._loaded_fields:
payload["name"] = Value.Text(self.name)
+
if "code" in self._loaded_fields:
payload["code"] = Value.Text(self.code)
+
if "displayOrder" in self._loaded_fields:
payload["display_order"] = Value.Decimal(self.displayOrder)
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -112,44 +176,49 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("SchoolType", payload)
elif action == "Update":
- cmd = UpdateCommand("SchoolType", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("SchoolType", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("SchoolType", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("SchoolType", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "platform" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("platform"), message="Mutation requires a fully loaded entity")])
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "name" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
- if "code" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("code"), message="Mutation requires a fully loaded entity")])
- if "displayOrder" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("display_order"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "platform" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("platform"), message="Mutation requires a fully loaded entity")])
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "code" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("code"), message="Mutation requires a fully loaded entity")])
+ if "displayOrder" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("display_order"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
for index, child in enumerate(self._school_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "schoolType", self)
- child._loaded_fields.add("schoolType")
- child._entity_root.set(child._teaql_entity_key(), "school_type", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "schoolType", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_school_type(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("school_list").index(index)
raise CheckException([
@@ -157,17 +226,19 @@ def _teaql_preflight_graph(self, context):
for v in error.violations
]) from error
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -224,6 +295,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -236,40 +308,45 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
- cascade_relations.append(("school_list", self._school_list, "update_school_type"))
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("school_list", self._school_list, "update_school_type", "schoolType"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/school-management/pyproject.toml b/examples/school-management/pyproject.toml
index 7984d34..062f286 100644
--- a/examples/school-management/pyproject.toml
+++ b/examples/school-management/pyproject.toml
@@ -2,7 +2,7 @@
name = "school-management-service-lib"
version = "1.0.0"
description = "Generated python library"
-dependencies = ["teaql==0.2.7", "aiosqlite>=0.22.1"]
+dependencies = ["teaql==0.2.9", "aiosqlite>=0.22.1"]
[tool.setuptools]
py-modules = ["Q", "E"]
diff --git a/examples/school-management/requests/platform_request.py b/examples/school-management/requests/platform_request.py
index 503e9ec..d07370d 100644
--- a/examples/school-management/requests/platform_request.py
+++ b/examples/school-management/requests/platform_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -781,8 +782,7 @@ def comment(self, c: str):
def new_entity(self, context) -> Platform:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("Platform", Platform())
if not isinstance(entity, Platform):
raise TypeError("entity initializer returned an incompatible Platform")
@@ -790,10 +790,9 @@ def new_entity(self, context) -> Platform:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -801,53 +800,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[Platform]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (Platform(_entity_root=query_root, **row) for row in result.rows),
+ (Platform(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[Platform]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(Platform(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(Platform(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutablePlatformRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield Platform(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield Platform(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/school-management/requests/school_request.py b/examples/school-management/requests/school_request.py
index 9d33284..3a20d9c 100644
--- a/examples/school-management/requests/school_request.py
+++ b/examples/school-management/requests/school_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -850,8 +851,7 @@ def comment(self, c: str):
def new_entity(self, context) -> School:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("School", School())
if not isinstance(entity, School):
raise TypeError("entity initializer returned an incompatible School")
@@ -859,10 +859,9 @@ def new_entity(self, context) -> School:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -870,53 +869,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[School]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (School(_entity_root=query_root, **row) for row in result.rows),
+ (School(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[School]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(School(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(School(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableSchoolRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield School(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield School(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/school-management/requests/school_type_request.py b/examples/school-management/requests/school_type_request.py
index 76a097a..605dca8 100644
--- a/examples/school-management/requests/school_type_request.py
+++ b/examples/school-management/requests/school_type_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -677,8 +678,7 @@ def comment(self, c: str):
def new_entity(self, context) -> SchoolType:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("SchoolType", SchoolType())
if not isinstance(entity, SchoolType):
raise TypeError("entity initializer returned an incompatible SchoolType")
@@ -686,10 +686,9 @@ def new_entity(self, context) -> SchoolType:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -697,53 +696,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[SchoolType]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (SchoolType(_entity_root=query_root, **row) for row in result.rows),
+ (SchoolType(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[SchoolType]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(SchoolType(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(SchoolType(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableSchoolTypeRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield SchoolType(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield SchoolType(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/task_board/generated/Q.py b/examples/task_board/generated/Q.py
index 7cb255d..660153c 100644
--- a/examples/task_board/generated/Q.py
+++ b/examples/task_board/generated/Q.py
@@ -36,3 +36,5 @@ def task_execution_logs() -> TaskExecutionLogRequest:
@staticmethod
def task_execution_logs_minimal() -> TaskExecutionLogRequest:
return TaskExecutionLogRequest(minimal=True)
+
+
diff --git a/examples/task_board/generated/models/platform.py b/examples/task_board/generated/models/platform.py
index fc1ab44..176e1b4 100644
--- a/examples/task_board/generated/models/platform.py
+++ b/examples/task_board/generated/models/platform.py
@@ -1,8 +1,11 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
+
class Platform:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -35,10 +38,15 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.name = kwargs.get("name")
+
self.founded = kwargs.get("founded")
+
self.userEmail = kwargs.get("userEmail")
+
self.version = kwargs.get("version")
+
self._task_status_list = kwargs.get("task_status_list", [])
if "task_status_list" in kwargs or kwargs.get("id") is None:
self._loaded_fields.add("task_status_list")
@@ -48,13 +56,13 @@ def __init__(self, **kwargs):
if self._task_status_list:
from models.task_status import TaskStatus
self._task_status_list = [
- item if isinstance(item, TaskStatus) else TaskStatus(**item)
+ item if isinstance(item, TaskStatus) else TaskStatus(_entity_root=self._entity_root, **item)
for item in self._task_status_list
]
if self._task_list:
from models.task import Task
self._task_list = [
- item if isinstance(item, Task) else Task(**item)
+ item if isinstance(item, Task) else Task(_entity_root=self._entity_root, **item)
for item in self._task_list
]
self._ledger_id = getattr(self, "id", None)
@@ -65,13 +73,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("Platform", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
for child in self._task_status_list:
child._teaql_attach_root(root)
@@ -84,30 +95,80 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("Platform"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._task_status_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+ for child in self._task_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "name" in self._loaded_fields:
payload["name"] = Value.Text(self.name)
+
if "founded" in self._loaded_fields:
payload["founded"] = Value.DateTime(self.founded)
+
if "userEmail" in self._loaded_fields:
payload["user_email"] = Value.Text(self.userEmail)
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -115,42 +176,47 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("Platform", payload)
elif action == "Update":
- cmd = UpdateCommand("Platform", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("Platform", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("Platform", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("Platform", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "name" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
- if "founded" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("founded"), message="Mutation requires a fully loaded entity")])
- if "userEmail" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("user_email"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "founded" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("founded"), message="Mutation requires a fully loaded entity")])
+ if "userEmail" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("user_email"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
for index, child in enumerate(self._task_status_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "platform", self)
- child._loaded_fields.add("platform")
- child._entity_root.set(child._teaql_entity_key(), "platform", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "platform", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_platform(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("task_status_list").index(index)
raise CheckException([
@@ -159,12 +225,11 @@ def _teaql_preflight_graph(self, context):
]) from error
for index, child in enumerate(self._task_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "platform", self)
- child._loaded_fields.add("platform")
- child._entity_root.set(child._teaql_entity_key(), "platform", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "platform", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_platform(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("task_list").index(index)
raise CheckException([
@@ -172,17 +237,19 @@ def _teaql_preflight_graph(self, context):
for v in error.violations
]) from error
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -233,6 +300,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -245,41 +313,46 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
- cascade_relations.append(("task_status_list", self._task_status_list, "update_platform"))
- cascade_relations.append(("task_list", self._task_list, "update_platform"))
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("task_status_list", self._task_status_list, "update_platform", "platform"))
+ cascade_relations.append(("task_list", self._task_list, "update_platform", "platform"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/task_board/generated/models/task.py b/examples/task_board/generated/models/task.py
index c938894..c022a34 100644
--- a/examples/task_board/generated/models/task.py
+++ b/examples/task_board/generated/models/task.py
@@ -1,10 +1,13 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
from models.task_status import TaskStatus
from models.platform import Platform
+
class Task:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -37,10 +40,15 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.name = kwargs.get("name")
+
self.status = kwargs.get("status")
+
self.platform = kwargs.get("platform")
+
self.version = kwargs.get("version")
+
if isinstance(self.status, dict):
self.status = TaskStatus(**self.status)
if isinstance(self.platform, dict):
@@ -51,7 +59,7 @@ def __init__(self, **kwargs):
if self._task_execution_log_list:
from models.task_execution_log import TaskExecutionLog
self._task_execution_log_list = [
- item if isinstance(item, TaskExecutionLog) else TaskExecutionLog(**item)
+ item if isinstance(item, TaskExecutionLog) else TaskExecutionLog(_entity_root=self._entity_root, **item)
for item in self._task_execution_log_list
]
self._ledger_id = getattr(self, "id", None)
@@ -62,13 +70,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("Task", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
for child in self._task_execution_log_list:
child._teaql_attach_root(root)
@@ -79,30 +90,85 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("Task"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._task_execution_log_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "name" in self._loaded_fields:
payload["name"] = Value.Text(self.name)
+
if "status" in self._loaded_fields:
- payload["status"] = Value.Object(self.status)
+ reference = self.status
+ reference_id = getattr(reference, "id", reference)
+ payload["status"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "platform" in self._loaded_fields:
- payload["platform"] = Value.Object(self.platform)
+ reference = self.platform
+ reference_id = getattr(reference, "id", reference)
+ payload["platform"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -110,42 +176,47 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("Task", payload)
elif action == "Update":
- cmd = UpdateCommand("Task", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("Task", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("Task", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("Task", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "name" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
- if "status" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("status"), message="Mutation requires a fully loaded entity")])
- if "platform" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("platform"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "status" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("status"), message="Mutation requires a fully loaded entity")])
+ if "platform" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("platform"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
for index, child in enumerate(self._task_execution_log_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "task", self)
- child._loaded_fields.add("task")
- child._entity_root.set(child._teaql_entity_key(), "task", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "task", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_task(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("task_execution_log_list").index(index)
raise CheckException([
@@ -153,17 +224,19 @@ def _teaql_preflight_graph(self, context):
for v in error.violations
]) from error
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -214,6 +287,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -226,40 +300,45 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
- cascade_relations.append(("task_execution_log_list", self._task_execution_log_list, "update_task"))
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("task_execution_log_list", self._task_execution_log_list, "update_task", "task"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/task_board/generated/models/task_execution_log.py b/examples/task_board/generated/models/task_execution_log.py
index 15294c5..c9063dc 100644
--- a/examples/task_board/generated/models/task_execution_log.py
+++ b/examples/task_board/generated/models/task_execution_log.py
@@ -1,9 +1,12 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
from models.task import Task
+
class TaskExecutionLog:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -36,10 +39,15 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.task = kwargs.get("task")
+
self.action = kwargs.get("action")
+
self.detail = kwargs.get("detail")
+
self.version = kwargs.get("version")
+
if isinstance(self.task, dict):
self.task = Task(**self.task)
self._ledger_id = getattr(self, "id", None)
@@ -50,13 +58,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("TaskExecutionLog", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
return self
def mark_for_deletion(self):
@@ -65,30 +76,78 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("TaskExecutionLog"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "task" in self._loaded_fields:
- payload["task"] = Value.Object(self.task)
+ reference = self.task
+ reference_id = getattr(reference, "id", reference)
+ payload["task"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "action" in self._loaded_fields:
payload["action"] = Value.Text(self.action)
+
if "detail" in self._loaded_fields:
payload["detail"] = Value.Text(self.detail)
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -96,46 +155,54 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("TaskExecutionLog", payload)
elif action == "Update":
- cmd = UpdateCommand("TaskExecutionLog", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("TaskExecutionLog", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("TaskExecutionLog", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("TaskExecutionLog", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "task" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("task"), message="Mutation requires a fully loaded entity")])
- if "action" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("action"), message="Mutation requires a fully loaded entity")])
- if "detail" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("detail"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "task" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("task"), message="Mutation requires a fully loaded entity")])
+ if "action" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("action"), message="Mutation requires a fully loaded entity")])
+ if "detail" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("detail"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -186,6 +253,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -198,39 +266,44 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/task_board/generated/models/task_status.py b/examples/task_board/generated/models/task_status.py
index 047cad1..91e2afd 100644
--- a/examples/task_board/generated/models/task_status.py
+++ b/examples/task_board/generated/models/task_status.py
@@ -1,9 +1,12 @@
from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot
+from teaql.core import MutationIntent
from teaql.core.value import Value
from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
import itertools
from models.platform import Platform
+
class TaskStatus:
_teaql_temporary_ids = itertools.count(1)
@classmethod
@@ -42,13 +45,21 @@ def __init__(self, **kwargs):
self._comment = None
self._loaded_fields = set(kwargs.keys())
self.id = kwargs.get("id")
+
self.name = kwargs.get("name")
+
self.code = kwargs.get("code")
+
self.color = kwargs.get("color")
+
self.displayOrder = kwargs.get("displayOrder")
+
self.progress = kwargs.get("progress")
+
self.platform = kwargs.get("platform")
+
self.version = kwargs.get("version")
+
if isinstance(self.platform, dict):
self.platform = Platform(**self.platform)
self._task_list = kwargs.get("task_list", [])
@@ -57,7 +68,7 @@ def __init__(self, **kwargs):
if self._task_list:
from models.task import Task
self._task_list = [
- item if isinstance(item, Task) else Task(**item)
+ item if isinstance(item, Task) else Task(_entity_root=self._entity_root, **item)
for item in self._task_list
]
self._ledger_id = getattr(self, "id", None)
@@ -68,13 +79,16 @@ def __init__(self, **kwargs):
self._entity_root.mark_as_new(key)
elif getattr(self, "version", None) is not None:
self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
def _teaql_entity_key(self):
return EntityKey("TaskStatus", self._ledger_id)
def _teaql_attach_root(self, root):
- if self._entity_root is not root:
- root.merge_from(self._entity_root)
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
self._entity_root = root
for child in self._task_list:
child._teaql_attach_root(root)
@@ -85,36 +99,90 @@ def mark_for_deletion(self):
return self
def audit_as(self, comment: str):
- if not isinstance(comment, str) or not comment.strip():
- raise ValueError("Security audit failure: audit_as() requires a non-empty reason")
+ MutationIntent(comment)
self._comment = comment
return self
async def save(self, context):
- return await context.execute_graph_save(lambda: self._teaql_preflight_and_save(context))
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
- async def _teaql_preflight_and_save(self, context):
- self._teaql_preflight_graph(context)
- return await self._teaql_save_within_graph(context)
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
- def _teaql_build_command(self):
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("TaskStatus"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._task_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
payload = {}
if "id" in self._loaded_fields:
payload["id"] = Value.I64(self.id)
+
if "name" in self._loaded_fields:
payload["name"] = Value.Text(self.name)
+
if "code" in self._loaded_fields:
payload["code"] = Value.Text(self.code)
+
if "color" in self._loaded_fields:
payload["color"] = Value.Text(self.color)
+
if "displayOrder" in self._loaded_fields:
payload["display_order"] = Value.Decimal(self.displayOrder)
+
if "progress" in self._loaded_fields:
payload["progress"] = Value.Decimal(self.progress)
+
if "platform" in self._loaded_fields:
- payload["platform"] = Value.Object(self.platform)
+ reference = self.platform
+ reference_id = getattr(reference, "id", reference)
+ payload["platform"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
if "version" in self._loaded_fields:
payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
action = self._action
if action == "Update":
ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
@@ -122,48 +190,53 @@ def _teaql_build_command(self):
if action == "Create":
cmd = InsertCommand("TaskStatus", payload)
elif action == "Update":
- cmd = UpdateCommand("TaskStatus", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("TaskStatus", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
for key, value in payload.items():
if key not in ("id", "version"): cmd.value(key, value)
else:
- cmd = DeleteCommand("TaskStatus", Value.from_any(getattr(self, "id", None)), getattr(self, "version", None))
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("TaskStatus", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
return action, cmd
- def _teaql_preflight_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
- if self._action == "Update":
- if "id" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
- if "name" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
- if "code" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("code"), message="Mutation requires a fully loaded entity")])
- if "color" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("color"), message="Mutation requires a fully loaded entity")])
- if "displayOrder" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("display_order"), message="Mutation requires a fully loaded entity")])
- if "progress" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("progress"), message="Mutation requires a fully loaded entity")])
- if "platform" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("platform"), message="Mutation requires a fully loaded entity")])
- if "version" not in self._loaded_fields:
- raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
- _action, cmd = self._teaql_build_command()
- try:
- context.check_and_fix_mutation(cmd)
- finally:
- for field, value in getattr(cmd, "values", {}).items():
- if field not in ("id", "version"):
- self._entity_root.set(self._teaql_entity_key(), field, value)
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "code" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("code"), message="Mutation requires a fully loaded entity")])
+ if "color" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("color"), message="Mutation requires a fully loaded entity")])
+ if "displayOrder" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("display_order"), message="Mutation requires a fully loaded entity")])
+ if "progress" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("progress"), message="Mutation requires a fully loaded entity")])
+ if "platform" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("platform"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
for index, child in enumerate(self._task_list):
child._teaql_attach_root(self._entity_root)
- setattr(child, "status", self)
- child._loaded_fields.add("status")
- child._entity_root.set(child._teaql_entity_key(), "status", Value.Object(self))
- child.audit_as(self._comment)
+ current = getattr(child, "status", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_status(self)
try:
- child._teaql_preflight_graph(context)
+ child._teaql_preflight_graph(graph)
except CheckException as error:
prefix = ObjectLocation().property("task_list").index(index)
raise CheckException([
@@ -171,17 +244,19 @@ def _teaql_preflight_graph(self, context):
for v in error.violations
]) from error
- async def _teaql_save_within_graph(self, context):
- if not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: audit_as() must be called before save()")
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
self._teaql_attach_root(self._entity_root)
action, cmd = self._teaql_build_command()
- req = MutationRequest(cmd)
- if self._comment:
- req.comment = self._comment
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
try:
context.check_and_fix_mutation(cmd)
@@ -250,6 +325,7 @@ async def _teaql_save_within_graph(self, context):
elif "version" in persisted:
self.version = persisted["version"]
self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
self._ledger_id = getattr(self, "id", self._ledger_id)
new_key = self._teaql_entity_key()
if old_key != new_key:
@@ -262,40 +338,45 @@ def rollback_entity():
self._loaded_fields = rollback_loaded_fields
if old_key != new_key:
self._entity_root.rekey(new_key, old_key)
- context.after_graph_rollback(rollback_entity)
+ graph.after_rollback(rollback_entity)
if action != "Delete":
self._action = "Update"
- cascade_relations = []
- cascade_relations.append(("task_list", self._task_list, "update_status"))
if action != "Delete":
- for relation_name, children, updater in cascade_relations:
- for index, child in enumerate(children):
- child._teaql_attach_root(self._entity_root)
- getattr(child, updater)(self)
- child.audit_as(self._comment)
- try:
- await child._teaql_save_within_graph(context)
- except CheckException as error:
- prefix = ObjectLocation().property(relation_name).index(index)
- raise CheckException([
- CheckResult(
- violation.rule_id,
- violation.location.prefixed_by(prefix),
- violation.input_value,
- violation.system_value,
- violation.message,
- )
- for violation in error.violations
- ]) from error
+ await self._teaql_save_children(graph, scope)
def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
self._entity_root.clear_entity(new_key)
if getattr(self, "version", None) is not None:
- self._entity_root.set_original_version(new_key, int(self.version))
- context.after_graph_commit(commit_entity)
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
return self
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("task_list", self._task_list, "update_status", "status"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
self.id = value
self._loaded_fields.add("id")
self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
diff --git a/examples/task_board/generated/pyproject.toml b/examples/task_board/generated/pyproject.toml
index bee20bc..684b260 100644
--- a/examples/task_board/generated/pyproject.toml
+++ b/examples/task_board/generated/pyproject.toml
@@ -2,7 +2,7 @@
name = "robot-kanban-service-lib"
version = "1.0.0"
description = "Generated python library"
-dependencies = ["teaql==0.2.7", "aiosqlite>=0.22.1"]
+dependencies = ["teaql==0.2.9", "aiosqlite>=0.22.1"]
[tool.setuptools]
py-modules = ["Q", "E"]
diff --git a/examples/task_board/generated/requests/platform_request.py b/examples/task_board/generated/requests/platform_request.py
index c47f851..4fd798b 100644
--- a/examples/task_board/generated/requests/platform_request.py
+++ b/examples/task_board/generated/requests/platform_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -719,8 +720,7 @@ def comment(self, c: str):
def new_entity(self, context) -> Platform:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("Platform", Platform())
if not isinstance(entity, Platform):
raise TypeError("entity initializer returned an incompatible Platform")
@@ -728,10 +728,9 @@ def new_entity(self, context) -> Platform:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -739,53 +738,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[Platform]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (Platform(_entity_root=query_root, **row) for row in result.rows),
+ (Platform(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[Platform]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(Platform(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(Platform(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutablePlatformRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield Platform(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield Platform(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/task_board/generated/requests/task_execution_log_request.py b/examples/task_board/generated/requests/task_execution_log_request.py
index 1cbb08a..895a961 100644
--- a/examples/task_board/generated/requests/task_execution_log_request.py
+++ b/examples/task_board/generated/requests/task_execution_log_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -442,8 +443,7 @@ def comment(self, c: str):
def new_entity(self, context) -> TaskExecutionLog:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("TaskExecutionLog", TaskExecutionLog())
if not isinstance(entity, TaskExecutionLog):
raise TypeError("entity initializer returned an incompatible TaskExecutionLog")
@@ -451,10 +451,9 @@ def new_entity(self, context) -> TaskExecutionLog:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -462,53 +461,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[TaskExecutionLog]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (TaskExecutionLog(_entity_root=query_root, **row) for row in result.rows),
+ (TaskExecutionLog(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[TaskExecutionLog]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(TaskExecutionLog(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(TaskExecutionLog(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableTaskExecutionLogRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield TaskExecutionLog(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield TaskExecutionLog(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/task_board/generated/requests/task_request.py b/examples/task_board/generated/requests/task_request.py
index eaa6d57..fb2965a 100644
--- a/examples/task_board/generated/requests/task_request.py
+++ b/examples/task_board/generated/requests/task_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -442,8 +443,7 @@ def comment(self, c: str):
def new_entity(self, context) -> Task:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("Task", Task())
if not isinstance(entity, Task):
raise TypeError("entity initializer returned an incompatible Task")
@@ -451,10 +451,9 @@ def new_entity(self, context) -> Task:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -462,53 +461,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[Task]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (Task(_entity_root=query_root, **row) for row in result.rows),
+ (Task(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[Task]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(Task(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(Task(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableTaskRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield Task(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield Task(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/task_board/generated/requests/task_status_request.py b/examples/task_board/generated/requests/task_status_request.py
index 737eb50..0bc30e6 100644
--- a/examples/task_board/generated/requests/task_status_request.py
+++ b/examples/task_board/generated/requests/task_status_request.py
@@ -1,7 +1,8 @@
from teaql.core.query import RelationAggregate, SelectQuery
from teaql.core.list import SmartList, TeaQLPage
-from teaql.runtime import EntityRoot
from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
from teaql.core.expr import (
begin_with, between, column, contain, end_with, eq, gt, gte,
in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
@@ -793,8 +794,7 @@ def comment(self, c: str):
def new_entity(self, context) -> TaskStatus:
request = self._request
- if not request._comment or not request._comment.strip() or not request._purpose or not request._purpose.strip():
- raise ValueError("Security audit failure: non-empty comment() and purpose() are required before new_entity()")
+ QueryIntent(request._comment, request._purpose)
entity = context.initialize_entity("TaskStatus", TaskStatus())
if not isinstance(entity, TaskStatus):
raise TypeError("entity initializer returned an incompatible TaskStatus")
@@ -802,10 +802,9 @@ def new_entity(self, context) -> TaskStatus:
async def execute_for_result(self, context):
self = self._request
- if not self._purpose or not self._purpose.strip() or not self._comment or not self._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_rows()")
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
- req = QueryRequest(context.prepare_query(self.query), _comment=self._comment, _purpose=self._purpose)
return await service.query(context, req)
async def execute_for_rows(self, context):
@@ -813,53 +812,59 @@ async def execute_for_rows(self, context):
async def execute_for_list(self, context) -> SmartList[TaskStatus]:
result = await self.execute_for_result(context)
- query_root = EntityRoot()
return SmartList(
- (TaskStatus(_entity_root=query_root, **row) for row in result.rows),
+ (TaskStatus(**row) for row in result.rows),
facets=result.facets)
async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[TaskStatus]:
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise ValueError("Security audit failure: comment() and purpose() must be called before execute_for_page()")
- request.query.offset(offset).limit(limit)
- authorized = context.prepare_query(request.query)
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
service = context.require_resource("dataService")
alias = "__teaql_total"
- if authorized.id_set_pagination is not None:
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
retained_count, accuracy = context.id_set_count()
if accuracy == "EXACT":
total_count = retained_count
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
else:
- count_result = await service.query(context, QueryRequest(authorized.for_exact_count(alias), _comment=request._comment, _purpose=request._purpose))
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
raise RuntimeError("dataService did not return an exact page count")
total_count = int(count_result.rows[0][alias])
- row_result = await service.query(context, QueryRequest(authorized, _comment=request._comment, _purpose=request._purpose))
- query_root = EntityRoot()
- data = SmartList(TaskStatus(_entity_root=query_root, **row) for row in row_result.rows)
+ row_result = await service.query(context, authorized)
+ data = SmartList(TaskStatus(**row) for row in row_result.rows)
return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
async def execute_for_one(self, context):
- self._request.limit(1)
- entities = await self.execute_for_list(context)
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableTaskStatusRequest(request).execute_for_list(context)
return entities[0] if entities else None
- async def execute_for_stream(self, context, chunk_size: int = 1000):
+ def execute_for_stream(self, context, chunk_size: int = 1000):
"""Yield entity chunks lazily from the provider cursor."""
request = self._request
- if not request._purpose or not request._purpose.strip() or not request._comment or not request._comment.strip():
- raise Exception("Security audit failure: comment() and purpose() must be called before execute_for_stream()")
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
service = context.require_resource("dataService")
if not hasattr(service, "query_stream"):
raise RuntimeError("dataService does not implement query_stream")
- query_root = EntityRoot()
- async for chunk in service.query_stream(context, QueryRequest(context.prepare_query(request.query), _comment=request._comment, _purpose=request._purpose), chunk_size):
- for row in chunk.rows:
- yield TaskStatus(_entity_root=query_root, **row)
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield TaskStatus(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/trace-chain/AGENTS.md b/examples/trace-chain/AGENTS.md
new file mode 100644
index 0000000..399301f
--- /dev/null
+++ b/examples/trace-chain/AGENTS.md
@@ -0,0 +1,31 @@
+# trace-chain-service application instructions
+
+Implement only application-owned code. Generated models, requests, expressions,
+Q, E and Runtime Module are read-only. Do not read or search generated library
+source for API discovery, including verification-only discovery.
+
+Use current model-aware Assist for the next operation, not every operation:
+
+```text
+cargo teaql --input model.xml python-assist-query/ksml_entity
+cargo teaql --input model.xml python-assist-query/ksml_entity.ksml_field
+cargo teaql --input model.xml python-assist-create/ksml_entity
+cargo teaql --input model.xml python-assist-update/ksml_entity
+cargo teaql --input model.xml python-assist-delete/ksml_entity
+cargo teaql --input model.xml python-assist-expression/ksml_entity
+```
+
+Use actual KSML snake_case entity/field names and the actual model path. If
+Assist cannot supply the required operation, report MISSING_ASSIST; do not guess
+or open generated source. Compiler diagnostics and executable tests validate usage.
+
+Every query is bounded and has non-blank comment and purpose. Use generated Q
+for loading and E for loaded traversal. Load all scalar fields before mutation;
+mark_for_deletion() and save(context) the composed graph with a non-blank audit_as
+reason. Child reasons remain local; unannotated children inherit their parent.
+Public execution accepts only UserContext. Schema changes are explicit through
+context.ensure_schema(), with generated bootstrap intact.
+
+Verify twice on one SQLite file without database cleanup. Use unique test labels,
+keep generated library hashes unchanged, and retain commands, exit statuses and
+skipped/unverified checks. Local source tests do not prove a published artifact.
\ No newline at end of file
diff --git a/examples/trace-chain/README.md b/examples/trace-chain/README.md
new file mode 100644
index 0000000..aa1a30e
--- /dev/null
+++ b/examples/trace-chain/README.md
@@ -0,0 +1,161 @@
+# Python Trace Chain Example
+
+This example uses an evaluated six-entity KSML definition and an unchanged
+generated library. Application code uses generated Q for loading, E for loaded
+traversal, and audited save for the composed graph. A transparent observer
+captures actual requests and SQL metadata; the safe audit sink asserts that
+database commit has already completed. No expected trace frames are supplied
+to the planner or provider.
+
+The generated aggregate suite observes every physical SQL's unmodified runtime
+metadata with logging enabled and disabled. Root/nested aggregates retain the
+original query, verified reverse-relation edges and all loaded ancestors. In the
+same query, a loaded Payment list preserves membership and its real parent ID
+even when a forward detail filter excludes the parent; generated E rejects
+unfetched description as NotLoaded. Counts remain 1 filtered / 2 total. Numeric
+non-relation grouping/window paths are separately tested by
+`tests/provider/sqlite/test_numeric_partition_trace.py`; they must not invent a
+relation edge. These checks do not decide whether to allow forward-detail
+filtering as an application API, which remains a design discussion.
+
+```bash
+cd /path/to/teaql-python
+bash examples/trace-chain/verify.sh
+```
+
+The verifier uses this repository's runtime source. It executes the native
+request-intent gate twice on fresh fixtures and every application suite twice
+on retained databases without cleanup, then checks generated library hashes. The normative
+graph and ownership fixtures require separate SQLite files because their graph
+sizes differ. Set `TEAQL_TRACE_CHAIN_DB` and `TEAQL_TRACE_CHAIN_SHARED_DB` to retain
+and replay those files. The page/stream suite has its own
+`TEAQL_TRACE_CHAIN_PAGE_STREAM_DB` file. Each suite reuses its file across both runs. Unique
+business labels avoid deleting existing demo records.
+
+The native intent gate runs 90 SQLite cases: all seventeen shared rejection
+vectors across transaction/direct execution and logging on/off, ten missing-root
+graph-save cases, and twelve explicit-comment/blank-route-tail positive cases.
+Rejections assert the exact code, field, request kind and repair hint, zero
+Query/Mutation Policy and Checker calls, no physical reads/writes/stream opens,
+no invalid-input transaction begin and no SQL/audit events. Positive controls
+prove the observed policies and real SQLite reads/writes remain functional.
+Blank Entity/Provider/SQL tail nodes cannot erase the explicit root comment;
+actual statement metadata and committed safe audit retain it. These native
+fixtures are not a substitute for generated-entry or protocol-decoder coverage.
+
+The verifier also runs 66 native LIKE privacy cases twice: typed contains,
+starts/ends and negations retain exact original operands for safe intent logging,
+without changing executable patterns or caller/policy inputs. Tests cover direct
+and transaction queries, count, future-child secrets before first-root SQL,
+failure/next-request isolation, logging off, raw/literal wildcard controls,
+rewritten ASTs and scalar streaming with debug opt-in/revocation. Private source
+provenance is not a wire field. These are native small fixtures, not a new
+generated-query scenario or an all-provider acceptance claim.
+
+Seven scenario groups verify missing root reason before transaction access,
+six-item branch/deletion lineage at request/SQL/audit boundaries, Q/E reload and
+retained soft deletion, three relation levels with inherited query intent, two
+concurrent graph saves in one Context, failed mutation, and failed authoritative
+readback. Failures retain SQL evidence and emit no committed audits.
+
+Successful mutations also retain their actually executed readback SELECTs. The
+six-item mutation verifies six writes followed individually by six readbacks,
+with six committed audit events, not twelve. Logical mutation metadata preserves
+its operation and affected-row count; ordered `statements` contain the write and
+readback leaves. Readback paths use `Operation(CustomerOrder, query)` and
+`Request(CustomerOrder)` even for a PaymentAttempt, while typed target identity
+and branch responsibility stay in the mutation lineage. Diagnostic SELECT
+switches do not erase the physical result metadata. Native tests cover no-Context
+results, grouped batches, zero affected rows, hard deletes and masked readback
+intent.
+
+The six-item fixture also checks exact `(entity type, ID)` lists before creating
+lineage dictionaries, so duplicate rows cannot disappear during comparison.
+CustomerOrder and Payment deliberately share a numeric ID. Creation, the
+update/deletion save, and each concurrent independent graph must retain all six
+distinct typed targets in commands, command-bound physical SQL and committed
+safe audit. SQL paths themselves do not carry IDs: each actual write/readback
+pair is bound to the observed provider command, with its target entity frame,
+operation, successful outcome and row counts checked. The normative save emits
+one `GRAPH IDENTITY EVIDENCE` record. Three negative controls reject duplicate,
+missing and equal-ID type-collapsed targets; the verifier requires their marker
+and the complete example marker on both retained-database runs.
+
+The mutation privacy suite configures Payment.reference_code as sensitive and
+creates/updates a three-entity graph using generated Q/E/save. Root and sibling
+write/readback intent plus committed safe audit must hide the future child's new
+and old values. Alphabetic canaries avoid false positives from numeric identity
+redaction. Generated E verifies original stored values and versions; the next
+independent query proves there is no ambient privacy on shared Context. Original
+scalar snapshot ownership is implemented in the runtime; generation supplies
+loaded scalar data and advances the snapshot only after successful commit.
+Loaded deletion and updates of another field also retain private old scalar
+provenance, including unchanged business fields but excluding structural ID and
+version. The generated command passes this detached snapshot without adding
+fields to SQL writes. These cases require regeneration with the matching local
+runtime. Native RecoverCommand capture is tested separately; this example does
+not claim an end-to-end generated recovery flow.
+Native tests additionally cover batch/graph rollback, readback cancellation,
+concurrent native batches, debug opt-in, credentials and safe reprojection.
+
+Four additional ownership cases use generated Q/E/save: two independent loaded
+roots share the same provider-returned Platform record without sharing mutable
+ledgers; adoption imports only a reached changed child; a clean ancestor emits
+no write while retaining its changed child's audit scope; and conflicting loaded
+versions reject before business SQL. Public async saves overlap but transactions
+and Checker preparation serialize at the Context gate. The shared record is a
+mutable dictionary deliberately used read-only and checked unchanged, not a
+structurally immutable snapshot. JSON observations retain actual commands,
+optimistic versions, SQL metadata and committed audits.
+
+Eight page/stream phases use the same model and current list-page/field Assist.
+Offset 1 / limit 2 returns two independent roots with original versions 2/1 and
+exact total 3. Changing the caller's builder while COUNT is paused does not
+change later row/relation intent. Root and child saves emit only their actual
+changed operations in the customer policy's reviewed plan, SQL and committed
+audit. Shared Platform data remains unchanged.
+
+Count safely retains private bindings declared by removed descendants, without
+executing those descendants or storing privacy on Context. The next independent
+query does not inherit that privacy. Two scalar streams retain creation-time
+intent across delayed consumption, with two actual SQLite driver generators
+live while an independent query runs. Explicit close releases both nested
+iterators immediately; successful exhaustion and early cancellation retain
+distinct outcomes/cardinalities. Streamed rows own independent ledgers; saving
+one row does not write or clear another row's pending changes. Relation hydration
+is not supported by scalar streaming; use list or page for relation graphs.
+This rejection includes related aggregate enhancements and occurs before opening
+a provider cursor, even with SQL logging disabled. Native SQLite trace tests also
+cover loading and aggregating the same forward relation, nested scalar-key
+recovery and aggregate failure/privacy. These native tests do not yet constitute
+generated acceptance of a forward-relation aggregate API; the generated
+`relation_aggregate.py` suite covers reverse counts combined with filtered
+forward detail and checked list membership.
+
+`PAGE_STREAM_OBSERVED` records actual SQL, policy-reviewed operations, commands,
+optimistic versions and safe audits. Native tests additionally cover COUNT
+failure and trusted policy removing the private child. Current generation also
+rekeys a new object's ledger when an explicit positive integer ID is assigned,
+rejecting invalid IDs before changing object/ledger state.
+
+The model evaluation reports zero errors, warnings and suggestions, with
+seventeen Solids. Current model-aware Assist and evaluation evidence are retained
+under `evidence/`. `AGENTS.md` governs application implementation. The library is
+regenerated only by the upstream `PythonTraceChainExampleGenerationTest`; do not
+edit generated files to fix application usage.
+
+The assigned-identity suite verifies `TC-MUT-07` through actual generated saves.
+New root/annotated child/unannotated sibling start with negative temporary typed
+ledger keys; no explicit IDs or expected trace nodes are injected. Saving
+allocates positive IDs which appear in emitted commands, every actual
+write/readback metadata entry and committed safe audit. Q/E reload verifies
+stored names, versions and parent IDs. A freshly loaded update carries only
+its own reason. Both diagnostic logging modes run twice using the retained
+ownership database, not the normative fixture's equal-ID sequence. The verifier
+requires GNU coreutils `timeout` (60-second deadline, 5-second kill grace) and
+the exact completion marker. This proves allocation and lineage, not batch
+grouping or a complete-ledger override.
+
+Prepared batch grouping, generated ledger override, complete mutation privacy,
+external database acceptance and immutable Registry consumer replay are not
+established by this example. Local tests do not prove any published version.
diff --git a/examples/trace-chain/assigned_identity.py b/examples/trace-chain/assigned_identity.py
new file mode 100644
index 0000000..ee676cb
--- /dev/null
+++ b/examples/trace-chain/assigned_identity.py
@@ -0,0 +1,146 @@
+"""TC-MUT-07: observe assigned identities from generated graph saves, not fixtures."""
+import asyncio
+import json
+import os
+import uuid
+
+from E import E
+from Q import Q
+from runtime_module import GENERATED_RUNTIME_MODULE
+from teaql.core.entity import EntityKey
+from teaql.provider.sqlite import create_sqlite_service
+from teaql.runtime import UserContext
+
+from main import AuditSink, ObservedService, chain, new, reset, scalar
+
+
+async def load(context, identity):
+ return await (Q.customer_orders().with_id_is(identity).limit(1)
+ .select_order_item_list_with(Q.order_items().limit(10))
+ .comment('load assigned identity graph').purpose('verify generated persistence and lineage')
+ .execute_for_one(context))
+
+
+def verify_sinks(service, sink, context, expected, reason, operation, logging):
+ assert service.finishes == ['commit']
+ assert len(service.requests) == len(service.results) == len(sink.events) == len(expected)
+ assert {(entity, identity): nodes for entity, identity, nodes in service.requests} == expected
+ assert {(event.entity, scalar(event.entity_id)): chain(event.trace_chain)
+ for event in sink.events} == expected
+ for (entity, identity, nodes), metadata in zip(service.requests, service.results):
+ assert identity > 0 and chain(metadata.mutation_lineage) == expected[(entity, identity)]
+ assert metadata.comment == reason and len(metadata.statements) == 2
+ for index, statement in enumerate(metadata.statements):
+ assert statement.execution_outcome == 'success'
+ assert statement.comment == reason and chain(statement.mutation_lineage) == nodes
+ assert statement.result_count == 1 if index else statement.affected_rows == 1
+ assert all(node.kind == 'auditReason' and node.entity_id > 0
+ for node in statement.mutation_lineage)
+ path = statement.trace_chain
+ assert [node.kind for node in path] == [
+ 'operation', 'request' if index else 'entity', 'provider', 'sql']
+ assert [node.name for node in path] == [
+ 'CustomerOrder', 'CustomerOrder' if index else entity, 'sqlite',
+ 'select' if index else operation]
+ logs = list(context.sql_logs())
+ assert len(logs) == (2 * len(expected) if logging else 0)
+ if logging:
+ assert [chain(entry.mutation_lineage) for entry in logs] == [
+ nodes for _, _, nodes in service.requests for _ in range(2)]
+ assert all(entry.comment == reason and entry.execution_outcome == 'success' for entry in logs)
+
+
+async def verify(context, platform, service, sink, logging):
+ if logging:
+ context.enable_all_sql_log()
+ else:
+ context.disable_sql_log()
+ label = 'ASSIGNED-' + uuid.uuid4().hex
+ root = new(Q.customer_orders(), context).update_platform(platform)
+ root.update_order_number(label).update_description('new assigned graph')
+ annotated = new(Q.order_items(), context).update_customer_order(root).update_name('annotated ' + label)
+ sibling = new(Q.order_items(), context).update_customer_order(root).update_name('ordinary ' + label)
+ annotated.audit_as('annotated newly allocated child')
+ root.order_item_list().extend([annotated, sibling])
+ # Observe only native ledger state. No explicit ID assignment or trace injection.
+ temporary = [entity._teaql_entity_key() for entity in (root, annotated, sibling)]
+ assert all(isinstance(key.id, int) and key.id < 0 for key in temporary)
+ assert len(set(temporary)) == 3
+ assert all(key in entity._entity_root.new_keys()
+ for entity, key in zip((root, annotated, sibling), temporary))
+ reset(service, sink, context)
+ await root.audit_as('create graph with allocated identities').save(context)
+ identities = [E.customer_order(root).id().eval(), E.order_item(annotated).id().eval(),
+ E.order_item(sibling).id().eval()]
+ assert all(isinstance(identity, int) and identity > 0 for identity in identities)
+ assert identities[1] != identities[2]
+ reason = 'create graph with allocated identities'
+ root_node = ('CustomerOrder', identities[0], reason)
+ expected = {
+ ('CustomerOrder', identities[0]): [root_node],
+ ('OrderItem', identities[1]): [root_node, ('OrderItem', identities[1], 'annotated newly allocated child')],
+ ('OrderItem', identities[2]): [root_node],
+ }
+ verify_sinks(service, sink, context, expected, reason, 'insert', logging)
+ for entity, old, identity in zip((root, annotated, sibling), temporary, identities):
+ ledger = entity._entity_root
+ assert old not in ledger.new_keys() and not ledger.has_pending(old)
+ assert not ledger.has_pending(EntityKey(old.entity, identity))
+ print('ASSIGNED_IDENTITY_OBSERVED ' + json.dumps(dict(
+ logging=logging, temporary=[(key.entity, key.id) for key in temporary],
+ commands=service.requests,
+ sql=[dict(comment=item.comment, sql=item.parameterized_sql,
+ parameters=[scalar(value) for value in item.parameters],
+ outcome=item.execution_outcome, result_count=item.result_count,
+ affected_rows=item.affected_rows, lineage=chain(item.mutation_lineage),
+ path=[(node.kind, node.name) for node in item.trace_chain])
+ for metadata in service.results for item in metadata.statements],
+ audit=[dict(entity=event.entity, id=scalar(event.entity_id), lineage=chain(event.trace_chain))
+ for event in sink.events]), sort_keys=True))
+ loaded = await load(context, identities[0])
+ assert E.customer_order(loaded).order_number().eval() == label
+ assert E.customer_order(loaded).version().eval() == 1
+ children = {E.order_item(item).id().eval(): item for item in loaded.order_item_list()}
+ assert set(children) == set(identities[1:])
+ for identity in identities[1:]:
+ assert E.order_item(children[identity]).version().eval() == 1
+ assert E.order_item(children[identity]).customer_order_id().eval() == identities[0]
+ assert E.order_item(children[identities[1]]).name().eval() == 'annotated ' + label
+ assert E.order_item(children[identities[2]]).name().eval() == 'ordinary ' + label
+
+ # A fresh loaded graph has no previous operation's local comments.
+ loaded.update_description('independent loaded update')
+ for item in loaded.order_item_list():
+ item.update_name('revised ' + label)
+ reset(service, sink, context)
+ reason = 'update independently loaded graph'
+ await loaded.audit_as(reason).save(context)
+ next_expected = {key: [('CustomerOrder', identities[0], reason)] for key in expected}
+ verify_sinks(service, sink, context, next_expected, reason, 'update', logging)
+ reloaded = await load(context, identities[0])
+ assert E.customer_order(reloaded).version().eval() == 2
+ assert E.customer_order(reloaded).description().eval() == 'independent loaded update'
+ assert {E.order_item(item).id().eval() for item in reloaded.order_item_list()} == set(identities[1:])
+ assert all(E.order_item(item).version().eval() == 2 and
+ E.order_item(item).name().eval() == 'revised ' + label
+ for item in reloaded.order_item_list())
+ print(f'PASS: assigned identity logging={logging}; 3 inserts then 3 independent updates')
+
+
+async def main():
+ context = UserContext.new().install(GENERATED_RUNTIME_MODULE)
+ service = ObservedService(create_sqlite_service(os.environ['TEAQL_TRACE_CHAIN_DB']))
+ sink = AuditSink(service)
+ context.insert_resource('dataService', service).with_app_audit_event_sink(sink)
+ context.set_diagnostic_sql_log_sink(type('Silent', (), {'write': lambda self, entry: None})())
+ await context.ensure_schema()
+ platform = await Q.platforms().with_id_is(1).limit(1).comment('reuse bootstrap root').purpose(
+ 'attach generated assigned identity graph').execute_for_one(context)
+ assert platform is not None
+ for logging in (False, True):
+ await verify(context, platform, service, sink, logging)
+ print('PASS: Python generated assigned identity 2 scenarios; command/SQL/audit and independent reload')
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/examples/trace-chain/checker_overlap.py b/examples/trace-chain/checker_overlap.py
new file mode 100644
index 0000000..002f060
--- /dev/null
+++ b/examples/trace-chain/checker_overlap.py
@@ -0,0 +1,285 @@
+"""Real generated Checker overlap; app observers never synthesize validation or traces."""
+import asyncio
+import copy
+import json
+import os
+import uuid
+
+from E import E
+from Q import Q
+from runtime_module import GENERATED_RUNTIME_MODULE
+from teaql.core.entity import EntityKey
+from teaql.provider.sqlite import create_sqlite_service
+from teaql.runtime import UserContext
+from teaql.runtime.i18n import CheckException
+
+from main import AuditSink, ObservedTransaction, chain, reset, scalar
+from shared_reference import SharedService, pending, seed
+
+
+class ObservedContext(UserContext):
+ def __init__(self):
+ super().__init__()
+ self.observation = dict(active=0, peak=0, callbacks=0, callback_peak=0, checks=[])
+
+ async def execute_graph_save(self, work, *, comment=None):
+ state = self.observation
+ state['active'] += 1
+ state['peak'] = max(state['peak'], state['active'])
+
+ async def observed(session):
+ state['callbacks'] += 1
+ state['callback_peak'] = max(state['callback_peak'], state['callbacks'])
+ session.transaction.entry['reason'] = comment
+ try:
+ return await work(session)
+ finally:
+ state['callbacks'] -= 1
+
+ try:
+ return await super().execute_graph_save(observed, comment=comment)
+ finally:
+ state['active'] -= 1
+
+
+class ObservedRegistry:
+ def __init__(self, delegate, state):
+ self.delegate, self.state = delegate, state
+
+ def checker(self, entity):
+ delegate = self.delegate.checker(entity)
+ if delegate is None:
+ return None
+ state = self.state
+
+ class Checker:
+ def check_and_fix(self, context, record, location, results):
+ before = len(results)
+ delegate.check_and_fix(context, record, location, results)
+ state['checks'].append({
+ 'entity': entity,
+ 'reason': context._graph_session.intent.comment,
+ 'violations': [result.to_wire() for result in results[before:]],
+ })
+
+ return Checker()
+
+
+class Transaction(ObservedTransaction):
+ def __init__(self, owner, transaction):
+ super().__init__(owner, transaction)
+ self.entry = dict(reason=None, outcome='pending', physical=[])
+ owner.transactions.append(self.entry)
+ for name in ('execute_sql', 'fetch_all_sql'):
+ delegate = getattr(transaction.transport, name)
+
+ async def observed(compiled, delegate=delegate, name=name):
+ self.entry['physical'].append(dict(
+ method=name, sql=compiled.sql, params=[scalar(value) for value in compiled.params]))
+ return await delegate(compiled)
+
+ setattr(transaction.transport, name, observed)
+
+ async def mutate(self, context, request):
+ self.owner.request_comments.append(request.comment())
+ return await super().mutate(context, request)
+
+ async def commit(self, context):
+ await super().commit(context)
+ self.entry['outcome'] = 'commit'
+
+ async def rollback(self, context):
+ await super().rollback(context)
+ self.entry['outcome'] = 'rollback'
+
+
+class Service(SharedService):
+ def __init__(self, service):
+ super().__init__(service)
+ self.transactions = []
+ self.request_comments = []
+
+ async def begin(self, context):
+ pause, self.pause = self.pause, None
+ if pause:
+ entered, release = pause
+ entered.set()
+ await asyncio.wait_for(release.wait(), 10)
+ self.begins += 1
+ return Transaction(self, await self.service.begin(context))
+
+
+async def load(context, identity):
+ return await (Q.customer_orders().with_id_is(identity)
+ .select_platform_with(Q.platforms().limit(1))
+ .select_order_item_list_with(Q.order_items().limit(10)).limit(1)
+ .comment('load complete Checker graph').purpose('preserve versions and readonly references')
+ .execute_for_one(context))
+
+
+def clear(context, service, sink):
+ reset(service, sink, context)
+ service.transactions.clear()
+ service.request_comments.clear()
+ state = context.observation
+ assert state['active'] == state['callbacks'] == 0
+ state['peak'] = state['callback_peak'] = 0
+ state['checks'].clear()
+
+
+async def verify(context, platform, service, sink, logging, invalid_first):
+ if logging:
+ context.enable_all_sql_log()
+ else:
+ context.disable_sql_log()
+ accepted, rejected = await seed(context, platform), await seed(context, platform)
+ accepted_id, rejected_id = E.customer_order(accepted).id().eval(), E.customer_order(rejected).id().eval()
+ service.platform_snapshot = None
+ service.snapshot_uses = 0
+ service.reuse_platform = True
+ try:
+ accepted, rejected = await load(context, accepted_id), await load(context, rejected_id)
+ finally:
+ service.reuse_platform = False
+ assert service.snapshot_uses == 2
+ snapshot = copy.deepcopy(service.platform_snapshot)
+ left = E.customer_order(accepted).platform().eval()
+ right = E.customer_order(rejected).platform().eval()
+ assert accepted._entity_root is not rejected._entity_root
+ assert left is not right and left._entity_root is not right._entity_root
+ platform_version = E.platform(left).version().eval()
+ original_description = E.customer_order(rejected).description().eval()
+ original_version = E.customer_order(rejected).version().eval()
+ nonce = uuid.uuid4().hex.translate(str.maketrans('0123456789', 'ghijklmnop'))
+ accepted_secret, rejected_secret = 'PRIVATE-ACCEPT-' + nonce, 'PRIVATE-REJECT-' + nonce
+ accepted_reason = f'accept {accepted_secret}; unrelated {rejected_secret}'
+ rejected_reason = f'reject {rejected_secret}'
+ accepted.update_description(accepted_secret)
+ child = accepted.order_item_list()[0]
+ child.update_name('accepted-child-' + nonce).audit_as('accepted child responsibility')
+ rejected.update_description(rejected_secret)
+ # An existing child avoids unrelated ID allocation before validation.
+ rejected.order_item_list()[0].update_name(None).audit_as('rejected child responsibility')
+ clear(context, service, sink)
+ entered, release = asyncio.Event(), asyncio.Event()
+ service.pause = (entered, release)
+ ordered = [(rejected, rejected_reason), (accepted, accepted_reason)] if invalid_first else [
+ (accepted, accepted_reason), (rejected, rejected_reason)]
+ first = asyncio.create_task(ordered[0][0].audit_as(ordered[0][1]).save(context))
+ await asyncio.wait_for(entered.wait(), 10)
+ second = asyncio.create_task(ordered[1][0].audit_as(ordered[1][1]).save(context))
+ await asyncio.sleep(0)
+ assert not first.done() and not second.done()
+ assert context._graph_save_lock.locked()
+ assert context.observation['peak'] == 2, 'two real public Save calls must be live'
+ release.set()
+ results = await asyncio.wait_for(asyncio.gather(first, second, return_exceptions=True), 15)
+ valid_result, invalid_result = (results[1], results[0]) if invalid_first else results
+ assert not isinstance(valid_result, BaseException), repr(valid_result)
+ assert isinstance(invalid_result, CheckException), repr(invalid_result)
+ assert any(result.rule_id.upper() == 'REQUIRED' and result.location.native_path == 'order_item_list[0].name'
+ for result in invalid_result.violations), str(invalid_result)
+ assert context.observation['callback_peak'] == 1, 'the existing graph gate serializes Checker callbacks'
+ checks = context.observation['checks']
+ assert any(check['reason'] == rejected_reason and check['entity'] == 'OrderItem'
+ and check['violations'] for check in checks)
+ assert any(check['reason'] == accepted_reason and check['entity'] == 'OrderItem'
+ and not check['violations'] for check in checks)
+ expected = [[('CustomerOrder', accepted_id, accepted_reason)],
+ [('CustomerOrder', accepted_id, accepted_reason),
+ ('OrderItem', E.order_item(child).id().eval(), 'accepted child responsibility')]]
+ assert service.requests == [('CustomerOrder', accepted_id, expected[0]),
+ ('OrderItem', E.order_item(child).id().eval(), expected[1])]
+ assert service.request_comments == [accepted_reason, accepted_reason]
+ assert len(service.results) == len(sink.events) == 2
+ physical = [statement for result in service.results for statement in result.statements]
+ assert len(physical) == 4
+ assert [chain(statement.mutation_lineage) for statement in physical] == [
+ expected[0], expected[0], expected[1], expected[1]]
+ assert all(statement.comment == accepted_reason and statement.execution_outcome == 'success'
+ for statement in physical)
+ for index, statement in enumerate(physical):
+ assert [node.kind for node in statement.trace_chain] == [
+ 'operation', 'request' if index % 2 else 'entity', 'provider', 'sql']
+ assert statement.trace_chain[0].name == 'CustomerOrder'
+ assert [node.name for node in statement.trace_chain[-2:]] == ['sqlite', 'select' if index % 2 else 'update']
+ assert statement.result_count == 1 if index % 2 else statement.affected_rows == 1
+ assert len(service.transactions) == 2
+ failed = next(item for item in service.transactions if item['reason'] == rejected_reason)
+ committed = next(item for item in service.transactions if item['reason'] == accepted_reason)
+ assert failed['outcome'] == 'rollback' and failed['physical'] == [], failed
+ assert committed['outcome'] == 'commit' and len(committed['physical']) == 4
+ assert accepted_secret in committed['physical'][0]['params'], 'real SQL values are unchanged'
+ assert [(event.entity, scalar(event.entity_id)) for event in sink.events] == [
+ ('CustomerOrder', accepted_id), ('OrderItem', E.order_item(child).id().eval())]
+ safe_expected = []
+ for event, lineage in zip(sink.events, expected):
+ reason = chain(event.trace_chain)[0][2]
+ assert accepted_secret not in repr(event) and rejected_secret in reason
+ safe_lineage = [('CustomerOrder', accepted_id, reason), *lineage[1:]]
+ assert chain(event.trace_chain) == safe_lineage
+ safe_expected.append(safe_lineage)
+ logs = list(context.sql_logs())
+ assert len(logs) == (4 if logging else 0)
+ for index, entry in enumerate(logs):
+ assert accepted_secret not in repr(entry) and rejected_secret in entry.comment
+ assert entry.trace_path[0].name == 'CustomerOrder'
+ assert chain(entry.mutation_lineage) == safe_expected[index // 2]
+ assert service.platform_snapshot == snapshot
+ assert E.platform(left).version().eval() == E.platform(right).version().eval() == platform_version
+ for reference in (left, right):
+ assert not pending(reference._entity_root, EntityKey('Platform', E.platform(reference).id().eval()))
+ assert context.get_resource('fix_time') is None and context._graph_session is None
+ assert context._checked_mutations == set() and context._fix_evidence_current == []
+ proof = dict(logging=logging, invalidFirst=invalid_first, publicOverlap=context.observation['peak'],
+ serializedCallbacks=context.observation['callback_peak'], checks=copy.deepcopy(checks),
+ commands=copy.deepcopy(service.requests), transactions=copy.deepcopy(service.transactions),
+ committedAudit=[dict(entity=event.entity, identity=scalar(event.entity_id),
+ lineage=chain(event.trace_chain)) for event in sink.events],
+ sql=[dict(comment=entry.comment, lineage=chain(entry.mutation_lineage)) for entry in logs])
+ stored_valid, stored_invalid = await load(context, accepted_id), await load(context, rejected_id)
+ assert E.customer_order(stored_valid).description().eval() == accepted_secret
+ assert E.order_item(stored_valid.order_item_list()[0]).name().eval() == 'accepted-child-' + nonce
+ assert E.customer_order(stored_invalid).description().eval() == original_description
+ assert E.customer_order(stored_invalid).version().eval() == original_version
+ assert len(stored_invalid.order_item_list()) == 1
+ assert E.platform(E.customer_order(stored_invalid).platform().eval()).version().eval() == platform_version
+ assert pending(rejected._entity_root, EntityKey('CustomerOrder', rejected_id))
+ assert E.customer_order(rejected).description().eval() == rejected_secret
+ clear(context, service, sink)
+ next_reason = f'independent after rejection {accepted_secret} {rejected_secret}'
+ stored_invalid.update_description('independent committed value')
+ await stored_invalid.audit_as(next_reason).save(context)
+ next_lineage = [('CustomerOrder', rejected_id, next_reason)]
+ assert service.requests == [('CustomerOrder', rejected_id, next_lineage)]
+ assert service.request_comments == [next_reason]
+ assert len(sink.events) == 1 and chain(sink.events[0].trace_chain) == next_lineage
+ assert [chain(item.mutation_lineage) for item in service.results[0].statements] == [next_lineage, next_lineage]
+ assert len(context.sql_logs()) == (2 if logging else 0)
+ for entry in context.sql_logs():
+ assert entry.comment == next_reason
+ assert E.customer_order(await load(context, rejected_id)).description().eval() == 'independent committed value'
+ print('CHECKER EVIDENCE ' + json.dumps(dict(proof, nextIndependentSave='committed', sharedReadonlyReference=True)))
+
+
+async def main():
+ context = ObservedContext().install(GENERATED_RUNTIME_MODULE)
+ registry = context.get_resource('checker_registry')
+ assert registry.checker('CustomerOrder') is not None and registry.checker('OrderItem') is not None
+ context.set_checker_registry(ObservedRegistry(registry, context.observation))
+ service = Service(create_sqlite_service(os.environ['TEAQL_TRACE_CHAIN_DB']))
+ sink = AuditSink(service)
+ context.insert_resource('dataService', service).with_app_audit_event_sink(sink)
+ context.set_diagnostic_sql_log_sink(type('Silent', (), {'write': lambda self, entry: None})())
+ context.configure_audit_policy('CustomerOrder', ['description'])
+ await context.ensure_schema()
+ platform = await Q.platforms().with_id_is(1).limit(1).comment('reuse generated root').purpose(
+ 'attach generated Checker fixtures').execute_for_one(context)
+ for logging in (False, True):
+ for invalid_first in (False, True):
+ await verify(context, platform, service, sink, logging, invalid_first)
+ print('PASS: Python generated Checker overlap 4 scenarios; accepted-only command/SQL/audit; serialized callbacks')
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/examples/trace-chain/evidence/assist/python-assist-create/customer_order.md b/examples/trace-chain/evidence/assist/python-assist-create/customer_order.md
new file mode 100644
index 0000000..40eab47
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-create/customer_order.md
@@ -0,0 +1,94 @@
+
+
+# Python Assist — Create `CustomerOrder`
+
+Use the exact generated `Q.customer_orders()` entry point. Trusted actor,
+tenant, policy, provider, initialization, and audit infrastructure belong to
+`UserContext`, never to business input.
+
+```python
+from Q import Q
+
+
+async def create_customer_order(
+ platform,
+ order_number,
+ description,
+ context,
+):
+ entity = (
+ Q.customer_orders()
+ .comment("what: initialize Customer Order")
+ .purpose("why: create Customer Order")
+ .new_entity(context)
+ )
+
+ entity.update_platform(platform)
+ entity.update_order_number(order_number)
+ entity.update_description(description)
+
+ await entity.audit_as(
+ "Create Customer Order for the requested business operation"
+ ).save(context)
+ return entity
+```
+
+Only the generated updater methods above are writable. Constant candidates,
+when present, use these exact allow-listed public methods:
+
+Compile and execute the source unchanged. Verify persistence and query-back.
+Missing/blank intent, missing/blank audit reason, unknown fields, and attempted
+trusted-context overrides must fail. Do not edit generated sources.
+## Compose a graph before one audited save
+
+Create children through their generated Q new_entity(context), populate their
+required fields using child Create Assist, then append them to these exact lists:
+
+- `entity.order_item_list().append(child)` — generated `OrderItem` child; use `python-assist-create/order_item`
+- `entity.payment_list().append(child)` — generated `Payment` child; use `python-assist-create/payment`
+- `entity.shipment_list().append(child)` — generated `Shipment` child; use `python-assist-create/shipment`
+
+
+For a branch-local reason call `child.audit_as("non-blank local reason")` before
+appending. An unannotated child inherits its parent's lineage. Save the composed
+root once with `entity.audit_as("root reason").save(context)`. Do not separately
+save children or override their comments with the root's comment. Deletion is
+`child.mark_for_deletion()` followed by saving the composed root.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `create`.
+
+- Validate and allow-list writable business fields; never mass-assign dynamic JSON.
+- Create through the generated request/entity API, attach a non-empty audit reason,
+ save with the same UserContext, and return the runtime's native save result.
+- Add a negative test proving a missing audit reason cannot write.
diff --git a/examples/trace-chain/evidence/assist/python-assist-create/order_item.md b/examples/trace-chain/evidence/assist/python-assist-create/order_item.md
new file mode 100644
index 0000000..0e3378b
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-create/order_item.md
@@ -0,0 +1,77 @@
+
+
+# Python Assist — Create `OrderItem`
+
+Use the exact generated `Q.order_items()` entry point. Trusted actor,
+tenant, policy, provider, initialization, and audit infrastructure belong to
+`UserContext`, never to business input.
+
+```python
+from Q import Q
+
+
+async def create_order_item(
+ customer_order,
+ name,
+ context,
+):
+ entity = (
+ Q.order_items()
+ .comment("what: initialize Order Item")
+ .purpose("why: create Order Item")
+ .new_entity(context)
+ )
+
+ entity.update_customer_order(customer_order)
+ entity.update_name(name)
+
+ await entity.audit_as(
+ "Create Order Item for the requested business operation"
+ ).save(context)
+ return entity
+```
+
+Only the generated updater methods above are writable. Constant candidates,
+when present, use these exact allow-listed public methods:
+
+Compile and execute the source unchanged. Verify persistence and query-back.
+Missing/blank intent, missing/blank audit reason, unknown fields, and attempted
+trusted-context overrides must fail. Do not edit generated sources.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `create`.
+
+- Validate and allow-list writable business fields; never mass-assign dynamic JSON.
+- Create through the generated request/entity API, attach a non-empty audit reason,
+ save with the same UserContext, and return the runtime's native save result.
+- Add a negative test proving a missing audit reason cannot write.
diff --git a/examples/trace-chain/evidence/assist/python-assist-create/payment.md b/examples/trace-chain/evidence/assist/python-assist-create/payment.md
new file mode 100644
index 0000000..b62f08f
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-create/payment.md
@@ -0,0 +1,90 @@
+
+
+# Python Assist — Create `Payment`
+
+Use the exact generated `Q.payments()` entry point. Trusted actor,
+tenant, policy, provider, initialization, and audit infrastructure belong to
+`UserContext`, never to business input.
+
+```python
+from Q import Q
+
+
+async def create_payment(
+ customer_order,
+ reference_code,
+ context,
+):
+ entity = (
+ Q.payments()
+ .comment("what: initialize Payment")
+ .purpose("why: create Payment")
+ .new_entity(context)
+ )
+
+ entity.update_customer_order(customer_order)
+ entity.update_reference_code(reference_code)
+
+ await entity.audit_as(
+ "Create Payment for the requested business operation"
+ ).save(context)
+ return entity
+```
+
+Only the generated updater methods above are writable. Constant candidates,
+when present, use these exact allow-listed public methods:
+
+Compile and execute the source unchanged. Verify persistence and query-back.
+Missing/blank intent, missing/blank audit reason, unknown fields, and attempted
+trusted-context overrides must fail. Do not edit generated sources.
+## Compose a graph before one audited save
+
+Create children through their generated Q new_entity(context), populate their
+required fields using child Create Assist, then append them to these exact lists:
+
+- `entity.payment_attempt_list().append(child)` — generated `PaymentAttempt` child; use `python-assist-create/payment_attempt`
+
+
+For a branch-local reason call `child.audit_as("non-blank local reason")` before
+appending. An unannotated child inherits its parent's lineage. Save the composed
+root once with `entity.audit_as("root reason").save(context)`. Do not separately
+save children or override their comments with the root's comment. Deletion is
+`child.mark_for_deletion()` followed by saving the composed root.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `create`.
+
+- Validate and allow-list writable business fields; never mass-assign dynamic JSON.
+- Create through the generated request/entity API, attach a non-empty audit reason,
+ save with the same UserContext, and return the runtime's native save result.
+- Add a negative test proving a missing audit reason cannot write.
diff --git a/examples/trace-chain/evidence/assist/python-assist-create/payment_attempt.md b/examples/trace-chain/evidence/assist/python-assist-create/payment_attempt.md
new file mode 100644
index 0000000..46b1733
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-create/payment_attempt.md
@@ -0,0 +1,77 @@
+
+
+# Python Assist — Create `PaymentAttempt`
+
+Use the exact generated `Q.payment_attempts()` entry point. Trusted actor,
+tenant, policy, provider, initialization, and audit infrastructure belong to
+`UserContext`, never to business input.
+
+```python
+from Q import Q
+
+
+async def create_payment_attempt(
+ payment,
+ reference_code,
+ context,
+):
+ entity = (
+ Q.payment_attempts()
+ .comment("what: initialize Payment Attempt")
+ .purpose("why: create Payment Attempt")
+ .new_entity(context)
+ )
+
+ entity.update_payment(payment)
+ entity.update_reference_code(reference_code)
+
+ await entity.audit_as(
+ "Create Payment Attempt for the requested business operation"
+ ).save(context)
+ return entity
+```
+
+Only the generated updater methods above are writable. Constant candidates,
+when present, use these exact allow-listed public methods:
+
+Compile and execute the source unchanged. Verify persistence and query-back.
+Missing/blank intent, missing/blank audit reason, unknown fields, and attempted
+trusted-context overrides must fail. Do not edit generated sources.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `create`.
+
+- Validate and allow-list writable business fields; never mass-assign dynamic JSON.
+- Create through the generated request/entity API, attach a non-empty audit reason,
+ save with the same UserContext, and return the runtime's native save result.
+- Add a negative test proving a missing audit reason cannot write.
diff --git a/examples/trace-chain/evidence/assist/python-assist-create/platform.md b/examples/trace-chain/evidence/assist/python-assist-create/platform.md
new file mode 100644
index 0000000..e47ed31
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-create/platform.md
@@ -0,0 +1,88 @@
+
+
+# Python Assist — Create `Platform`
+
+Use the exact generated `Q.platforms()` entry point. Trusted actor,
+tenant, policy, provider, initialization, and audit infrastructure belong to
+`UserContext`, never to business input.
+
+```python
+from Q import Q
+
+
+async def create_platform(
+ name,
+ context,
+):
+ entity = (
+ Q.platforms()
+ .comment("what: initialize Platform")
+ .purpose("why: create Platform")
+ .new_entity(context)
+ )
+
+ entity.update_name(name)
+
+ await entity.audit_as(
+ "Create Platform for the requested business operation"
+ ).save(context)
+ return entity
+```
+
+Only the generated updater methods above are writable. Constant candidates,
+when present, use these exact allow-listed public methods:
+
+Compile and execute the source unchanged. Verify persistence and query-back.
+Missing/blank intent, missing/blank audit reason, unknown fields, and attempted
+trusted-context overrides must fail. Do not edit generated sources.
+## Compose a graph before one audited save
+
+Create children through their generated Q new_entity(context), populate their
+required fields using child Create Assist, then append them to these exact lists:
+
+- `entity.customer_order_list().append(child)` — generated `CustomerOrder` child; use `python-assist-create/customer_order`
+
+
+For a branch-local reason call `child.audit_as("non-blank local reason")` before
+appending. An unannotated child inherits its parent's lineage. Save the composed
+root once with `entity.audit_as("root reason").save(context)`. Do not separately
+save children or override their comments with the root's comment. Deletion is
+`child.mark_for_deletion()` followed by saving the composed root.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `create`.
+
+- Validate and allow-list writable business fields; never mass-assign dynamic JSON.
+- Create through the generated request/entity API, attach a non-empty audit reason,
+ save with the same UserContext, and return the runtime's native save result.
+- Add a negative test proving a missing audit reason cannot write.
diff --git a/examples/trace-chain/evidence/assist/python-assist-create/shipment.md b/examples/trace-chain/evidence/assist/python-assist-create/shipment.md
new file mode 100644
index 0000000..f74731d
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-create/shipment.md
@@ -0,0 +1,77 @@
+
+
+# Python Assist — Create `Shipment`
+
+Use the exact generated `Q.shipments()` entry point. Trusted actor,
+tenant, policy, provider, initialization, and audit infrastructure belong to
+`UserContext`, never to business input.
+
+```python
+from Q import Q
+
+
+async def create_shipment(
+ customer_order,
+ reference_code,
+ context,
+):
+ entity = (
+ Q.shipments()
+ .comment("what: initialize Shipment")
+ .purpose("why: create Shipment")
+ .new_entity(context)
+ )
+
+ entity.update_customer_order(customer_order)
+ entity.update_reference_code(reference_code)
+
+ await entity.audit_as(
+ "Create Shipment for the requested business operation"
+ ).save(context)
+ return entity
+```
+
+Only the generated updater methods above are writable. Constant candidates,
+when present, use these exact allow-listed public methods:
+
+Compile and execute the source unchanged. Verify persistence and query-back.
+Missing/blank intent, missing/blank audit reason, unknown fields, and attempted
+trusted-context overrides must fail. Do not edit generated sources.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `create`.
+
+- Validate and allow-list writable business fields; never mass-assign dynamic JSON.
+- Create through the generated request/entity API, attach a non-empty audit reason,
+ save with the same UserContext, and return the runtime's native save result.
+- Add a negative test proving a missing audit reason cannot write.
diff --git a/examples/trace-chain/evidence/assist/python-assist-delete/customer_order.md b/examples/trace-chain/evidence/assist/python-assist-delete/customer_order.md
new file mode 100644
index 0000000..0954fe7
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-delete/customer_order.md
@@ -0,0 +1,67 @@
+
+
+# Python Assist — Delete `Customer Order`
+
+Load the current row and its original optimistic version. Delete is audited,
+version-aware soft deletion; never reconstruct an entity from client-supplied
+ID/version or issue physical-delete SQL.
+
+```python
+from Q import Q
+
+async def delete_customer_order(context, entity_id) -> bool:
+ entity = await (Q.customer_orders()
+ .with_id_is(entity_id)
+
+ .comment("what: load current Customer Order for deletion")
+ .purpose("why: preserve original version for optimistic locking")
+ .execute_for_one(context))
+ if entity is None:
+ return False
+
+ await (entity.mark_for_deletion()
+ .audit_as("Delete Customer Order for the requested business operation")
+ .save(context))
+ return True
+```
+
+Compile the source unchanged. Prove retained negative-version persistence,
+normal-query hiding, `deleted_rows_only()` visibility, stale-copy rejection,
+not-found false, audit enforcement, and absence of physical-delete APIs.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `delete`.
+
+- Load the policy-scoped current entity, mark it for deletion, then use audited
+ save with the same UserContext. Do not invent a physical-delete API.
+- Require an audit reason and optimistic version. Test missing audit and stale
+ version as explicit failures.
diff --git a/examples/trace-chain/evidence/assist/python-assist-delete/order_item.md b/examples/trace-chain/evidence/assist/python-assist-delete/order_item.md
new file mode 100644
index 0000000..a163ea4
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-delete/order_item.md
@@ -0,0 +1,67 @@
+
+
+# Python Assist — Delete `Order Item`
+
+Load the current row and its original optimistic version. Delete is audited,
+version-aware soft deletion; never reconstruct an entity from client-supplied
+ID/version or issue physical-delete SQL.
+
+```python
+from Q import Q
+
+async def delete_order_item(context, entity_id) -> bool:
+ entity = await (Q.order_items()
+ .with_id_is(entity_id)
+
+ .comment("what: load current Order Item for deletion")
+ .purpose("why: preserve original version for optimistic locking")
+ .execute_for_one(context))
+ if entity is None:
+ return False
+
+ await (entity.mark_for_deletion()
+ .audit_as("Delete Order Item for the requested business operation")
+ .save(context))
+ return True
+```
+
+Compile the source unchanged. Prove retained negative-version persistence,
+normal-query hiding, `deleted_rows_only()` visibility, stale-copy rejection,
+not-found false, audit enforcement, and absence of physical-delete APIs.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `delete`.
+
+- Load the policy-scoped current entity, mark it for deletion, then use audited
+ save with the same UserContext. Do not invent a physical-delete API.
+- Require an audit reason and optimistic version. Test missing audit and stale
+ version as explicit failures.
diff --git a/examples/trace-chain/evidence/assist/python-assist-delete/payment.md b/examples/trace-chain/evidence/assist/python-assist-delete/payment.md
new file mode 100644
index 0000000..ef93b65
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-delete/payment.md
@@ -0,0 +1,67 @@
+
+
+# Python Assist — Delete `Payment`
+
+Load the current row and its original optimistic version. Delete is audited,
+version-aware soft deletion; never reconstruct an entity from client-supplied
+ID/version or issue physical-delete SQL.
+
+```python
+from Q import Q
+
+async def delete_payment(context, entity_id) -> bool:
+ entity = await (Q.payments()
+ .with_id_is(entity_id)
+
+ .comment("what: load current Payment for deletion")
+ .purpose("why: preserve original version for optimistic locking")
+ .execute_for_one(context))
+ if entity is None:
+ return False
+
+ await (entity.mark_for_deletion()
+ .audit_as("Delete Payment for the requested business operation")
+ .save(context))
+ return True
+```
+
+Compile the source unchanged. Prove retained negative-version persistence,
+normal-query hiding, `deleted_rows_only()` visibility, stale-copy rejection,
+not-found false, audit enforcement, and absence of physical-delete APIs.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `delete`.
+
+- Load the policy-scoped current entity, mark it for deletion, then use audited
+ save with the same UserContext. Do not invent a physical-delete API.
+- Require an audit reason and optimistic version. Test missing audit and stale
+ version as explicit failures.
diff --git a/examples/trace-chain/evidence/assist/python-assist-delete/payment_attempt.md b/examples/trace-chain/evidence/assist/python-assist-delete/payment_attempt.md
new file mode 100644
index 0000000..8fc3fca
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-delete/payment_attempt.md
@@ -0,0 +1,67 @@
+
+
+# Python Assist — Delete `Payment Attempt`
+
+Load the current row and its original optimistic version. Delete is audited,
+version-aware soft deletion; never reconstruct an entity from client-supplied
+ID/version or issue physical-delete SQL.
+
+```python
+from Q import Q
+
+async def delete_payment_attempt(context, entity_id) -> bool:
+ entity = await (Q.payment_attempts()
+ .with_id_is(entity_id)
+
+ .comment("what: load current Payment Attempt for deletion")
+ .purpose("why: preserve original version for optimistic locking")
+ .execute_for_one(context))
+ if entity is None:
+ return False
+
+ await (entity.mark_for_deletion()
+ .audit_as("Delete Payment Attempt for the requested business operation")
+ .save(context))
+ return True
+```
+
+Compile the source unchanged. Prove retained negative-version persistence,
+normal-query hiding, `deleted_rows_only()` visibility, stale-copy rejection,
+not-found false, audit enforcement, and absence of physical-delete APIs.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `delete`.
+
+- Load the policy-scoped current entity, mark it for deletion, then use audited
+ save with the same UserContext. Do not invent a physical-delete API.
+- Require an audit reason and optimistic version. Test missing audit and stale
+ version as explicit failures.
diff --git a/examples/trace-chain/evidence/assist/python-assist-delete/platform.md b/examples/trace-chain/evidence/assist/python-assist-delete/platform.md
new file mode 100644
index 0000000..37243ea
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-delete/platform.md
@@ -0,0 +1,67 @@
+
+
+# Python Assist — Delete `Platform`
+
+Load the current row and its original optimistic version. Delete is audited,
+version-aware soft deletion; never reconstruct an entity from client-supplied
+ID/version or issue physical-delete SQL.
+
+```python
+from Q import Q
+
+async def delete_platform(context, entity_id) -> bool:
+ entity = await (Q.platforms()
+ .with_id_is(entity_id)
+
+ .comment("what: load current Platform for deletion")
+ .purpose("why: preserve original version for optimistic locking")
+ .execute_for_one(context))
+ if entity is None:
+ return False
+
+ await (entity.mark_for_deletion()
+ .audit_as("Delete Platform for the requested business operation")
+ .save(context))
+ return True
+```
+
+Compile the source unchanged. Prove retained negative-version persistence,
+normal-query hiding, `deleted_rows_only()` visibility, stale-copy rejection,
+not-found false, audit enforcement, and absence of physical-delete APIs.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `delete`.
+
+- Load the policy-scoped current entity, mark it for deletion, then use audited
+ save with the same UserContext. Do not invent a physical-delete API.
+- Require an audit reason and optimistic version. Test missing audit and stale
+ version as explicit failures.
diff --git a/examples/trace-chain/evidence/assist/python-assist-delete/shipment.md b/examples/trace-chain/evidence/assist/python-assist-delete/shipment.md
new file mode 100644
index 0000000..c89e451
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-delete/shipment.md
@@ -0,0 +1,67 @@
+
+
+# Python Assist — Delete `Shipment`
+
+Load the current row and its original optimistic version. Delete is audited,
+version-aware soft deletion; never reconstruct an entity from client-supplied
+ID/version or issue physical-delete SQL.
+
+```python
+from Q import Q
+
+async def delete_shipment(context, entity_id) -> bool:
+ entity = await (Q.shipments()
+ .with_id_is(entity_id)
+
+ .comment("what: load current Shipment for deletion")
+ .purpose("why: preserve original version for optimistic locking")
+ .execute_for_one(context))
+ if entity is None:
+ return False
+
+ await (entity.mark_for_deletion()
+ .audit_as("Delete Shipment for the requested business operation")
+ .save(context))
+ return True
+```
+
+Compile the source unchanged. Prove retained negative-version persistence,
+normal-query hiding, `deleted_rows_only()` visibility, stale-copy rejection,
+not-found false, audit enforcement, and absence of physical-delete APIs.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `delete`.
+
+- Load the policy-scoped current entity, mark it for deletion, then use audited
+ save with the same UserContext. Do not invent a physical-delete API.
+- Require an audit reason and optimistic version. Test missing audit and stale
+ version as explicit failures.
diff --git a/examples/trace-chain/evidence/assist/python-assist-expression/customer_order.md b/examples/trace-chain/evidence/assist/python-assist-expression/customer_order.md
new file mode 100644
index 0000000..8daee67
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-expression/customer_order.md
@@ -0,0 +1,132 @@
+
+
+# Python Assist — Expression `Customer Order`
+
+The generated E facade preserves Value, loaded `None`, and NotLoaded. `eval()`
+returns a native value for the first two and raises `TeaQLNotLoadedError` for
+the third. `or_if_null` is null-only and never hides NotLoaded.
+
+```python
+from E import E
+from models.customer_order import CustomerOrder
+
+
+def extract_customer_order_id(entity: CustomerOrder):
+ return E.customer_order(entity).id().eval()
+
+
+def extract_customer_order_id_or_if_null(entity: CustomerOrder, fallback):
+ return E.customer_order(entity).id().or_if_null(fallback)
+
+
+def extract_customer_order_order_number(entity: CustomerOrder):
+ return E.customer_order(entity).order_number().eval()
+
+
+def extract_customer_order_order_number_or_if_null(entity: CustomerOrder, fallback):
+ return E.customer_order(entity).order_number().or_if_null(fallback)
+
+
+def extract_customer_order_description(entity: CustomerOrder):
+ return E.customer_order(entity).description().eval()
+
+
+def extract_customer_order_description_or_if_null(entity: CustomerOrder, fallback):
+ return E.customer_order(entity).description().or_if_null(fallback)
+
+
+def extract_customer_order_version(entity: CustomerOrder):
+ return E.customer_order(entity).version().eval()
+
+
+def extract_customer_order_version_or_if_null(entity: CustomerOrder, fallback):
+ return E.customer_order(entity).version().or_if_null(fallback)
+
+
+def extract_customer_order_platform_id(entity: CustomerOrder):
+ return E.customer_order(entity).platform_id().eval()
+
+
+def traverse_customer_order_platform(entity: CustomerOrder):
+ return E.customer_order(entity).platform().eval()
+
+
+def aggregate_customer_order_order_item_list_size(entity: CustomerOrder):
+ return E.customer_order(entity).order_item_list().size().eval()
+
+
+def first_customer_order_order_item_list_id(entity: CustomerOrder):
+ return E.customer_order(entity).order_item_list().first().id().eval()
+
+
+def get_customer_order_order_item_list_id(entity: CustomerOrder, index: int):
+ return E.customer_order(entity).order_item_list().get(index).id().eval()
+
+
+def aggregate_customer_order_payment_list_size(entity: CustomerOrder):
+ return E.customer_order(entity).payment_list().size().eval()
+
+
+def first_customer_order_payment_list_id(entity: CustomerOrder):
+ return E.customer_order(entity).payment_list().first().id().eval()
+
+
+def get_customer_order_payment_list_id(entity: CustomerOrder, index: int):
+ return E.customer_order(entity).payment_list().get(index).id().eval()
+
+
+def aggregate_customer_order_shipment_list_size(entity: CustomerOrder):
+ return E.customer_order(entity).shipment_list().size().eval()
+
+
+def first_customer_order_shipment_list_id(entity: CustomerOrder):
+ return E.customer_order(entity).shipment_list().first().id().eval()
+
+
+def get_customer_order_shipment_list_id(entity: CustomerOrder, index: int):
+ return E.customer_order(entity).shipment_list().get(index).id().eval()
+
+
+
+```
+
+Select every traversed field and relation. Never catch `TeaQLNotLoadedError`
+merely to supply a default, and never replace generated E accessors with direct
+attribute access.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `expression`.
+
+- Distinguish a loaded null from a field or relation that was not projected. A
+ NotLoaded/coding error must remain visible; do not turn it into an ordinary null.
+- Select every traversed relation first and use the generated E/expression API for
+ scalar, object, and list traversal. Do not translate Java accessor names by guess.
diff --git a/examples/trace-chain/evidence/assist/python-assist-expression/order_item.md b/examples/trace-chain/evidence/assist/python-assist-expression/order_item.md
new file mode 100644
index 0000000..e393781
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-expression/order_item.md
@@ -0,0 +1,88 @@
+
+
+# Python Assist — Expression `Order Item`
+
+The generated E facade preserves Value, loaded `None`, and NotLoaded. `eval()`
+returns a native value for the first two and raises `TeaQLNotLoadedError` for
+the third. `or_if_null` is null-only and never hides NotLoaded.
+
+```python
+from E import E
+from models.order_item import OrderItem
+
+
+def extract_order_item_id(entity: OrderItem):
+ return E.order_item(entity).id().eval()
+
+
+def extract_order_item_id_or_if_null(entity: OrderItem, fallback):
+ return E.order_item(entity).id().or_if_null(fallback)
+
+
+def extract_order_item_name(entity: OrderItem):
+ return E.order_item(entity).name().eval()
+
+
+def extract_order_item_name_or_if_null(entity: OrderItem, fallback):
+ return E.order_item(entity).name().or_if_null(fallback)
+
+
+def extract_order_item_version(entity: OrderItem):
+ return E.order_item(entity).version().eval()
+
+
+def extract_order_item_version_or_if_null(entity: OrderItem, fallback):
+ return E.order_item(entity).version().or_if_null(fallback)
+
+
+def extract_order_item_customer_order_id(entity: OrderItem):
+ return E.order_item(entity).customer_order_id().eval()
+
+
+def traverse_order_item_customer_order(entity: OrderItem):
+ return E.order_item(entity).customer_order().eval()
+
+
+
+```
+
+Select every traversed field and relation. Never catch `TeaQLNotLoadedError`
+merely to supply a default, and never replace generated E accessors with direct
+attribute access.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `expression`.
+
+- Distinguish a loaded null from a field or relation that was not projected. A
+ NotLoaded/coding error must remain visible; do not turn it into an ordinary null.
+- Select every traversed relation first and use the generated E/expression API for
+ scalar, object, and list traversal. Do not translate Java accessor names by guess.
diff --git a/examples/trace-chain/evidence/assist/python-assist-expression/payment.md b/examples/trace-chain/evidence/assist/python-assist-expression/payment.md
new file mode 100644
index 0000000..6ea4873
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-expression/payment.md
@@ -0,0 +1,100 @@
+
+
+# Python Assist — Expression `Payment`
+
+The generated E facade preserves Value, loaded `None`, and NotLoaded. `eval()`
+returns a native value for the first two and raises `TeaQLNotLoadedError` for
+the third. `or_if_null` is null-only and never hides NotLoaded.
+
+```python
+from E import E
+from models.payment import Payment
+
+
+def extract_payment_id(entity: Payment):
+ return E.payment(entity).id().eval()
+
+
+def extract_payment_id_or_if_null(entity: Payment, fallback):
+ return E.payment(entity).id().or_if_null(fallback)
+
+
+def extract_payment_reference_code(entity: Payment):
+ return E.payment(entity).reference_code().eval()
+
+
+def extract_payment_reference_code_or_if_null(entity: Payment, fallback):
+ return E.payment(entity).reference_code().or_if_null(fallback)
+
+
+def extract_payment_version(entity: Payment):
+ return E.payment(entity).version().eval()
+
+
+def extract_payment_version_or_if_null(entity: Payment, fallback):
+ return E.payment(entity).version().or_if_null(fallback)
+
+
+def extract_payment_customer_order_id(entity: Payment):
+ return E.payment(entity).customer_order_id().eval()
+
+
+def traverse_payment_customer_order(entity: Payment):
+ return E.payment(entity).customer_order().eval()
+
+
+def aggregate_payment_payment_attempt_list_size(entity: Payment):
+ return E.payment(entity).payment_attempt_list().size().eval()
+
+
+def first_payment_payment_attempt_list_id(entity: Payment):
+ return E.payment(entity).payment_attempt_list().first().id().eval()
+
+
+def get_payment_payment_attempt_list_id(entity: Payment, index: int):
+ return E.payment(entity).payment_attempt_list().get(index).id().eval()
+
+
+
+```
+
+Select every traversed field and relation. Never catch `TeaQLNotLoadedError`
+merely to supply a default, and never replace generated E accessors with direct
+attribute access.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `expression`.
+
+- Distinguish a loaded null from a field or relation that was not projected. A
+ NotLoaded/coding error must remain visible; do not turn it into an ordinary null.
+- Select every traversed relation first and use the generated E/expression API for
+ scalar, object, and list traversal. Do not translate Java accessor names by guess.
diff --git a/examples/trace-chain/evidence/assist/python-assist-expression/payment_attempt.md b/examples/trace-chain/evidence/assist/python-assist-expression/payment_attempt.md
new file mode 100644
index 0000000..3dca7e1
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-expression/payment_attempt.md
@@ -0,0 +1,88 @@
+
+
+# Python Assist — Expression `Payment Attempt`
+
+The generated E facade preserves Value, loaded `None`, and NotLoaded. `eval()`
+returns a native value for the first two and raises `TeaQLNotLoadedError` for
+the third. `or_if_null` is null-only and never hides NotLoaded.
+
+```python
+from E import E
+from models.payment_attempt import PaymentAttempt
+
+
+def extract_payment_attempt_id(entity: PaymentAttempt):
+ return E.payment_attempt(entity).id().eval()
+
+
+def extract_payment_attempt_id_or_if_null(entity: PaymentAttempt, fallback):
+ return E.payment_attempt(entity).id().or_if_null(fallback)
+
+
+def extract_payment_attempt_reference_code(entity: PaymentAttempt):
+ return E.payment_attempt(entity).reference_code().eval()
+
+
+def extract_payment_attempt_reference_code_or_if_null(entity: PaymentAttempt, fallback):
+ return E.payment_attempt(entity).reference_code().or_if_null(fallback)
+
+
+def extract_payment_attempt_version(entity: PaymentAttempt):
+ return E.payment_attempt(entity).version().eval()
+
+
+def extract_payment_attempt_version_or_if_null(entity: PaymentAttempt, fallback):
+ return E.payment_attempt(entity).version().or_if_null(fallback)
+
+
+def extract_payment_attempt_payment_id(entity: PaymentAttempt):
+ return E.payment_attempt(entity).payment_id().eval()
+
+
+def traverse_payment_attempt_payment(entity: PaymentAttempt):
+ return E.payment_attempt(entity).payment().eval()
+
+
+
+```
+
+Select every traversed field and relation. Never catch `TeaQLNotLoadedError`
+merely to supply a default, and never replace generated E accessors with direct
+attribute access.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `expression`.
+
+- Distinguish a loaded null from a field or relation that was not projected. A
+ NotLoaded/coding error must remain visible; do not turn it into an ordinary null.
+- Select every traversed relation first and use the generated E/expression API for
+ scalar, object, and list traversal. Do not translate Java accessor names by guess.
diff --git a/examples/trace-chain/evidence/assist/python-assist-expression/platform.md b/examples/trace-chain/evidence/assist/python-assist-expression/platform.md
new file mode 100644
index 0000000..4d081f3
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-expression/platform.md
@@ -0,0 +1,92 @@
+
+
+# Python Assist — Expression `Platform`
+
+The generated E facade preserves Value, loaded `None`, and NotLoaded. `eval()`
+returns a native value for the first two and raises `TeaQLNotLoadedError` for
+the third. `or_if_null` is null-only and never hides NotLoaded.
+
+```python
+from E import E
+from models.platform import Platform
+
+
+def extract_platform_id(entity: Platform):
+ return E.platform(entity).id().eval()
+
+
+def extract_platform_id_or_if_null(entity: Platform, fallback):
+ return E.platform(entity).id().or_if_null(fallback)
+
+
+def extract_platform_name(entity: Platform):
+ return E.platform(entity).name().eval()
+
+
+def extract_platform_name_or_if_null(entity: Platform, fallback):
+ return E.platform(entity).name().or_if_null(fallback)
+
+
+def extract_platform_version(entity: Platform):
+ return E.platform(entity).version().eval()
+
+
+def extract_platform_version_or_if_null(entity: Platform, fallback):
+ return E.platform(entity).version().or_if_null(fallback)
+
+
+def aggregate_platform_customer_order_list_size(entity: Platform):
+ return E.platform(entity).customer_order_list().size().eval()
+
+
+def first_platform_customer_order_list_id(entity: Platform):
+ return E.platform(entity).customer_order_list().first().id().eval()
+
+
+def get_platform_customer_order_list_id(entity: Platform, index: int):
+ return E.platform(entity).customer_order_list().get(index).id().eval()
+
+
+
+```
+
+Select every traversed field and relation. Never catch `TeaQLNotLoadedError`
+merely to supply a default, and never replace generated E accessors with direct
+attribute access.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `expression`.
+
+- Distinguish a loaded null from a field or relation that was not projected. A
+ NotLoaded/coding error must remain visible; do not turn it into an ordinary null.
+- Select every traversed relation first and use the generated E/expression API for
+ scalar, object, and list traversal. Do not translate Java accessor names by guess.
diff --git a/examples/trace-chain/evidence/assist/python-assist-expression/shipment.md b/examples/trace-chain/evidence/assist/python-assist-expression/shipment.md
new file mode 100644
index 0000000..0d01cf3
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-expression/shipment.md
@@ -0,0 +1,88 @@
+
+
+# Python Assist — Expression `Shipment`
+
+The generated E facade preserves Value, loaded `None`, and NotLoaded. `eval()`
+returns a native value for the first two and raises `TeaQLNotLoadedError` for
+the third. `or_if_null` is null-only and never hides NotLoaded.
+
+```python
+from E import E
+from models.shipment import Shipment
+
+
+def extract_shipment_id(entity: Shipment):
+ return E.shipment(entity).id().eval()
+
+
+def extract_shipment_id_or_if_null(entity: Shipment, fallback):
+ return E.shipment(entity).id().or_if_null(fallback)
+
+
+def extract_shipment_reference_code(entity: Shipment):
+ return E.shipment(entity).reference_code().eval()
+
+
+def extract_shipment_reference_code_or_if_null(entity: Shipment, fallback):
+ return E.shipment(entity).reference_code().or_if_null(fallback)
+
+
+def extract_shipment_version(entity: Shipment):
+ return E.shipment(entity).version().eval()
+
+
+def extract_shipment_version_or_if_null(entity: Shipment, fallback):
+ return E.shipment(entity).version().or_if_null(fallback)
+
+
+def extract_shipment_customer_order_id(entity: Shipment):
+ return E.shipment(entity).customer_order_id().eval()
+
+
+def traverse_shipment_customer_order(entity: Shipment):
+ return E.shipment(entity).customer_order().eval()
+
+
+
+```
+
+Select every traversed field and relation. Never catch `TeaQLNotLoadedError`
+merely to supply a default, and never replace generated E accessors with direct
+attribute access.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `expression`.
+
+- Distinguish a loaded null from a field or relation that was not projected. A
+ NotLoaded/coding error must remain visible; do not turn it into an ordinary null.
+- Select every traversed relation first and use the generated E/expression API for
+ scalar, object, and list traversal. Do not translate Java accessor names by guess.
diff --git a/examples/trace-chain/evidence/assist/python-assist-list-page/customer_order.md b/examples/trace-chain/evidence/assist/python-assist-list-page/customer_order.md
new file mode 100644
index 0000000..7a5a3c5
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-list-page/customer_order.md
@@ -0,0 +1,98 @@
+
+
+# Python Assist — List page `Customer Order`
+
+Use the exact generated entry point and APIs below. The result is a typed page
+with exact filtered `total_count`; generated client code cannot override the
+runtime's fixed 10,000-row materialization ceiling.
+
+```python
+from Q import Q
+from models.customer_order import CustomerOrder
+from teaql.core.list import TeaQLPage
+
+async def list_active_customer_order_page(
+ context, offset: int, limit: int
+) -> TeaQLPage[CustomerOrder]:
+ return await (
+ Q.customer_orders_minimal()
+ .select_order_number()
+ .select_description()
+ .select_platform_with(Q.platforms_minimal())
+ .select_order_item_list()
+ .select_payment_list()
+ .select_shipment_list()
+ .order_by_id_ascending()
+ .comment("what: list the active Customer Order page")
+ .purpose("why: serve the authorized Customer Order directory")
+ .execute_for_page(context, offset, limit)
+ )
+```
+
+Compile and execute this source unchanged. Rows and count share the exact
+generated and trusted-policy filters. Count excludes projection, relations,
+ordering, offset, and limit. Reject negative offsets, limits outside
+`1...10000`, unknown filters/sorts, missing intent, raw query dictionaries, and
+every generated-client attempt to call `hard_limit`.
+
+## Bounded scalar streaming
+
+```python
+stream = (
+ Q.customer_orders()
+ .order_by_id_ascending()
+ .limit(10)
+ .comment("what: stream bounded Customer Order rows")
+ .purpose("why: process independent loaded entities")
+ .execute_for_stream(context, chunk_size=2)
+)
+try:
+ async for entity in stream:
+ pass # Traverse loaded scalars with E; save only a changed entity.
+finally:
+ await stream.aclose()
+```
+
+Stream creation captures the request and runs trusted policy before delayed
+consumption. The database cursor remains lazy. Each yielded entity has its own
+mutation ledger. Relation hydration and aggregate enhancement are not supported
+by the scalar stream; use the list or page API for those graphs. Explicit close
+releases the provider cursor even when the consumer stops before exhaustion.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `list-page`.
+
+- Validate offset, page size, filters, deep paths, IN-list size, and sort against
+ explicit allow-lists. Reject invalid input instead of widening the query.
+- Use a stable unique ordering and retain the runtime hard limit. Continuous-page
+ optimization is opt-in, browsing-only, local runtime policy and cannot cross TFP.
+- Run count only when explicitly requested; otherwise use the returned list length.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/customer_order.id.md b/examples/trace-chain/evidence/assist/python-assist-query/customer_order.id.md
new file mode 100644
index 0000000..2770507
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/customer_order.id.md
@@ -0,0 +1,72 @@
+
+
+# Python Query Field Assist — `customer_order.id`
+
+KSML entity: `Customer Order`
+KSML field: `id`
+Type: `id`
+
+| Capability | Generated API |
+| --- | --- |
+| Select | `select_id()` |
+| Equality/set | `with_id_is(value)`, `with_id_is_not(value)`, `with_id_in(*values)`, `with_id_not_in(*values)` |
+| Comparison | `with_id_greater_than(value)`, `with_id_greater_than_or_equal_to(value)`, `with_id_less_than(value)`, `with_id_less_than_or_equal_to(value)`, `with_id_between(lower, upper)` |
+| Null state | `with_id_is_known()`, `with_id_is_unknown()` |
+| Order | `order_by_id_ascending()`, `order_by_id_descending()` |
+| Group | `group_by_id()`, `group_by_id_as(alias)` |
+Set predicates accept positional values. Pass separate arguments, for example
+`with_id_in(first, second)`, or unpack a sequence with
+`with_id_in(*values)`. Do not pass a list as one argument.
+Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/customer_order.md b/examples/trace-chain/evidence/assist/python-assist-query/customer_order.md
new file mode 100644
index 0000000..204371f
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/customer_order.md
@@ -0,0 +1,102 @@
+
+
+# Python Assist — Query `Customer Order`
+
+Use the exact generated `Q.customer_orders_minimal()` entry
+point. Trusted runtime and policy state comes only from the context.
+
+```python
+from Q import Q
+from models.customer_order import CustomerOrder
+from teaql.core.list import SmartList
+
+
+async def query_customer_order_by_id(context, entity_id: int) -> SmartList[CustomerOrder]:
+ return await (
+ Q.customer_orders_minimal()
+ .with_id_is(entity_id)
+ .order_by_id_ascending()
+ .limit(20)
+ .comment("what: load the requested Customer Order row")
+ .purpose("why: serve the authorized Customer Order view")
+ .execute_for_list(context)
+ )
+```
+
+Compile and execute this source unchanged. Use only generated projection,
+predicate, ordering, and relation-selection APIs. Missing or blank intent,
+unknown fields, unbounded reads, and trusted-context inputs must fail. Use generated
+relation loading rather than handwritten child-query loops; never guess a plural.
+
+## Field-specific Query Assist
+
+Use the canonical KSML field name from this list. Do not substitute a language member name, JSON name, or database column.
+
+| KSML field | Type | Field help |
+| --- | --- | --- |
+| `id` | `id` | `python-assist-query/customer_order.id` |
+| `platform` | `relation` | `python-assist-query/customer_order.platform` |
+| `order_number` | `string` | `python-assist-query/customer_order.order_number` |
+| `description` | `string` | `python-assist-query/customer_order.description` |
+| `version` | `version` | `python-assist-query/customer_order.version` |
+
+Generated reverse relations are derived from referencing KSML fields. Use their exact generated location for relation selection and access:
+
+| Generated reverse relation | Child entity | Field help |
+| --- | --- | --- |
+| `order_item_list` | `order_item` | `python-assist-query/customer_order.order_item_list` |
+| `payment_list` | `payment` | `python-assist-query/customer_order.payment_list` |
+| `shipment_list` | `shipment` | `python-assist-query/customer_order.shipment_list` |
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/customer_order.order_item_list.md b/examples/trace-chain/evidence/assist/python-assist-query/customer_order.order_item_list.md
new file mode 100644
index 0000000..62d8423
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/customer_order.order_item_list.md
@@ -0,0 +1,72 @@
+
+
+# Query Reverse-Relation Assist — `customer_order.order_item_list`
+
+KSML entity: `customer_order`
+Derived reverse relation: `order_item_list`
+Child entity: `order_item`
+
+| Capability | Generated API |
+| --- | --- |
+| Select | `.select_order_item_list()`, `.select_order_item_list_with(child_request)` |
+| Related count | `.count_order_items_as(alias)` |
+| Filtered related count | `.count_order_items_with(alias, child_request)` |
+| Read related count | `entity.query_projection(alias)`; missing raises `KeyError` |
+| Check projection presence | `entity.has_query_projection(alias)`; null and zero remain present |
+
+Related counts are query-only projections, not modeled fields or mutation setters. Use a dedicated child request for the aggregate; do not reuse it as a row selection. An alias has no generated E accessor; use E for modeled fields and loaded relations. List and nested loaded entities carry projections; scalar-only streaming rejects relation aggregates. Choose aliases that do not collide with model fields or relations. Projection reads never perform database I/O and returned mutable values are isolated copies.
+
+Reverse relations are model-derived and never perform an implicit database query. Select the relation explicitly before expression access. Apply query methods before the executable purpose stage, then execute with exactly one trusted context.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/customer_order.order_number.md b/examples/trace-chain/evidence/assist/python-assist-query/customer_order.order_number.md
new file mode 100644
index 0000000..2954022
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/customer_order.order_number.md
@@ -0,0 +1,73 @@
+
+
+# Python Query Field Assist — `customer_order.order_number`
+
+KSML entity: `Customer Order`
+KSML field: `order_number`
+Type: `string`
+
+| Capability | Generated API |
+| --- | --- |
+| Select | `select_order_number()` |
+| Equality/set | `with_order_number_is(value)`, `with_order_number_is_not(value)`, `with_order_number_in(*values)`, `with_order_number_not_in(*values)` |
+| Comparison | `with_order_number_greater_than(value)`, `with_order_number_greater_than_or_equal_to(value)`, `with_order_number_less_than(value)`, `with_order_number_less_than_or_equal_to(value)`, `with_order_number_between(lower, upper)` |
+| Null state | `with_order_number_is_known()`, `with_order_number_is_unknown()` |
+| String | `with_order_number_containing(value)`, `with_order_number_not_containing(value)`, `with_order_number_starting_with(value)`, `with_order_number_ending_with(value)`, `with_order_number_sounding_like(value)` |
+| Order | `order_by_order_number_ascending()`, `order_by_order_number_descending()` |
+| Group | `group_by_order_number()`, `group_by_order_number_as(alias)` |
+Set predicates accept positional values. Pass separate arguments, for example
+`with_order_number_in(first, second)`, or unpack a sequence with
+`with_order_number_in(*values)`. Do not pass a list as one argument.
+Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/customer_order.payment_list.md b/examples/trace-chain/evidence/assist/python-assist-query/customer_order.payment_list.md
new file mode 100644
index 0000000..fe6833a
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/customer_order.payment_list.md
@@ -0,0 +1,72 @@
+
+
+# Query Reverse-Relation Assist — `customer_order.payment_list`
+
+KSML entity: `customer_order`
+Derived reverse relation: `payment_list`
+Child entity: `payment`
+
+| Capability | Generated API |
+| --- | --- |
+| Select | `.select_payment_list()`, `.select_payment_list_with(child_request)` |
+| Related count | `.count_payments_as(alias)` |
+| Filtered related count | `.count_payments_with(alias, child_request)` |
+| Read related count | `entity.query_projection(alias)`; missing raises `KeyError` |
+| Check projection presence | `entity.has_query_projection(alias)`; null and zero remain present |
+
+Related counts are query-only projections, not modeled fields or mutation setters. Use a dedicated child request for the aggregate; do not reuse it as a row selection. An alias has no generated E accessor; use E for modeled fields and loaded relations. List and nested loaded entities carry projections; scalar-only streaming rejects relation aggregates. Choose aliases that do not collide with model fields or relations. Projection reads never perform database I/O and returned mutable values are isolated copies.
+
+Reverse relations are model-derived and never perform an implicit database query. Select the relation explicitly before expression access. Apply query methods before the executable purpose stage, then execute with exactly one trusted context.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/customer_order.platform.md b/examples/trace-chain/evidence/assist/python-assist-query/customer_order.platform.md
new file mode 100644
index 0000000..14dd6e1
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/customer_order.platform.md
@@ -0,0 +1,68 @@
+
+
+# Python Query Field Assist — `customer_order.platform`
+
+KSML entity: `Customer Order`
+KSML field: `Platform`
+Type: `Platform`
+
+| Capability | Generated API |
+| --- | --- |
+| Select | `select_platform_with(request)`` |
+| Filter identity | `filter_by_platform(id)` |
+| Filter nested request | `with_platform_matching(request)`, `without_platform_matching(request)` |
+| Group | `group_by_platform()`, `group_by_platform_as(alias)` |
+| Facet | `facet_by_platform_as(name, request, include_all_facets)` |
+Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/customer_order.shipment_list.md b/examples/trace-chain/evidence/assist/python-assist-query/customer_order.shipment_list.md
new file mode 100644
index 0000000..9a4f03c
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/customer_order.shipment_list.md
@@ -0,0 +1,72 @@
+
+
+# Query Reverse-Relation Assist — `customer_order.shipment_list`
+
+KSML entity: `customer_order`
+Derived reverse relation: `shipment_list`
+Child entity: `shipment`
+
+| Capability | Generated API |
+| --- | --- |
+| Select | `.select_shipment_list()`, `.select_shipment_list_with(child_request)` |
+| Related count | `.count_shipments_as(alias)` |
+| Filtered related count | `.count_shipments_with(alias, child_request)` |
+| Read related count | `entity.query_projection(alias)`; missing raises `KeyError` |
+| Check projection presence | `entity.has_query_projection(alias)`; null and zero remain present |
+
+Related counts are query-only projections, not modeled fields or mutation setters. Use a dedicated child request for the aggregate; do not reuse it as a row selection. An alias has no generated E accessor; use E for modeled fields and loaded relations. List and nested loaded entities carry projections; scalar-only streaming rejects relation aggregates. Choose aliases that do not collide with model fields or relations. Projection reads never perform database I/O and returned mutable values are isolated copies.
+
+Reverse relations are model-derived and never perform an implicit database query. Select the relation explicitly before expression access. Apply query methods before the executable purpose stage, then execute with exactly one trusted context.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/order_item.md b/examples/trace-chain/evidence/assist/python-assist-query/order_item.md
new file mode 100644
index 0000000..7e9d248
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/order_item.md
@@ -0,0 +1,93 @@
+
+
+# Python Assist — Query `Order Item`
+
+Use the exact generated `Q.order_items_minimal()` entry
+point. Trusted runtime and policy state comes only from the context.
+
+```python
+from Q import Q
+from models.order_item import OrderItem
+from teaql.core.list import SmartList
+
+
+async def query_order_item_by_id(context, entity_id: int) -> SmartList[OrderItem]:
+ return await (
+ Q.order_items_minimal()
+ .with_id_is(entity_id)
+ .order_by_id_ascending()
+ .limit(20)
+ .comment("what: load the requested Order Item row")
+ .purpose("why: serve the authorized Order Item view")
+ .execute_for_list(context)
+ )
+```
+
+Compile and execute this source unchanged. Use only generated projection,
+predicate, ordering, and relation-selection APIs. Missing or blank intent,
+unknown fields, unbounded reads, and trusted-context inputs must fail. Use generated
+relation loading rather than handwritten child-query loops; never guess a plural.
+
+## Field-specific Query Assist
+
+Use the canonical KSML field name from this list. Do not substitute a language member name, JSON name, or database column.
+
+| KSML field | Type | Field help |
+| --- | --- | --- |
+| `id` | `id` | `python-assist-query/order_item.id` |
+| `customer_order` | `relation` | `python-assist-query/order_item.customer_order` |
+| `name` | `string` | `python-assist-query/order_item.name` |
+| `version` | `version` | `python-assist-query/order_item.version` |
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/order_item.name.md b/examples/trace-chain/evidence/assist/python-assist-query/order_item.name.md
new file mode 100644
index 0000000..bc387db
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/order_item.name.md
@@ -0,0 +1,73 @@
+
+
+# Python Query Field Assist — `order_item.name`
+
+KSML entity: `Order Item`
+KSML field: `name`
+Type: `string`
+
+| Capability | Generated API |
+| --- | --- |
+| Select | `select_name()` |
+| Equality/set | `with_name_is(value)`, `with_name_is_not(value)`, `with_name_in(*values)`, `with_name_not_in(*values)` |
+| Comparison | `with_name_greater_than(value)`, `with_name_greater_than_or_equal_to(value)`, `with_name_less_than(value)`, `with_name_less_than_or_equal_to(value)`, `with_name_between(lower, upper)` |
+| Null state | `with_name_is_known()`, `with_name_is_unknown()` |
+| String | `with_name_containing(value)`, `with_name_not_containing(value)`, `with_name_starting_with(value)`, `with_name_ending_with(value)`, `with_name_sounding_like(value)` |
+| Order | `order_by_name_ascending()`, `order_by_name_descending()` |
+| Group | `group_by_name()`, `group_by_name_as(alias)` |
+Set predicates accept positional values. Pass separate arguments, for example
+`with_name_in(first, second)`, or unpack a sequence with
+`with_name_in(*values)`. Do not pass a list as one argument.
+Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/payment.customer_order.md b/examples/trace-chain/evidence/assist/python-assist-query/payment.customer_order.md
new file mode 100644
index 0000000..3f5bb28
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/payment.customer_order.md
@@ -0,0 +1,68 @@
+
+
+# Python Query Field Assist — `payment.customer_order`
+
+KSML entity: `Payment`
+KSML field: `Customer Order`
+Type: `Customer Order`
+
+| Capability | Generated API |
+| --- | --- |
+| Select | `select_customer_order_with(request)`` |
+| Filter identity | `filter_by_customer_order(id)` |
+| Filter nested request | `with_customer_order_matching(request)`, `without_customer_order_matching(request)` |
+| Group | `group_by_customer_order()`, `group_by_customer_order_as(alias)` |
+| Facet | `facet_by_customer_order_as(name, request, include_all_facets)` |
+Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/payment.md b/examples/trace-chain/evidence/assist/python-assist-query/payment.md
new file mode 100644
index 0000000..49e91d1
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/payment.md
@@ -0,0 +1,99 @@
+
+
+# Python Assist — Query `Payment`
+
+Use the exact generated `Q.payments_minimal()` entry
+point. Trusted runtime and policy state comes only from the context.
+
+```python
+from Q import Q
+from models.payment import Payment
+from teaql.core.list import SmartList
+
+
+async def query_payment_by_id(context, entity_id: int) -> SmartList[Payment]:
+ return await (
+ Q.payments_minimal()
+ .with_id_is(entity_id)
+ .order_by_id_ascending()
+ .limit(20)
+ .comment("what: load the requested Payment row")
+ .purpose("why: serve the authorized Payment view")
+ .execute_for_list(context)
+ )
+```
+
+Compile and execute this source unchanged. Use only generated projection,
+predicate, ordering, and relation-selection APIs. Missing or blank intent,
+unknown fields, unbounded reads, and trusted-context inputs must fail. Use generated
+relation loading rather than handwritten child-query loops; never guess a plural.
+
+## Field-specific Query Assist
+
+Use the canonical KSML field name from this list. Do not substitute a language member name, JSON name, or database column.
+
+| KSML field | Type | Field help |
+| --- | --- | --- |
+| `id` | `id` | `python-assist-query/payment.id` |
+| `customer_order` | `relation` | `python-assist-query/payment.customer_order` |
+| `reference_code` | `string` | `python-assist-query/payment.reference_code` |
+| `version` | `version` | `python-assist-query/payment.version` |
+
+Generated reverse relations are derived from referencing KSML fields. Use their exact generated location for relation selection and access:
+
+| Generated reverse relation | Child entity | Field help |
+| --- | --- | --- |
+| `payment_attempt_list` | `payment_attempt` | `python-assist-query/payment.payment_attempt_list` |
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/payment.payment_attempt_list.md b/examples/trace-chain/evidence/assist/python-assist-query/payment.payment_attempt_list.md
new file mode 100644
index 0000000..e5b0830
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/payment.payment_attempt_list.md
@@ -0,0 +1,72 @@
+
+
+# Query Reverse-Relation Assist — `payment.payment_attempt_list`
+
+KSML entity: `payment`
+Derived reverse relation: `payment_attempt_list`
+Child entity: `payment_attempt`
+
+| Capability | Generated API |
+| --- | --- |
+| Select | `.select_payment_attempt_list()`, `.select_payment_attempt_list_with(child_request)` |
+| Related count | `.count_payment_attempts_as(alias)` |
+| Filtered related count | `.count_payment_attempts_with(alias, child_request)` |
+| Read related count | `entity.query_projection(alias)`; missing raises `KeyError` |
+| Check projection presence | `entity.has_query_projection(alias)`; null and zero remain present |
+
+Related counts are query-only projections, not modeled fields or mutation setters. Use a dedicated child request for the aggregate; do not reuse it as a row selection. An alias has no generated E accessor; use E for modeled fields and loaded relations. List and nested loaded entities carry projections; scalar-only streaming rejects relation aggregates. Choose aliases that do not collide with model fields or relations. Projection reads never perform database I/O and returned mutable values are isolated copies.
+
+Reverse relations are model-derived and never perform an implicit database query. Select the relation explicitly before expression access. Apply query methods before the executable purpose stage, then execute with exactly one trusted context.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/payment.reference_code.md b/examples/trace-chain/evidence/assist/python-assist-query/payment.reference_code.md
new file mode 100644
index 0000000..2f9c634
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/payment.reference_code.md
@@ -0,0 +1,73 @@
+
+
+# Python Query Field Assist — `payment.reference_code`
+
+KSML entity: `Payment`
+KSML field: `reference_code`
+Type: `string`
+
+| Capability | Generated API |
+| --- | --- |
+| Select | `select_reference_code()` |
+| Equality/set | `with_reference_code_is(value)`, `with_reference_code_is_not(value)`, `with_reference_code_in(*values)`, `with_reference_code_not_in(*values)` |
+| Comparison | `with_reference_code_greater_than(value)`, `with_reference_code_greater_than_or_equal_to(value)`, `with_reference_code_less_than(value)`, `with_reference_code_less_than_or_equal_to(value)`, `with_reference_code_between(lower, upper)` |
+| Null state | `with_reference_code_is_known()`, `with_reference_code_is_unknown()` |
+| String | `with_reference_code_containing(value)`, `with_reference_code_not_containing(value)`, `with_reference_code_starting_with(value)`, `with_reference_code_ending_with(value)`, `with_reference_code_sounding_like(value)` |
+| Order | `order_by_reference_code_ascending()`, `order_by_reference_code_descending()` |
+| Group | `group_by_reference_code()`, `group_by_reference_code_as(alias)` |
+Set predicates accept positional values. Pass separate arguments, for example
+`with_reference_code_in(first, second)`, or unpack a sequence with
+`with_reference_code_in(*values)`. Do not pass a list as one argument.
+Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/payment_attempt.md b/examples/trace-chain/evidence/assist/python-assist-query/payment_attempt.md
new file mode 100644
index 0000000..2acdca5
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/payment_attempt.md
@@ -0,0 +1,93 @@
+
+
+# Python Assist — Query `Payment Attempt`
+
+Use the exact generated `Q.payment_attempts_minimal()` entry
+point. Trusted runtime and policy state comes only from the context.
+
+```python
+from Q import Q
+from models.payment_attempt import PaymentAttempt
+from teaql.core.list import SmartList
+
+
+async def query_payment_attempt_by_id(context, entity_id: int) -> SmartList[PaymentAttempt]:
+ return await (
+ Q.payment_attempts_minimal()
+ .with_id_is(entity_id)
+ .order_by_id_ascending()
+ .limit(20)
+ .comment("what: load the requested Payment Attempt row")
+ .purpose("why: serve the authorized Payment Attempt view")
+ .execute_for_list(context)
+ )
+```
+
+Compile and execute this source unchanged. Use only generated projection,
+predicate, ordering, and relation-selection APIs. Missing or blank intent,
+unknown fields, unbounded reads, and trusted-context inputs must fail. Use generated
+relation loading rather than handwritten child-query loops; never guess a plural.
+
+## Field-specific Query Assist
+
+Use the canonical KSML field name from this list. Do not substitute a language member name, JSON name, or database column.
+
+| KSML field | Type | Field help |
+| --- | --- | --- |
+| `id` | `id` | `python-assist-query/payment_attempt.id` |
+| `payment` | `relation` | `python-assist-query/payment_attempt.payment` |
+| `reference_code` | `string` | `python-assist-query/payment_attempt.reference_code` |
+| `version` | `version` | `python-assist-query/payment_attempt.version` |
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/payment_attempt.payment.md b/examples/trace-chain/evidence/assist/python-assist-query/payment_attempt.payment.md
new file mode 100644
index 0000000..b10e930
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/payment_attempt.payment.md
@@ -0,0 +1,68 @@
+
+
+# Python Query Field Assist — `payment_attempt.payment`
+
+KSML entity: `Payment Attempt`
+KSML field: `Payment`
+Type: `Payment`
+
+| Capability | Generated API |
+| --- | --- |
+| Select | `select_payment_with(request)`` |
+| Filter identity | `filter_by_payment(id)` |
+| Filter nested request | `with_payment_matching(request)`, `without_payment_matching(request)` |
+| Group | `group_by_payment()`, `group_by_payment_as(alias)` |
+| Facet | `facet_by_payment_as(name, request, include_all_facets)` |
+Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/payment_attempt.reference_code.md b/examples/trace-chain/evidence/assist/python-assist-query/payment_attempt.reference_code.md
new file mode 100644
index 0000000..b23f56e
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/payment_attempt.reference_code.md
@@ -0,0 +1,73 @@
+
+
+# Python Query Field Assist — `payment_attempt.reference_code`
+
+KSML entity: `Payment Attempt`
+KSML field: `reference_code`
+Type: `string`
+
+| Capability | Generated API |
+| --- | --- |
+| Select | `select_reference_code()` |
+| Equality/set | `with_reference_code_is(value)`, `with_reference_code_is_not(value)`, `with_reference_code_in(*values)`, `with_reference_code_not_in(*values)` |
+| Comparison | `with_reference_code_greater_than(value)`, `with_reference_code_greater_than_or_equal_to(value)`, `with_reference_code_less_than(value)`, `with_reference_code_less_than_or_equal_to(value)`, `with_reference_code_between(lower, upper)` |
+| Null state | `with_reference_code_is_known()`, `with_reference_code_is_unknown()` |
+| String | `with_reference_code_containing(value)`, `with_reference_code_not_containing(value)`, `with_reference_code_starting_with(value)`, `with_reference_code_ending_with(value)`, `with_reference_code_sounding_like(value)` |
+| Order | `order_by_reference_code_ascending()`, `order_by_reference_code_descending()` |
+| Group | `group_by_reference_code()`, `group_by_reference_code_as(alias)` |
+Set predicates accept positional values. Pass separate arguments, for example
+`with_reference_code_in(first, second)`, or unpack a sequence with
+`with_reference_code_in(*values)`. Do not pass a list as one argument.
+Execution requires `comment(...)`, `purpose(...)`, and exactly one `context`.
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/platform.md b/examples/trace-chain/evidence/assist/python-assist-query/platform.md
new file mode 100644
index 0000000..f0b27b5
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/platform.md
@@ -0,0 +1,98 @@
+
+
+# Python Assist — Query `Platform`
+
+Use the exact generated `Q.platforms_minimal()` entry
+point. Trusted runtime and policy state comes only from the context.
+
+```python
+from Q import Q
+from models.platform import Platform
+from teaql.core.list import SmartList
+
+
+async def query_platform_by_id(context, entity_id: int) -> SmartList[Platform]:
+ return await (
+ Q.platforms_minimal()
+ .with_id_is(entity_id)
+ .order_by_id_ascending()
+ .limit(20)
+ .comment("what: load the requested Platform row")
+ .purpose("why: serve the authorized Platform view")
+ .execute_for_list(context)
+ )
+```
+
+Compile and execute this source unchanged. Use only generated projection,
+predicate, ordering, and relation-selection APIs. Missing or blank intent,
+unknown fields, unbounded reads, and trusted-context inputs must fail. Use generated
+relation loading rather than handwritten child-query loops; never guess a plural.
+
+## Field-specific Query Assist
+
+Use the canonical KSML field name from this list. Do not substitute a language member name, JSON name, or database column.
+
+| KSML field | Type | Field help |
+| --- | --- | --- |
+| `id` | `id` | `python-assist-query/platform.id` |
+| `name` | `string` | `python-assist-query/platform.name` |
+| `version` | `version` | `python-assist-query/platform.version` |
+
+Generated reverse relations are derived from referencing KSML fields. Use their exact generated location for relation selection and access:
+
+| Generated reverse relation | Child entity | Field help |
+| --- | --- | --- |
+| `customer_order_list` | `customer_order` | `python-assist-query/platform.customer_order_list` |
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-query/shipment.md b/examples/trace-chain/evidence/assist/python-assist-query/shipment.md
new file mode 100644
index 0000000..43eac2c
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-query/shipment.md
@@ -0,0 +1,93 @@
+
+
+# Python Assist — Query `Shipment`
+
+Use the exact generated `Q.shipments_minimal()` entry
+point. Trusted runtime and policy state comes only from the context.
+
+```python
+from Q import Q
+from models.shipment import Shipment
+from teaql.core.list import SmartList
+
+
+async def query_shipment_by_id(context, entity_id: int) -> SmartList[Shipment]:
+ return await (
+ Q.shipments_minimal()
+ .with_id_is(entity_id)
+ .order_by_id_ascending()
+ .limit(20)
+ .comment("what: load the requested Shipment row")
+ .purpose("why: serve the authorized Shipment view")
+ .execute_for_list(context)
+ )
+```
+
+Compile and execute this source unchanged. Use only generated projection,
+predicate, ordering, and relation-selection APIs. Missing or blank intent,
+unknown fields, unbounded reads, and trusted-context inputs must fail. Use generated
+relation loading rather than handwritten child-query loops; never guess a plural.
+
+## Field-specific Query Assist
+
+Use the canonical KSML field name from this list. Do not substitute a language member name, JSON name, or database column.
+
+| KSML field | Type | Field help |
+| --- | --- | --- |
+| `id` | `id` | `python-assist-query/shipment.id` |
+| `customer_order` | `relation` | `python-assist-query/shipment.customer_order` |
+| `reference_code` | `string` | `python-assist-query/shipment.reference_code` |
+| `version` | `version` | `python-assist-query/shipment.version` |
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `query`.
+
+- Use generated projection, relation, predicate, sort, and aggregate APIs; never
+ concatenate application SQL or silently ignore an unsupported filter.
+- Rows, record count, facets, and aggregates must share the same active filter.
+- Add negative tests for missing purpose/comment and forbidden dynamic fields.
+
+### Optional per-parent Top-N optimization
+
+- Configure `top_n_probe_parent_threshold(threshold)` on the nested child request before its intent/execution
+ stage. Use the field-specific relation Assist for the exact generated selector.
+- Give the child a per-parent limit and deterministic ordering with an ID tie-breaker;
+ bound the parent query separately. A single-parent detail query is not batch Top-N.
+- Server providers default to a window query (`threshold = 0`). A positive threshold
+ permits bounded probes only when the already-loaded parent count is at or below it;
+ above it, use the window plan. SQLite uses its internal AlwaysProbe policy by default.
+- Measure before opting in. Do not infer a threshold from table size or execute COUNT
+ or statistics queries to choose a plan. Keep an index on foreign key + sort + ID.
+- Inspect selected-plan, parent-count, per-parent-limit and probe-count telemetry.
+ Runtime-managed probes are not application-owned N+1 loops; neither strategy changes
+ authorization, loaded-state semantics or the required comment/purpose.
diff --git a/examples/trace-chain/evidence/assist/python-assist-update/customer_order.md b/examples/trace-chain/evidence/assist/python-assist-update/customer_order.md
new file mode 100644
index 0000000..8c833ab
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-update/customer_order.md
@@ -0,0 +1,82 @@
+
+
+# Python Assist — Update `Customer Order`
+
+Load the current generated entity so its stored ID and original version
+participate in optimistic locking. Never reconstruct it from untrusted JSON.
+
+An entity update is not a partial DTO patch. Load every scalar field before
+modification so checker rules can validate the complete business state. Never
+use the `_minimal()` request or a reduced projection for an entity that will be saved.
+
+```python
+from Q import Q
+
+async def update_customer_order(
+ context,
+ entity_id,
+ order_number,
+ description,
+ throw_if_missing=False):
+ entity = await (Q.customer_orders()
+ .with_id_is(entity_id)
+
+ .comment("what: load current Customer Order for update")
+ .purpose("why: preserve original version for optimistic locking")
+ .execute_for_one(context))
+ if entity is None:
+ if throw_if_missing:
+ raise LookupError("Customer Order not found: " + str(entity_id))
+ return None
+
+ entity.update_order_number(order_number)
+ entity.update_description(description)
+
+ await entity.audit_as(
+ "Update Customer Order for the requested business operation").save(context)
+ return entity
+```
+
+Compile and execute this source unchanged. Prove persistence and query-back, a
+conflict from an independently loaded stale copy, None and throwing not-found
+behavior, missing/blank audit rejection, and failure for unknown or trusted
+fields. Constant and relation changes require an explicitly selected generated
+method; do not guess one.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `update`.
+
+- Load the tenant-scoped current entity first so its original version participates
+ in optimistic locking; do not reconstruct versioned state from untrusted JSON.
+- Allow-list writable fields, attach the generated audit-reason API, and save with
+ the same UserContext. Add a stale-version rejection test.
diff --git a/examples/trace-chain/evidence/assist/python-assist-update/order_item.md b/examples/trace-chain/evidence/assist/python-assist-update/order_item.md
new file mode 100644
index 0000000..3f4b572
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-update/order_item.md
@@ -0,0 +1,80 @@
+
+
+# Python Assist — Update `Order Item`
+
+Load the current generated entity so its stored ID and original version
+participate in optimistic locking. Never reconstruct it from untrusted JSON.
+
+An entity update is not a partial DTO patch. Load every scalar field before
+modification so checker rules can validate the complete business state. Never
+use the `_minimal()` request or a reduced projection for an entity that will be saved.
+
+```python
+from Q import Q
+
+async def update_order_item(
+ context,
+ entity_id,
+ name,
+ throw_if_missing=False):
+ entity = await (Q.order_items()
+ .with_id_is(entity_id)
+
+ .comment("what: load current Order Item for update")
+ .purpose("why: preserve original version for optimistic locking")
+ .execute_for_one(context))
+ if entity is None:
+ if throw_if_missing:
+ raise LookupError("Order Item not found: " + str(entity_id))
+ return None
+
+ entity.update_name(name)
+
+ await entity.audit_as(
+ "Update Order Item for the requested business operation").save(context)
+ return entity
+```
+
+Compile and execute this source unchanged. Prove persistence and query-back, a
+conflict from an independently loaded stale copy, None and throwing not-found
+behavior, missing/blank audit rejection, and failure for unknown or trusted
+fields. Constant and relation changes require an explicitly selected generated
+method; do not guess one.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `update`.
+
+- Load the tenant-scoped current entity first so its original version participates
+ in optimistic locking; do not reconstruct versioned state from untrusted JSON.
+- Allow-list writable fields, attach the generated audit-reason API, and save with
+ the same UserContext. Add a stale-version rejection test.
diff --git a/examples/trace-chain/evidence/assist/python-assist-update/payment.md b/examples/trace-chain/evidence/assist/python-assist-update/payment.md
new file mode 100644
index 0000000..4efb0be
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-update/payment.md
@@ -0,0 +1,80 @@
+
+
+# Python Assist — Update `Payment`
+
+Load the current generated entity so its stored ID and original version
+participate in optimistic locking. Never reconstruct it from untrusted JSON.
+
+An entity update is not a partial DTO patch. Load every scalar field before
+modification so checker rules can validate the complete business state. Never
+use the `_minimal()` request or a reduced projection for an entity that will be saved.
+
+```python
+from Q import Q
+
+async def update_payment(
+ context,
+ entity_id,
+ reference_code,
+ throw_if_missing=False):
+ entity = await (Q.payments()
+ .with_id_is(entity_id)
+
+ .comment("what: load current Payment for update")
+ .purpose("why: preserve original version for optimistic locking")
+ .execute_for_one(context))
+ if entity is None:
+ if throw_if_missing:
+ raise LookupError("Payment not found: " + str(entity_id))
+ return None
+
+ entity.update_reference_code(reference_code)
+
+ await entity.audit_as(
+ "Update Payment for the requested business operation").save(context)
+ return entity
+```
+
+Compile and execute this source unchanged. Prove persistence and query-back, a
+conflict from an independently loaded stale copy, None and throwing not-found
+behavior, missing/blank audit rejection, and failure for unknown or trusted
+fields. Constant and relation changes require an explicitly selected generated
+method; do not guess one.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `update`.
+
+- Load the tenant-scoped current entity first so its original version participates
+ in optimistic locking; do not reconstruct versioned state from untrusted JSON.
+- Allow-list writable fields, attach the generated audit-reason API, and save with
+ the same UserContext. Add a stale-version rejection test.
diff --git a/examples/trace-chain/evidence/assist/python-assist-update/payment_attempt.md b/examples/trace-chain/evidence/assist/python-assist-update/payment_attempt.md
new file mode 100644
index 0000000..3e63d6e
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-update/payment_attempt.md
@@ -0,0 +1,80 @@
+
+
+# Python Assist — Update `Payment Attempt`
+
+Load the current generated entity so its stored ID and original version
+participate in optimistic locking. Never reconstruct it from untrusted JSON.
+
+An entity update is not a partial DTO patch. Load every scalar field before
+modification so checker rules can validate the complete business state. Never
+use the `_minimal()` request or a reduced projection for an entity that will be saved.
+
+```python
+from Q import Q
+
+async def update_payment_attempt(
+ context,
+ entity_id,
+ reference_code,
+ throw_if_missing=False):
+ entity = await (Q.payment_attempts()
+ .with_id_is(entity_id)
+
+ .comment("what: load current Payment Attempt for update")
+ .purpose("why: preserve original version for optimistic locking")
+ .execute_for_one(context))
+ if entity is None:
+ if throw_if_missing:
+ raise LookupError("Payment Attempt not found: " + str(entity_id))
+ return None
+
+ entity.update_reference_code(reference_code)
+
+ await entity.audit_as(
+ "Update Payment Attempt for the requested business operation").save(context)
+ return entity
+```
+
+Compile and execute this source unchanged. Prove persistence and query-back, a
+conflict from an independently loaded stale copy, None and throwing not-found
+behavior, missing/blank audit rejection, and failure for unknown or trusted
+fields. Constant and relation changes require an explicitly selected generated
+method; do not guess one.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `update`.
+
+- Load the tenant-scoped current entity first so its original version participates
+ in optimistic locking; do not reconstruct versioned state from untrusted JSON.
+- Allow-list writable fields, attach the generated audit-reason API, and save with
+ the same UserContext. Add a stale-version rejection test.
diff --git a/examples/trace-chain/evidence/assist/python-assist-update/platform.md b/examples/trace-chain/evidence/assist/python-assist-update/platform.md
new file mode 100644
index 0000000..b4c2e3f
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-update/platform.md
@@ -0,0 +1,80 @@
+
+
+# Python Assist — Update `Platform`
+
+Load the current generated entity so its stored ID and original version
+participate in optimistic locking. Never reconstruct it from untrusted JSON.
+
+An entity update is not a partial DTO patch. Load every scalar field before
+modification so checker rules can validate the complete business state. Never
+use the `_minimal()` request or a reduced projection for an entity that will be saved.
+
+```python
+from Q import Q
+
+async def update_platform(
+ context,
+ entity_id,
+ name,
+ throw_if_missing=False):
+ entity = await (Q.platforms()
+ .with_id_is(entity_id)
+
+ .comment("what: load current Platform for update")
+ .purpose("why: preserve original version for optimistic locking")
+ .execute_for_one(context))
+ if entity is None:
+ if throw_if_missing:
+ raise LookupError("Platform not found: " + str(entity_id))
+ return None
+
+ entity.update_name(name)
+
+ await entity.audit_as(
+ "Update Platform for the requested business operation").save(context)
+ return entity
+```
+
+Compile and execute this source unchanged. Prove persistence and query-back, a
+conflict from an independently loaded stale copy, None and throwing not-found
+behavior, missing/blank audit rejection, and failure for unknown or trusted
+fields. Constant and relation changes require an explicitly selected generated
+method; do not guess one.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `update`.
+
+- Load the tenant-scoped current entity first so its original version participates
+ in optimistic locking; do not reconstruct versioned state from untrusted JSON.
+- Allow-list writable fields, attach the generated audit-reason API, and save with
+ the same UserContext. Add a stale-version rejection test.
diff --git a/examples/trace-chain/evidence/assist/python-assist-update/shipment.md b/examples/trace-chain/evidence/assist/python-assist-update/shipment.md
new file mode 100644
index 0000000..9e360b3
--- /dev/null
+++ b/examples/trace-chain/evidence/assist/python-assist-update/shipment.md
@@ -0,0 +1,80 @@
+
+
+# Python Assist — Update `Shipment`
+
+Load the current generated entity so its stored ID and original version
+participate in optimistic locking. Never reconstruct it from untrusted JSON.
+
+An entity update is not a partial DTO patch. Load every scalar field before
+modification so checker rules can validate the complete business state. Never
+use the `_minimal()` request or a reduced projection for an entity that will be saved.
+
+```python
+from Q import Q
+
+async def update_shipment(
+ context,
+ entity_id,
+ reference_code,
+ throw_if_missing=False):
+ entity = await (Q.shipments()
+ .with_id_is(entity_id)
+
+ .comment("what: load current Shipment for update")
+ .purpose("why: preserve original version for optimistic locking")
+ .execute_for_one(context))
+ if entity is None:
+ if throw_if_missing:
+ raise LookupError("Shipment not found: " + str(entity_id))
+ return None
+
+ entity.update_reference_code(reference_code)
+
+ await entity.audit_as(
+ "Update Shipment for the requested business operation").save(context)
+ return entity
+```
+
+Compile and execute this source unchanged. Prove persistence and query-back, a
+conflict from an independently loaded stale copy, None and throwing not-found
+behavior, missing/blank audit rejection, and failure for unknown or trusted
+fields. Constant and relation changes require an explicitly selected generated
+method; do not guess one.
+
+
+---
+
+## TeaQL seven-language assist contract
+
+Apply the verified Rust semantic ceiling while using only the exact PYTHON generated and
+runtime APIs. Discover APIs through the generated application AGENTS.md and progressive
+model-aware Assist. Do not inspect generated domain-library source.
+
+- Do not create plurals by appending `s` or `es`; use the centralized generated plural.
+- Human and non-human entities use different generated predicate vocabularies. Preserve
+ forms such as “who are active” and “whose email is”; never infer them from English.
+- Configure filters, projection, paging, and other query options before `purpose(...)`.
+ Comment may appear anywhere in the chain. Purpose enters the executable stage; execution
+ requires both values, but comment does not have to immediately precede purpose.
+- Every execute/list/stream and every save accepts exactly one context argument:
+ `UserContext`. Name that argument `context`, never `runtime`; data services and global
+ policy are injected when the context is built. Reserve `runtime` for process-level
+ runtime ownership, provider/pool setup, and module assembly.
+- Tenant, merchant, identity, permissions, request policy, purpose policy, hard limit,
+ and continuous-page cursor policy come only from trusted context, never dynamic JSON or TFP.
+- If the required operation is absent after current entity/action and required field
+ Assist, stop that path and report MISSING_ASSIST. Do not guess an API or search the
+ generated library as a fallback.
+- Create each application-owned source file once. After its first compile attempt,
+ repair only the smallest block identified by the exact compiler or test diagnostic.
+ Preserve unrelated code; do not rewrite the complete file as an error-recovery loop.
+- Before a repair that would replace more than 25% of an existing application file,
+ stop and report LARGE_REWRITE_REQUEST with the file, exact diagnostic, reason, and
+ estimated scope. Initial creation and model-driven regeneration are not repairs.
+
+Capability: `update`.
+
+- Load the tenant-scoped current entity first so its original version participates
+ in optimistic locking; do not reconstruct versioned state from untrusted JSON.
+- Allow-list writable fields, attach the generated audit-reason API, and save with
+ the same UserContext. Add a stale-version rejection test.
diff --git a/examples/trace-chain/evidence/evaluation.md b/examples/trace-chain/evidence/evaluation.md
new file mode 100644
index 0000000..1342e1e
--- /dev/null
+++ b/examples/trace-chain/evidence/evaluation.md
@@ -0,0 +1,30 @@
+
+# KSML Evaluation Report
+## 📊 Summary
+- **Errors**: 0
+- **Warnings**: 0
+- **Suggestions**: 0
+- **Solids**: 17
+
+## ✅ Solids
+
+| Rule ID | Target | Source | Message |
+|---------|--------|--------|---------|
+| `KSML-UPLOAD-001` | `` | — | The upload contains 1 files. |
+| `KSML-UPLOAD-005` | `` | `model.xml:2` | Resolved entrypoint: model.xml |
+| `KSML-XML-002` | `` | `model.xml:2` | The XML document model.xml was parsed successfully. |
+| `KSML-ROOT-003` | `` | `model.xml:2` | Root name 'trace-chain-service' is well-formed. |
+| `KSML-ROOT-008` | `` | `model.xml:2` | External JSON business fields use the default camelCase profile. |
+| `KSML-OBJECT-001` | `platform` | `model.xml:4` | Object 'platform' defines display name, module, and module key metadata. |
+| `KSML-OBJECT-001` | `customer_order` | `model.xml:6` | Object 'customer_order' defines display name, module, and module key metadata. |
+| `KSML-OBJECT-001` | `order_item` | `model.xml:9` | Object 'order_item' defines display name, module, and module key metadata. |
+| `KSML-OBJECT-001` | `payment` | `model.xml:11` | Object 'payment' defines display name, module, and module key metadata. |
+| `KSML-OBJECT-001` | `payment_attempt` | `model.xml:13` | Object 'payment_attempt' defines display name, module, and module key metadata. |
+| `KSML-OBJECT-001` | `shipment` | `model.xml:15` | Object 'shipment' defines display name, module, and module key metadata. |
+| `KSML-REFERENCE-003` | `customer_order.platform` | `model.xml:7` | Reference 'platform' in 'customer_order' successfully resolves to target object 'platform'. |
+| `KSML-REFERENCE-003` | `order_item.customer_order` | `model.xml:10` | Reference 'customer_order' in 'order_item' successfully resolves to target object 'customer_order'. |
+| `KSML-REFERENCE-003` | `payment.customer_order` | `model.xml:12` | Reference 'customer_order' in 'payment' successfully resolves to target object 'customer_order'. |
+| `KSML-REFERENCE-003` | `payment_attempt.payment` | `model.xml:14` | Reference 'payment' in 'payment_attempt' successfully resolves to target object 'payment'. |
+| `KSML-REFERENCE-003` | `shipment.customer_order` | `model.xml:16` | Reference 'customer_order' in 'shipment' successfully resolves to target object 'customer_order'. |
+| `KSML-DOMAIN-ROOT-003` | `` | `model.xml:2` | Exactly one domain root candidate 'platform' was found. |
+
diff --git a/examples/trace-chain/evidence/library-sha256.txt b/examples/trace-chain/evidence/library-sha256.txt
new file mode 100644
index 0000000..9448bdd
--- /dev/null
+++ b/examples/trace-chain/evidence/library-sha256.txt
@@ -0,0 +1,19 @@
+3dd62d0762032c4d90b64583a0eff75115a848b374a0ee2978bc6f7684380a6b E.py
+555a60a9561499fb3ce7a6a3e40e7004d0a79b8ea01f2d80686c999db4873352 Q.py
+e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 models/__init__.py
+dcbd451e2d2ea1551d902610d8aec47bc20f1a4a5c8e871beb31371f716dc6a5 models/customer_order.py
+4d8b2a27d1538189ce71eb1a14ebb9c5eb3e9c607ea5b2b65a49ae9bed5b129f models/order_item.py
+90e9e711ec80c16fdeed70393e4f43607554cb5a3bd148d4e7d1c562eb2313d5 models/payment.py
+b6c35aa3753234b504ded8ccff85df0b1407fd3c77690b9df5d34e0d86d564eb models/payment_attempt.py
+bea4d609d76cfd1738416cb17e7144b48173a7763c551f3eee59b7474bd886cb models/platform.py
+318d0d9eb2f0bfefd6d2fd93375666d85999d72c35ce36049783cff84806b929 models/shipment.py
+f5820a2cbf1c4a29053e53e4d12e8304aee78eb52b6c1cda987daea3f22eff95 pyproject.toml
+e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 requests/__init__.py
+91f91c4faf84f8829b86febf274ac999d07715173734561b3ae7a31dc1987f29 requests/customer_order_request.py
+3d75a7d34ed73aaf981f92a96ad378e28b4d70b5dc5bb6120e5972cfc8277f4c requests/order_item_request.py
+4d707c1f2468a30ebb21fef597677535a7abb544c30608b72ce18267b0f3e918 requests/payment_attempt_request.py
+b21db39354b156ac8a94b16407bdf92bfa9e6067038171cc69a2dfaa11a87982 requests/payment_request.py
+3ff6c80155d072bb78dcd9f4cb72e915fe4eec184de0ae007c12500ad4fda38e requests/platform_request.py
+83b59281f359eb215a8db358465dfb9ea574ed6001add38ab7b4e1a4d8431a1a requests/shipment_request.py
+adf7af0e37f2d7b25d2fc709353bdfca207a2db3fe53989b1dbe37e0e2f2e63b runtime_module.py
+5ff4a2f49563da43619a3bde0da4d3b73105e87824aef4851ba050b0509272d3 teaql-i18n.json
diff --git a/examples/trace-chain/lib/E.py b/examples/trace-chain/lib/E.py
new file mode 100644
index 0000000..b9d6821
--- /dev/null
+++ b/examples/trace-chain/lib/E.py
@@ -0,0 +1,245 @@
+class TeaQLNotLoadedError(RuntimeError):
+ def __init__(self, root, access_path, break_point):
+ self.root = root
+ self.access_path = access_path
+ self.break_point = break_point
+ super().__init__(
+ f"TeaQLNotLoadedError: root={root} access_path={access_path} "
+ f"break_point={break_point} suggested_fix=select_{break_point}(...)"
+ )
+
+
+class ValueExpression:
+ def __init__(self, value=None, error=None):
+ self._value = value
+ self._error = error
+
+ def eval(self):
+ if self._error is not None:
+ raise self._error
+ return self._value
+
+ def or_if_null(self, fallback):
+ value = self.eval()
+ return fallback if value is None else value
+
+
+class EntityExpression:
+ def __init__(self, value, root=None, path="", error=None):
+ self._value = value
+ self._root = root or f"{type(value).__name__ if value is not None else 'Entity'}(null)"
+ self._path = path
+ self._error = error
+
+ def eval(self):
+ if self._error is not None:
+ raise self._error
+ return self._value
+
+ def _path_for(self, field):
+ return f"{self._path}.{field}" if self._path else field
+
+ def _not_loaded(self, field):
+ path = self._path_for(field)
+ return TeaQLNotLoadedError(self._root, path, field)
+
+ def _scalar(self, field, relation_id=False):
+ if self._error is not None:
+ return ValueExpression(error=self._error)
+ if self._value is None:
+ return ValueExpression(None)
+ if field not in getattr(self._value, "_loaded_fields", set()):
+ return ValueExpression(error=self._not_loaded(field))
+ value = getattr(self._value, field)
+ if relation_id and value is not None and not isinstance(value, (int, str)):
+ value = getattr(value, "id", None)
+ return ValueExpression(value)
+
+ def _relation(self, field, expression_type):
+ path = self._path_for(field)
+ if self._error is not None:
+ return expression_type(None, self._root, path, self._error)
+ if self._value is None:
+ return expression_type(None, self._root, path)
+ if field not in getattr(self._value, "_loaded_fields", set()):
+ return expression_type(None, self._root, path, self._not_loaded(field))
+ value = getattr(self._value, field)
+ if value is not None and isinstance(value, (int, str)):
+ return expression_type(None, self._root, path, self._not_loaded(field))
+ return expression_type(value, self._root, path)
+
+
+class ListExpression:
+ def __init__(self, values, root, path, item_expression, error=None):
+ self._values = values
+ self._root = root
+ self._path = path
+ self._item_expression = item_expression
+ self._error = error
+
+ def size(self):
+ return ValueExpression(error=self._error) if self._error else ValueExpression(len(self._values))
+
+ def first(self):
+ return self.get(0)
+
+ def get(self, index):
+ path = f"{self._path}.get({index})"
+ if self._error is not None:
+ return self._item_expression(None, self._root, path, self._error)
+ value = self._values[index] if 0 <= index < len(self._values) else None
+ return self._item_expression(value, self._root, path)
+
+
+class PlatformExpression(EntityExpression):
+ def id(self):
+ return self._scalar("id")
+ def name(self):
+ return self._scalar("name")
+ def version(self):
+ return self._scalar("version")
+ def customer_order_list(self):
+ path = self._path_for("customer_order_list")
+ if self._error is not None:
+ return ListExpression([], self._root, path, CustomerOrderExpression, self._error)
+ if self._value is None:
+ return ListExpression([], self._root, path, CustomerOrderExpression)
+ if "customer_order_list" not in getattr(self._value, "_loaded_fields", set()):
+ return ListExpression([], self._root, path, CustomerOrderExpression, self._not_loaded("customer_order_list"))
+ return ListExpression(getattr(self._value, "_customer_order_list"), self._root, path, CustomerOrderExpression)
+ pass
+
+class CustomerOrderExpression(EntityExpression):
+ def id(self):
+ return self._scalar("id")
+ def order_number(self):
+ return self._scalar("orderNumber")
+ def description(self):
+ return self._scalar("description")
+ def version(self):
+ return self._scalar("version")
+ def platform_id(self):
+ return self._scalar("platform", relation_id=True)
+
+ def platform(self):
+ return self._relation("platform", PlatformExpression)
+ def order_item_list(self):
+ path = self._path_for("order_item_list")
+ if self._error is not None:
+ return ListExpression([], self._root, path, OrderItemExpression, self._error)
+ if self._value is None:
+ return ListExpression([], self._root, path, OrderItemExpression)
+ if "order_item_list" not in getattr(self._value, "_loaded_fields", set()):
+ return ListExpression([], self._root, path, OrderItemExpression, self._not_loaded("order_item_list"))
+ return ListExpression(getattr(self._value, "_order_item_list"), self._root, path, OrderItemExpression)
+ def payment_list(self):
+ path = self._path_for("payment_list")
+ if self._error is not None:
+ return ListExpression([], self._root, path, PaymentExpression, self._error)
+ if self._value is None:
+ return ListExpression([], self._root, path, PaymentExpression)
+ if "payment_list" not in getattr(self._value, "_loaded_fields", set()):
+ return ListExpression([], self._root, path, PaymentExpression, self._not_loaded("payment_list"))
+ return ListExpression(getattr(self._value, "_payment_list"), self._root, path, PaymentExpression)
+ def shipment_list(self):
+ path = self._path_for("shipment_list")
+ if self._error is not None:
+ return ListExpression([], self._root, path, ShipmentExpression, self._error)
+ if self._value is None:
+ return ListExpression([], self._root, path, ShipmentExpression)
+ if "shipment_list" not in getattr(self._value, "_loaded_fields", set()):
+ return ListExpression([], self._root, path, ShipmentExpression, self._not_loaded("shipment_list"))
+ return ListExpression(getattr(self._value, "_shipment_list"), self._root, path, ShipmentExpression)
+ pass
+
+class OrderItemExpression(EntityExpression):
+ def id(self):
+ return self._scalar("id")
+ def name(self):
+ return self._scalar("name")
+ def version(self):
+ return self._scalar("version")
+ def customer_order_id(self):
+ return self._scalar("customerOrder", relation_id=True)
+
+ def customer_order(self):
+ return self._relation("customerOrder", CustomerOrderExpression)
+ pass
+
+class PaymentExpression(EntityExpression):
+ def id(self):
+ return self._scalar("id")
+ def reference_code(self):
+ return self._scalar("referenceCode")
+ def version(self):
+ return self._scalar("version")
+ def customer_order_id(self):
+ return self._scalar("customerOrder", relation_id=True)
+
+ def customer_order(self):
+ return self._relation("customerOrder", CustomerOrderExpression)
+ def payment_attempt_list(self):
+ path = self._path_for("payment_attempt_list")
+ if self._error is not None:
+ return ListExpression([], self._root, path, PaymentAttemptExpression, self._error)
+ if self._value is None:
+ return ListExpression([], self._root, path, PaymentAttemptExpression)
+ if "payment_attempt_list" not in getattr(self._value, "_loaded_fields", set()):
+ return ListExpression([], self._root, path, PaymentAttemptExpression, self._not_loaded("payment_attempt_list"))
+ return ListExpression(getattr(self._value, "_payment_attempt_list"), self._root, path, PaymentAttemptExpression)
+ pass
+
+class PaymentAttemptExpression(EntityExpression):
+ def id(self):
+ return self._scalar("id")
+ def reference_code(self):
+ return self._scalar("referenceCode")
+ def version(self):
+ return self._scalar("version")
+ def payment_id(self):
+ return self._scalar("payment", relation_id=True)
+
+ def payment(self):
+ return self._relation("payment", PaymentExpression)
+ pass
+
+class ShipmentExpression(EntityExpression):
+ def id(self):
+ return self._scalar("id")
+ def reference_code(self):
+ return self._scalar("referenceCode")
+ def version(self):
+ return self._scalar("version")
+ def customer_order_id(self):
+ return self._scalar("customerOrder", relation_id=True)
+
+ def customer_order(self):
+ return self._relation("customerOrder", CustomerOrderExpression)
+ pass
+
+class E:
+ @staticmethod
+ def platform(value):
+ entity_id = getattr(value, "id", None)
+ return PlatformExpression(value, "Platform(id={})".format(entity_id))
+ @staticmethod
+ def customer_order(value):
+ entity_id = getattr(value, "id", None)
+ return CustomerOrderExpression(value, "CustomerOrder(id={})".format(entity_id))
+ @staticmethod
+ def order_item(value):
+ entity_id = getattr(value, "id", None)
+ return OrderItemExpression(value, "OrderItem(id={})".format(entity_id))
+ @staticmethod
+ def payment(value):
+ entity_id = getattr(value, "id", None)
+ return PaymentExpression(value, "Payment(id={})".format(entity_id))
+ @staticmethod
+ def payment_attempt(value):
+ entity_id = getattr(value, "id", None)
+ return PaymentAttemptExpression(value, "PaymentAttempt(id={})".format(entity_id))
+ @staticmethod
+ def shipment(value):
+ entity_id = getattr(value, "id", None)
+ return ShipmentExpression(value, "Shipment(id={})".format(entity_id))
+ pass
\ No newline at end of file
diff --git a/examples/trace-chain/lib/Q.py b/examples/trace-chain/lib/Q.py
new file mode 100644
index 0000000..29b065e
--- /dev/null
+++ b/examples/trace-chain/lib/Q.py
@@ -0,0 +1,58 @@
+# Generated by teaql-code-gen
+from requests.platform_request import PlatformRequest
+from requests.customer_order_request import CustomerOrderRequest
+from requests.order_item_request import OrderItemRequest
+from requests.payment_request import PaymentRequest
+from requests.payment_attempt_request import PaymentAttemptRequest
+from requests.shipment_request import ShipmentRequest
+
+class Q:
+ @staticmethod
+ def platforms() -> PlatformRequest:
+ return PlatformRequest(minimal=False)
+
+ @staticmethod
+ def platforms_minimal() -> PlatformRequest:
+ return PlatformRequest(minimal=True)
+
+ @staticmethod
+ def customer_orders() -> CustomerOrderRequest:
+ return CustomerOrderRequest(minimal=False)
+
+ @staticmethod
+ def customer_orders_minimal() -> CustomerOrderRequest:
+ return CustomerOrderRequest(minimal=True)
+
+ @staticmethod
+ def order_items() -> OrderItemRequest:
+ return OrderItemRequest(minimal=False)
+
+ @staticmethod
+ def order_items_minimal() -> OrderItemRequest:
+ return OrderItemRequest(minimal=True)
+
+ @staticmethod
+ def payments() -> PaymentRequest:
+ return PaymentRequest(minimal=False)
+
+ @staticmethod
+ def payments_minimal() -> PaymentRequest:
+ return PaymentRequest(minimal=True)
+
+ @staticmethod
+ def payment_attempts() -> PaymentAttemptRequest:
+ return PaymentAttemptRequest(minimal=False)
+
+ @staticmethod
+ def payment_attempts_minimal() -> PaymentAttemptRequest:
+ return PaymentAttemptRequest(minimal=True)
+
+ @staticmethod
+ def shipments() -> ShipmentRequest:
+ return ShipmentRequest(minimal=False)
+
+ @staticmethod
+ def shipments_minimal() -> ShipmentRequest:
+ return ShipmentRequest(minimal=True)
+
+
diff --git a/examples/trace-chain/lib/models/__init__.py b/examples/trace-chain/lib/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/trace-chain/lib/models/customer_order.py b/examples/trace-chain/lib/models/customer_order.py
new file mode 100644
index 0000000..34459e4
--- /dev/null
+++ b/examples/trace-chain/lib/models/customer_order.py
@@ -0,0 +1,446 @@
+from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot, _QueryProjectionSnapshot
+from teaql.core import MutationIntent
+from teaql.core.value import Value
+from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
+import itertools
+from models.platform import Platform
+
+
+class CustomerOrder:
+ _teaql_temporary_ids = itertools.count(1)
+ @classmethod
+ def refer(cls, entity_id):
+ return cls(id=entity_id)
+
+ @classmethod
+ def _teaql_new_with_fixed_id(cls, entity_id):
+ """Generated bootstrap capability; application code must not call it."""
+ return cls(id=entity_id)._teaql_force_create()
+
+ def _teaql_force_create(self):
+ self._action = "Create"
+ self._entity_root.mark_as_new(self._teaql_entity_key())
+ return self
+
+ def __init__(self, **kwargs):
+ self._entity_root = kwargs.pop("_entity_root", None) or EntityRoot()
+ if "id" in kwargs and "id" not in kwargs:
+ kwargs["id"] = kwargs.pop("id")
+ if "platform" in kwargs and "platform" not in kwargs:
+ kwargs["platform"] = kwargs.pop("platform")
+ if "order_number" in kwargs and "orderNumber" not in kwargs:
+ kwargs["orderNumber"] = kwargs.pop("order_number")
+ if "description" in kwargs and "description" not in kwargs:
+ kwargs["description"] = kwargs.pop("description")
+ if "version" in kwargs and "version" not in kwargs:
+ kwargs["version"] = kwargs.pop("version")
+ self._teaql_query_projections = _QueryProjectionSnapshot(kwargs, (
+ "id", "id",
+ "platform", "platform",
+ "orderNumber", "order_number",
+ "description", "description",
+ "version", "version",
+ "order_item_list",
+ "payment_list",
+ "shipment_list",
+ ))
+ self._action = "Update" if kwargs.get("id") else "Create"
+ self._comment = None
+ self._loaded_fields = set(kwargs.keys())
+ self.id = kwargs.get("id")
+
+ self.platform = kwargs.get("platform")
+
+ self.orderNumber = kwargs.get("orderNumber")
+
+ self.description = kwargs.get("description")
+
+ self.version = kwargs.get("version")
+
+ if isinstance(self.platform, dict):
+ self.platform = Platform(**self.platform)
+ self._order_item_list = kwargs.get("order_item_list", [])
+ if "order_item_list" in kwargs or kwargs.get("id") is None:
+ self._loaded_fields.add("order_item_list")
+ self._payment_list = kwargs.get("payment_list", [])
+ if "payment_list" in kwargs or kwargs.get("id") is None:
+ self._loaded_fields.add("payment_list")
+ self._shipment_list = kwargs.get("shipment_list", [])
+ if "shipment_list" in kwargs or kwargs.get("id") is None:
+ self._loaded_fields.add("shipment_list")
+ if self._order_item_list:
+ from models.order_item import OrderItem
+ self._order_item_list = [
+ item if isinstance(item, OrderItem) else OrderItem(_entity_root=self._entity_root, **item)
+ for item in self._order_item_list
+ ]
+ if self._payment_list:
+ from models.payment import Payment
+ self._payment_list = [
+ item if isinstance(item, Payment) else Payment(_entity_root=self._entity_root, **item)
+ for item in self._payment_list
+ ]
+ if self._shipment_list:
+ from models.shipment import Shipment
+ self._shipment_list = [
+ item if isinstance(item, Shipment) else Shipment(_entity_root=self._entity_root, **item)
+ for item in self._shipment_list
+ ]
+ self._ledger_id = getattr(self, "id", None)
+ if self._ledger_id is None:
+ self._ledger_id = -next(self._teaql_temporary_ids)
+ key = self._teaql_entity_key()
+ if self._action == "Create":
+ self._entity_root.mark_as_new(key)
+ elif getattr(self, "version", None) is not None:
+ self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
+
+ def query_projection(self, alias):
+ """Read a query-only alias; raises KeyError when it was not returned."""
+ return self._teaql_query_projections.get(alias)
+
+ def has_query_projection(self, alias):
+ return self._teaql_query_projections.contains(alias)
+
+ def _teaql_entity_key(self):
+ return EntityKey("CustomerOrder", self._ledger_id)
+
+ def _teaql_attach_root(self, root):
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
+ self._entity_root = root
+ for child in self._order_item_list:
+ child._teaql_attach_root(root)
+ for child in self._payment_list:
+ child._teaql_attach_root(root)
+ for child in self._shipment_list:
+ child._teaql_attach_root(root)
+ return self
+ def mark_for_deletion(self):
+ self._action = "Delete"
+ self._entity_root.mark_as_deleted(self._teaql_entity_key())
+ return self
+
+ def audit_as(self, comment: str):
+ MutationIntent(comment)
+ self._comment = comment
+ return self
+
+ async def save(self, context):
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
+
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
+
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("CustomerOrder"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._order_item_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+ for child in self._payment_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+ for child in self._shipment_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
+ payload = {}
+ if "id" in self._loaded_fields:
+ payload["id"] = Value.I64(self.id)
+
+ if "platform" in self._loaded_fields:
+ reference = self.platform
+ reference_id = getattr(reference, "id", reference)
+ payload["platform"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
+ if "orderNumber" in self._loaded_fields:
+ payload["order_number"] = Value.Text(self.orderNumber)
+
+ if "description" in self._loaded_fields:
+ payload["description"] = Value.Text(self.description)
+
+ if "version" in self._loaded_fields:
+ payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
+ action = self._action
+ if action == "Update":
+ ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
+ payload = {field: value for field, value in ledger.items() if field not in ("id", "version")}
+ if action == "Create":
+ cmd = InsertCommand("CustomerOrder", payload)
+ elif action == "Update":
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("CustomerOrder", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ for key, value in payload.items():
+ if key not in ("id", "version"): cmd.value(key, value)
+ else:
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("CustomerOrder", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
+ return action, cmd
+
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "platform" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("platform"), message="Mutation requires a fully loaded entity")])
+ if "orderNumber" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("order_number"), message="Mutation requires a fully loaded entity")])
+ if "description" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("description"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+ for index, child in enumerate(self._order_item_list):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, "customerOrder", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_customer_order(self)
+ try:
+ child._teaql_preflight_graph(graph)
+ except CheckException as error:
+ prefix = ObjectLocation().property("order_item_list").index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+ for index, child in enumerate(self._payment_list):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, "customerOrder", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_customer_order(self)
+ try:
+ child._teaql_preflight_graph(graph)
+ except CheckException as error:
+ prefix = ObjectLocation().property("payment_list").index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+ for index, child in enumerate(self._shipment_list):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, "customerOrder", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_customer_order(self)
+ try:
+ child._teaql_preflight_graph(graph)
+ except CheckException as error:
+ prefix = ObjectLocation().property("shipment_list").index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
+
+ self._teaql_attach_root(self._entity_root)
+ action, cmd = self._teaql_build_command()
+
+
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
+
+ try:
+ context.check_and_fix_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+ context.mark_mutation_checked(cmd)
+ service = context.require_resource("dataService")
+ result = await service.mutate(context, req)
+ persisted = result.persisted_record
+ if persisted is None:
+ raise RuntimeError(
+ "Mutation provider did not return authoritative persisted state for CustomerOrder"
+ )
+ rollback_payload = {field: getattr(self, field, None) for field in self._loaded_fields | {"id", "version"}}
+ rollback_ledger_id = self._ledger_id
+ rollback_action = self._action
+ rollback_loaded_fields = set(self._loaded_fields)
+ old_key = self._teaql_entity_key()
+ if "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ elif "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ if "platform" in persisted:
+ self.platform = persisted["platform"]
+ self._loaded_fields.add("platform")
+ elif "platform" in persisted:
+ self.platform = persisted["platform"]
+ self._loaded_fields.add("platform")
+ if "order_number" in persisted:
+ self.orderNumber = persisted["order_number"]
+ self._loaded_fields.add("orderNumber")
+ elif "orderNumber" in persisted:
+ self.orderNumber = persisted["orderNumber"]
+ self._loaded_fields.add("orderNumber")
+ if "description" in persisted:
+ self.description = persisted["description"]
+ self._loaded_fields.add("description")
+ elif "description" in persisted:
+ self.description = persisted["description"]
+ self._loaded_fields.add("description")
+ if "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ elif "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
+ self._ledger_id = getattr(self, "id", self._ledger_id)
+ new_key = self._teaql_entity_key()
+ if old_key != new_key:
+ self._entity_root.rekey(old_key, new_key)
+ def rollback_entity():
+ for field, value in rollback_payload.items():
+ setattr(self, field, value)
+ self._ledger_id = rollback_ledger_id
+ self._action = rollback_action
+ self._loaded_fields = rollback_loaded_fields
+ if old_key != new_key:
+ self._entity_root.rekey(new_key, old_key)
+ graph.after_rollback(rollback_entity)
+ if action != "Delete":
+ self._action = "Update"
+
+ if action != "Delete":
+ await self._teaql_save_children(graph, scope)
+ def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
+ self._entity_root.clear_entity(new_key)
+ if getattr(self, "version", None) is not None:
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
+ return self
+
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("order_item_list", self._order_item_list, "update_customer_order", "customerOrder"))
+ cascade_relations.append(("payment_list", self._payment_list, "update_customer_order", "customerOrder"))
+ cascade_relations.append(("shipment_list", self._shipment_list, "update_customer_order", "customerOrder"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
+ def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
+ self.id = value
+ self._loaded_fields.add("id")
+ self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
+ return self
+
+ def update_order_number(self, value):
+ self.orderNumber = value
+ self._loaded_fields.add("orderNumber")
+ self._entity_root.set(self._teaql_entity_key(), "order_number", Value.from_any(value))
+ return self
+
+ def update_description(self, value):
+ self.description = value
+ self._loaded_fields.add("description")
+ self._entity_root.set(self._teaql_entity_key(), "description", Value.from_any(value))
+ return self
+
+ def update_version(self, value):
+ self.version = value
+ self._loaded_fields.add("version")
+ self._entity_root.set(self._teaql_entity_key(), "version", Value.from_any(value))
+ return self
+ def update_platform(self, value):
+ self.platform = getattr(value, "id", value) if value else None
+ self._loaded_fields.add("platform")
+ self._entity_root.set(self._teaql_entity_key(), "platform", Value.from_any(self.platform))
+ return self
+
+ def order_item_list(self) -> list:
+ self._loaded_fields.add("order_item_list")
+ return self._order_item_list
+
+ def payment_list(self) -> list:
+ self._loaded_fields.add("payment_list")
+ return self._payment_list
+
+ def shipment_list(self) -> list:
+ self._loaded_fields.add("shipment_list")
+ return self._shipment_list
diff --git a/examples/trace-chain/lib/models/order_item.py b/examples/trace-chain/lib/models/order_item.py
new file mode 100644
index 0000000..b6fd2a5
--- /dev/null
+++ b/examples/trace-chain/lib/models/order_item.py
@@ -0,0 +1,325 @@
+from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot, _QueryProjectionSnapshot
+from teaql.core import MutationIntent
+from teaql.core.value import Value
+from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
+import itertools
+from models.customer_order import CustomerOrder
+
+
+class OrderItem:
+ _teaql_temporary_ids = itertools.count(1)
+ @classmethod
+ def refer(cls, entity_id):
+ return cls(id=entity_id)
+
+ @classmethod
+ def _teaql_new_with_fixed_id(cls, entity_id):
+ """Generated bootstrap capability; application code must not call it."""
+ return cls(id=entity_id)._teaql_force_create()
+
+ def _teaql_force_create(self):
+ self._action = "Create"
+ self._entity_root.mark_as_new(self._teaql_entity_key())
+ return self
+
+ def __init__(self, **kwargs):
+ self._entity_root = kwargs.pop("_entity_root", None) or EntityRoot()
+ if "id" in kwargs and "id" not in kwargs:
+ kwargs["id"] = kwargs.pop("id")
+ if "customer_order" in kwargs and "customerOrder" not in kwargs:
+ kwargs["customerOrder"] = kwargs.pop("customer_order")
+ if "name" in kwargs and "name" not in kwargs:
+ kwargs["name"] = kwargs.pop("name")
+ if "version" in kwargs and "version" not in kwargs:
+ kwargs["version"] = kwargs.pop("version")
+ self._teaql_query_projections = _QueryProjectionSnapshot(kwargs, (
+ "id", "id",
+ "customerOrder", "customer_order",
+ "name", "name",
+ "version", "version",
+ ))
+ self._action = "Update" if kwargs.get("id") else "Create"
+ self._comment = None
+ self._loaded_fields = set(kwargs.keys())
+ self.id = kwargs.get("id")
+
+ self.customerOrder = kwargs.get("customerOrder")
+
+ self.name = kwargs.get("name")
+
+ self.version = kwargs.get("version")
+
+ if isinstance(self.customerOrder, dict):
+ self.customerOrder = CustomerOrder(**self.customerOrder)
+ self._ledger_id = getattr(self, "id", None)
+ if self._ledger_id is None:
+ self._ledger_id = -next(self._teaql_temporary_ids)
+ key = self._teaql_entity_key()
+ if self._action == "Create":
+ self._entity_root.mark_as_new(key)
+ elif getattr(self, "version", None) is not None:
+ self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
+
+ def query_projection(self, alias):
+ """Read a query-only alias; raises KeyError when it was not returned."""
+ return self._teaql_query_projections.get(alias)
+
+ def has_query_projection(self, alias):
+ return self._teaql_query_projections.contains(alias)
+
+ def _teaql_entity_key(self):
+ return EntityKey("OrderItem", self._ledger_id)
+
+ def _teaql_attach_root(self, root):
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
+ self._entity_root = root
+ return self
+ def mark_for_deletion(self):
+ self._action = "Delete"
+ self._entity_root.mark_as_deleted(self._teaql_entity_key())
+ return self
+
+ def audit_as(self, comment: str):
+ MutationIntent(comment)
+ self._comment = comment
+ return self
+
+ async def save(self, context):
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
+
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
+
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("OrderItem"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
+ payload = {}
+ if "id" in self._loaded_fields:
+ payload["id"] = Value.I64(self.id)
+
+ if "customerOrder" in self._loaded_fields:
+ reference = self.customerOrder
+ reference_id = getattr(reference, "id", reference)
+ payload["customer_order"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
+ if "name" in self._loaded_fields:
+ payload["name"] = Value.Text(self.name)
+
+ if "version" in self._loaded_fields:
+ payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
+ action = self._action
+ if action == "Update":
+ ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
+ payload = {field: value for field, value in ledger.items() if field not in ("id", "version")}
+ if action == "Create":
+ cmd = InsertCommand("OrderItem", payload)
+ elif action == "Update":
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("OrderItem", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ for key, value in payload.items():
+ if key not in ("id", "version"): cmd.value(key, value)
+ else:
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("OrderItem", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
+ return action, cmd
+
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "customerOrder" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("customer_order"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
+
+ self._teaql_attach_root(self._entity_root)
+ action, cmd = self._teaql_build_command()
+
+
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
+
+ try:
+ context.check_and_fix_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+ context.mark_mutation_checked(cmd)
+ service = context.require_resource("dataService")
+ result = await service.mutate(context, req)
+ persisted = result.persisted_record
+ if persisted is None:
+ raise RuntimeError(
+ "Mutation provider did not return authoritative persisted state for OrderItem"
+ )
+ rollback_payload = {field: getattr(self, field, None) for field in self._loaded_fields | {"id", "version"}}
+ rollback_ledger_id = self._ledger_id
+ rollback_action = self._action
+ rollback_loaded_fields = set(self._loaded_fields)
+ old_key = self._teaql_entity_key()
+ if "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ elif "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ if "customer_order" in persisted:
+ self.customerOrder = persisted["customer_order"]
+ self._loaded_fields.add("customerOrder")
+ elif "customerOrder" in persisted:
+ self.customerOrder = persisted["customerOrder"]
+ self._loaded_fields.add("customerOrder")
+ if "name" in persisted:
+ self.name = persisted["name"]
+ self._loaded_fields.add("name")
+ elif "name" in persisted:
+ self.name = persisted["name"]
+ self._loaded_fields.add("name")
+ if "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ elif "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
+ self._ledger_id = getattr(self, "id", self._ledger_id)
+ new_key = self._teaql_entity_key()
+ if old_key != new_key:
+ self._entity_root.rekey(old_key, new_key)
+ def rollback_entity():
+ for field, value in rollback_payload.items():
+ setattr(self, field, value)
+ self._ledger_id = rollback_ledger_id
+ self._action = rollback_action
+ self._loaded_fields = rollback_loaded_fields
+ if old_key != new_key:
+ self._entity_root.rekey(new_key, old_key)
+ graph.after_rollback(rollback_entity)
+ if action != "Delete":
+ self._action = "Update"
+
+ if action != "Delete":
+ await self._teaql_save_children(graph, scope)
+ def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
+ self._entity_root.clear_entity(new_key)
+ if getattr(self, "version", None) is not None:
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
+ return self
+
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
+ def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
+ self.id = value
+ self._loaded_fields.add("id")
+ self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
+ return self
+
+ def update_name(self, value):
+ self.name = value
+ self._loaded_fields.add("name")
+ self._entity_root.set(self._teaql_entity_key(), "name", Value.from_any(value))
+ return self
+
+ def update_version(self, value):
+ self.version = value
+ self._loaded_fields.add("version")
+ self._entity_root.set(self._teaql_entity_key(), "version", Value.from_any(value))
+ return self
+ def update_customer_order(self, value):
+ self.customerOrder = getattr(value, "id", value) if value else None
+ self._loaded_fields.add("customerOrder")
+ self._entity_root.set(self._teaql_entity_key(), "customer_order", Value.from_any(self.customerOrder))
+ return self
diff --git a/examples/trace-chain/lib/models/payment.py b/examples/trace-chain/lib/models/payment.py
new file mode 100644
index 0000000..36256e0
--- /dev/null
+++ b/examples/trace-chain/lib/models/payment.py
@@ -0,0 +1,358 @@
+from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot, _QueryProjectionSnapshot
+from teaql.core import MutationIntent
+from teaql.core.value import Value
+from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
+import itertools
+from models.customer_order import CustomerOrder
+
+
+class Payment:
+ _teaql_temporary_ids = itertools.count(1)
+ @classmethod
+ def refer(cls, entity_id):
+ return cls(id=entity_id)
+
+ @classmethod
+ def _teaql_new_with_fixed_id(cls, entity_id):
+ """Generated bootstrap capability; application code must not call it."""
+ return cls(id=entity_id)._teaql_force_create()
+
+ def _teaql_force_create(self):
+ self._action = "Create"
+ self._entity_root.mark_as_new(self._teaql_entity_key())
+ return self
+
+ def __init__(self, **kwargs):
+ self._entity_root = kwargs.pop("_entity_root", None) or EntityRoot()
+ if "id" in kwargs and "id" not in kwargs:
+ kwargs["id"] = kwargs.pop("id")
+ if "customer_order" in kwargs and "customerOrder" not in kwargs:
+ kwargs["customerOrder"] = kwargs.pop("customer_order")
+ if "reference_code" in kwargs and "referenceCode" not in kwargs:
+ kwargs["referenceCode"] = kwargs.pop("reference_code")
+ if "version" in kwargs and "version" not in kwargs:
+ kwargs["version"] = kwargs.pop("version")
+ self._teaql_query_projections = _QueryProjectionSnapshot(kwargs, (
+ "id", "id",
+ "customerOrder", "customer_order",
+ "referenceCode", "reference_code",
+ "version", "version",
+ "payment_attempt_list",
+ ))
+ self._action = "Update" if kwargs.get("id") else "Create"
+ self._comment = None
+ self._loaded_fields = set(kwargs.keys())
+ self.id = kwargs.get("id")
+
+ self.customerOrder = kwargs.get("customerOrder")
+
+ self.referenceCode = kwargs.get("referenceCode")
+
+ self.version = kwargs.get("version")
+
+ if isinstance(self.customerOrder, dict):
+ self.customerOrder = CustomerOrder(**self.customerOrder)
+ self._payment_attempt_list = kwargs.get("payment_attempt_list", [])
+ if "payment_attempt_list" in kwargs or kwargs.get("id") is None:
+ self._loaded_fields.add("payment_attempt_list")
+ if self._payment_attempt_list:
+ from models.payment_attempt import PaymentAttempt
+ self._payment_attempt_list = [
+ item if isinstance(item, PaymentAttempt) else PaymentAttempt(_entity_root=self._entity_root, **item)
+ for item in self._payment_attempt_list
+ ]
+ self._ledger_id = getattr(self, "id", None)
+ if self._ledger_id is None:
+ self._ledger_id = -next(self._teaql_temporary_ids)
+ key = self._teaql_entity_key()
+ if self._action == "Create":
+ self._entity_root.mark_as_new(key)
+ elif getattr(self, "version", None) is not None:
+ self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
+
+ def query_projection(self, alias):
+ """Read a query-only alias; raises KeyError when it was not returned."""
+ return self._teaql_query_projections.get(alias)
+
+ def has_query_projection(self, alias):
+ return self._teaql_query_projections.contains(alias)
+
+ def _teaql_entity_key(self):
+ return EntityKey("Payment", self._ledger_id)
+
+ def _teaql_attach_root(self, root):
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
+ self._entity_root = root
+ for child in self._payment_attempt_list:
+ child._teaql_attach_root(root)
+ return self
+ def mark_for_deletion(self):
+ self._action = "Delete"
+ self._entity_root.mark_as_deleted(self._teaql_entity_key())
+ return self
+
+ def audit_as(self, comment: str):
+ MutationIntent(comment)
+ self._comment = comment
+ return self
+
+ async def save(self, context):
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
+
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
+
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("Payment"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._payment_attempt_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
+ payload = {}
+ if "id" in self._loaded_fields:
+ payload["id"] = Value.I64(self.id)
+
+ if "customerOrder" in self._loaded_fields:
+ reference = self.customerOrder
+ reference_id = getattr(reference, "id", reference)
+ payload["customer_order"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
+ if "referenceCode" in self._loaded_fields:
+ payload["reference_code"] = Value.Text(self.referenceCode)
+
+ if "version" in self._loaded_fields:
+ payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
+ action = self._action
+ if action == "Update":
+ ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
+ payload = {field: value for field, value in ledger.items() if field not in ("id", "version")}
+ if action == "Create":
+ cmd = InsertCommand("Payment", payload)
+ elif action == "Update":
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("Payment", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ for key, value in payload.items():
+ if key not in ("id", "version"): cmd.value(key, value)
+ else:
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("Payment", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
+ return action, cmd
+
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "customerOrder" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("customer_order"), message="Mutation requires a fully loaded entity")])
+ if "referenceCode" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("reference_code"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+ for index, child in enumerate(self._payment_attempt_list):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, "payment", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_payment(self)
+ try:
+ child._teaql_preflight_graph(graph)
+ except CheckException as error:
+ prefix = ObjectLocation().property("payment_attempt_list").index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
+
+ self._teaql_attach_root(self._entity_root)
+ action, cmd = self._teaql_build_command()
+
+
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
+
+ try:
+ context.check_and_fix_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+ context.mark_mutation_checked(cmd)
+ service = context.require_resource("dataService")
+ result = await service.mutate(context, req)
+ persisted = result.persisted_record
+ if persisted is None:
+ raise RuntimeError(
+ "Mutation provider did not return authoritative persisted state for Payment"
+ )
+ rollback_payload = {field: getattr(self, field, None) for field in self._loaded_fields | {"id", "version"}}
+ rollback_ledger_id = self._ledger_id
+ rollback_action = self._action
+ rollback_loaded_fields = set(self._loaded_fields)
+ old_key = self._teaql_entity_key()
+ if "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ elif "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ if "customer_order" in persisted:
+ self.customerOrder = persisted["customer_order"]
+ self._loaded_fields.add("customerOrder")
+ elif "customerOrder" in persisted:
+ self.customerOrder = persisted["customerOrder"]
+ self._loaded_fields.add("customerOrder")
+ if "reference_code" in persisted:
+ self.referenceCode = persisted["reference_code"]
+ self._loaded_fields.add("referenceCode")
+ elif "referenceCode" in persisted:
+ self.referenceCode = persisted["referenceCode"]
+ self._loaded_fields.add("referenceCode")
+ if "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ elif "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
+ self._ledger_id = getattr(self, "id", self._ledger_id)
+ new_key = self._teaql_entity_key()
+ if old_key != new_key:
+ self._entity_root.rekey(old_key, new_key)
+ def rollback_entity():
+ for field, value in rollback_payload.items():
+ setattr(self, field, value)
+ self._ledger_id = rollback_ledger_id
+ self._action = rollback_action
+ self._loaded_fields = rollback_loaded_fields
+ if old_key != new_key:
+ self._entity_root.rekey(new_key, old_key)
+ graph.after_rollback(rollback_entity)
+ if action != "Delete":
+ self._action = "Update"
+
+ if action != "Delete":
+ await self._teaql_save_children(graph, scope)
+ def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
+ self._entity_root.clear_entity(new_key)
+ if getattr(self, "version", None) is not None:
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
+ return self
+
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("payment_attempt_list", self._payment_attempt_list, "update_payment", "payment"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
+ def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
+ self.id = value
+ self._loaded_fields.add("id")
+ self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
+ return self
+
+ def update_reference_code(self, value):
+ self.referenceCode = value
+ self._loaded_fields.add("referenceCode")
+ self._entity_root.set(self._teaql_entity_key(), "reference_code", Value.from_any(value))
+ return self
+
+ def update_version(self, value):
+ self.version = value
+ self._loaded_fields.add("version")
+ self._entity_root.set(self._teaql_entity_key(), "version", Value.from_any(value))
+ return self
+ def update_customer_order(self, value):
+ self.customerOrder = getattr(value, "id", value) if value else None
+ self._loaded_fields.add("customerOrder")
+ self._entity_root.set(self._teaql_entity_key(), "customer_order", Value.from_any(self.customerOrder))
+ return self
+
+ def payment_attempt_list(self) -> list:
+ self._loaded_fields.add("payment_attempt_list")
+ return self._payment_attempt_list
diff --git a/examples/trace-chain/lib/models/payment_attempt.py b/examples/trace-chain/lib/models/payment_attempt.py
new file mode 100644
index 0000000..8aeeac0
--- /dev/null
+++ b/examples/trace-chain/lib/models/payment_attempt.py
@@ -0,0 +1,325 @@
+from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot, _QueryProjectionSnapshot
+from teaql.core import MutationIntent
+from teaql.core.value import Value
+from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
+import itertools
+from models.payment import Payment
+
+
+class PaymentAttempt:
+ _teaql_temporary_ids = itertools.count(1)
+ @classmethod
+ def refer(cls, entity_id):
+ return cls(id=entity_id)
+
+ @classmethod
+ def _teaql_new_with_fixed_id(cls, entity_id):
+ """Generated bootstrap capability; application code must not call it."""
+ return cls(id=entity_id)._teaql_force_create()
+
+ def _teaql_force_create(self):
+ self._action = "Create"
+ self._entity_root.mark_as_new(self._teaql_entity_key())
+ return self
+
+ def __init__(self, **kwargs):
+ self._entity_root = kwargs.pop("_entity_root", None) or EntityRoot()
+ if "id" in kwargs and "id" not in kwargs:
+ kwargs["id"] = kwargs.pop("id")
+ if "payment" in kwargs and "payment" not in kwargs:
+ kwargs["payment"] = kwargs.pop("payment")
+ if "reference_code" in kwargs and "referenceCode" not in kwargs:
+ kwargs["referenceCode"] = kwargs.pop("reference_code")
+ if "version" in kwargs and "version" not in kwargs:
+ kwargs["version"] = kwargs.pop("version")
+ self._teaql_query_projections = _QueryProjectionSnapshot(kwargs, (
+ "id", "id",
+ "payment", "payment",
+ "referenceCode", "reference_code",
+ "version", "version",
+ ))
+ self._action = "Update" if kwargs.get("id") else "Create"
+ self._comment = None
+ self._loaded_fields = set(kwargs.keys())
+ self.id = kwargs.get("id")
+
+ self.payment = kwargs.get("payment")
+
+ self.referenceCode = kwargs.get("referenceCode")
+
+ self.version = kwargs.get("version")
+
+ if isinstance(self.payment, dict):
+ self.payment = Payment(**self.payment)
+ self._ledger_id = getattr(self, "id", None)
+ if self._ledger_id is None:
+ self._ledger_id = -next(self._teaql_temporary_ids)
+ key = self._teaql_entity_key()
+ if self._action == "Create":
+ self._entity_root.mark_as_new(key)
+ elif getattr(self, "version", None) is not None:
+ self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
+
+ def query_projection(self, alias):
+ """Read a query-only alias; raises KeyError when it was not returned."""
+ return self._teaql_query_projections.get(alias)
+
+ def has_query_projection(self, alias):
+ return self._teaql_query_projections.contains(alias)
+
+ def _teaql_entity_key(self):
+ return EntityKey("PaymentAttempt", self._ledger_id)
+
+ def _teaql_attach_root(self, root):
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
+ self._entity_root = root
+ return self
+ def mark_for_deletion(self):
+ self._action = "Delete"
+ self._entity_root.mark_as_deleted(self._teaql_entity_key())
+ return self
+
+ def audit_as(self, comment: str):
+ MutationIntent(comment)
+ self._comment = comment
+ return self
+
+ async def save(self, context):
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
+
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
+
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("PaymentAttempt"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
+ payload = {}
+ if "id" in self._loaded_fields:
+ payload["id"] = Value.I64(self.id)
+
+ if "payment" in self._loaded_fields:
+ reference = self.payment
+ reference_id = getattr(reference, "id", reference)
+ payload["payment"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
+ if "referenceCode" in self._loaded_fields:
+ payload["reference_code"] = Value.Text(self.referenceCode)
+
+ if "version" in self._loaded_fields:
+ payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
+ action = self._action
+ if action == "Update":
+ ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
+ payload = {field: value for field, value in ledger.items() if field not in ("id", "version")}
+ if action == "Create":
+ cmd = InsertCommand("PaymentAttempt", payload)
+ elif action == "Update":
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("PaymentAttempt", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ for key, value in payload.items():
+ if key not in ("id", "version"): cmd.value(key, value)
+ else:
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("PaymentAttempt", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
+ return action, cmd
+
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "payment" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("payment"), message="Mutation requires a fully loaded entity")])
+ if "referenceCode" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("reference_code"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
+
+ self._teaql_attach_root(self._entity_root)
+ action, cmd = self._teaql_build_command()
+
+
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
+
+ try:
+ context.check_and_fix_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+ context.mark_mutation_checked(cmd)
+ service = context.require_resource("dataService")
+ result = await service.mutate(context, req)
+ persisted = result.persisted_record
+ if persisted is None:
+ raise RuntimeError(
+ "Mutation provider did not return authoritative persisted state for PaymentAttempt"
+ )
+ rollback_payload = {field: getattr(self, field, None) for field in self._loaded_fields | {"id", "version"}}
+ rollback_ledger_id = self._ledger_id
+ rollback_action = self._action
+ rollback_loaded_fields = set(self._loaded_fields)
+ old_key = self._teaql_entity_key()
+ if "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ elif "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ if "payment" in persisted:
+ self.payment = persisted["payment"]
+ self._loaded_fields.add("payment")
+ elif "payment" in persisted:
+ self.payment = persisted["payment"]
+ self._loaded_fields.add("payment")
+ if "reference_code" in persisted:
+ self.referenceCode = persisted["reference_code"]
+ self._loaded_fields.add("referenceCode")
+ elif "referenceCode" in persisted:
+ self.referenceCode = persisted["referenceCode"]
+ self._loaded_fields.add("referenceCode")
+ if "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ elif "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
+ self._ledger_id = getattr(self, "id", self._ledger_id)
+ new_key = self._teaql_entity_key()
+ if old_key != new_key:
+ self._entity_root.rekey(old_key, new_key)
+ def rollback_entity():
+ for field, value in rollback_payload.items():
+ setattr(self, field, value)
+ self._ledger_id = rollback_ledger_id
+ self._action = rollback_action
+ self._loaded_fields = rollback_loaded_fields
+ if old_key != new_key:
+ self._entity_root.rekey(new_key, old_key)
+ graph.after_rollback(rollback_entity)
+ if action != "Delete":
+ self._action = "Update"
+
+ if action != "Delete":
+ await self._teaql_save_children(graph, scope)
+ def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
+ self._entity_root.clear_entity(new_key)
+ if getattr(self, "version", None) is not None:
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
+ return self
+
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
+ def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
+ self.id = value
+ self._loaded_fields.add("id")
+ self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
+ return self
+
+ def update_reference_code(self, value):
+ self.referenceCode = value
+ self._loaded_fields.add("referenceCode")
+ self._entity_root.set(self._teaql_entity_key(), "reference_code", Value.from_any(value))
+ return self
+
+ def update_version(self, value):
+ self.version = value
+ self._loaded_fields.add("version")
+ self._entity_root.set(self._teaql_entity_key(), "version", Value.from_any(value))
+ return self
+ def update_payment(self, value):
+ self.payment = getattr(value, "id", value) if value else None
+ self._loaded_fields.add("payment")
+ self._entity_root.set(self._teaql_entity_key(), "payment", Value.from_any(self.payment))
+ return self
diff --git a/examples/trace-chain/lib/models/platform.py b/examples/trace-chain/lib/models/platform.py
new file mode 100644
index 0000000..b1cf88e
--- /dev/null
+++ b/examples/trace-chain/lib/models/platform.py
@@ -0,0 +1,329 @@
+from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot, _QueryProjectionSnapshot
+from teaql.core import MutationIntent
+from teaql.core.value import Value
+from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
+import itertools
+
+
+class Platform:
+ _teaql_temporary_ids = itertools.count(1)
+ @classmethod
+ def refer(cls, entity_id):
+ return cls(id=entity_id)
+
+ @classmethod
+ def _teaql_new_with_fixed_id(cls, entity_id):
+ """Generated bootstrap capability; application code must not call it."""
+ return cls(id=entity_id)._teaql_force_create()
+
+ def _teaql_force_create(self):
+ self._action = "Create"
+ self._entity_root.mark_as_new(self._teaql_entity_key())
+ return self
+
+ def __init__(self, **kwargs):
+ self._entity_root = kwargs.pop("_entity_root", None) or EntityRoot()
+ if "id" in kwargs and "id" not in kwargs:
+ kwargs["id"] = kwargs.pop("id")
+ if "name" in kwargs and "name" not in kwargs:
+ kwargs["name"] = kwargs.pop("name")
+ if "version" in kwargs and "version" not in kwargs:
+ kwargs["version"] = kwargs.pop("version")
+ self._teaql_query_projections = _QueryProjectionSnapshot(kwargs, (
+ "id", "id",
+ "name", "name",
+ "version", "version",
+ "customer_order_list",
+ ))
+ self._action = "Update" if kwargs.get("id") else "Create"
+ self._comment = None
+ self._loaded_fields = set(kwargs.keys())
+ self.id = kwargs.get("id")
+
+ self.name = kwargs.get("name")
+
+ self.version = kwargs.get("version")
+
+ self._customer_order_list = kwargs.get("customer_order_list", [])
+ if "customer_order_list" in kwargs or kwargs.get("id") is None:
+ self._loaded_fields.add("customer_order_list")
+ if self._customer_order_list:
+ from models.customer_order import CustomerOrder
+ self._customer_order_list = [
+ item if isinstance(item, CustomerOrder) else CustomerOrder(_entity_root=self._entity_root, **item)
+ for item in self._customer_order_list
+ ]
+ self._ledger_id = getattr(self, "id", None)
+ if self._ledger_id is None:
+ self._ledger_id = -next(self._teaql_temporary_ids)
+ key = self._teaql_entity_key()
+ if self._action == "Create":
+ self._entity_root.mark_as_new(key)
+ elif getattr(self, "version", None) is not None:
+ self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
+
+ def query_projection(self, alias):
+ """Read a query-only alias; raises KeyError when it was not returned."""
+ return self._teaql_query_projections.get(alias)
+
+ def has_query_projection(self, alias):
+ return self._teaql_query_projections.contains(alias)
+
+ def _teaql_entity_key(self):
+ return EntityKey("Platform", self._ledger_id)
+
+ def _teaql_attach_root(self, root):
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
+ self._entity_root = root
+ for child in self._customer_order_list:
+ child._teaql_attach_root(root)
+ return self
+ def mark_for_deletion(self):
+ self._action = "Delete"
+ self._entity_root.mark_as_deleted(self._teaql_entity_key())
+ return self
+
+ def audit_as(self, comment: str):
+ MutationIntent(comment)
+ self._comment = comment
+ return self
+
+ async def save(self, context):
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
+
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
+
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("Platform"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+ for child in self._customer_order_list:
+ child._teaql_attach_root(self._entity_root)
+ await child._teaql_reserve_graph_ids(graph, visited)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
+ payload = {}
+ if "id" in self._loaded_fields:
+ payload["id"] = Value.I64(self.id)
+
+ if "name" in self._loaded_fields:
+ payload["name"] = Value.Text(self.name)
+
+ if "version" in self._loaded_fields:
+ payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
+ action = self._action
+ if action == "Update":
+ ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
+ payload = {field: value for field, value in ledger.items() if field not in ("id", "version")}
+ if action == "Create":
+ cmd = InsertCommand("Platform", payload)
+ elif action == "Update":
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("Platform", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ for key, value in payload.items():
+ if key not in ("id", "version"): cmd.value(key, value)
+ else:
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("Platform", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
+ return action, cmd
+
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "name" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("name"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+ for index, child in enumerate(self._customer_order_list):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, "platform", None)
+ if getattr(current, "id", current) != self.id:
+ child.update_platform(self)
+ try:
+ child._teaql_preflight_graph(graph)
+ except CheckException as error:
+ prefix = ObjectLocation().property("customer_order_list").index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
+
+ self._teaql_attach_root(self._entity_root)
+ action, cmd = self._teaql_build_command()
+
+
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
+
+ try:
+ context.check_and_fix_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+ context.mark_mutation_checked(cmd)
+ service = context.require_resource("dataService")
+ result = await service.mutate(context, req)
+ persisted = result.persisted_record
+ if persisted is None:
+ raise RuntimeError(
+ "Mutation provider did not return authoritative persisted state for Platform"
+ )
+ rollback_payload = {field: getattr(self, field, None) for field in self._loaded_fields | {"id", "version"}}
+ rollback_ledger_id = self._ledger_id
+ rollback_action = self._action
+ rollback_loaded_fields = set(self._loaded_fields)
+ old_key = self._teaql_entity_key()
+ if "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ elif "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ if "name" in persisted:
+ self.name = persisted["name"]
+ self._loaded_fields.add("name")
+ elif "name" in persisted:
+ self.name = persisted["name"]
+ self._loaded_fields.add("name")
+ if "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ elif "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
+ self._ledger_id = getattr(self, "id", self._ledger_id)
+ new_key = self._teaql_entity_key()
+ if old_key != new_key:
+ self._entity_root.rekey(old_key, new_key)
+ def rollback_entity():
+ for field, value in rollback_payload.items():
+ setattr(self, field, value)
+ self._ledger_id = rollback_ledger_id
+ self._action = rollback_action
+ self._loaded_fields = rollback_loaded_fields
+ if old_key != new_key:
+ self._entity_root.rekey(new_key, old_key)
+ graph.after_rollback(rollback_entity)
+ if action != "Delete":
+ self._action = "Update"
+
+ if action != "Delete":
+ await self._teaql_save_children(graph, scope)
+ def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
+ self._entity_root.clear_entity(new_key)
+ if getattr(self, "version", None) is not None:
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
+ return self
+
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ cascade_relations.append(("customer_order_list", self._customer_order_list, "update_platform", "platform"))
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
+ def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
+ self.id = value
+ self._loaded_fields.add("id")
+ self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
+ return self
+
+ def update_name(self, value):
+ self.name = value
+ self._loaded_fields.add("name")
+ self._entity_root.set(self._teaql_entity_key(), "name", Value.from_any(value))
+ return self
+
+ def update_version(self, value):
+ self.version = value
+ self._loaded_fields.add("version")
+ self._entity_root.set(self._teaql_entity_key(), "version", Value.from_any(value))
+ return self
+ def customer_order_list(self) -> list:
+ self._loaded_fields.add("customer_order_list")
+ return self._customer_order_list
diff --git a/examples/trace-chain/lib/models/shipment.py b/examples/trace-chain/lib/models/shipment.py
new file mode 100644
index 0000000..406d1c6
--- /dev/null
+++ b/examples/trace-chain/lib/models/shipment.py
@@ -0,0 +1,325 @@
+from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand, MutationRequest
+from teaql.core.entity import _LoadedScalarSnapshot, _QueryProjectionSnapshot
+from teaql.core import MutationIntent
+from teaql.core.value import Value
+from teaql.runtime import CheckException, CheckResult, EntityKey, EntityRoot, ObjectLocation
+import itertools
+from models.customer_order import CustomerOrder
+
+
+class Shipment:
+ _teaql_temporary_ids = itertools.count(1)
+ @classmethod
+ def refer(cls, entity_id):
+ return cls(id=entity_id)
+
+ @classmethod
+ def _teaql_new_with_fixed_id(cls, entity_id):
+ """Generated bootstrap capability; application code must not call it."""
+ return cls(id=entity_id)._teaql_force_create()
+
+ def _teaql_force_create(self):
+ self._action = "Create"
+ self._entity_root.mark_as_new(self._teaql_entity_key())
+ return self
+
+ def __init__(self, **kwargs):
+ self._entity_root = kwargs.pop("_entity_root", None) or EntityRoot()
+ if "id" in kwargs and "id" not in kwargs:
+ kwargs["id"] = kwargs.pop("id")
+ if "customer_order" in kwargs and "customerOrder" not in kwargs:
+ kwargs["customerOrder"] = kwargs.pop("customer_order")
+ if "reference_code" in kwargs and "referenceCode" not in kwargs:
+ kwargs["referenceCode"] = kwargs.pop("reference_code")
+ if "version" in kwargs and "version" not in kwargs:
+ kwargs["version"] = kwargs.pop("version")
+ self._teaql_query_projections = _QueryProjectionSnapshot(kwargs, (
+ "id", "id",
+ "customerOrder", "customer_order",
+ "referenceCode", "reference_code",
+ "version", "version",
+ ))
+ self._action = "Update" if kwargs.get("id") else "Create"
+ self._comment = None
+ self._loaded_fields = set(kwargs.keys())
+ self.id = kwargs.get("id")
+
+ self.customerOrder = kwargs.get("customerOrder")
+
+ self.referenceCode = kwargs.get("referenceCode")
+
+ self.version = kwargs.get("version")
+
+ if isinstance(self.customerOrder, dict):
+ self.customerOrder = CustomerOrder(**self.customerOrder)
+ self._ledger_id = getattr(self, "id", None)
+ if self._ledger_id is None:
+ self._ledger_id = -next(self._teaql_temporary_ids)
+ key = self._teaql_entity_key()
+ if self._action == "Create":
+ self._entity_root.mark_as_new(key)
+ elif getattr(self, "version", None) is not None:
+ self._entity_root.set_original_version(key, int(self.version))
+ self._teaql_loaded_snapshot = _LoadedScalarSnapshot(
+ self._teaql_scalar_payload() if self._action == "Update" else {})
+
+ def query_projection(self, alias):
+ """Read a query-only alias; raises KeyError when it was not returned."""
+ return self._teaql_query_projections.get(alias)
+
+ def has_query_projection(self, alias):
+ return self._teaql_query_projections.contains(alias)
+
+ def _teaql_entity_key(self):
+ return EntityKey("Shipment", self._ledger_id)
+
+ def _teaql_attach_root(self, root):
+ key = self._teaql_entity_key()
+ if self._entity_root is not root and self._entity_root.has_pending(key):
+ root.merge_entity_from(self._entity_root, key)
+ self._entity_root = root
+ return self
+ def mark_for_deletion(self):
+ self._action = "Delete"
+ self._entity_root.mark_as_deleted(self._teaql_entity_key())
+ return self
+
+ def audit_as(self, comment: str):
+ MutationIntent(comment)
+ self._comment = comment
+ return self
+
+ async def save(self, context):
+ intent = MutationIntent(self._comment)
+ return await context.execute_graph_save(self._teaql_preflight_and_save, comment=intent.comment)
+
+ async def _teaql_ensure_business_ids(self, context):
+ if self._action != "Create":
+ return
+
+ async def _teaql_reserve_graph_ids(self, graph, visited):
+ context = graph.context
+ object_identity = id(self)
+ if object_identity in visited:
+ return
+ visited.add(object_identity)
+ if self._action == "Create" and getattr(self, "id", None) is None:
+ service = context.require_resource("dataService")
+ allocator = getattr(service, "next_id", None)
+ if not callable(allocator):
+ raise RuntimeError(
+ "Configured dataService does not support stable ID reservation"
+ )
+ old_key = self._teaql_entity_key()
+ old_loaded = set(self._loaded_fields)
+ self.id = int(await allocator("Shipment"))
+ self._loaded_fields.add("id")
+ self._ledger_id = self.id
+ new_key = self._teaql_entity_key()
+ self._entity_root.rekey(old_key, new_key)
+ self._entity_root.set(new_key, "id", Value.I64(self.id))
+ def rollback_reservation():
+ self.id = None
+ self._ledger_id = old_key.id
+ self._loaded_fields = old_loaded
+ self._entity_root.rekey(new_key, old_key)
+ self._entity_root.set(old_key, "id", Value.from_any(None))
+ graph.after_rollback(rollback_reservation)
+ await self._teaql_ensure_business_ids(context)
+
+ async def _teaql_preflight_and_save(self, graph):
+ await self._teaql_reserve_graph_ids(graph, set())
+ self._teaql_preflight_graph(graph)
+ return await self._teaql_save_within_graph(graph)
+
+ def _teaql_scalar_payload(self):
+ payload = {}
+ if "id" in self._loaded_fields:
+ payload["id"] = Value.I64(self.id)
+
+ if "customerOrder" in self._loaded_fields:
+ reference = self.customerOrder
+ reference_id = getattr(reference, "id", reference)
+ payload["customer_order"] = (Value.Object(reference)
+ if reference is not None and hasattr(reference, "id") and reference_id is None
+ else Value.I64(reference_id))
+
+ if "referenceCode" in self._loaded_fields:
+ payload["reference_code"] = Value.Text(self.referenceCode)
+
+ if "version" in self._loaded_fields:
+ payload["version"] = Value.I64(self.version)
+
+ return payload
+
+ def _teaql_build_command(self):
+ payload = self._teaql_scalar_payload()
+ action = self._action
+ if action == "Update":
+ ledger = dict(self._entity_root.current_change_set().changes()).get(self._teaql_entity_key(), {})
+ payload = {field: value for field, value in ledger.items() if field not in ("id", "version")}
+ if action == "Create":
+ cmd = InsertCommand("Shipment", payload)
+ elif action == "Update":
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = UpdateCommand("Shipment", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ for key, value in payload.items():
+ if key not in ("id", "version"): cmd.value(key, value)
+ else:
+ original_version = self._entity_root.original_version(self._teaql_entity_key())
+ cmd = DeleteCommand("Shipment", Value.from_any(getattr(self, "id", None)),
+ original_version if original_version is not None else getattr(self, "version", None))
+ if action != "Create":
+ cmd.old_values = self._teaql_loaded_snapshot.business_values()
+ return action, cmd
+
+ def _teaql_preflight_graph(self, graph):
+ context = graph.context
+ if self._action != "Update" or self._entity_root.has_pending(self._teaql_entity_key()):
+ if self._action == "Update":
+ if "id" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("id"), message="Mutation requires a fully loaded entity")])
+ if "customerOrder" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("customer_order"), message="Mutation requires a fully loaded entity")])
+ if "referenceCode" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("reference_code"), message="Mutation requires a fully loaded entity")])
+ if "version" not in self._loaded_fields:
+ raise CheckException([CheckResult("invalid_type", ObjectLocation().property("version"), message="Mutation requires a fully loaded entity")])
+ _action, cmd = self._teaql_build_command()
+ try:
+ context.preflight_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+
+ async def _teaql_save_within_graph(self, graph, parent_scope=None):
+ context = graph.context
+ scope = graph.scope(self._teaql_entity_key(), parent_scope, self._comment)
+
+ if self._action == "Update" and not self._entity_root.has_pending(self._teaql_entity_key()):
+ await self._teaql_save_children(graph, scope)
+ return self
+
+ self._teaql_attach_root(self._entity_root)
+ action, cmd = self._teaql_build_command()
+
+
+ req = graph.request(cmd, scope, self._entity_root, self._teaql_entity_key())
+
+ try:
+ context.check_and_fix_mutation(cmd)
+ finally:
+ for field, value in getattr(cmd, "values", {}).items():
+ if field not in ("id", "version"):
+ self._entity_root.set(self._teaql_entity_key(), field, value)
+ context.mark_mutation_checked(cmd)
+ service = context.require_resource("dataService")
+ result = await service.mutate(context, req)
+ persisted = result.persisted_record
+ if persisted is None:
+ raise RuntimeError(
+ "Mutation provider did not return authoritative persisted state for Shipment"
+ )
+ rollback_payload = {field: getattr(self, field, None) for field in self._loaded_fields | {"id", "version"}}
+ rollback_ledger_id = self._ledger_id
+ rollback_action = self._action
+ rollback_loaded_fields = set(self._loaded_fields)
+ old_key = self._teaql_entity_key()
+ if "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ elif "id" in persisted:
+ self.id = persisted["id"]
+ self._loaded_fields.add("id")
+ if "customer_order" in persisted:
+ self.customerOrder = persisted["customer_order"]
+ self._loaded_fields.add("customerOrder")
+ elif "customerOrder" in persisted:
+ self.customerOrder = persisted["customerOrder"]
+ self._loaded_fields.add("customerOrder")
+ if "reference_code" in persisted:
+ self.referenceCode = persisted["reference_code"]
+ self._loaded_fields.add("referenceCode")
+ elif "referenceCode" in persisted:
+ self.referenceCode = persisted["referenceCode"]
+ self._loaded_fields.add("referenceCode")
+ if "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ elif "version" in persisted:
+ self.version = persisted["version"]
+ self._loaded_fields.add("version")
+ committed_snapshot = _LoadedScalarSnapshot(self._teaql_scalar_payload())
+ self._ledger_id = getattr(self, "id", self._ledger_id)
+ new_key = self._teaql_entity_key()
+ if old_key != new_key:
+ self._entity_root.rekey(old_key, new_key)
+ def rollback_entity():
+ for field, value in rollback_payload.items():
+ setattr(self, field, value)
+ self._ledger_id = rollback_ledger_id
+ self._action = rollback_action
+ self._loaded_fields = rollback_loaded_fields
+ if old_key != new_key:
+ self._entity_root.rekey(new_key, old_key)
+ graph.after_rollback(rollback_entity)
+ if action != "Delete":
+ self._action = "Update"
+
+ if action != "Delete":
+ await self._teaql_save_children(graph, scope)
+ def commit_entity():
+ self._teaql_loaded_snapshot = committed_snapshot
+ self._entity_root.clear_entity(new_key)
+ if getattr(self, "version", None) is not None:
+ self._entity_root.accept_committed_version(new_key, int(self.version))
+ graph.after_commit(commit_entity)
+ return self
+
+ async def _teaql_save_children(self, graph, scope):
+ cascade_relations = []
+ for relation_name, children, updater, member in cascade_relations:
+ for index, child in enumerate(children):
+ child._teaql_attach_root(self._entity_root)
+ current = getattr(child, member, None)
+ if getattr(current, "id", current) != self.id:
+ getattr(child, updater)(self)
+ try:
+ await child._teaql_save_within_graph(graph, scope)
+ except CheckException as error:
+ prefix = ObjectLocation().property(relation_name).index(index)
+ raise CheckException([
+ CheckResult(v.rule_id, v.location.prefixed_by(prefix), v.input_value, v.system_value, v.message)
+ for v in error.violations
+ ]) from error
+
+ def update_id(self, value):
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
+ raise ValueError("id must be a positive integer")
+ old_key = self._teaql_entity_key()
+ new_key = EntityKey(old_key.entity, value)
+ self._entity_root.rekey(old_key, new_key)
+ self._ledger_id = value
+ self.id = value
+ self._loaded_fields.add("id")
+ self._entity_root.set(self._teaql_entity_key(), "id", Value.from_any(value))
+ return self
+
+ def update_reference_code(self, value):
+ self.referenceCode = value
+ self._loaded_fields.add("referenceCode")
+ self._entity_root.set(self._teaql_entity_key(), "reference_code", Value.from_any(value))
+ return self
+
+ def update_version(self, value):
+ self.version = value
+ self._loaded_fields.add("version")
+ self._entity_root.set(self._teaql_entity_key(), "version", Value.from_any(value))
+ return self
+ def update_customer_order(self, value):
+ self.customerOrder = getattr(value, "id", value) if value else None
+ self._loaded_fields.add("customerOrder")
+ self._entity_root.set(self._teaql_entity_key(), "customer_order", Value.from_any(self.customerOrder))
+ return self
diff --git a/examples/trace-chain/lib/pyproject.toml b/examples/trace-chain/lib/pyproject.toml
new file mode 100644
index 0000000..9539c37
--- /dev/null
+++ b/examples/trace-chain/lib/pyproject.toml
@@ -0,0 +1,16 @@
+[project]
+name = "trace-chain-service-lib"
+version = "1.0.0"
+description = "Generated python library"
+dependencies = ["teaql==0.2.9", "aiosqlite>=0.22.1"]
+
+[tool.setuptools]
+py-modules = ["Q", "E"]
+
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["models*", "requests*"]
+
+[build-system]
+requires = ["setuptools>=42"]
+build-backend = "setuptools.build_meta"
\ No newline at end of file
diff --git a/examples/trace-chain/lib/requests/__init__.py b/examples/trace-chain/lib/requests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/trace-chain/lib/requests/customer_order_request.py b/examples/trace-chain/lib/requests/customer_order_request.py
new file mode 100644
index 0000000..98aa857
--- /dev/null
+++ b/examples/trace-chain/lib/requests/customer_order_request.py
@@ -0,0 +1,636 @@
+from teaql.core.query import RelationAggregate, SelectQuery
+from teaql.core.list import SmartList, TeaQLPage
+from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
+from teaql.core.expr import (
+ begin_with, between, column, contain, end_with, eq, gt, gte,
+ in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
+ not_contain, not_end_with, not_in_list, not_in_subquery, value,
+ sound_like,
+)
+from models.customer_order import CustomerOrder
+from typing import Protocol
+
+class QuerySelection(Protocol):
+ query: SelectQuery
+
+class CustomerOrderRequest:
+ def __init__(self, minimal=False):
+ self.query = SelectQuery("CustomerOrder")
+ self._purpose = None
+ self._comment = None
+ self.query.and_filter(gte("version", 1))
+ if minimal:
+ self.select_id()
+ self.select_version()
+ else:
+ self.select_self_fields()
+
+ def comment(self, c: str):
+ self.query.comment(c)
+ self._comment = c
+ return self
+
+ def purpose(self, p: str):
+ self.query.purpose(p)
+ self._purpose = p
+ return ExecutableCustomerOrderRequest(self)
+
+ def optimize_for_continuous_page_fetch(self):
+ self.query.optimize_for_continuous_page_fetch()
+ return self
+
+ def optimize_for_continuous_page_fetch_with(self, namespace: str, ttl_seconds: int):
+ self.query.optimize_for_continuous_page_fetch_with(namespace, ttl_seconds)
+ return self
+
+ def optimize_pagination_with_id_set(self):
+ self.query.optimize_pagination_with_id_set()
+ return self
+
+ def optimize_pagination_with_id_set_config(self, namespace: str, ttl_seconds: int, max_ids: int):
+ self.query.optimize_pagination_with_id_set_config(namespace, ttl_seconds, max_ids)
+ return self
+
+ def top_n_probe_parent_threshold(self, threshold: int):
+ self.query.top_n_probe_parent_threshold(threshold)
+ return self
+
+ def limit(self, n: int):
+ self.query.limit(n)
+ return self
+
+ def offset(self, n: int):
+ self.query.offset(n)
+ return self
+
+ def with_deleted_rows(self):
+ self.query.with_deleted_rows()
+ return self
+
+ def deleted_rows_only(self):
+ self.query.deleted_rows_only()
+ return self
+
+ def select_self_fields(self):
+ self.query.project("id", "platform", "order_number", "description", "version")
+ return self
+
+ def select_id(self):
+ self.query.project("id")
+ return self
+
+
+ def select_order_number(self):
+ self.query.project("order_number")
+ return self
+
+ def select_description(self):
+ self.query.project("description")
+ return self
+
+ def select_version(self):
+ self.query.project("version")
+ return self
+
+ def select_platform_with(self, child_request):
+ self.query.project("platform")
+ self.query.relation_query("platform", child_request.query)
+ return self
+ def with_platform_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(in_subquery(column("platform"), "Platform", child_request.query))
+ return self
+
+ def without_platform_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(not_in_subquery(column("platform"), "Platform", child_request.query))
+ return self
+
+ def have_platform(self):
+ self.query.and_filter(is_not_null(column("platform")))
+ return self
+
+ def have_no_platform(self):
+ self.query.and_filter(is_null(column("platform")))
+ return self
+
+ def with_id_is(self, val):
+ self.query.and_filter(eq("id", val))
+ return self
+
+ def with_id_is_not(self, val):
+ self.query.and_filter(ne("id", val))
+ return self
+
+ def with_id_in(self, *vals):
+ self.query.and_filter(in_list("id", list(vals)))
+ return self
+
+ def with_id_not_in(self, *vals):
+ self.query.and_filter(not_in_list("id", list(vals)))
+ return self
+
+ def with_id_greater_than(self, val):
+ self.query.and_filter(gt("id", val))
+ return self
+
+ def with_id_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("id", val))
+ return self
+
+ def with_id_less_than(self, val):
+ self.query.and_filter(lt("id", val))
+ return self
+
+ def with_id_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("id", val))
+ return self
+
+ def with_id_between(self, lower, upper):
+ self.query.and_filter(between(column("id"), value(lower), value(upper)))
+ return self
+
+ def with_id_is_known(self):
+ self.query.and_filter(is_not_null(column("id")))
+ return self
+
+ def with_id_is_unknown(self):
+ self.query.and_filter(is_null(column("id")))
+ return self
+
+ def filter_by_platform(self, val):
+ self.query.and_filter(eq("platform", val))
+ return self
+
+ def with_order_number_containing(self, val: str):
+ self.query.and_filter(contain("order_number", val))
+ return self
+
+ def with_order_number_not_containing(self, val: str):
+ self.query.and_filter(not_contain("order_number", val))
+ return self
+
+ def with_order_number_starting_with(self, val: str):
+ self.query.and_filter(begin_with("order_number", val))
+ return self
+
+ def with_order_number_not_starting_with(self, val: str):
+ self.query.and_filter(not_begin_with("order_number", val))
+ return self
+
+ def with_order_number_ending_with(self, val: str):
+ self.query.and_filter(end_with("order_number", val))
+ return self
+
+ def with_order_number_not_ending_with(self, val: str):
+ self.query.and_filter(not_end_with("order_number", val))
+ return self
+
+ def with_order_number_sounding_like(self, val: str):
+ self.query.and_filter(sound_like("order_number", val))
+ return self
+
+ def with_order_number_is(self, val: str):
+ self.query.and_filter(eq("order_number", val))
+ return self
+ def with_order_number_is_not(self, val):
+ self.query.and_filter(ne("order_number", val))
+ return self
+
+ def with_order_number_in(self, *vals):
+ self.query.and_filter(in_list("order_number", list(vals)))
+ return self
+
+ def with_order_number_not_in(self, *vals):
+ self.query.and_filter(not_in_list("order_number", list(vals)))
+ return self
+
+ def with_order_number_greater_than(self, val):
+ self.query.and_filter(gt("order_number", val))
+ return self
+
+ def with_order_number_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("order_number", val))
+ return self
+
+ def with_order_number_less_than(self, val):
+ self.query.and_filter(lt("order_number", val))
+ return self
+
+ def with_order_number_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("order_number", val))
+ return self
+
+ def with_order_number_between(self, lower, upper):
+ self.query.and_filter(between(column("order_number"), value(lower), value(upper)))
+ return self
+
+ def with_order_number_is_known(self):
+ self.query.and_filter(is_not_null(column("order_number")))
+ return self
+
+ def with_order_number_is_unknown(self):
+ self.query.and_filter(is_null(column("order_number")))
+ return self
+
+ def with_description_containing(self, val: str):
+ self.query.and_filter(contain("description", val))
+ return self
+
+ def with_description_not_containing(self, val: str):
+ self.query.and_filter(not_contain("description", val))
+ return self
+
+ def with_description_starting_with(self, val: str):
+ self.query.and_filter(begin_with("description", val))
+ return self
+
+ def with_description_not_starting_with(self, val: str):
+ self.query.and_filter(not_begin_with("description", val))
+ return self
+
+ def with_description_ending_with(self, val: str):
+ self.query.and_filter(end_with("description", val))
+ return self
+
+ def with_description_not_ending_with(self, val: str):
+ self.query.and_filter(not_end_with("description", val))
+ return self
+
+ def with_description_sounding_like(self, val: str):
+ self.query.and_filter(sound_like("description", val))
+ return self
+
+ def with_description_is(self, val: str):
+ self.query.and_filter(eq("description", val))
+ return self
+ def with_description_is_not(self, val):
+ self.query.and_filter(ne("description", val))
+ return self
+
+ def with_description_in(self, *vals):
+ self.query.and_filter(in_list("description", list(vals)))
+ return self
+
+ def with_description_not_in(self, *vals):
+ self.query.and_filter(not_in_list("description", list(vals)))
+ return self
+
+ def with_description_greater_than(self, val):
+ self.query.and_filter(gt("description", val))
+ return self
+
+ def with_description_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("description", val))
+ return self
+
+ def with_description_less_than(self, val):
+ self.query.and_filter(lt("description", val))
+ return self
+
+ def with_description_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("description", val))
+ return self
+
+ def with_description_between(self, lower, upper):
+ self.query.and_filter(between(column("description"), value(lower), value(upper)))
+ return self
+
+ def with_description_is_known(self):
+ self.query.and_filter(is_not_null(column("description")))
+ return self
+
+ def with_description_is_unknown(self):
+ self.query.and_filter(is_null(column("description")))
+ return self
+
+ def with_version_is(self, val):
+ self.query.and_filter(eq("version", val))
+ return self
+
+ def with_version_is_not(self, val):
+ self.query.and_filter(ne("version", val))
+ return self
+
+ def with_version_in(self, *vals):
+ self.query.and_filter(in_list("version", list(vals)))
+ return self
+
+ def with_version_not_in(self, *vals):
+ self.query.and_filter(not_in_list("version", list(vals)))
+ return self
+
+ def with_version_greater_than(self, val):
+ self.query.and_filter(gt("version", val))
+ return self
+
+ def with_version_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("version", val))
+ return self
+
+ def with_version_less_than(self, val):
+ self.query.and_filter(lt("version", val))
+ return self
+
+ def with_version_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("version", val))
+ return self
+
+ def with_version_between(self, lower, upper):
+ self.query.and_filter(between(column("version"), value(lower), value(upper)))
+ return self
+
+ def with_version_is_known(self):
+ self.query.and_filter(is_not_null(column("version")))
+ return self
+
+ def with_version_is_unknown(self):
+ self.query.and_filter(is_null(column("version")))
+ return self
+
+ def order_by_id_ascending(self):
+ self.query.order_by("id", "asc")
+ return self
+
+ def order_by_id_descending(self):
+ self.query.order_by("id", "desc")
+ return self
+
+
+ def order_by_order_number_ascending(self):
+ self.query.order_by("order_number", "asc")
+ return self
+
+ def order_by_order_number_descending(self):
+ self.query.order_by("order_number", "desc")
+ return self
+
+ def order_by_description_ascending(self):
+ self.query.order_by("description", "asc")
+ return self
+
+ def order_by_description_descending(self):
+ self.query.order_by("description", "desc")
+ return self
+
+ def order_by_version_ascending(self):
+ self.query.order_by("version", "asc")
+ return self
+
+ def order_by_version_descending(self):
+ self.query.order_by("version", "desc")
+ return self
+
+
+ def count(self):
+ self.query.count_field("id", "count")
+ return self
+
+ def count_as(self, ret_name: str):
+ self.query.count_field("id", ret_name)
+ return self
+
+ def group_by_id(self):
+ self.query.group_by("id")
+ return self
+
+ def group_by_id_as(self, ret_name: str):
+ self.query.group_by("id")
+ return self
+ def group_by_platform(self):
+ self.query.group_by("platform")
+ return self
+
+ def group_by_platform_as(self, ret_name: str):
+ self.query.group_by("platform")
+ return self
+ def group_by_order_number(self):
+ self.query.group_by("order_number")
+ return self
+
+ def group_by_order_number_as(self, ret_name: str):
+ self.query.group_by("order_number")
+ return self
+ def group_by_description(self):
+ self.query.group_by("description")
+ return self
+
+ def group_by_description_as(self, ret_name: str):
+ self.query.group_by("description")
+ return self
+ def group_by_version(self):
+ self.query.group_by("version")
+ return self
+
+ def group_by_version_as(self, ret_name: str):
+ self.query.group_by("version")
+ return self
+ def select_order_item_list(self):
+ from requests.order_item_request import OrderItemRequest
+ return self.select_order_item_list_with(OrderItemRequest())
+
+ def select_order_item_list_with(self, child_request):
+ self.query.relation_query("order_item_list", child_request.query)
+ return self
+ def select_payment_list(self):
+ from requests.payment_request import PaymentRequest
+ return self.select_payment_list_with(PaymentRequest())
+
+ def select_payment_list_with(self, child_request):
+ self.query.relation_query("payment_list", child_request.query)
+ return self
+ def select_shipment_list(self):
+ from requests.shipment_request import ShipmentRequest
+ return self.select_shipment_list_with(ShipmentRequest())
+
+ def select_shipment_list_with(self, child_request):
+ self.query.relation_query("shipment_list", child_request.query)
+ return self
+ def have_order_items(self):
+ from requests.order_item_request import OrderItemRequest
+ return self.with_order_item_list_matching(OrderItemRequest())
+
+ def have_no_order_items(self):
+ from requests.order_item_request import OrderItemRequest
+ return self.without_order_item_list_matching(OrderItemRequest())
+
+ def with_order_item_list_matching(self, child_request):
+ child_request.query.projection = ["customer_order"]
+ self.query.and_filter(in_subquery(column("id"), "OrderItem", child_request.query))
+ return self
+
+ def without_order_item_list_matching(self, child_request):
+ child_request.query.projection = ["customer_order"]
+ self.query.and_filter(not_in_subquery(column("id"), "OrderItem", child_request.query))
+ return self
+ def have_payments(self):
+ from requests.payment_request import PaymentRequest
+ return self.with_payment_list_matching(PaymentRequest())
+
+ def have_no_payments(self):
+ from requests.payment_request import PaymentRequest
+ return self.without_payment_list_matching(PaymentRequest())
+
+ def with_payment_list_matching(self, child_request):
+ child_request.query.projection = ["customer_order"]
+ self.query.and_filter(in_subquery(column("id"), "Payment", child_request.query))
+ return self
+
+ def without_payment_list_matching(self, child_request):
+ child_request.query.projection = ["customer_order"]
+ self.query.and_filter(not_in_subquery(column("id"), "Payment", child_request.query))
+ return self
+ def have_shipments(self):
+ from requests.shipment_request import ShipmentRequest
+ return self.with_shipment_list_matching(ShipmentRequest())
+
+ def have_no_shipments(self):
+ from requests.shipment_request import ShipmentRequest
+ return self.without_shipment_list_matching(ShipmentRequest())
+
+ def with_shipment_list_matching(self, child_request):
+ child_request.query.projection = ["customer_order"]
+ self.query.and_filter(in_subquery(column("id"), "Shipment", child_request.query))
+ return self
+
+ def without_shipment_list_matching(self, child_request):
+ child_request.query.projection = ["customer_order"]
+ self.query.and_filter(not_in_subquery(column("id"), "Shipment", child_request.query))
+ return self
+ def count_order_items(self):
+ return self.count_order_items_as("count_order_items")
+
+ def count_order_items_as(self, alias: str):
+ from requests.order_item_request import OrderItemRequest
+ return self.count_order_items_with(alias, OrderItemRequest())
+
+ def count_order_items_with(self, alias: str, child_request):
+ child_request.query.count_field("id", alias)
+ self.query.relation_aggregates.append(
+ RelationAggregate("order_item_list", alias, child_request.query, True)
+ )
+ return self
+
+
+ def count_payments(self):
+ return self.count_payments_as("count_payments")
+
+ def count_payments_as(self, alias: str):
+ from requests.payment_request import PaymentRequest
+ return self.count_payments_with(alias, PaymentRequest())
+
+ def count_payments_with(self, alias: str, child_request):
+ child_request.query.count_field("id", alias)
+ self.query.relation_aggregates.append(
+ RelationAggregate("payment_list", alias, child_request.query, True)
+ )
+ return self
+
+
+ def count_shipments(self):
+ return self.count_shipments_as("count_shipments")
+
+ def count_shipments_as(self, alias: str):
+ from requests.shipment_request import ShipmentRequest
+ return self.count_shipments_with(alias, ShipmentRequest())
+
+ def count_shipments_with(self, alias: str, child_request):
+ child_request.query.count_field("id", alias)
+ self.query.relation_aggregates.append(
+ RelationAggregate("shipment_list", alias, child_request.query, True)
+ )
+ return self
+
+
+ def facet_by_platform_as(self, name: str, request: QuerySelection,
+ include_all_facets: bool = True):
+ self.query.facet_by(name, "platform", request.query, include_all_facets)
+ return self
+
+
+class ExecutableCustomerOrderRequest:
+ def __init__(self, request):
+ self._request = request
+
+ def comment(self, c: str):
+ self._request.comment(c)
+ return self
+
+ def new_entity(self, context) -> CustomerOrder:
+ request = self._request
+ QueryIntent(request._comment, request._purpose)
+ entity = context.initialize_entity("CustomerOrder", CustomerOrder())
+ if not isinstance(entity, CustomerOrder):
+ raise TypeError("entity initializer returned an incompatible CustomerOrder")
+ return entity
+
+ async def execute_for_result(self, context):
+ self = self._request
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ return await service.query(context, req)
+
+ async def execute_for_rows(self, context):
+ return (await self.execute_for_result(context)).rows
+
+ async def execute_for_list(self, context) -> SmartList[CustomerOrder]:
+ result = await self.execute_for_result(context)
+ return SmartList(
+ (CustomerOrder(**row) for row in result.rows),
+ facets=result.facets)
+
+ async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[CustomerOrder]:
+ request = self._request
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
+ service = context.require_resource("dataService")
+ alias = "__teaql_total"
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
+ retained_count, accuracy = context.id_set_count()
+ if accuracy == "EXACT":
+ total_count = retained_count
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ row_result = await service.query(context, authorized)
+ data = SmartList(CustomerOrder(**row) for row in row_result.rows)
+ return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
+
+ async def execute_for_one(self, context):
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableCustomerOrderRequest(request).execute_for_list(context)
+ return entities[0] if entities else None
+
+ def execute_for_stream(self, context, chunk_size: int = 1000):
+ """Yield entity chunks lazily from the provider cursor."""
+ request = self._request
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ if not hasattr(service, "query_stream"):
+ raise RuntimeError("dataService does not implement query_stream")
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield CustomerOrder(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/trace-chain/lib/requests/order_item_request.py b/examples/trace-chain/lib/requests/order_item_request.py
new file mode 100644
index 0000000..a1130e4
--- /dev/null
+++ b/examples/trace-chain/lib/requests/order_item_request.py
@@ -0,0 +1,429 @@
+from teaql.core.query import RelationAggregate, SelectQuery
+from teaql.core.list import SmartList, TeaQLPage
+from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
+from teaql.core.expr import (
+ begin_with, between, column, contain, end_with, eq, gt, gte,
+ in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
+ not_contain, not_end_with, not_in_list, not_in_subquery, value,
+ sound_like,
+)
+from models.order_item import OrderItem
+from typing import Protocol
+
+class QuerySelection(Protocol):
+ query: SelectQuery
+
+class OrderItemRequest:
+ def __init__(self, minimal=False):
+ self.query = SelectQuery("OrderItem")
+ self._purpose = None
+ self._comment = None
+ self.query.and_filter(gte("version", 1))
+ if minimal:
+ self.select_id()
+ self.select_version()
+ else:
+ self.select_self_fields()
+
+ def comment(self, c: str):
+ self.query.comment(c)
+ self._comment = c
+ return self
+
+ def purpose(self, p: str):
+ self.query.purpose(p)
+ self._purpose = p
+ return ExecutableOrderItemRequest(self)
+
+ def optimize_for_continuous_page_fetch(self):
+ self.query.optimize_for_continuous_page_fetch()
+ return self
+
+ def optimize_for_continuous_page_fetch_with(self, namespace: str, ttl_seconds: int):
+ self.query.optimize_for_continuous_page_fetch_with(namespace, ttl_seconds)
+ return self
+
+ def optimize_pagination_with_id_set(self):
+ self.query.optimize_pagination_with_id_set()
+ return self
+
+ def optimize_pagination_with_id_set_config(self, namespace: str, ttl_seconds: int, max_ids: int):
+ self.query.optimize_pagination_with_id_set_config(namespace, ttl_seconds, max_ids)
+ return self
+
+ def top_n_probe_parent_threshold(self, threshold: int):
+ self.query.top_n_probe_parent_threshold(threshold)
+ return self
+
+ def limit(self, n: int):
+ self.query.limit(n)
+ return self
+
+ def offset(self, n: int):
+ self.query.offset(n)
+ return self
+
+ def with_deleted_rows(self):
+ self.query.with_deleted_rows()
+ return self
+
+ def deleted_rows_only(self):
+ self.query.deleted_rows_only()
+ return self
+
+ def select_self_fields(self):
+ self.query.project("id", "customer_order", "name", "version")
+ return self
+
+ def select_id(self):
+ self.query.project("id")
+ return self
+
+
+ def select_name(self):
+ self.query.project("name")
+ return self
+
+ def select_version(self):
+ self.query.project("version")
+ return self
+
+ def select_customer_order_with(self, child_request):
+ self.query.project("customer_order")
+ self.query.relation_query("customer_order", child_request.query)
+ return self
+ def with_customer_order_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(in_subquery(column("customer_order"), "CustomerOrder", child_request.query))
+ return self
+
+ def without_customer_order_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(not_in_subquery(column("customer_order"), "CustomerOrder", child_request.query))
+ return self
+
+ def have_customer_order(self):
+ self.query.and_filter(is_not_null(column("customer_order")))
+ return self
+
+ def have_no_customer_order(self):
+ self.query.and_filter(is_null(column("customer_order")))
+ return self
+
+ def with_id_is(self, val):
+ self.query.and_filter(eq("id", val))
+ return self
+
+ def with_id_is_not(self, val):
+ self.query.and_filter(ne("id", val))
+ return self
+
+ def with_id_in(self, *vals):
+ self.query.and_filter(in_list("id", list(vals)))
+ return self
+
+ def with_id_not_in(self, *vals):
+ self.query.and_filter(not_in_list("id", list(vals)))
+ return self
+
+ def with_id_greater_than(self, val):
+ self.query.and_filter(gt("id", val))
+ return self
+
+ def with_id_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("id", val))
+ return self
+
+ def with_id_less_than(self, val):
+ self.query.and_filter(lt("id", val))
+ return self
+
+ def with_id_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("id", val))
+ return self
+
+ def with_id_between(self, lower, upper):
+ self.query.and_filter(between(column("id"), value(lower), value(upper)))
+ return self
+
+ def with_id_is_known(self):
+ self.query.and_filter(is_not_null(column("id")))
+ return self
+
+ def with_id_is_unknown(self):
+ self.query.and_filter(is_null(column("id")))
+ return self
+
+ def filter_by_customer_order(self, val):
+ self.query.and_filter(eq("customer_order", val))
+ return self
+
+ def with_name_containing(self, val: str):
+ self.query.and_filter(contain("name", val))
+ return self
+
+ def with_name_not_containing(self, val: str):
+ self.query.and_filter(not_contain("name", val))
+ return self
+
+ def with_name_starting_with(self, val: str):
+ self.query.and_filter(begin_with("name", val))
+ return self
+
+ def with_name_not_starting_with(self, val: str):
+ self.query.and_filter(not_begin_with("name", val))
+ return self
+
+ def with_name_ending_with(self, val: str):
+ self.query.and_filter(end_with("name", val))
+ return self
+
+ def with_name_not_ending_with(self, val: str):
+ self.query.and_filter(not_end_with("name", val))
+ return self
+
+ def with_name_sounding_like(self, val: str):
+ self.query.and_filter(sound_like("name", val))
+ return self
+
+ def with_name_is(self, val: str):
+ self.query.and_filter(eq("name", val))
+ return self
+ def with_name_is_not(self, val):
+ self.query.and_filter(ne("name", val))
+ return self
+
+ def with_name_in(self, *vals):
+ self.query.and_filter(in_list("name", list(vals)))
+ return self
+
+ def with_name_not_in(self, *vals):
+ self.query.and_filter(not_in_list("name", list(vals)))
+ return self
+
+ def with_name_greater_than(self, val):
+ self.query.and_filter(gt("name", val))
+ return self
+
+ def with_name_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("name", val))
+ return self
+
+ def with_name_less_than(self, val):
+ self.query.and_filter(lt("name", val))
+ return self
+
+ def with_name_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("name", val))
+ return self
+
+ def with_name_between(self, lower, upper):
+ self.query.and_filter(between(column("name"), value(lower), value(upper)))
+ return self
+
+ def with_name_is_known(self):
+ self.query.and_filter(is_not_null(column("name")))
+ return self
+
+ def with_name_is_unknown(self):
+ self.query.and_filter(is_null(column("name")))
+ return self
+
+ def with_version_is(self, val):
+ self.query.and_filter(eq("version", val))
+ return self
+
+ def with_version_is_not(self, val):
+ self.query.and_filter(ne("version", val))
+ return self
+
+ def with_version_in(self, *vals):
+ self.query.and_filter(in_list("version", list(vals)))
+ return self
+
+ def with_version_not_in(self, *vals):
+ self.query.and_filter(not_in_list("version", list(vals)))
+ return self
+
+ def with_version_greater_than(self, val):
+ self.query.and_filter(gt("version", val))
+ return self
+
+ def with_version_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("version", val))
+ return self
+
+ def with_version_less_than(self, val):
+ self.query.and_filter(lt("version", val))
+ return self
+
+ def with_version_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("version", val))
+ return self
+
+ def with_version_between(self, lower, upper):
+ self.query.and_filter(between(column("version"), value(lower), value(upper)))
+ return self
+
+ def with_version_is_known(self):
+ self.query.and_filter(is_not_null(column("version")))
+ return self
+
+ def with_version_is_unknown(self):
+ self.query.and_filter(is_null(column("version")))
+ return self
+
+ def order_by_id_ascending(self):
+ self.query.order_by("id", "asc")
+ return self
+
+ def order_by_id_descending(self):
+ self.query.order_by("id", "desc")
+ return self
+
+
+ def order_by_name_ascending(self):
+ self.query.order_by("name", "asc")
+ return self
+
+ def order_by_name_descending(self):
+ self.query.order_by("name", "desc")
+ return self
+
+ def order_by_version_ascending(self):
+ self.query.order_by("version", "asc")
+ return self
+
+ def order_by_version_descending(self):
+ self.query.order_by("version", "desc")
+ return self
+
+
+ def count(self):
+ self.query.count_field("id", "count")
+ return self
+
+ def count_as(self, ret_name: str):
+ self.query.count_field("id", ret_name)
+ return self
+
+ def group_by_id(self):
+ self.query.group_by("id")
+ return self
+
+ def group_by_id_as(self, ret_name: str):
+ self.query.group_by("id")
+ return self
+ def group_by_customer_order(self):
+ self.query.group_by("customer_order")
+ return self
+
+ def group_by_customer_order_as(self, ret_name: str):
+ self.query.group_by("customer_order")
+ return self
+ def group_by_name(self):
+ self.query.group_by("name")
+ return self
+
+ def group_by_name_as(self, ret_name: str):
+ self.query.group_by("name")
+ return self
+ def group_by_version(self):
+ self.query.group_by("version")
+ return self
+
+ def group_by_version_as(self, ret_name: str):
+ self.query.group_by("version")
+ return self
+ def facet_by_customer_order_as(self, name: str, request: QuerySelection,
+ include_all_facets: bool = True):
+ self.query.facet_by(name, "customer_order", request.query, include_all_facets)
+ return self
+
+
+class ExecutableOrderItemRequest:
+ def __init__(self, request):
+ self._request = request
+
+ def comment(self, c: str):
+ self._request.comment(c)
+ return self
+
+ def new_entity(self, context) -> OrderItem:
+ request = self._request
+ QueryIntent(request._comment, request._purpose)
+ entity = context.initialize_entity("OrderItem", OrderItem())
+ if not isinstance(entity, OrderItem):
+ raise TypeError("entity initializer returned an incompatible OrderItem")
+ return entity
+
+ async def execute_for_result(self, context):
+ self = self._request
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ return await service.query(context, req)
+
+ async def execute_for_rows(self, context):
+ return (await self.execute_for_result(context)).rows
+
+ async def execute_for_list(self, context) -> SmartList[OrderItem]:
+ result = await self.execute_for_result(context)
+ return SmartList(
+ (OrderItem(**row) for row in result.rows),
+ facets=result.facets)
+
+ async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[OrderItem]:
+ request = self._request
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
+ service = context.require_resource("dataService")
+ alias = "__teaql_total"
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
+ retained_count, accuracy = context.id_set_count()
+ if accuracy == "EXACT":
+ total_count = retained_count
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ row_result = await service.query(context, authorized)
+ data = SmartList(OrderItem(**row) for row in row_result.rows)
+ return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
+
+ async def execute_for_one(self, context):
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableOrderItemRequest(request).execute_for_list(context)
+ return entities[0] if entities else None
+
+ def execute_for_stream(self, context, chunk_size: int = 1000):
+ """Yield entity chunks lazily from the provider cursor."""
+ request = self._request
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ if not hasattr(service, "query_stream"):
+ raise RuntimeError("dataService does not implement query_stream")
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield OrderItem(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/trace-chain/lib/requests/payment_attempt_request.py b/examples/trace-chain/lib/requests/payment_attempt_request.py
new file mode 100644
index 0000000..d841ed0
--- /dev/null
+++ b/examples/trace-chain/lib/requests/payment_attempt_request.py
@@ -0,0 +1,429 @@
+from teaql.core.query import RelationAggregate, SelectQuery
+from teaql.core.list import SmartList, TeaQLPage
+from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
+from teaql.core.expr import (
+ begin_with, between, column, contain, end_with, eq, gt, gte,
+ in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
+ not_contain, not_end_with, not_in_list, not_in_subquery, value,
+ sound_like,
+)
+from models.payment_attempt import PaymentAttempt
+from typing import Protocol
+
+class QuerySelection(Protocol):
+ query: SelectQuery
+
+class PaymentAttemptRequest:
+ def __init__(self, minimal=False):
+ self.query = SelectQuery("PaymentAttempt")
+ self._purpose = None
+ self._comment = None
+ self.query.and_filter(gte("version", 1))
+ if minimal:
+ self.select_id()
+ self.select_version()
+ else:
+ self.select_self_fields()
+
+ def comment(self, c: str):
+ self.query.comment(c)
+ self._comment = c
+ return self
+
+ def purpose(self, p: str):
+ self.query.purpose(p)
+ self._purpose = p
+ return ExecutablePaymentAttemptRequest(self)
+
+ def optimize_for_continuous_page_fetch(self):
+ self.query.optimize_for_continuous_page_fetch()
+ return self
+
+ def optimize_for_continuous_page_fetch_with(self, namespace: str, ttl_seconds: int):
+ self.query.optimize_for_continuous_page_fetch_with(namespace, ttl_seconds)
+ return self
+
+ def optimize_pagination_with_id_set(self):
+ self.query.optimize_pagination_with_id_set()
+ return self
+
+ def optimize_pagination_with_id_set_config(self, namespace: str, ttl_seconds: int, max_ids: int):
+ self.query.optimize_pagination_with_id_set_config(namespace, ttl_seconds, max_ids)
+ return self
+
+ def top_n_probe_parent_threshold(self, threshold: int):
+ self.query.top_n_probe_parent_threshold(threshold)
+ return self
+
+ def limit(self, n: int):
+ self.query.limit(n)
+ return self
+
+ def offset(self, n: int):
+ self.query.offset(n)
+ return self
+
+ def with_deleted_rows(self):
+ self.query.with_deleted_rows()
+ return self
+
+ def deleted_rows_only(self):
+ self.query.deleted_rows_only()
+ return self
+
+ def select_self_fields(self):
+ self.query.project("id", "payment", "reference_code", "version")
+ return self
+
+ def select_id(self):
+ self.query.project("id")
+ return self
+
+
+ def select_reference_code(self):
+ self.query.project("reference_code")
+ return self
+
+ def select_version(self):
+ self.query.project("version")
+ return self
+
+ def select_payment_with(self, child_request):
+ self.query.project("payment")
+ self.query.relation_query("payment", child_request.query)
+ return self
+ def with_payment_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(in_subquery(column("payment"), "Payment", child_request.query))
+ return self
+
+ def without_payment_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(not_in_subquery(column("payment"), "Payment", child_request.query))
+ return self
+
+ def have_payment(self):
+ self.query.and_filter(is_not_null(column("payment")))
+ return self
+
+ def have_no_payment(self):
+ self.query.and_filter(is_null(column("payment")))
+ return self
+
+ def with_id_is(self, val):
+ self.query.and_filter(eq("id", val))
+ return self
+
+ def with_id_is_not(self, val):
+ self.query.and_filter(ne("id", val))
+ return self
+
+ def with_id_in(self, *vals):
+ self.query.and_filter(in_list("id", list(vals)))
+ return self
+
+ def with_id_not_in(self, *vals):
+ self.query.and_filter(not_in_list("id", list(vals)))
+ return self
+
+ def with_id_greater_than(self, val):
+ self.query.and_filter(gt("id", val))
+ return self
+
+ def with_id_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("id", val))
+ return self
+
+ def with_id_less_than(self, val):
+ self.query.and_filter(lt("id", val))
+ return self
+
+ def with_id_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("id", val))
+ return self
+
+ def with_id_between(self, lower, upper):
+ self.query.and_filter(between(column("id"), value(lower), value(upper)))
+ return self
+
+ def with_id_is_known(self):
+ self.query.and_filter(is_not_null(column("id")))
+ return self
+
+ def with_id_is_unknown(self):
+ self.query.and_filter(is_null(column("id")))
+ return self
+
+ def filter_by_payment(self, val):
+ self.query.and_filter(eq("payment", val))
+ return self
+
+ def with_reference_code_containing(self, val: str):
+ self.query.and_filter(contain("reference_code", val))
+ return self
+
+ def with_reference_code_not_containing(self, val: str):
+ self.query.and_filter(not_contain("reference_code", val))
+ return self
+
+ def with_reference_code_starting_with(self, val: str):
+ self.query.and_filter(begin_with("reference_code", val))
+ return self
+
+ def with_reference_code_not_starting_with(self, val: str):
+ self.query.and_filter(not_begin_with("reference_code", val))
+ return self
+
+ def with_reference_code_ending_with(self, val: str):
+ self.query.and_filter(end_with("reference_code", val))
+ return self
+
+ def with_reference_code_not_ending_with(self, val: str):
+ self.query.and_filter(not_end_with("reference_code", val))
+ return self
+
+ def with_reference_code_sounding_like(self, val: str):
+ self.query.and_filter(sound_like("reference_code", val))
+ return self
+
+ def with_reference_code_is(self, val: str):
+ self.query.and_filter(eq("reference_code", val))
+ return self
+ def with_reference_code_is_not(self, val):
+ self.query.and_filter(ne("reference_code", val))
+ return self
+
+ def with_reference_code_in(self, *vals):
+ self.query.and_filter(in_list("reference_code", list(vals)))
+ return self
+
+ def with_reference_code_not_in(self, *vals):
+ self.query.and_filter(not_in_list("reference_code", list(vals)))
+ return self
+
+ def with_reference_code_greater_than(self, val):
+ self.query.and_filter(gt("reference_code", val))
+ return self
+
+ def with_reference_code_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("reference_code", val))
+ return self
+
+ def with_reference_code_less_than(self, val):
+ self.query.and_filter(lt("reference_code", val))
+ return self
+
+ def with_reference_code_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("reference_code", val))
+ return self
+
+ def with_reference_code_between(self, lower, upper):
+ self.query.and_filter(between(column("reference_code"), value(lower), value(upper)))
+ return self
+
+ def with_reference_code_is_known(self):
+ self.query.and_filter(is_not_null(column("reference_code")))
+ return self
+
+ def with_reference_code_is_unknown(self):
+ self.query.and_filter(is_null(column("reference_code")))
+ return self
+
+ def with_version_is(self, val):
+ self.query.and_filter(eq("version", val))
+ return self
+
+ def with_version_is_not(self, val):
+ self.query.and_filter(ne("version", val))
+ return self
+
+ def with_version_in(self, *vals):
+ self.query.and_filter(in_list("version", list(vals)))
+ return self
+
+ def with_version_not_in(self, *vals):
+ self.query.and_filter(not_in_list("version", list(vals)))
+ return self
+
+ def with_version_greater_than(self, val):
+ self.query.and_filter(gt("version", val))
+ return self
+
+ def with_version_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("version", val))
+ return self
+
+ def with_version_less_than(self, val):
+ self.query.and_filter(lt("version", val))
+ return self
+
+ def with_version_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("version", val))
+ return self
+
+ def with_version_between(self, lower, upper):
+ self.query.and_filter(between(column("version"), value(lower), value(upper)))
+ return self
+
+ def with_version_is_known(self):
+ self.query.and_filter(is_not_null(column("version")))
+ return self
+
+ def with_version_is_unknown(self):
+ self.query.and_filter(is_null(column("version")))
+ return self
+
+ def order_by_id_ascending(self):
+ self.query.order_by("id", "asc")
+ return self
+
+ def order_by_id_descending(self):
+ self.query.order_by("id", "desc")
+ return self
+
+
+ def order_by_reference_code_ascending(self):
+ self.query.order_by("reference_code", "asc")
+ return self
+
+ def order_by_reference_code_descending(self):
+ self.query.order_by("reference_code", "desc")
+ return self
+
+ def order_by_version_ascending(self):
+ self.query.order_by("version", "asc")
+ return self
+
+ def order_by_version_descending(self):
+ self.query.order_by("version", "desc")
+ return self
+
+
+ def count(self):
+ self.query.count_field("id", "count")
+ return self
+
+ def count_as(self, ret_name: str):
+ self.query.count_field("id", ret_name)
+ return self
+
+ def group_by_id(self):
+ self.query.group_by("id")
+ return self
+
+ def group_by_id_as(self, ret_name: str):
+ self.query.group_by("id")
+ return self
+ def group_by_payment(self):
+ self.query.group_by("payment")
+ return self
+
+ def group_by_payment_as(self, ret_name: str):
+ self.query.group_by("payment")
+ return self
+ def group_by_reference_code(self):
+ self.query.group_by("reference_code")
+ return self
+
+ def group_by_reference_code_as(self, ret_name: str):
+ self.query.group_by("reference_code")
+ return self
+ def group_by_version(self):
+ self.query.group_by("version")
+ return self
+
+ def group_by_version_as(self, ret_name: str):
+ self.query.group_by("version")
+ return self
+ def facet_by_payment_as(self, name: str, request: QuerySelection,
+ include_all_facets: bool = True):
+ self.query.facet_by(name, "payment", request.query, include_all_facets)
+ return self
+
+
+class ExecutablePaymentAttemptRequest:
+ def __init__(self, request):
+ self._request = request
+
+ def comment(self, c: str):
+ self._request.comment(c)
+ return self
+
+ def new_entity(self, context) -> PaymentAttempt:
+ request = self._request
+ QueryIntent(request._comment, request._purpose)
+ entity = context.initialize_entity("PaymentAttempt", PaymentAttempt())
+ if not isinstance(entity, PaymentAttempt):
+ raise TypeError("entity initializer returned an incompatible PaymentAttempt")
+ return entity
+
+ async def execute_for_result(self, context):
+ self = self._request
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ return await service.query(context, req)
+
+ async def execute_for_rows(self, context):
+ return (await self.execute_for_result(context)).rows
+
+ async def execute_for_list(self, context) -> SmartList[PaymentAttempt]:
+ result = await self.execute_for_result(context)
+ return SmartList(
+ (PaymentAttempt(**row) for row in result.rows),
+ facets=result.facets)
+
+ async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[PaymentAttempt]:
+ request = self._request
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
+ service = context.require_resource("dataService")
+ alias = "__teaql_total"
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
+ retained_count, accuracy = context.id_set_count()
+ if accuracy == "EXACT":
+ total_count = retained_count
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ row_result = await service.query(context, authorized)
+ data = SmartList(PaymentAttempt(**row) for row in row_result.rows)
+ return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
+
+ async def execute_for_one(self, context):
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutablePaymentAttemptRequest(request).execute_for_list(context)
+ return entities[0] if entities else None
+
+ def execute_for_stream(self, context, chunk_size: int = 1000):
+ """Yield entity chunks lazily from the provider cursor."""
+ request = self._request
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ if not hasattr(service, "query_stream"):
+ raise RuntimeError("dataService does not implement query_stream")
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield PaymentAttempt(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/trace-chain/lib/requests/payment_request.py b/examples/trace-chain/lib/requests/payment_request.py
new file mode 100644
index 0000000..9c63b9b
--- /dev/null
+++ b/examples/trace-chain/lib/requests/payment_request.py
@@ -0,0 +1,468 @@
+from teaql.core.query import RelationAggregate, SelectQuery
+from teaql.core.list import SmartList, TeaQLPage
+from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
+from teaql.core.expr import (
+ begin_with, between, column, contain, end_with, eq, gt, gte,
+ in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
+ not_contain, not_end_with, not_in_list, not_in_subquery, value,
+ sound_like,
+)
+from models.payment import Payment
+from typing import Protocol
+
+class QuerySelection(Protocol):
+ query: SelectQuery
+
+class PaymentRequest:
+ def __init__(self, minimal=False):
+ self.query = SelectQuery("Payment")
+ self._purpose = None
+ self._comment = None
+ self.query.and_filter(gte("version", 1))
+ if minimal:
+ self.select_id()
+ self.select_version()
+ else:
+ self.select_self_fields()
+
+ def comment(self, c: str):
+ self.query.comment(c)
+ self._comment = c
+ return self
+
+ def purpose(self, p: str):
+ self.query.purpose(p)
+ self._purpose = p
+ return ExecutablePaymentRequest(self)
+
+ def optimize_for_continuous_page_fetch(self):
+ self.query.optimize_for_continuous_page_fetch()
+ return self
+
+ def optimize_for_continuous_page_fetch_with(self, namespace: str, ttl_seconds: int):
+ self.query.optimize_for_continuous_page_fetch_with(namespace, ttl_seconds)
+ return self
+
+ def optimize_pagination_with_id_set(self):
+ self.query.optimize_pagination_with_id_set()
+ return self
+
+ def optimize_pagination_with_id_set_config(self, namespace: str, ttl_seconds: int, max_ids: int):
+ self.query.optimize_pagination_with_id_set_config(namespace, ttl_seconds, max_ids)
+ return self
+
+ def top_n_probe_parent_threshold(self, threshold: int):
+ self.query.top_n_probe_parent_threshold(threshold)
+ return self
+
+ def limit(self, n: int):
+ self.query.limit(n)
+ return self
+
+ def offset(self, n: int):
+ self.query.offset(n)
+ return self
+
+ def with_deleted_rows(self):
+ self.query.with_deleted_rows()
+ return self
+
+ def deleted_rows_only(self):
+ self.query.deleted_rows_only()
+ return self
+
+ def select_self_fields(self):
+ self.query.project("id", "customer_order", "reference_code", "version")
+ return self
+
+ def select_id(self):
+ self.query.project("id")
+ return self
+
+
+ def select_reference_code(self):
+ self.query.project("reference_code")
+ return self
+
+ def select_version(self):
+ self.query.project("version")
+ return self
+
+ def select_customer_order_with(self, child_request):
+ self.query.project("customer_order")
+ self.query.relation_query("customer_order", child_request.query)
+ return self
+ def with_customer_order_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(in_subquery(column("customer_order"), "CustomerOrder", child_request.query))
+ return self
+
+ def without_customer_order_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(not_in_subquery(column("customer_order"), "CustomerOrder", child_request.query))
+ return self
+
+ def have_customer_order(self):
+ self.query.and_filter(is_not_null(column("customer_order")))
+ return self
+
+ def have_no_customer_order(self):
+ self.query.and_filter(is_null(column("customer_order")))
+ return self
+
+ def with_id_is(self, val):
+ self.query.and_filter(eq("id", val))
+ return self
+
+ def with_id_is_not(self, val):
+ self.query.and_filter(ne("id", val))
+ return self
+
+ def with_id_in(self, *vals):
+ self.query.and_filter(in_list("id", list(vals)))
+ return self
+
+ def with_id_not_in(self, *vals):
+ self.query.and_filter(not_in_list("id", list(vals)))
+ return self
+
+ def with_id_greater_than(self, val):
+ self.query.and_filter(gt("id", val))
+ return self
+
+ def with_id_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("id", val))
+ return self
+
+ def with_id_less_than(self, val):
+ self.query.and_filter(lt("id", val))
+ return self
+
+ def with_id_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("id", val))
+ return self
+
+ def with_id_between(self, lower, upper):
+ self.query.and_filter(between(column("id"), value(lower), value(upper)))
+ return self
+
+ def with_id_is_known(self):
+ self.query.and_filter(is_not_null(column("id")))
+ return self
+
+ def with_id_is_unknown(self):
+ self.query.and_filter(is_null(column("id")))
+ return self
+
+ def filter_by_customer_order(self, val):
+ self.query.and_filter(eq("customer_order", val))
+ return self
+
+ def with_reference_code_containing(self, val: str):
+ self.query.and_filter(contain("reference_code", val))
+ return self
+
+ def with_reference_code_not_containing(self, val: str):
+ self.query.and_filter(not_contain("reference_code", val))
+ return self
+
+ def with_reference_code_starting_with(self, val: str):
+ self.query.and_filter(begin_with("reference_code", val))
+ return self
+
+ def with_reference_code_not_starting_with(self, val: str):
+ self.query.and_filter(not_begin_with("reference_code", val))
+ return self
+
+ def with_reference_code_ending_with(self, val: str):
+ self.query.and_filter(end_with("reference_code", val))
+ return self
+
+ def with_reference_code_not_ending_with(self, val: str):
+ self.query.and_filter(not_end_with("reference_code", val))
+ return self
+
+ def with_reference_code_sounding_like(self, val: str):
+ self.query.and_filter(sound_like("reference_code", val))
+ return self
+
+ def with_reference_code_is(self, val: str):
+ self.query.and_filter(eq("reference_code", val))
+ return self
+ def with_reference_code_is_not(self, val):
+ self.query.and_filter(ne("reference_code", val))
+ return self
+
+ def with_reference_code_in(self, *vals):
+ self.query.and_filter(in_list("reference_code", list(vals)))
+ return self
+
+ def with_reference_code_not_in(self, *vals):
+ self.query.and_filter(not_in_list("reference_code", list(vals)))
+ return self
+
+ def with_reference_code_greater_than(self, val):
+ self.query.and_filter(gt("reference_code", val))
+ return self
+
+ def with_reference_code_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("reference_code", val))
+ return self
+
+ def with_reference_code_less_than(self, val):
+ self.query.and_filter(lt("reference_code", val))
+ return self
+
+ def with_reference_code_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("reference_code", val))
+ return self
+
+ def with_reference_code_between(self, lower, upper):
+ self.query.and_filter(between(column("reference_code"), value(lower), value(upper)))
+ return self
+
+ def with_reference_code_is_known(self):
+ self.query.and_filter(is_not_null(column("reference_code")))
+ return self
+
+ def with_reference_code_is_unknown(self):
+ self.query.and_filter(is_null(column("reference_code")))
+ return self
+
+ def with_version_is(self, val):
+ self.query.and_filter(eq("version", val))
+ return self
+
+ def with_version_is_not(self, val):
+ self.query.and_filter(ne("version", val))
+ return self
+
+ def with_version_in(self, *vals):
+ self.query.and_filter(in_list("version", list(vals)))
+ return self
+
+ def with_version_not_in(self, *vals):
+ self.query.and_filter(not_in_list("version", list(vals)))
+ return self
+
+ def with_version_greater_than(self, val):
+ self.query.and_filter(gt("version", val))
+ return self
+
+ def with_version_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("version", val))
+ return self
+
+ def with_version_less_than(self, val):
+ self.query.and_filter(lt("version", val))
+ return self
+
+ def with_version_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("version", val))
+ return self
+
+ def with_version_between(self, lower, upper):
+ self.query.and_filter(between(column("version"), value(lower), value(upper)))
+ return self
+
+ def with_version_is_known(self):
+ self.query.and_filter(is_not_null(column("version")))
+ return self
+
+ def with_version_is_unknown(self):
+ self.query.and_filter(is_null(column("version")))
+ return self
+
+ def order_by_id_ascending(self):
+ self.query.order_by("id", "asc")
+ return self
+
+ def order_by_id_descending(self):
+ self.query.order_by("id", "desc")
+ return self
+
+
+ def order_by_reference_code_ascending(self):
+ self.query.order_by("reference_code", "asc")
+ return self
+
+ def order_by_reference_code_descending(self):
+ self.query.order_by("reference_code", "desc")
+ return self
+
+ def order_by_version_ascending(self):
+ self.query.order_by("version", "asc")
+ return self
+
+ def order_by_version_descending(self):
+ self.query.order_by("version", "desc")
+ return self
+
+
+ def count(self):
+ self.query.count_field("id", "count")
+ return self
+
+ def count_as(self, ret_name: str):
+ self.query.count_field("id", ret_name)
+ return self
+
+ def group_by_id(self):
+ self.query.group_by("id")
+ return self
+
+ def group_by_id_as(self, ret_name: str):
+ self.query.group_by("id")
+ return self
+ def group_by_customer_order(self):
+ self.query.group_by("customer_order")
+ return self
+
+ def group_by_customer_order_as(self, ret_name: str):
+ self.query.group_by("customer_order")
+ return self
+ def group_by_reference_code(self):
+ self.query.group_by("reference_code")
+ return self
+
+ def group_by_reference_code_as(self, ret_name: str):
+ self.query.group_by("reference_code")
+ return self
+ def group_by_version(self):
+ self.query.group_by("version")
+ return self
+
+ def group_by_version_as(self, ret_name: str):
+ self.query.group_by("version")
+ return self
+ def select_payment_attempt_list(self):
+ from requests.payment_attempt_request import PaymentAttemptRequest
+ return self.select_payment_attempt_list_with(PaymentAttemptRequest())
+
+ def select_payment_attempt_list_with(self, child_request):
+ self.query.relation_query("payment_attempt_list", child_request.query)
+ return self
+ def have_payment_attempts(self):
+ from requests.payment_attempt_request import PaymentAttemptRequest
+ return self.with_payment_attempt_list_matching(PaymentAttemptRequest())
+
+ def have_no_payment_attempts(self):
+ from requests.payment_attempt_request import PaymentAttemptRequest
+ return self.without_payment_attempt_list_matching(PaymentAttemptRequest())
+
+ def with_payment_attempt_list_matching(self, child_request):
+ child_request.query.projection = ["payment"]
+ self.query.and_filter(in_subquery(column("id"), "PaymentAttempt", child_request.query))
+ return self
+
+ def without_payment_attempt_list_matching(self, child_request):
+ child_request.query.projection = ["payment"]
+ self.query.and_filter(not_in_subquery(column("id"), "PaymentAttempt", child_request.query))
+ return self
+ def count_payment_attempts(self):
+ return self.count_payment_attempts_as("count_payment_attempts")
+
+ def count_payment_attempts_as(self, alias: str):
+ from requests.payment_attempt_request import PaymentAttemptRequest
+ return self.count_payment_attempts_with(alias, PaymentAttemptRequest())
+
+ def count_payment_attempts_with(self, alias: str, child_request):
+ child_request.query.count_field("id", alias)
+ self.query.relation_aggregates.append(
+ RelationAggregate("payment_attempt_list", alias, child_request.query, True)
+ )
+ return self
+
+
+ def facet_by_customer_order_as(self, name: str, request: QuerySelection,
+ include_all_facets: bool = True):
+ self.query.facet_by(name, "customer_order", request.query, include_all_facets)
+ return self
+
+
+class ExecutablePaymentRequest:
+ def __init__(self, request):
+ self._request = request
+
+ def comment(self, c: str):
+ self._request.comment(c)
+ return self
+
+ def new_entity(self, context) -> Payment:
+ request = self._request
+ QueryIntent(request._comment, request._purpose)
+ entity = context.initialize_entity("Payment", Payment())
+ if not isinstance(entity, Payment):
+ raise TypeError("entity initializer returned an incompatible Payment")
+ return entity
+
+ async def execute_for_result(self, context):
+ self = self._request
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ return await service.query(context, req)
+
+ async def execute_for_rows(self, context):
+ return (await self.execute_for_result(context)).rows
+
+ async def execute_for_list(self, context) -> SmartList[Payment]:
+ result = await self.execute_for_result(context)
+ return SmartList(
+ (Payment(**row) for row in result.rows),
+ facets=result.facets)
+
+ async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[Payment]:
+ request = self._request
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
+ service = context.require_resource("dataService")
+ alias = "__teaql_total"
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
+ retained_count, accuracy = context.id_set_count()
+ if accuracy == "EXACT":
+ total_count = retained_count
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ row_result = await service.query(context, authorized)
+ data = SmartList(Payment(**row) for row in row_result.rows)
+ return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
+
+ async def execute_for_one(self, context):
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutablePaymentRequest(request).execute_for_list(context)
+ return entities[0] if entities else None
+
+ def execute_for_stream(self, context, chunk_size: int = 1000):
+ """Yield entity chunks lazily from the provider cursor."""
+ request = self._request
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ if not hasattr(service, "query_stream"):
+ raise RuntimeError("dataService does not implement query_stream")
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield Payment(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/trace-chain/lib/requests/platform_request.py b/examples/trace-chain/lib/requests/platform_request.py
new file mode 100644
index 0000000..7839df1
--- /dev/null
+++ b/examples/trace-chain/lib/requests/platform_request.py
@@ -0,0 +1,429 @@
+from teaql.core.query import RelationAggregate, SelectQuery
+from teaql.core.list import SmartList, TeaQLPage
+from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
+from teaql.core.expr import (
+ begin_with, between, column, contain, end_with, eq, gt, gte,
+ in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
+ not_contain, not_end_with, not_in_list, not_in_subquery, value,
+ sound_like,
+)
+from models.platform import Platform
+from typing import Protocol
+
+class QuerySelection(Protocol):
+ query: SelectQuery
+
+class PlatformRequest:
+ def __init__(self, minimal=False):
+ self.query = SelectQuery("Platform")
+ self._purpose = None
+ self._comment = None
+ self.query.and_filter(gte("version", 1))
+ if minimal:
+ self.select_id()
+ self.select_version()
+ else:
+ self.select_self_fields()
+
+ def comment(self, c: str):
+ self.query.comment(c)
+ self._comment = c
+ return self
+
+ def purpose(self, p: str):
+ self.query.purpose(p)
+ self._purpose = p
+ return ExecutablePlatformRequest(self)
+
+ def optimize_for_continuous_page_fetch(self):
+ self.query.optimize_for_continuous_page_fetch()
+ return self
+
+ def optimize_for_continuous_page_fetch_with(self, namespace: str, ttl_seconds: int):
+ self.query.optimize_for_continuous_page_fetch_with(namespace, ttl_seconds)
+ return self
+
+ def optimize_pagination_with_id_set(self):
+ self.query.optimize_pagination_with_id_set()
+ return self
+
+ def optimize_pagination_with_id_set_config(self, namespace: str, ttl_seconds: int, max_ids: int):
+ self.query.optimize_pagination_with_id_set_config(namespace, ttl_seconds, max_ids)
+ return self
+
+ def top_n_probe_parent_threshold(self, threshold: int):
+ self.query.top_n_probe_parent_threshold(threshold)
+ return self
+
+ def limit(self, n: int):
+ self.query.limit(n)
+ return self
+
+ def offset(self, n: int):
+ self.query.offset(n)
+ return self
+
+ def with_deleted_rows(self):
+ self.query.with_deleted_rows()
+ return self
+
+ def deleted_rows_only(self):
+ self.query.deleted_rows_only()
+ return self
+
+ def select_self_fields(self):
+ self.query.project("id", "name", "version")
+ return self
+
+ def select_id(self):
+ self.query.project("id")
+ return self
+
+ def select_name(self):
+ self.query.project("name")
+ return self
+
+ def select_version(self):
+ self.query.project("version")
+ return self
+
+
+ def with_id_is(self, val):
+ self.query.and_filter(eq("id", val))
+ return self
+
+ def with_id_is_not(self, val):
+ self.query.and_filter(ne("id", val))
+ return self
+
+ def with_id_in(self, *vals):
+ self.query.and_filter(in_list("id", list(vals)))
+ return self
+
+ def with_id_not_in(self, *vals):
+ self.query.and_filter(not_in_list("id", list(vals)))
+ return self
+
+ def with_id_greater_than(self, val):
+ self.query.and_filter(gt("id", val))
+ return self
+
+ def with_id_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("id", val))
+ return self
+
+ def with_id_less_than(self, val):
+ self.query.and_filter(lt("id", val))
+ return self
+
+ def with_id_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("id", val))
+ return self
+
+ def with_id_between(self, lower, upper):
+ self.query.and_filter(between(column("id"), value(lower), value(upper)))
+ return self
+
+ def with_id_is_known(self):
+ self.query.and_filter(is_not_null(column("id")))
+ return self
+
+ def with_id_is_unknown(self):
+ self.query.and_filter(is_null(column("id")))
+ return self
+
+ def with_name_containing(self, val: str):
+ self.query.and_filter(contain("name", val))
+ return self
+
+ def with_name_not_containing(self, val: str):
+ self.query.and_filter(not_contain("name", val))
+ return self
+
+ def with_name_starting_with(self, val: str):
+ self.query.and_filter(begin_with("name", val))
+ return self
+
+ def with_name_not_starting_with(self, val: str):
+ self.query.and_filter(not_begin_with("name", val))
+ return self
+
+ def with_name_ending_with(self, val: str):
+ self.query.and_filter(end_with("name", val))
+ return self
+
+ def with_name_not_ending_with(self, val: str):
+ self.query.and_filter(not_end_with("name", val))
+ return self
+
+ def with_name_sounding_like(self, val: str):
+ self.query.and_filter(sound_like("name", val))
+ return self
+
+ def with_name_is(self, val: str):
+ self.query.and_filter(eq("name", val))
+ return self
+ def with_name_is_not(self, val):
+ self.query.and_filter(ne("name", val))
+ return self
+
+ def with_name_in(self, *vals):
+ self.query.and_filter(in_list("name", list(vals)))
+ return self
+
+ def with_name_not_in(self, *vals):
+ self.query.and_filter(not_in_list("name", list(vals)))
+ return self
+
+ def with_name_greater_than(self, val):
+ self.query.and_filter(gt("name", val))
+ return self
+
+ def with_name_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("name", val))
+ return self
+
+ def with_name_less_than(self, val):
+ self.query.and_filter(lt("name", val))
+ return self
+
+ def with_name_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("name", val))
+ return self
+
+ def with_name_between(self, lower, upper):
+ self.query.and_filter(between(column("name"), value(lower), value(upper)))
+ return self
+
+ def with_name_is_known(self):
+ self.query.and_filter(is_not_null(column("name")))
+ return self
+
+ def with_name_is_unknown(self):
+ self.query.and_filter(is_null(column("name")))
+ return self
+
+ def with_version_is(self, val):
+ self.query.and_filter(eq("version", val))
+ return self
+
+ def with_version_is_not(self, val):
+ self.query.and_filter(ne("version", val))
+ return self
+
+ def with_version_in(self, *vals):
+ self.query.and_filter(in_list("version", list(vals)))
+ return self
+
+ def with_version_not_in(self, *vals):
+ self.query.and_filter(not_in_list("version", list(vals)))
+ return self
+
+ def with_version_greater_than(self, val):
+ self.query.and_filter(gt("version", val))
+ return self
+
+ def with_version_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("version", val))
+ return self
+
+ def with_version_less_than(self, val):
+ self.query.and_filter(lt("version", val))
+ return self
+
+ def with_version_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("version", val))
+ return self
+
+ def with_version_between(self, lower, upper):
+ self.query.and_filter(between(column("version"), value(lower), value(upper)))
+ return self
+
+ def with_version_is_known(self):
+ self.query.and_filter(is_not_null(column("version")))
+ return self
+
+ def with_version_is_unknown(self):
+ self.query.and_filter(is_null(column("version")))
+ return self
+
+ def order_by_id_ascending(self):
+ self.query.order_by("id", "asc")
+ return self
+
+ def order_by_id_descending(self):
+ self.query.order_by("id", "desc")
+ return self
+
+ def order_by_name_ascending(self):
+ self.query.order_by("name", "asc")
+ return self
+
+ def order_by_name_descending(self):
+ self.query.order_by("name", "desc")
+ return self
+
+ def order_by_version_ascending(self):
+ self.query.order_by("version", "asc")
+ return self
+
+ def order_by_version_descending(self):
+ self.query.order_by("version", "desc")
+ return self
+
+
+ def count(self):
+ self.query.count_field("id", "count")
+ return self
+
+ def count_as(self, ret_name: str):
+ self.query.count_field("id", ret_name)
+ return self
+
+ def group_by_id(self):
+ self.query.group_by("id")
+ return self
+
+ def group_by_id_as(self, ret_name: str):
+ self.query.group_by("id")
+ return self
+ def group_by_name(self):
+ self.query.group_by("name")
+ return self
+
+ def group_by_name_as(self, ret_name: str):
+ self.query.group_by("name")
+ return self
+ def group_by_version(self):
+ self.query.group_by("version")
+ return self
+
+ def group_by_version_as(self, ret_name: str):
+ self.query.group_by("version")
+ return self
+ def select_customer_order_list(self):
+ from requests.customer_order_request import CustomerOrderRequest
+ return self.select_customer_order_list_with(CustomerOrderRequest())
+
+ def select_customer_order_list_with(self, child_request):
+ self.query.relation_query("customer_order_list", child_request.query)
+ return self
+ def have_customer_orders(self):
+ from requests.customer_order_request import CustomerOrderRequest
+ return self.with_customer_order_list_matching(CustomerOrderRequest())
+
+ def have_no_customer_orders(self):
+ from requests.customer_order_request import CustomerOrderRequest
+ return self.without_customer_order_list_matching(CustomerOrderRequest())
+
+ def with_customer_order_list_matching(self, child_request):
+ child_request.query.projection = ["platform"]
+ self.query.and_filter(in_subquery(column("id"), "CustomerOrder", child_request.query))
+ return self
+
+ def without_customer_order_list_matching(self, child_request):
+ child_request.query.projection = ["platform"]
+ self.query.and_filter(not_in_subquery(column("id"), "CustomerOrder", child_request.query))
+ return self
+ def count_customer_orders(self):
+ return self.count_customer_orders_as("count_customer_orders")
+
+ def count_customer_orders_as(self, alias: str):
+ from requests.customer_order_request import CustomerOrderRequest
+ return self.count_customer_orders_with(alias, CustomerOrderRequest())
+
+ def count_customer_orders_with(self, alias: str, child_request):
+ child_request.query.count_field("id", alias)
+ self.query.relation_aggregates.append(
+ RelationAggregate("customer_order_list", alias, child_request.query, True)
+ )
+ return self
+
+
+
+class ExecutablePlatformRequest:
+ def __init__(self, request):
+ self._request = request
+
+ def comment(self, c: str):
+ self._request.comment(c)
+ return self
+
+ def new_entity(self, context) -> Platform:
+ request = self._request
+ QueryIntent(request._comment, request._purpose)
+ entity = context.initialize_entity("Platform", Platform())
+ if not isinstance(entity, Platform):
+ raise TypeError("entity initializer returned an incompatible Platform")
+ return entity
+
+ async def execute_for_result(self, context):
+ self = self._request
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ return await service.query(context, req)
+
+ async def execute_for_rows(self, context):
+ return (await self.execute_for_result(context)).rows
+
+ async def execute_for_list(self, context) -> SmartList[Platform]:
+ result = await self.execute_for_result(context)
+ return SmartList(
+ (Platform(**row) for row in result.rows),
+ facets=result.facets)
+
+ async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[Platform]:
+ request = self._request
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
+ service = context.require_resource("dataService")
+ alias = "__teaql_total"
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
+ retained_count, accuracy = context.id_set_count()
+ if accuracy == "EXACT":
+ total_count = retained_count
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ row_result = await service.query(context, authorized)
+ data = SmartList(Platform(**row) for row in row_result.rows)
+ return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
+
+ async def execute_for_one(self, context):
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutablePlatformRequest(request).execute_for_list(context)
+ return entities[0] if entities else None
+
+ def execute_for_stream(self, context, chunk_size: int = 1000):
+ """Yield entity chunks lazily from the provider cursor."""
+ request = self._request
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ if not hasattr(service, "query_stream"):
+ raise RuntimeError("dataService does not implement query_stream")
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield Platform(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/trace-chain/lib/requests/shipment_request.py b/examples/trace-chain/lib/requests/shipment_request.py
new file mode 100644
index 0000000..ceffe65
--- /dev/null
+++ b/examples/trace-chain/lib/requests/shipment_request.py
@@ -0,0 +1,429 @@
+from teaql.core.query import RelationAggregate, SelectQuery
+from teaql.core.list import SmartList, TeaQLPage
+from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent
+from copy import deepcopy
+from teaql.core.expr import (
+ begin_with, between, column, contain, end_with, eq, gt, gte,
+ in_list, in_subquery, is_not_null, is_null, lt, lte, ne, not_begin_with,
+ not_contain, not_end_with, not_in_list, not_in_subquery, value,
+ sound_like,
+)
+from models.shipment import Shipment
+from typing import Protocol
+
+class QuerySelection(Protocol):
+ query: SelectQuery
+
+class ShipmentRequest:
+ def __init__(self, minimal=False):
+ self.query = SelectQuery("Shipment")
+ self._purpose = None
+ self._comment = None
+ self.query.and_filter(gte("version", 1))
+ if minimal:
+ self.select_id()
+ self.select_version()
+ else:
+ self.select_self_fields()
+
+ def comment(self, c: str):
+ self.query.comment(c)
+ self._comment = c
+ return self
+
+ def purpose(self, p: str):
+ self.query.purpose(p)
+ self._purpose = p
+ return ExecutableShipmentRequest(self)
+
+ def optimize_for_continuous_page_fetch(self):
+ self.query.optimize_for_continuous_page_fetch()
+ return self
+
+ def optimize_for_continuous_page_fetch_with(self, namespace: str, ttl_seconds: int):
+ self.query.optimize_for_continuous_page_fetch_with(namespace, ttl_seconds)
+ return self
+
+ def optimize_pagination_with_id_set(self):
+ self.query.optimize_pagination_with_id_set()
+ return self
+
+ def optimize_pagination_with_id_set_config(self, namespace: str, ttl_seconds: int, max_ids: int):
+ self.query.optimize_pagination_with_id_set_config(namespace, ttl_seconds, max_ids)
+ return self
+
+ def top_n_probe_parent_threshold(self, threshold: int):
+ self.query.top_n_probe_parent_threshold(threshold)
+ return self
+
+ def limit(self, n: int):
+ self.query.limit(n)
+ return self
+
+ def offset(self, n: int):
+ self.query.offset(n)
+ return self
+
+ def with_deleted_rows(self):
+ self.query.with_deleted_rows()
+ return self
+
+ def deleted_rows_only(self):
+ self.query.deleted_rows_only()
+ return self
+
+ def select_self_fields(self):
+ self.query.project("id", "customer_order", "reference_code", "version")
+ return self
+
+ def select_id(self):
+ self.query.project("id")
+ return self
+
+
+ def select_reference_code(self):
+ self.query.project("reference_code")
+ return self
+
+ def select_version(self):
+ self.query.project("version")
+ return self
+
+ def select_customer_order_with(self, child_request):
+ self.query.project("customer_order")
+ self.query.relation_query("customer_order", child_request.query)
+ return self
+ def with_customer_order_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(in_subquery(column("customer_order"), "CustomerOrder", child_request.query))
+ return self
+
+ def without_customer_order_matching(self, child_request):
+ child_request.query.projection = ["id"]
+ self.query.and_filter(not_in_subquery(column("customer_order"), "CustomerOrder", child_request.query))
+ return self
+
+ def have_customer_order(self):
+ self.query.and_filter(is_not_null(column("customer_order")))
+ return self
+
+ def have_no_customer_order(self):
+ self.query.and_filter(is_null(column("customer_order")))
+ return self
+
+ def with_id_is(self, val):
+ self.query.and_filter(eq("id", val))
+ return self
+
+ def with_id_is_not(self, val):
+ self.query.and_filter(ne("id", val))
+ return self
+
+ def with_id_in(self, *vals):
+ self.query.and_filter(in_list("id", list(vals)))
+ return self
+
+ def with_id_not_in(self, *vals):
+ self.query.and_filter(not_in_list("id", list(vals)))
+ return self
+
+ def with_id_greater_than(self, val):
+ self.query.and_filter(gt("id", val))
+ return self
+
+ def with_id_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("id", val))
+ return self
+
+ def with_id_less_than(self, val):
+ self.query.and_filter(lt("id", val))
+ return self
+
+ def with_id_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("id", val))
+ return self
+
+ def with_id_between(self, lower, upper):
+ self.query.and_filter(between(column("id"), value(lower), value(upper)))
+ return self
+
+ def with_id_is_known(self):
+ self.query.and_filter(is_not_null(column("id")))
+ return self
+
+ def with_id_is_unknown(self):
+ self.query.and_filter(is_null(column("id")))
+ return self
+
+ def filter_by_customer_order(self, val):
+ self.query.and_filter(eq("customer_order", val))
+ return self
+
+ def with_reference_code_containing(self, val: str):
+ self.query.and_filter(contain("reference_code", val))
+ return self
+
+ def with_reference_code_not_containing(self, val: str):
+ self.query.and_filter(not_contain("reference_code", val))
+ return self
+
+ def with_reference_code_starting_with(self, val: str):
+ self.query.and_filter(begin_with("reference_code", val))
+ return self
+
+ def with_reference_code_not_starting_with(self, val: str):
+ self.query.and_filter(not_begin_with("reference_code", val))
+ return self
+
+ def with_reference_code_ending_with(self, val: str):
+ self.query.and_filter(end_with("reference_code", val))
+ return self
+
+ def with_reference_code_not_ending_with(self, val: str):
+ self.query.and_filter(not_end_with("reference_code", val))
+ return self
+
+ def with_reference_code_sounding_like(self, val: str):
+ self.query.and_filter(sound_like("reference_code", val))
+ return self
+
+ def with_reference_code_is(self, val: str):
+ self.query.and_filter(eq("reference_code", val))
+ return self
+ def with_reference_code_is_not(self, val):
+ self.query.and_filter(ne("reference_code", val))
+ return self
+
+ def with_reference_code_in(self, *vals):
+ self.query.and_filter(in_list("reference_code", list(vals)))
+ return self
+
+ def with_reference_code_not_in(self, *vals):
+ self.query.and_filter(not_in_list("reference_code", list(vals)))
+ return self
+
+ def with_reference_code_greater_than(self, val):
+ self.query.and_filter(gt("reference_code", val))
+ return self
+
+ def with_reference_code_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("reference_code", val))
+ return self
+
+ def with_reference_code_less_than(self, val):
+ self.query.and_filter(lt("reference_code", val))
+ return self
+
+ def with_reference_code_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("reference_code", val))
+ return self
+
+ def with_reference_code_between(self, lower, upper):
+ self.query.and_filter(between(column("reference_code"), value(lower), value(upper)))
+ return self
+
+ def with_reference_code_is_known(self):
+ self.query.and_filter(is_not_null(column("reference_code")))
+ return self
+
+ def with_reference_code_is_unknown(self):
+ self.query.and_filter(is_null(column("reference_code")))
+ return self
+
+ def with_version_is(self, val):
+ self.query.and_filter(eq("version", val))
+ return self
+
+ def with_version_is_not(self, val):
+ self.query.and_filter(ne("version", val))
+ return self
+
+ def with_version_in(self, *vals):
+ self.query.and_filter(in_list("version", list(vals)))
+ return self
+
+ def with_version_not_in(self, *vals):
+ self.query.and_filter(not_in_list("version", list(vals)))
+ return self
+
+ def with_version_greater_than(self, val):
+ self.query.and_filter(gt("version", val))
+ return self
+
+ def with_version_greater_than_or_equal_to(self, val):
+ self.query.and_filter(gte("version", val))
+ return self
+
+ def with_version_less_than(self, val):
+ self.query.and_filter(lt("version", val))
+ return self
+
+ def with_version_less_than_or_equal_to(self, val):
+ self.query.and_filter(lte("version", val))
+ return self
+
+ def with_version_between(self, lower, upper):
+ self.query.and_filter(between(column("version"), value(lower), value(upper)))
+ return self
+
+ def with_version_is_known(self):
+ self.query.and_filter(is_not_null(column("version")))
+ return self
+
+ def with_version_is_unknown(self):
+ self.query.and_filter(is_null(column("version")))
+ return self
+
+ def order_by_id_ascending(self):
+ self.query.order_by("id", "asc")
+ return self
+
+ def order_by_id_descending(self):
+ self.query.order_by("id", "desc")
+ return self
+
+
+ def order_by_reference_code_ascending(self):
+ self.query.order_by("reference_code", "asc")
+ return self
+
+ def order_by_reference_code_descending(self):
+ self.query.order_by("reference_code", "desc")
+ return self
+
+ def order_by_version_ascending(self):
+ self.query.order_by("version", "asc")
+ return self
+
+ def order_by_version_descending(self):
+ self.query.order_by("version", "desc")
+ return self
+
+
+ def count(self):
+ self.query.count_field("id", "count")
+ return self
+
+ def count_as(self, ret_name: str):
+ self.query.count_field("id", ret_name)
+ return self
+
+ def group_by_id(self):
+ self.query.group_by("id")
+ return self
+
+ def group_by_id_as(self, ret_name: str):
+ self.query.group_by("id")
+ return self
+ def group_by_customer_order(self):
+ self.query.group_by("customer_order")
+ return self
+
+ def group_by_customer_order_as(self, ret_name: str):
+ self.query.group_by("customer_order")
+ return self
+ def group_by_reference_code(self):
+ self.query.group_by("reference_code")
+ return self
+
+ def group_by_reference_code_as(self, ret_name: str):
+ self.query.group_by("reference_code")
+ return self
+ def group_by_version(self):
+ self.query.group_by("version")
+ return self
+
+ def group_by_version_as(self, ret_name: str):
+ self.query.group_by("version")
+ return self
+ def facet_by_customer_order_as(self, name: str, request: QuerySelection,
+ include_all_facets: bool = True):
+ self.query.facet_by(name, "customer_order", request.query, include_all_facets)
+ return self
+
+
+class ExecutableShipmentRequest:
+ def __init__(self, request):
+ self._request = request
+
+ def comment(self, c: str):
+ self._request.comment(c)
+ return self
+
+ def new_entity(self, context) -> Shipment:
+ request = self._request
+ QueryIntent(request._comment, request._purpose)
+ entity = context.initialize_entity("Shipment", Shipment())
+ if not isinstance(entity, Shipment):
+ raise TypeError("entity initializer returned an incompatible Shipment")
+ return entity
+
+ async def execute_for_result(self, context):
+ self = self._request
+ req = QueryRequest(self.query, _comment=self._comment, _purpose=self._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ return await service.query(context, req)
+
+ async def execute_for_rows(self, context):
+ return (await self.execute_for_result(context)).rows
+
+ async def execute_for_list(self, context) -> SmartList[Shipment]:
+ result = await self.execute_for_result(context)
+ return SmartList(
+ (Shipment(**row) for row in result.rows),
+ facets=result.facets)
+
+ async def execute_for_page(self, context, offset: int, limit: int) -> TeaQLPage[Shipment]:
+ request = self._request
+ intent = QueryIntent(request._comment, request._purpose)
+ query = deepcopy(request.query)
+ query.offset(offset).limit(limit)
+ authorized = context.prepare_query_request(QueryRequest(query, _comment=intent.comment, _purpose=intent.purpose))
+ service = context.require_resource("dataService")
+ alias = "__teaql_total"
+ if authorized.query.id_set_pagination is not None:
+ row_result = await service.query(context, authorized)
+ retained_count, accuracy = context.id_set_count()
+ if accuracy == "EXACT":
+ total_count = retained_count
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ else:
+ count_result = await service.query(context, authorized.with_query(authorized.query.for_exact_count(alias)))
+ if not count_result.rows or not isinstance(count_result.rows[0].get(alias), (int, float)):
+ raise RuntimeError("dataService did not return an exact page count")
+ total_count = int(count_result.rows[0][alias])
+ row_result = await service.query(context, authorized)
+ data = SmartList(Shipment(**row) for row in row_result.rows)
+ return TeaQLPage(data=data, total_count=total_count, offset=offset, limit=limit)
+
+ async def execute_for_one(self, context):
+ request = deepcopy(self._request)
+ request.limit(1)
+ entities = await ExecutableShipmentRequest(request).execute_for_list(context)
+ return entities[0] if entities else None
+
+ def execute_for_stream(self, context, chunk_size: int = 1000):
+ """Yield entity chunks lazily from the provider cursor."""
+ request = self._request
+ req = QueryRequest(request.query, _comment=request._comment, _purpose=request._purpose)
+ req = context.prepare_query_request(req)
+ service = context.require_resource("dataService")
+ if not hasattr(service, "query_stream"):
+ raise RuntimeError("dataService does not implement query_stream")
+ stream = service.query_stream(context, req, chunk_size)
+ async def entities():
+ try:
+ async for chunk in stream:
+ for row in chunk.rows:
+ yield Shipment(**row)
+ finally:
+ close = getattr(stream, "aclose", None)
+ if close is not None:
+ await close()
+ return entities()
diff --git a/examples/trace-chain/lib/runtime_module.py b/examples/trace-chain/lib/runtime_module.py
new file mode 100644
index 0000000..66c8d7c
--- /dev/null
+++ b/examples/trace-chain/lib/runtime_module.py
@@ -0,0 +1,214 @@
+import asyncio
+from datetime import datetime, timezone
+from teaql.runtime import CheckResult, ContextEntityRef, JsonFieldNamingProfile, ObjectLocation, RuntimeModule, create_wire_entity_metadata
+from teaql.core.meta import EntityDescriptor, PropertyDescriptor, RelationDescriptor
+from teaql.core.value import DataType
+from Q import Q
+from teaql.core.value import Value
+try:
+ from teaql.core.graph import GraphNode
+except ImportError:
+ class GraphNode:
+ def __init__(self, entity):
+ self.entity, self.fields = entity, {}
+ def set(self, field, value):
+ self.fields[field] = value
+ return self
+from models.platform import Platform
+from models.customer_order import CustomerOrder
+from models.order_item import OrderItem
+from models.payment import Payment
+from models.payment_attempt import PaymentAttempt
+from models.shipment import Shipment
+
+def _teaql_is_null(value):
+ return value.is_null() if hasattr(value, "is_null") else value is None
+
+def _teaql_raw(value):
+ return value.val if hasattr(value, "val") else value
+
+def _teaql_entity_id(value):
+ value = _teaql_raw(value)
+ if hasattr(value, "id"):
+ return value.id
+ if isinstance(value, dict):
+ return value.get("id")
+ return value
+
+class _PlatformChecker:
+ def check_and_fix(self, context, record, location, results):
+ operation = context.get_resource("fix_operation")
+ now = context.get_resource("fix_time")
+ if (operation == "insert" and "name" not in record) or ("name" in record and _teaql_is_null(record["name"])):
+ results.append(CheckResult("required", ObjectLocation().property("name")))
+ if "name" in record and _teaql_raw(record["name"]) is not None and len(_teaql_raw(record["name"])) > 100:
+ results.append(CheckResult("max_length", ObjectLocation().property("name"), _teaql_raw(record["name"]), 100))
+
+
+
+class _CustomerOrderChecker:
+ def check_and_fix(self, context, record, location, results):
+ operation = context.get_resource("fix_operation")
+ now = context.get_resource("fix_time")
+ if (operation == "insert" and "platform" not in record) or ("platform" in record and _teaql_is_null(record["platform"])):
+ results.append(CheckResult("required", ObjectLocation().property("platform")))
+
+ if (operation == "insert" and "order_number" not in record) or ("order_number" in record and _teaql_is_null(record["order_number"])):
+ results.append(CheckResult("required", ObjectLocation().property("order_number")))
+ if "order_number" in record and _teaql_raw(record["order_number"]) is not None and len(_teaql_raw(record["order_number"])) > 100:
+ results.append(CheckResult("max_length", ObjectLocation().property("order_number"), _teaql_raw(record["order_number"]), 100))
+
+ if (operation == "insert" and "description" not in record) or ("description" in record and _teaql_is_null(record["description"])):
+ results.append(CheckResult("required", ObjectLocation().property("description")))
+ if "description" in record and _teaql_raw(record["description"]) is not None and len(_teaql_raw(record["description"])) > 100:
+ results.append(CheckResult("max_length", ObjectLocation().property("description"), _teaql_raw(record["description"]), 100))
+
+
+
+class _OrderItemChecker:
+ def check_and_fix(self, context, record, location, results):
+ operation = context.get_resource("fix_operation")
+ now = context.get_resource("fix_time")
+ if (operation == "insert" and "customer_order" not in record) or ("customer_order" in record and _teaql_is_null(record["customer_order"])):
+ results.append(CheckResult("required", ObjectLocation().property("customer_order")))
+
+ if (operation == "insert" and "name" not in record) or ("name" in record and _teaql_is_null(record["name"])):
+ results.append(CheckResult("required", ObjectLocation().property("name")))
+ if "name" in record and _teaql_raw(record["name"]) is not None and len(_teaql_raw(record["name"])) > 100:
+ results.append(CheckResult("max_length", ObjectLocation().property("name"), _teaql_raw(record["name"]), 100))
+
+
+
+class _PaymentChecker:
+ def check_and_fix(self, context, record, location, results):
+ operation = context.get_resource("fix_operation")
+ now = context.get_resource("fix_time")
+ if (operation == "insert" and "customer_order" not in record) or ("customer_order" in record and _teaql_is_null(record["customer_order"])):
+ results.append(CheckResult("required", ObjectLocation().property("customer_order")))
+
+ if (operation == "insert" and "reference_code" not in record) or ("reference_code" in record and _teaql_is_null(record["reference_code"])):
+ results.append(CheckResult("required", ObjectLocation().property("reference_code")))
+ if "reference_code" in record and _teaql_raw(record["reference_code"]) is not None and len(_teaql_raw(record["reference_code"])) > 100:
+ results.append(CheckResult("max_length", ObjectLocation().property("reference_code"), _teaql_raw(record["reference_code"]), 100))
+
+
+
+class _PaymentAttemptChecker:
+ def check_and_fix(self, context, record, location, results):
+ operation = context.get_resource("fix_operation")
+ now = context.get_resource("fix_time")
+ if (operation == "insert" and "payment" not in record) or ("payment" in record and _teaql_is_null(record["payment"])):
+ results.append(CheckResult("required", ObjectLocation().property("payment")))
+
+ if (operation == "insert" and "reference_code" not in record) or ("reference_code" in record and _teaql_is_null(record["reference_code"])):
+ results.append(CheckResult("required", ObjectLocation().property("reference_code")))
+ if "reference_code" in record and _teaql_raw(record["reference_code"]) is not None and len(_teaql_raw(record["reference_code"])) > 100:
+ results.append(CheckResult("max_length", ObjectLocation().property("reference_code"), _teaql_raw(record["reference_code"]), 100))
+
+
+
+class _ShipmentChecker:
+ def check_and_fix(self, context, record, location, results):
+ operation = context.get_resource("fix_operation")
+ now = context.get_resource("fix_time")
+ if (operation == "insert" and "customer_order" not in record) or ("customer_order" in record and _teaql_is_null(record["customer_order"])):
+ results.append(CheckResult("required", ObjectLocation().property("customer_order")))
+
+ if (operation == "insert" and "reference_code" not in record) or ("reference_code" in record and _teaql_is_null(record["reference_code"])):
+ results.append(CheckResult("required", ObjectLocation().property("reference_code")))
+ if "reference_code" in record and _teaql_raw(record["reference_code"]) is not None and len(_teaql_raw(record["reference_code"])) > 100:
+ results.append(CheckResult("max_length", ObjectLocation().property("reference_code"), _teaql_raw(record["reference_code"]), 100))
+
+
+
+_Platform_DESCRIPTOR = (EntityDescriptor("Platform")
+ .audit_mask_fields([])
+ .table_name("platform_data").property(PropertyDescriptor("id", DataType.I64).column_name("id").log_policy("plain").is_id().required()).property(PropertyDescriptor("name", DataType.Text).column_name("name").log_policy("plain").required()).property(PropertyDescriptor("version", DataType.I64).column_name("version").log_policy("plain").is_version().required()).relation(RelationDescriptor("customer_order_list", "CustomerOrder").local("id").foreign("platform").many())
+)
+
+_CustomerOrder_DESCRIPTOR = (EntityDescriptor("CustomerOrder")
+ .audit_mask_fields([])
+ .table_name("customer_order_data").property(PropertyDescriptor("id", DataType.I64).column_name("id").log_policy("plain").is_id().required()).property(PropertyDescriptor("platform", DataType.I64).column_name("platform").log_policy("plain").required()).property(PropertyDescriptor("order_number", DataType.Text).column_name("order_number").log_policy("plain").required()).property(PropertyDescriptor("description", DataType.Text).column_name("description").log_policy("plain").required()).property(PropertyDescriptor("version", DataType.I64).column_name("version").log_policy("plain").is_version().required()).relation(RelationDescriptor("platform", "Platform").local("platform").foreign("id")).relation(RelationDescriptor("order_item_list", "OrderItem").local("id").foreign("customer_order").many()).relation(RelationDescriptor("payment_list", "Payment").local("id").foreign("customer_order").many()).relation(RelationDescriptor("shipment_list", "Shipment").local("id").foreign("customer_order").many())
+)
+
+_OrderItem_DESCRIPTOR = (EntityDescriptor("OrderItem")
+ .audit_mask_fields([])
+ .table_name("order_item_data").property(PropertyDescriptor("id", DataType.I64).column_name("id").log_policy("plain").is_id().required()).property(PropertyDescriptor("customer_order", DataType.I64).column_name("customer_order").log_policy("plain").required()).property(PropertyDescriptor("name", DataType.Text).column_name("name").log_policy("plain").required()).property(PropertyDescriptor("version", DataType.I64).column_name("version").log_policy("plain").is_version().required()).relation(RelationDescriptor("customer_order", "CustomerOrder").local("customer_order").foreign("id"))
+)
+
+_Payment_DESCRIPTOR = (EntityDescriptor("Payment")
+ .audit_mask_fields([])
+ .table_name("payment_data").property(PropertyDescriptor("id", DataType.I64).column_name("id").log_policy("plain").is_id().required()).property(PropertyDescriptor("customer_order", DataType.I64).column_name("customer_order").log_policy("plain").required()).property(PropertyDescriptor("reference_code", DataType.Text).column_name("reference_code").log_policy("plain").required()).property(PropertyDescriptor("version", DataType.I64).column_name("version").log_policy("plain").is_version().required()).relation(RelationDescriptor("customer_order", "CustomerOrder").local("customer_order").foreign("id")).relation(RelationDescriptor("payment_attempt_list", "PaymentAttempt").local("id").foreign("payment").many())
+)
+
+_PaymentAttempt_DESCRIPTOR = (EntityDescriptor("PaymentAttempt")
+ .audit_mask_fields([])
+ .table_name("payment_attempt_data").property(PropertyDescriptor("id", DataType.I64).column_name("id").log_policy("plain").is_id().required()).property(PropertyDescriptor("payment", DataType.I64).column_name("payment").log_policy("plain").required()).property(PropertyDescriptor("reference_code", DataType.Text).column_name("reference_code").log_policy("plain").required()).property(PropertyDescriptor("version", DataType.I64).column_name("version").log_policy("plain").is_version().required()).relation(RelationDescriptor("payment", "Payment").local("payment").foreign("id"))
+)
+
+_Shipment_DESCRIPTOR = (EntityDescriptor("Shipment")
+ .audit_mask_fields([])
+ .table_name("shipment_data").property(PropertyDescriptor("id", DataType.I64).column_name("id").log_policy("plain").is_id().required()).property(PropertyDescriptor("customer_order", DataType.I64).column_name("customer_order").log_policy("plain").required()).property(PropertyDescriptor("reference_code", DataType.Text).column_name("reference_code").log_policy("plain").required()).property(PropertyDescriptor("version", DataType.I64).column_name("version").log_policy("plain").is_version().required()).relation(RelationDescriptor("customer_order", "CustomerOrder").local("customer_order").foreign("id"))
+)
+
+async def _ensure_generated_bootstrap_once(context):
+ previous_actor = context.user_identifier() if hasattr(context, 'user_identifier') else None
+ previous_category = context.get_resource('bootstrapCategory')
+ if hasattr(context, 'set_user_identifier'):
+ context.set_user_identifier('teaql-generated-bootstrap')
+ context.insert_resource('bootstrapCategory', 'runtime-bootstrap')
+ try:
+ platform_1 = await (Q.platforms().with_id_is(1).comment('what: locate generated bootstrap entity').purpose('why: idempotent runtime bootstrap').execute_for_one(context))
+ if platform_1 is None:
+ platform_1 = Platform._teaql_new_with_fixed_id(1)
+ platform_1.update_name("Trace Chain Verification")
+ try:
+ await platform_1.audit_as('create model root Platform(1)').save(context)
+ except Exception as _teaql_create_error:
+ for _teaql_attempt in range(5):
+ platform_1 = await (Q.platforms().with_id_is(1).comment('what: recover concurrent bootstrap').purpose('why: make generated bootstrap idempotent').execute_for_one(context))
+ if platform_1 is not None:
+ break
+ if _teaql_attempt < 4:
+ await asyncio.sleep((_teaql_attempt + 1) * 0.01)
+ if platform_1 is None:
+ raise _teaql_create_error
+ context.with_active_root(ContextEntityRef("Platform", 1))
+ finally:
+ if hasattr(context, 'set_user_identifier'):
+ context.set_user_identifier(previous_actor)
+ context.insert_resource('bootstrapCategory', previous_category)
+
+async def _ensure_generated_bootstrap(context):
+ for _teaql_attempt in range(5):
+ try:
+ await _ensure_generated_bootstrap_once(context)
+ return
+ except Exception:
+ if _teaql_attempt == 4:
+ raise
+ await asyncio.sleep((_teaql_attempt + 1) * 0.01)
+
+
+# Passive generated manifest. Call ensure_schema() separately and explicitly.
+GENERATED_RUNTIME_MODULE = (RuntimeModule().entity(Platform)
+ .schema_entity(_Platform_DESCRIPTOR)
+ .checker("Platform", _PlatformChecker())
+ .wire_metadata("Platform", create_wire_entity_metadata("Platform", ["id", "name", "version"], JsonFieldNamingProfile.CAMEL_CASE, {"id": ["id"], "name": ["name"], "version": ["version"]})).entity(CustomerOrder)
+ .schema_entity(_CustomerOrder_DESCRIPTOR)
+ .checker("CustomerOrder", _CustomerOrderChecker())
+ .wire_metadata("CustomerOrder", create_wire_entity_metadata("CustomerOrder", ["id", "platform", "order_number", "description", "version"], JsonFieldNamingProfile.CAMEL_CASE, {"id": ["id"], "platform": ["platform"], "order_number": ["order_number"], "description": ["description"], "version": ["version"]})).entity(OrderItem)
+ .schema_entity(_OrderItem_DESCRIPTOR)
+ .checker("OrderItem", _OrderItemChecker())
+ .wire_metadata("OrderItem", create_wire_entity_metadata("OrderItem", ["id", "customer_order", "name", "version"], JsonFieldNamingProfile.CAMEL_CASE, {"id": ["id"], "customer_order": ["customer_order"], "name": ["name"], "version": ["version"]})).entity(Payment)
+ .schema_entity(_Payment_DESCRIPTOR)
+ .checker("Payment", _PaymentChecker())
+ .wire_metadata("Payment", create_wire_entity_metadata("Payment", ["id", "customer_order", "reference_code", "version"], JsonFieldNamingProfile.CAMEL_CASE, {"id": ["id"], "customer_order": ["customer_order"], "reference_code": ["reference_code"], "version": ["version"]})).entity(PaymentAttempt)
+ .schema_entity(_PaymentAttempt_DESCRIPTOR)
+ .checker("PaymentAttempt", _PaymentAttemptChecker())
+ .wire_metadata("PaymentAttempt", create_wire_entity_metadata("PaymentAttempt", ["id", "payment", "reference_code", "version"], JsonFieldNamingProfile.CAMEL_CASE, {"id": ["id"], "payment": ["payment"], "reference_code": ["reference_code"], "version": ["version"]})).entity(Shipment)
+ .schema_entity(_Shipment_DESCRIPTOR)
+ .checker("Shipment", _ShipmentChecker())
+ .wire_metadata("Shipment", create_wire_entity_metadata("Shipment", ["id", "customer_order", "reference_code", "version"], JsonFieldNamingProfile.CAMEL_CASE, {"id": ["id"], "customer_order": ["customer_order"], "reference_code": ["reference_code"], "version": ["version"]}))
+ .generated_bootstrap(_ensure_generated_bootstrap)
+)
\ No newline at end of file
diff --git a/examples/trace-chain/lib/teaql-i18n.json b/examples/trace-chain/lib/teaql-i18n.json
new file mode 100644
index 0000000..f63a000
--- /dev/null
+++ b/examples/trace-chain/lib/teaql-i18n.json
@@ -0,0 +1,126 @@
+{
+ "schema": "teaql.i18n/v1",
+ "defaultLocale": "en",
+ "locales": {
+ "de": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "ko": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "pt": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "zh-TW": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "fil": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "en": {
+ "vocabulary": {
+ "property.platform.name": "Name",
+ "property.shipment.version": "Version",
+ "property.customerOrder.orderNumber": "Order Number",
+ "property.orderItem.id": "Id",
+ "property.shipment.referenceCode": "Reference Code",
+ "property.customerOrder.description": "Description",
+ "entity.orderItem": "Order Item",
+ "property.orderItem.customerOrder": "Customer Order",
+ "property.orderItem.name": "Name",
+ "entity.customerOrder": "Customer Order",
+ "property.payment.customerOrder": "Customer Order",
+ "property.customerOrder.id": "Id",
+ "property.paymentAttempt.version": "Version",
+ "property.payment.version": "Version",
+ "property.shipment.customerOrder": "Customer Order",
+ "property.paymentAttempt.id": "Id",
+ "property.customerOrder.version": "Version",
+ "property.payment.id": "Id",
+ "entity.platform": "Platform",
+ "property.platform.id": "Id",
+ "property.customerOrder.platform": "Platform",
+ "entity.payment": "Payment",
+ "property.shipment.id": "Id",
+ "property.payment.referenceCode": "Reference Code",
+ "property.paymentAttempt.referenceCode": "Reference Code",
+ "entity.shipment": "Shipment",
+ "property.orderItem.version": "Version",
+ "property.platform.version": "Version",
+ "property.paymentAttempt.payment": "Payment",
+ "entity.paymentAttempt": "Payment Attempt"
+ },
+ "messages": {
+ }
+ },
+ "fr": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "zh-CN": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "es": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "ar": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "vi": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "th": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "uk": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "ja": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ },
+ "id": {
+ "vocabulary": {
+ },
+ "messages": {
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/trace-chain/main.py b/examples/trace-chain/main.py
new file mode 100644
index 0000000..1a46c0e
--- /dev/null
+++ b/examples/trace-chain/main.py
@@ -0,0 +1,493 @@
+"""Generated Q/E/Mutation traversal observed at three real SQLite boundaries."""
+import asyncio
+import json
+import os
+import sqlite3
+import uuid
+from types import SimpleNamespace
+
+from E import E
+from Q import Q
+from runtime_module import GENERATED_RUNTIME_MODULE
+from teaql.provider.sqlite import create_sqlite_service
+from teaql.runtime import UserContext, DelegatingMutationPolicyRegistry
+from teaql.core.request_intent import RequestIntentError
+from teaql.core.mutation import InsertCommand, UpdateCommand, DeleteCommand
+from teaql.sql.executor import TransportError
+
+
+def chain(nodes):
+ values = list(nodes)
+ assert all(node.kind == 'auditReason' for node in values), 'lineage requires typed AuditReason nodes'
+ return [(node.name or node.entity_type, node.entity_id, node.comment) for node in values]
+
+
+def scalar(value):
+ return getattr(value, 'val', value)
+
+
+def graph_identities(graph):
+ order, item, payment, attempt, shipment, removed = graph
+ return [
+ ('CustomerOrder', E.customer_order(order).id().eval()),
+ ('OrderItem', E.order_item(item).id().eval()),
+ ('Payment', E.payment(payment).id().eval()),
+ ('PaymentAttempt', E.payment_attempt(attempt).id().eval()),
+ ('Shipment', E.shipment(shipment).id().eval()),
+ ('OrderItem', E.order_item(removed).id().eval()),
+ ]
+
+
+def check_identities(expected, actual, boundary):
+ # Check lists before making dictionaries: duplicate rows must not disappear.
+ assert len(expected) == len(actual) == 6, boundary + ': six identities required'
+ for entity, identity in [*expected, *actual]:
+ assert isinstance(entity, str) and entity, boundary + ': entity type required'
+ assert type(identity) is int and 0 < identity <= 2**64 - 1, boundary + ': positive u64 ID required'
+ assert len(set(expected)) == 6, boundary + ': duplicate expected identity'
+ assert len(set(actual)) == 6, boundary + ': duplicate identity'
+ assert set(actual) == set(expected), boundary + ': missing or unexpected identity'
+
+
+def identity_controls():
+ # Pure oracle inputs only: these nodes are never supplied to the runtime.
+ def sample(kind):
+ return SimpleNamespace(kind=kind, name='CustomerOrder', entity_type='CustomerOrder',
+ entity_id=1, comment='submit order')
+ assert chain([sample('auditReason')]) == [('CustomerOrder', 1, 'submit order')]
+ for kind in ('comment', 'purpose', 'entity', 'sql'):
+ try:
+ chain([sample(kind)])
+ except AssertionError as error:
+ assert str(error) == 'lineage requires typed AuditReason nodes'
+ else:
+ raise AssertionError('wrong-kind lineage oracle input was accepted: ' + kind)
+ print('PASS: typed lineage oracle rejects correct-text Comment/Purpose/Entity/Sql nodes')
+ expected = [('CustomerOrder', 1), ('OrderItem', 1), ('Payment', 1),
+ ('PaymentAttempt', 1), ('Shipment', 1), ('OrderItem', 2)]
+ check_identities(expected, expected, 'positive control')
+ controls = [expected[:-1] + [expected[1]], expected[:-1],
+ [('CustomerOrder' if entity == 'Payment' else entity, identity)
+ for entity, identity in expected]]
+ for actual in controls:
+ try:
+ check_identities(expected, actual, 'negative control')
+ except AssertionError:
+ pass
+ else:
+ raise AssertionError('identity negative control was accepted')
+ print('PASS: identity controls reject duplicate, missing and equal-ID type collapse')
+
+
+def physical_identities(service, context):
+ """Bind actual ordered SQL facts to independently observed provider commands.
+
+ Canonical SQL paths deliberately do not contain IDs. Readback paths name
+ the operation root, not the target. Neither responsibility chain is a key.
+ """
+ physical = [entry for entry in context.sql_logs() if entry.mutation_lineage]
+ assert len(service.requests) == len(service.results) == len(service.actions)
+ assert len(physical) == 2 * len(service.requests)
+ identities = []
+ for index, ((entity, identity, nodes), record, action) in enumerate(
+ zip(service.requests, service.results, service.actions)):
+ write, read = physical[index * 2:index * 2 + 2]
+ assert len(record.statements) == 2
+ assert record.affected_rows == write.affected_rows == 1
+ assert write.execution_outcome == read.execution_outcome == 'success'
+ assert read.result_count == 1 and read.affected_rows is None
+ assert [node.kind for node in write.trace_path] == ['operation', 'entity', 'provider', 'sql']
+ assert write.trace_path[1].name == entity
+ assert write.trace_path[-1].name == action and read.trace_path[-1].name == 'select'
+ assert write.trace_path == record.statements[0].trace_chain
+ assert read.trace_path == record.statements[1].trace_chain
+ assert chain(write.mutation_lineage) == chain(read.mutation_lineage) == nodes
+ assert write.comment == read.comment == record.comment
+ identities.append((entity, identity))
+ return identities
+
+
+def readback_fact(entry):
+ """Project actual safe SQL observations, omitting all parameter values."""
+ def nodes(values):
+ return [{'Kind': node.kind, 'Name': node.name, 'EntityId': node.entity_id,
+ 'Comment': node.comment} for node in values]
+ return {'Operation': entry.operation.name, 'Comment': entry.comment,
+ 'Purpose': entry.purpose, 'AuditReason': entry.audit_reason,
+ 'ExecutionOutcome': entry.execution_outcome,
+ 'AffectedRows': entry.affected_rows, 'ResultCount': entry.result_count,
+ 'StartedAt': entry.started_at.isoformat(), 'EndedAt': entry.ended_at.isoformat(),
+ 'ParameterizedSQL': entry.sql, 'TraceChain': nodes(entry.trace_path),
+ 'MutationLineage': nodes(entry.mutation_lineage)}
+
+
+class ObservedService:
+ """Transparent observer, not a replacement planner or provider."""
+ def __init__(self, service):
+ self.service = service
+ self.requests = []
+ self.results = []
+ self.actions = []
+ self.finishes = []
+ self.queries = []
+ self.raw_requests = []
+ self.begins = 0
+ self.fault = None
+
+ def __getattr__(self, name):
+ return getattr(self.service, name)
+
+ async def query(self, context, request):
+ result = await self.service.query(context, request)
+ self.queries.append((request, result.metadata))
+ return result
+
+ async def begin(self, context):
+ self.begins += 1
+ transaction = await self.service.begin(context)
+ return ObservedTransaction(self, transaction)
+
+
+class ObservedTransaction:
+ def __init__(self, owner, transaction):
+ self.owner, self.transaction = owner, transaction
+ for method_name in ('execute_sql', 'fetch_all_sql'):
+ original = getattr(transaction.transport, method_name)
+ async def intercepted(compiled, original=original, method_name=method_name):
+ if self.owner.fault == method_name and 'payment_attempt_data' in compiled.sql:
+ raise RuntimeError('synthetic payment attempt failure')
+ return await original(compiled)
+ setattr(transaction.transport, method_name, intercepted)
+
+ def __getattr__(self, name):
+ return getattr(self.transaction, name)
+
+ async def mutate(self, context, request):
+ command = request._data
+ self.owner.raw_requests.append(request)
+ identity = scalar(getattr(command, 'id', None) or command.values.get('id'))
+ self.owner.requests.append((command.entity, identity, chain(request.mutation_lineage)))
+ self.owner.actions.append({InsertCommand: 'insert', UpdateCommand: 'update',
+ DeleteCommand: 'delete'}[type(command)])
+ result = await self.transaction.mutate(context, request)
+ self.owner.results.append(result.metadata)
+ return result
+
+ async def commit(self, context):
+ await self.transaction.commit(context)
+ self.owner.finishes.append('commit')
+
+ async def rollback(self, context):
+ await self.transaction.rollback(context)
+ self.owner.finishes.append('rollback')
+
+
+class AuditSink:
+ def __init__(self, service):
+ self.service, self.events = service, []
+
+ def on_safe_event(self, context, event):
+ assert self.service.finishes[-1] == 'commit', 'audit escaped before database commit'
+ self.events.append(event)
+
+
+def new(request, context):
+ return request.comment('what: construct a trace example entity').purpose(
+ 'why: verify generated graph behavior').new_entity(context)
+
+
+def make_graph(context, platform, label):
+ order = new(Q.customer_orders(), context).update_platform(platform)
+ order.update_order_number(label).update_description('Trace example draft')
+ available = new(Q.order_items(), context).update_customer_order(order).update_name('Available item')
+ removed = new(Q.order_items(), context).update_customer_order(order).update_name('Unavailable item')
+ payment = new(Q.payments(), context).update_customer_order(order).update_reference_code(label + '-payment')
+ attempt = new(Q.payment_attempts(), context).update_payment(payment).update_reference_code(label + '-attempt')
+ shipment = new(Q.shipments(), context).update_customer_order(order).update_reference_code(label + '-shipment')
+ payment.payment_attempt_list().append(attempt)
+ order.order_item_list().extend([available, removed])
+ order.payment_list().append(payment)
+ order.shipment_list().append(shipment)
+ return order, available, payment, attempt, shipment, removed
+
+
+def expected_graph(graph, reason):
+ order, item, payment, attempt, shipment, removed = graph
+ root = ('CustomerOrder', E.customer_order(order).id().eval(), reason)
+ payment_node = ('Payment', E.payment(payment).id().eval(), 'authorize payment')
+ shipment_node = ('Shipment', E.shipment(shipment).id().eval(), 'dispatch shipment')
+ deleted_node = ('OrderItem', E.order_item(removed).id().eval(), 'remove unavailable item')
+ return {
+ ('CustomerOrder', root[1]): [root],
+ ('OrderItem', E.order_item(item).id().eval()): [root],
+ ('Payment', payment_node[1]): [root, payment_node],
+ ('PaymentAttempt', E.payment_attempt(attempt).id().eval()): [root, payment_node],
+ ('Shipment', shipment_node[1]): [root, shipment_node],
+ ('OrderItem', deleted_node[1]): [root, deleted_node],
+ }
+
+
+def reset(service, sink, context):
+ service.requests.clear()
+ service.results.clear()
+ service.actions.clear()
+ service.finishes.clear()
+ service.queries.clear()
+ service.raw_requests.clear()
+ service.begins = 0
+ sink.events.clear()
+ context.clear_sql_logs()
+
+
+async def verify_bootstrap(path, logging):
+ """Observe generated startup without supplying caller intent or trace frames."""
+ context = UserContext.new().install(GENERATED_RUNTIME_MODULE)
+ context.with_user_identifier('trace-example-user')
+ service = ObservedService(create_sqlite_service(path))
+ sink = AuditSink(service)
+ context.insert_resource('dataService', service).with_app_audit_event_sink(sink)
+ context.set_diagnostic_sql_log_sink(type('Silent', (), {'write': lambda self, entry: None})())
+ if not logging:
+ context.disable_sql_log()
+ await context.ensure_schema()
+ first_queries = tuple(service.queries)
+ first_requests = tuple(service.raw_requests)
+ first_results = tuple(service.results)
+ first_audits = tuple(sink.events)
+ first_sql = tuple(context.sql_logs())
+ assert first_queries, 'observe generated bootstrap lookup even with logging disabled'
+ first_intent = first_queries[0][1].comment
+ first_purpose = first_queries[0][1].purpose
+ assert first_intent and first_intent.strip(), 'bootstrap owns a nonblank lookup comment'
+ assert first_purpose and first_purpose.strip(), 'bootstrap owns a nonblank lookup purpose'
+ assert len(first_requests) == len(first_results) == len(first_audits) <= 1
+ for request, result, event in zip(first_requests, first_results, first_audits):
+ assert request.comment() and request.comment().strip()
+ assert chain(request.mutation_lineage) == [('Platform', 1, request.comment())]
+ assert result.comment == request.comment()
+ assert len(result.statements) == 2 and result.affected_rows == 1
+ assert event.actor == 'teaql-generated-bootstrap' and event.category == 'runtime-bootstrap'
+ assert event.entity == 'Platform' and scalar(event.entity_id) == 1
+ assert chain(event.trace_chain) == [('Platform', 1, request.comment().replace('1', '[REDACTED]'))], 'safe bootstrap audit preserves responsibility while redacting identity text'
+ for statement in result.statements:
+ assert statement.comment == request.comment()
+ assert statement.execution_outcome == 'success'
+ assert chain(statement.mutation_lineage) == chain(request.mutation_lineage)
+ assert result.statements[1].result_count == 1
+ assert result.statements[1].purpose and result.statements[1].purpose.strip()
+ # The sink already checks that audit followed commit. Independently verify
+ # the resulting row through a read-only connection, not the runtime cache.
+ with sqlite3.connect('file:' + os.path.abspath(path) + '?mode=ro', uri=True) as connection:
+ assert connection.execute('SELECT id, version FROM platform_data WHERE id = 1').fetchall() == [(1, 1)]
+ reset(service, sink, context)
+ await context.ensure_schema()
+ assert service.raw_requests == [] and service.results == [] and sink.events == [], 'repeat bootstrap cannot write or audit'
+ assert service.queries, 'repeat bootstrap must inspect persisted root'
+ for _, metadata in (*first_queries, *service.queries):
+ assert metadata.comment == first_intent, 'generated bootstrap lookup intent is stable'
+ assert metadata.purpose == first_purpose
+ assert metadata.execution_outcome == 'success'
+ assert [node.kind for node in metadata.trace_chain] == ['operation', 'request', 'provider', 'sql']
+ assert metadata.trace_chain[0].name == 'Platform'
+ if logging:
+ assert len(first_sql) == len(first_queries) + 2 * len(first_requests)
+ assert len(context.sql_logs()) == len(service.queries)
+ for entry in (*first_sql, *context.sql_logs()):
+ select = entry.operation.name.lower() == 'select'
+ assert [node.kind for node in entry.trace_path] == ['operation', 'request' if select else 'entity', 'provider', 'sql']
+ assert entry.comment and entry.comment.strip()
+ if select:
+ assert entry.purpose and entry.purpose.strip()
+ if entry.mutation_lineage:
+ assert len(first_audits) == 1
+ assert chain(entry.mutation_lineage) == chain(first_audits[0].trace_chain)
+ assert entry.audit_reason == first_audits[0].trace_chain[0].comment
+ else:
+ assert first_sql == () and context.sql_logs() == []
+ assert context.user_identifier() == 'trace-example-user', 'bootstrap must restore caller identity'
+ print('BOOTSTRAP INTENT EVIDENCE ' + json.dumps({'logging': logging,
+ 'first_writes': len(first_requests), 'repeat_writes': 0,
+ 'comment': first_intent, 'purpose': first_purpose,
+ 'queries': len(first_queries) + len(service.queries)}))
+ reset(service, sink, context)
+ return context, service, sink
+
+
+async def main():
+ identity_controls()
+ path = os.environ['TEAQL_TRACE_CHAIN_DB']
+ label = 'TRACE-' + uuid.uuid4().hex[:16]
+ await verify_bootstrap(path + '.bootstrap-off', False)
+ context, service, sink = await verify_bootstrap(path, True)
+ policy_calls = []
+ def resolve_policy(request_key):
+ policy_calls.append(request_key)
+ return None # Observe the configured resolver; retain generated-default governance.
+ context.with_mutation_policy_registry(DelegatingMutationPolicyRegistry(resolve_policy))
+ platforms = await Q.platforms().with_id_is(1).limit(1).comment(
+ 'what: reuse bootstrap root').purpose('why: attach trace example').execute_for_list(context)
+ assert len(platforms) == 1
+ platform = platforms[0]
+
+ rejected = make_graph(context, platform, label + '-rejected')
+ rejected[2].audit_as('valid child reason cannot substitute for root')
+ reset(service, sink, context)
+ policy_calls.clear()
+ for reason in (None, '', ' \t'):
+ try:
+ if reason is None:
+ await rejected[0].save(context)
+ else:
+ await rejected[0].audit_as(reason).save(context)
+ except RequestIntentError as error:
+ assert error.code == 'REQUEST_COMMENT_REQUIRED'
+ else:
+ raise AssertionError('missing root reason was accepted')
+ assert service.begins == 0 and service.requests == [] and sink.events == [] and policy_calls == []
+ print('PASS: missing/empty/blank root intent rejected before any transaction, despite valid child intent')
+
+ graph = make_graph(context, platform, label)
+ order, item, payment, attempt, shipment, removed = graph
+ reset(service, sink, context)
+ await order.audit_as('prepare complete graph').save(context)
+ created = graph_identities(graph)
+ check_identities(created, [(entity, identity) for entity, identity, _ in service.requests], 'created commands')
+ check_identities(created, physical_identities(service, context), 'created physical SQL')
+ check_identities(created, [(event.entity, scalar(event.entity_id)) for event in sink.events], 'created audit')
+ assert E.payment(payment).id().eval() == E.customer_order(order).id().eval(), 'fixture must exercise equal IDs of different types'
+ order.update_description('Submitted trace example')
+ item.update_name('Verified available item')
+ payment.update_reference_code(label + '-payment-authorized').audit_as('authorize payment')
+ attempt.update_reference_code(label + '-attempt-authorized')
+ shipment.update_reference_code(label + '-shipment-dispatched').audit_as('dispatch shipment')
+ removed.mark_for_deletion().audit_as('remove unavailable item')
+ reset(service, sink, context)
+ await order.audit_as('submit order').save(context)
+ want_identities = graph_identities(graph)
+ command_identities = [(entity, identity) for entity, identity, _ in service.requests]
+ statement_identities = physical_identities(service, context)
+ committed_identities = [(event.entity, scalar(event.entity_id)) for event in sink.events]
+ check_identities(want_identities, command_identities, 'actual commands')
+ check_identities(want_identities, statement_identities, 'command-bound physical SQL')
+ check_identities(want_identities, committed_identities, 'committed audit')
+ print('GRAPH IDENTITY EVIDENCE ' + json.dumps({
+ name: [{'entity': entity, 'id': identity} for entity, identity in values]
+ for name, values in [('expected', want_identities), ('commands', command_identities),
+ ('physical', statement_identities), ('audit', committed_identities)]
+ }, sort_keys=True))
+ expected = expected_graph(graph, 'submit order')
+ commands = {(entity, identity): nodes for entity, identity, nodes in service.requests}
+ metadata = {(entity, identity): chain(item.mutation_lineage)
+ for (entity, identity, _), item in zip(service.requests, service.results)}
+ audits = {(event.entity, scalar(event.entity_id)): chain(event.trace_chain) for event in sink.events}
+ assert len(service.raw_requests) == len(service.requests) == len(service.results) == len(sink.events) == 6
+ assert commands == metadata == audits == expected
+ for request, record in zip(service.raw_requests, service.results):
+ assert record.trace_chain[0].name == 'CustomerOrder'
+ assert [node.kind for node in record.trace_chain] == ['operation', 'entity', 'provider', 'sql']
+ assert all(node.kind == 'auditReason' for node in record.mutation_lineage)
+ assert len(record.statements) == 2
+ write, read = record.statements
+ assert not write.statements and not read.statements
+ assert request.comment() == 'submit order'
+ assert read.comment == write.comment == request.comment(), 'derived readback inherits actual mutation comment'
+ assert read.audit_reason == write.audit_reason == request.comment(), 'derived readback inherits root audit reason'
+ assert read.purpose == 'verify the persisted mutation result', 'derived readback owns verification purpose'
+ assert read.mutation_lineage == write.mutation_lineage == record.mutation_lineage
+ assert read.result_count == 1 and read.affected_rows is None
+ assert read.execution_outcome == 'success'
+ assert [node.kind for node in read.trace_chain] == ['operation', 'request', 'provider', 'sql']
+ assert [node.name for node in read.trace_chain] == ['CustomerOrder', 'CustomerOrder', 'sqlite', 'select']
+ assert read.trace_chain[0].comment == 'query'
+ assert write.ended_at <= read.started_at, 'physical write precedes its authoritative readback'
+ print('TC-REQ-10 PYTHON READBACK EVIDENCE ' + json.dumps({
+ 'case': 'TC-REQ-10', 'writes': 6, 'readbacks': 6,
+ 'statements': [readback_fact(entry) for entry in context.sql_logs()],
+ }, sort_keys=True))
+ print('PASS: six writes plus six successful readbacks; root request paths; no duplicate SQL facts')
+ print('PASS: normative six items; assigned typed identity; branch/deletion reasons; request/SQL/audit boundaries')
+
+ loaded = await (Q.customer_orders().with_id_is(E.customer_order(order).id().eval())
+ .select_order_item_list_with(Q.order_items().limit(10))
+ .select_payment_list_with(Q.payments().limit(10)
+ .select_payment_attempt_list_with(Q.payment_attempts().limit(10)))
+ .select_shipment_list_with(Q.shipments().limit(10)).limit(1)
+ .comment('what: reload committed trace graph').purpose('why: verify generated Q and loaded E').execute_for_one(context))
+ assert E.customer_order(loaded).order_number().eval() == label
+ assert E.customer_order(loaded).order_item_list().size().eval() == 1
+ assert E.customer_order(loaded).payment_list().size().eval() == 1
+ deleted = await Q.order_items().with_id_is(E.order_item(removed).id().eval()).deleted_rows_only().limit(1).comment(
+ 'what: verify retained deletion').purpose('why: test graph save soft deletion').execute_for_one(context)
+ assert E.order_item(deleted).version().eval() < 0
+ print('PASS: generated Q graph reload; loaded E; deleted row retained and hidden')
+
+ reset(service, sink, context)
+ deep = await (Q.payment_attempts().with_id_is(E.payment_attempt(attempt).id().eval())
+ .select_payment_with(Q.payments().limit(1).select_customer_order_with(
+ Q.customer_orders().limit(1).select_platform_with(Q.platforms().limit(1))))
+ .limit(1).comment('what: inspect three relation levels').purpose('why: retain root query intent').execute_for_one(context))
+ assert E.payment_attempt(deep).reference_code().eval() == label + '-attempt-authorized'
+ entries = context.sql_logs()
+ assert len(entries) == 4
+ for depth, entry in enumerate(entries):
+ assert entry.comment == 'what: inspect three relation levels'
+ assert entry.purpose == 'why: retain root query intent'
+ assert entry.trace_path[0].name == 'PaymentAttempt'
+ assert [node.name for node in entry.trace_path[2:-2]] == ['payment', 'customer_order', 'platform'][:depth]
+ route = [('payment', 'PaymentAttempt.payment'), ('customer_order', 'Payment.customer_order'),
+ ('platform', 'CustomerOrder.platform')]
+ assert [(node.kind, node.name, node.comment) for node in entry.trace_path] == [
+ ('operation', 'PaymentAttempt', 'query'), ('request', 'PaymentAttempt', ''),
+ *[('relation', name, detail) for name, detail in route[:depth]],
+ ('provider', 'sqlite', ''), ('sql', 'select', '')], 'canonical generated path at every physical boundary'
+ print('PASS: generated query traverses three actual relations with inherited intent')
+
+ graphs = [make_graph(context, platform, label + '-' + name) for name in ('first', 'second')]
+ reset(service, sink, context)
+ await asyncio.gather(*(g[0].audit_as('independent ' + name).save(context)
+ for g, name in zip(graphs, ('first', 'second'))))
+ assert service.finishes == ['commit', 'commit'] and len(sink.events) == 12
+ roots = {E.customer_order(g[0]).id().eval(): 'independent ' + name
+ for g, name in zip(graphs, ('first', 'second'))}
+ assert all(chain(event.trace_chain) == [('CustomerOrder', chain(event.trace_chain)[0][1],
+ roots[chain(event.trace_chain)[0][1]])] for event in sink.events)
+ concurrent_physical = physical_identities(service, context)
+ for graph in graphs:
+ root_id = E.customer_order(graph[0]).id().eval()
+ expected = graph_identities(graph)
+ indices = [index for index, (_, _, nodes) in enumerate(service.requests) if nodes[0][1] == root_id]
+ check_identities(expected, [service.requests[index][:2] for index in indices], 'concurrent commands')
+ check_identities(expected, [concurrent_physical[index] for index in indices], 'concurrent physical SQL')
+ check_identities(expected, [(event.entity, scalar(event.entity_id)) for event in sink.events
+ if chain(event.trace_chain)[0][1] == root_id], 'concurrent audit')
+ assert context.require_resource('dataService') is service
+ print('PASS: two concurrent generated graph saves in one real Context remain isolated')
+
+ for failure in ('execute_sql', 'fetch_all_sql'):
+ failed = make_graph(context, platform, label + '-' + failure)
+ failed[2].audit_as('authorize failing payment')
+ reset(service, sink, context)
+ service.fault = failure
+ try:
+ await failed[0].audit_as('attempt failing graph').save(context)
+ except (TransportError, RuntimeError) as error:
+ actual = error.error if isinstance(error, TransportError) else error
+ assert str(actual) == 'synthetic payment attempt failure'
+ else:
+ raise AssertionError('injected failure did not interrupt graph')
+ finally:
+ service.fault = None
+ assert service.finishes == ['rollback'] and sink.events == []
+ evidence = context.sql_logs()
+ assert evidence[-1].execution_outcome == 'failure'
+ assert evidence[-1].mutation_lineage[-1].comment == 'authorize failing payment'
+ if failure == 'fetch_all_sql':
+ assert evidence[-2].execution_outcome == 'success'
+ assert evidence[-2].trace_path[-1].name == 'insert'
+ assert evidence[-1].trace_path[-1].name == 'select'
+ print('PASS: ' + failure + ' failure retains lineage, rolls back and emits zero committed audit')
+ print('PASS: generated library unchanged; all trace example checks passed')
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/examples/trace-chain/model.xml b/examples/trace-chain/model.xml
new file mode 100644
index 0000000..aecdef5
--- /dev/null
+++ b/examples/trace-chain/model.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/trace-chain/mutation_privacy.py b/examples/trace-chain/mutation_privacy.py
new file mode 100644
index 0000000..2000277
--- /dev/null
+++ b/examples/trace-chain/mutation_privacy.py
@@ -0,0 +1,198 @@
+"""Generated save privacy; no generated source inspection or synthetic traces."""
+import asyncio
+import json
+import os
+import uuid
+
+from E import E
+from Q import Q
+from runtime_module import GENERATED_RUNTIME_MODULE
+from teaql.provider.sqlite import create_sqlite_service
+from teaql.runtime import UserContext
+
+from main import AuditSink, ObservedService, new, reset, readback_fact, scalar
+
+
+def complete_lineage(nodes, root_id, reason, boundary):
+ """Empty/redacted-away responsibility is not a successful privacy projection."""
+ assert len(nodes) == 1, boundary + ': complete typed root lineage required'
+ node = nodes[0]
+ assert (node.kind, node.name, node.entity_id, node.comment) == (
+ 'auditReason', 'CustomerOrder', root_id, reason), boundary + ': wrong root responsibility'
+
+
+def assert_private_lineage(service, sink, context, root_id, reason, identities, secrets,
+ expected_bindings, phase):
+ safe_reason = reason
+ for secret in secrets:
+ safe_reason = safe_reason.replace(secret, '[REDACTED]')
+ expected = set(identities)
+ assert len(expected) == len(identities), 'privacy graph needs distinct typed identities'
+ count = len(identities)
+ assert len(service.raw_requests) == len(service.results) == len(sink.events) == count
+ assert service.finishes == ['commit'], 'privacy graph must commit before audit delivery'
+ assert len(context.sql_logs()) == count * 2
+ commands = []
+ seen = set()
+ for request, result in zip(service.raw_requests, service.results):
+ command = request._data
+ identity = (command.entity, scalar(getattr(command, 'id', None) or command.values.get('id')))
+ assert identity in expected and identity not in seen, 'actual privacy command identity mismatch'
+ seen.add(identity)
+ assert request.comment() == result.comment == reason, 'privacy must not rewrite command reason'
+ complete_lineage(request.mutation_lineage, root_id, reason, 'raw privacy command')
+ complete_lineage(result.mutation_lineage, root_id, reason, 'raw provider mutation')
+ assert len(result.statements) == 2 and result.statements[0].affected_rows == 1
+ for field, value in expected_bindings.get(command.entity, {}).items():
+ assert scalar(command.values[field]) == value, 'privacy must not rewrite command bindings'
+ assert value in [scalar(parameter) for parameter in result.statements[0].parameters], 'privacy must not rewrite provider parameters'
+ commands.append({'entity': identity[0], 'id': identity[1], 'comment': request.comment(),
+ 'lineage': readback_nodes(request.mutation_lineage)})
+ assert seen == expected
+ for index, (request, result) in enumerate(zip(service.raw_requests, service.results)):
+ write, read = context.sql_logs()[index * 2:index * 2 + 2]
+ for fact in (write, read):
+ complete_lineage(fact.mutation_lineage, root_id, safe_reason, 'safe privacy SQL')
+ assert fact.comment == fact.audit_reason == safe_reason, 'safe SQL must retain public reason'
+ assert fact.execution_outcome == 'success'
+ assert [node.kind for node in fact.trace_path] == [
+ 'operation', 'entity' if fact is write else 'request', 'provider', 'sql']
+ assert fact.trace_path[0].name == 'CustomerOrder'
+ assert fact.trace_path[2].name == 'sqlite'
+ assert write.trace_path[1].name == request._data.entity
+ assert write.trace_path[-1].name == service.actions[index]
+ assert read.trace_path[-1].name == 'select'
+ assert write.affected_rows == read.result_count == 1 and read.affected_rows is None
+ assert read.purpose == 'verify the persisted mutation result'
+ assert write.ended_at <= read.started_at
+ assert not result.statements[0].statements and not result.statements[1].statements
+ audits = []
+ seen = set()
+ for event in sink.events:
+ identity = (event.entity, scalar(event.entity_id))
+ assert identity in expected and identity not in seen, 'committed privacy audit identity mismatch'
+ seen.add(identity)
+ complete_lineage(event.trace_chain, root_id, safe_reason, 'safe privacy audit')
+ audits.append({'entity': identity[0], 'id': identity[1],
+ 'lineage': readback_nodes(event.trace_chain)})
+ assert seen == expected
+ for secret in secrets:
+ assert secret not in repr(context.sql_logs()) and secret not in repr(sink.events)
+ print('PRIVATE_LINEAGE_OBSERVED ' + json.dumps({
+ 'phase': phase, 'rootId': root_id, 'rawReason': reason, 'safeReason': safe_reason,
+ 'commands': commands, 'sql': [readback_fact(fact) for fact in context.sql_logs()],
+ 'audit': audits}, sort_keys=True))
+ print('PASS Python complete private lineage: raw commands, safe SQL/readback and committed audit')
+
+
+def readback_nodes(nodes):
+ return [{'Kind': node.kind, 'Name': node.name, 'EntityId': node.entity_id,
+ 'Comment': node.comment} for node in nodes]
+
+
+async def main():
+ context = UserContext.new().install(GENERATED_RUNTIME_MODULE)
+ service = ObservedService(create_sqlite_service(os.environ['TEAQL_TRACE_CHAIN_DB']))
+ sink = AuditSink(service)
+ context.insert_resource('dataService', service).with_app_audit_event_sink(sink)
+ context.set_diagnostic_sql_log_sink(type('Silent', (), {'write': lambda self, entry: None})())
+ context.configure_audit_policy('Payment', ['reference_code'])
+ await context.ensure_schema()
+ platform = await Q.platforms().with_id_is(1).limit(1).comment('reuse seeded root').purpose(
+ 'attach generated privacy graph').execute_for_one(context)
+ label = 'PRIVACY-' + uuid.uuid4().hex[:16]
+ # Alphabetic canaries prevent numeric identity redaction from accidentally
+ # making a missing old-value snapshot appear private.
+ alphabetic_nonce = lambda: uuid.uuid4().hex[:16].translate(str.maketrans('0123456789', 'ghijklmnop'))
+ secret = 'PRIVATE-PAYMENT-' + alphabetic_nonce()
+ root = new(Q.customer_orders(), context).update_platform(platform)
+ root.update_order_number(label).update_description('privacy graph')
+ item = new(Q.order_items(), context).update_customer_order(root).update_name('ordinary sibling')
+ payment = new(Q.payments(), context).update_customer_order(root).update_reference_code(secret)
+ root.order_item_list().append(item)
+ root.payment_list().append(payment)
+ for phase in ('create', 'update'):
+ old = secret
+ if phase == 'update':
+ secret = 'PRIVATE-REVISED-' + alphabetic_nonce()
+ root.update_description('revised privacy graph')
+ item.update_name('revised ordinary sibling')
+ payment.update_reference_code(secret)
+ reset(service, sink, context)
+ reason = f'{phase} payment {secret} previous {old}'
+ await root.audit_as(reason).save(context)
+ assert len(service.requests) == len(sink.events) == 3
+ logs = context.sql_logs()
+ assert len(logs) == 6
+ for value in (secret, old):
+ assert value not in repr(logs)
+ assert value not in repr(sink.events)
+ for metadata in service.results:
+ assert len(metadata.statements) == 2
+ assert metadata.comment == reason # raw owned intent is not rewritten
+ assert_private_lineage(service, sink, context, E.customer_order(root).id().eval(), reason,
+ [('CustomerOrder', E.customer_order(root).id().eval()),
+ ('OrderItem', E.order_item(item).id().eval()), ('Payment', E.payment(payment).id().eval())],
+ [secret, old], {'Payment': {'reference_code': secret}}, phase)
+ loaded = await Q.payments().with_id_is(E.payment(payment).id().eval()).limit(1).comment(
+ 'reload authoritative payment').purpose('verify safe logs do not alter business data').execute_for_one(context)
+ assert E.payment(loaded).reference_code().eval() == secret
+ assert E.payment(loaded).version().eval() == (1 if phase == 'create' else 2)
+ await Q.customer_orders().with_id_is(E.customer_order(root).id().eval()).limit(1).comment(
+ f'independent {secret} {old}').purpose('ensure no ambient redaction values').execute_for_one(context)
+ assert secret in context.sql_logs()[-1].comment and old in context.sql_logs()[-1].comment
+ print(f'PASS: Python generated mutation privacy {phase}; 3 writes/3 reads/3 audits; independent next request')
+
+ # Deletion binds identity/version, not the private business scalar. Its
+ # loaded original must still protect the whole graph's audit prose.
+ root = await Q.customer_orders().with_id_is(E.customer_order(root).id().eval()).limit(1).select_payment_list_with(
+ Q.payments().limit(2)).comment('reload deletion graph').purpose('retain loaded private scalars').execute_for_one(context)
+ payment = root.payment_list()[0]
+ payment_id = E.payment(payment).id().eval()
+ root.update_description('delete the private payment')
+ payment.mark_for_deletion()
+ reset(service, sink, context)
+ reason = f'delete payment {secret}'
+ await root.audit_as(reason).save(context)
+ assert len(service.requests) == len(sink.events) == 2
+ assert len(context.sql_logs()) == 4
+ assert secret not in repr(context.sql_logs()), 'loaded delete value leaked into SQL intent'
+ assert secret not in repr(sink.events), 'loaded delete value leaked into committed audit'
+ assert all(metadata.comment == reason for metadata in service.results)
+ assert_private_lineage(service, sink, context, E.customer_order(root).id().eval(), reason,
+ [('CustomerOrder', E.customer_order(root).id().eval()), ('Payment', payment_id)],
+ [secret], {}, 'delete')
+ missing = await Q.payments().with_id_is(payment_id).limit(1).comment('verify deleted payment').purpose(
+ 'prove soft delete hides the row').execute_for_one(context)
+ assert missing is None
+ await Q.customer_orders().with_id_is(E.customer_order(root).id().eval()).limit(1).comment(
+ f'independent {secret}').purpose('ensure delete privacy stays invocation-local').execute_for_one(context)
+ assert secret in context.sql_logs()[-1].comment
+ print('PASS: Python generated loaded delete privacy; 2 writes/2 reads/2 audits; independent next request')
+
+ # Changing one field must not lose privacy for another loaded scalar.
+ context.configure_audit_policy('CustomerOrder', ['description'])
+ private_description = 'PRIVATE-DESCRIPTION-' + alphabetic_nonce()
+ root.update_description(private_description)
+ await root.audit_as('prepare unchanged scalar privacy').save(context)
+ root = await Q.customer_orders().with_id_is(E.customer_order(root).id().eval()).limit(1).comment(
+ 'load all order fields').purpose('retain an unchanged private description').execute_for_one(context)
+ root.update_order_number(label + '-revised')
+ reset(service, sink, context)
+ reason = f'renumber {private_description}'
+ await root.audit_as(reason).save(context)
+ assert len(service.requests) == len(sink.events) == 1
+ assert private_description not in repr(context.sql_logs()), 'unchanged loaded scalar leaked into SQL intent'
+ assert private_description not in repr(sink.events), 'unchanged loaded scalar leaked into audit'
+ assert_private_lineage(service, sink, context, E.customer_order(root).id().eval(), reason,
+ [('CustomerOrder', E.customer_order(root).id().eval())], [private_description],
+ {'CustomerOrder': {'order_number': label + '-revised'}}, 'unchanged')
+ reloaded = await Q.customer_orders().with_id_is(E.customer_order(root).id().eval()).limit(1).comment(
+ 'verify unchanged description').purpose('prove provenance did not alter business values').execute_for_one(context)
+ assert E.customer_order(reloaded).description().eval() == private_description
+ assert E.customer_order(reloaded).order_number().eval() == label + '-revised'
+ print('PASS: Python generated unchanged scalar privacy; 1 write/1 read/1 audit')
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/examples/trace-chain/page_stream.py b/examples/trace-chain/page_stream.py
new file mode 100644
index 0000000..3ce877f
--- /dev/null
+++ b/examples/trace-chain/page_stream.py
@@ -0,0 +1,263 @@
+"""Current Assist -> generated Q/E/save -> observed real SQLite pages/cursors."""
+import asyncio
+import copy
+import json
+import os
+import uuid
+
+from E import E
+from Q import Q
+from runtime_module import GENERATED_RUNTIME_MODULE
+from teaql.core.entity import EntityKey
+from teaql.provider.sqlite import create_sqlite_service
+from teaql.runtime import UserContext
+from teaql.runtime.mutation_policy import (
+ DelegatingMutationPolicyRegistry, MutationDecision, MutationPolicyIdentity,
+)
+
+from main import AuditSink, reset, scalar
+from shared_reference import SharedService, assert_writes, load, pending, seed
+
+
+class PageStreamService(SharedService):
+ def __init__(self, service):
+ super().__init__(service)
+ self.count_pause = None
+ self.active_cursors = set()
+ self.opened_cursors = 0
+ self.closed_cursors = 0
+ self.active_driver_cursors = set()
+ self.closed_driver_cursors = 0
+ original_stream_sql = service.transport.stream_sql
+ def observed_driver_sql(compiled, chunk_size):
+ inner = original_stream_sql(compiled, chunk_size)
+ token = object()
+ async def rows():
+ try:
+ async for batch in inner:
+ self.active_driver_cursors.add(token)
+ assert inner.ag_frame is not None
+ yield batch
+ finally:
+ try:
+ await inner.aclose()
+ finally:
+ self.active_driver_cursors.discard(token)
+ self.closed_driver_cursors += 1
+ return rows()
+ service.transport.stream_sql = observed_driver_sql
+
+ async def query(self, context, request):
+ result = await super().query(context, request)
+ if self.count_pause and request.query.aggregates:
+ entered, release = self.count_pause
+ self.count_pause = None
+ entered.set()
+ await asyncio.wait_for(release.wait(), 10)
+ return result
+
+ def query_stream(self, context, request, chunk_size):
+ inner = self.service.query_stream(context, request, chunk_size)
+ token = object()
+ async def observed():
+ self.opened_cursors += 1
+ self.active_cursors.add(token)
+ try:
+ async for chunk in inner:
+ yield chunk
+ finally:
+ try:
+ await inner.aclose()
+ finally:
+ self.active_cursors.remove(token)
+ self.closed_cursors += 1
+ return observed()
+
+
+class RecordingPolicy:
+ identity = MutationPolicyIdentity('page-stream-example', '1', 'page-stream-v1')
+
+ def __init__(self):
+ self.plans = []
+
+ def review(self, context, plan):
+ self.plans.append(plan)
+ return MutationDecision.allowed()
+
+
+def log_row(entry):
+ return {'comment': entry.comment, 'purpose': entry.purpose,
+ 'outcome': entry.execution_outcome, 'sql': entry.sql,
+ 'debugSQL': entry.debug_sql, 'count': entry.result_count,
+ 'path': [(n.kind, n.name, n.entity_id, n.comment) for n in entry.trace_path],
+ 'lineage': [(n.kind, n.name, n.entity_id, n.comment) for n in entry.mutation_lineage]}
+
+
+def evidence(phase, service, sink, context, policy, **facts):
+ print('PAGE_STREAM_OBSERVED ' + json.dumps({
+ 'phase': phase, 'facts': facts,
+ 'sql': [log_row(entry) for entry in context.sql_logs()],
+ 'commands': service.requests,
+ 'versions': service.versions[-len(service.requests):] if service.requests else [],
+ 'plans': [[{'entity': op.entity, 'id': scalar(op.entity_id),
+ 'original_version': op.original_version,
+ 'fields': sorted(op.changed_values)} for op in plan.operations] for plan in policy.plans],
+ 'audit': [(event.entity, scalar(event.entity_id),
+ [(n.kind, n.name, n.entity_id, n.comment) for n in event.trace_chain]) for event in sink.events],
+ }))
+
+
+def clear(service, sink, context, policy):
+ reset(service, sink, context)
+ policy.plans.clear()
+
+
+async def pages(context, platform, service, sink, policy):
+ seeded = [await seed(context, platform) for _ in range(3)]
+ ids = [E.customer_order(row).id().eval() for row in seeded]
+ revised = await load(context, ids[1])
+ await revised.update_description('prior page revision').audit_as('advance paged root').save(context)
+ builder = (Q.customer_orders().with_id_in(*ids).order_by_id_ascending()
+ .select_platform_with(Q.platforms().limit(1))
+ .select_order_item_list_with(Q.order_items().limit(10)).limit(3))
+ executable = builder.comment('load independent page graphs').purpose('verify page mutation boundaries')
+ entered, release = asyncio.Event(), asyncio.Event()
+ service.count_pause = (entered, release)
+ service.reuse_platform = True
+ clear(service, sink, context, policy)
+ task = asyncio.create_task(executable.execute_for_page(context, 1, 2))
+ await asyncio.wait_for(entered.wait(), 10)
+ builder.with_id_is(9223372036854775807).comment('LATE-PAGE-COMMENT').purpose('LATE-PAGE-PURPOSE')
+ release.set()
+ page = await asyncio.wait_for(task, 10)
+ service.reuse_platform = False
+ first, second = page.data
+ assert page.total_count == 3 and [E.customer_order(row).id().eval() for row in page.data] == ids[1:]
+ assert [E.customer_order(row).version().eval() for row in page.data] == [2, 1]
+ assert first._entity_root is not second._entity_root and service.snapshot_uses == 2
+ assert all(not row._entity_root.has_pending(row._teaql_entity_key()) for row in page.data)
+ assert service.requests == [] and policy.plans == [] and sink.events == []
+ assert all(entry.comment == 'load independent page graphs' and entry.purpose == 'verify page mutation boundaries'
+ for entry in context.sql_logs())
+ assert sum('COUNT(' in entry.sql.upper() for entry in context.sql_logs()) == 1
+ before = copy.deepcopy(service.platform_snapshot)
+ evidence('page-rows', service, sink, context, policy, ids=ids, total=page.total_count, versions=[2, 1],
+ independent_ledgers=True, actual_shared_provider_record=True)
+ first.update_description('saved first paged root')
+ second.update_description('saved second paged root')
+ second_key = EntityKey('CustomerOrder', ids[2])
+ for phase, row, expected_version in [('page-save-first', first, 2), ('page-save-second', second, 1)]:
+ clear(service, sink, context, policy)
+ await row.audit_as(phase).save(context)
+ assert_writes(service, sink, context, 1)
+ assert service.requests[0][0] == 'CustomerOrder' and service.versions[-1] == expected_version
+ assert len(policy.plans) == 1 and len(policy.plans[0].operations) == 1
+ assert policy.plans[0].operations[0].original_version == expected_version
+ if row is first:
+ assert pending(second._entity_root, second_key)
+ assert service.platform_snapshot == before
+ evidence(phase, service, sink, context, policy, id=E.customer_order(row).id().eval(),
+ second_pending=pending(second._entity_root, second_key))
+ child = first.order_item_list()[0]
+ child.update_name('only changed paged descendant').audit_as('repair page child')
+ clear(service, sink, context, policy)
+ await first.audit_as('save clean paged ancestor').save(context)
+ assert_writes(service, sink, context, 1)
+ assert service.requests[0][0] == 'OrderItem' and service.versions[-1] == 1
+ assert len(policy.plans) == 1 and len(policy.plans[0].operations) == 1
+ evidence('page-save-child', service, sink, context, policy, id=E.order_item(child).id().eval())
+ reloaded = [await load(context, identity) for identity in ids]
+ assert [E.customer_order(row).version().eval() for row in reloaded] == [1, 3, 2]
+ assert [E.order_item(row.order_item_list()[0]).version().eval() for row in reloaded] == [1, 2, 1]
+ return ids
+
+
+async def count_privacy(context, service, sink, policy, ids):
+ private = 'PAGE-CHILD-PRIVATE-' + uuid.uuid4().hex
+ context.configure_audit_policy('OrderItem', ['name'])
+ clear(service, sink, context, policy)
+ page = await (Q.customer_orders().with_id_in(*ids)
+ .select_order_item_list_with(Q.order_items().with_name_is(private).limit(1)).limit(3)
+ .comment('inspect page referencing ' + private).purpose('count page referencing ' + private)
+ .execute_for_page(context, 0, 2))
+ assert page.total_count == 3
+ logs = context.sql_logs()
+ assert any('COUNT(' in entry.sql.upper() for entry in logs)
+ assert all(private not in str(entry.comment) + str(entry.purpose) + entry.debug_sql for entry in logs)
+ assert all('[REDACTED]' in entry.comment and '[REDACTED]' in entry.purpose for entry in logs)
+ evidence('page-count-privacy', service, sink, context, policy, total=3, canary_absent=True)
+ clear(service, sink, context, policy)
+ await Q.customer_orders().with_id_is(ids[0]).limit(1).comment(
+ 'independent intent ' + private).purpose('no ambient privacy').execute_for_list(context)
+ assert context.sql_logs()[0].comment.endswith(private)
+ evidence('page-independent-privacy', service, sink, context, policy, no_ambient_privacy=True)
+
+
+async def streams(context, service, sink, policy, ids):
+ clear(service, sink, context, policy)
+ policy_calls = []
+ context.with_request_policy(lambda query: policy_calls.append(query.entity))
+ builder = Q.customer_orders().with_id_in(*ids).order_by_id_ascending().limit(3)
+ stream = builder.comment('first captured stream').purpose('verify delayed scalar rows').execute_for_stream(context, chunk_size=1)
+ other = (Q.customer_orders().with_id_in(*ids).order_by_id_ascending().limit(3).comment('second captured stream')
+ .purpose('verify overlapping cursor intent').execute_for_stream(context, chunk_size=1))
+ assert policy_calls == ['CustomerOrder', 'CustomerOrder'] and service.active_cursors == set()
+ builder.with_id_is(9223372036854775807).comment('LATE-STREAM-COMMENT').purpose('LATE-STREAM-PURPOSE')
+ first, other_first = await anext(stream), await anext(other)
+ assert len(service.active_cursors) == 2 and context._graph_session is None
+ assert len(service.active_driver_cursors) == 2
+ assert context._audit_journal is None
+ await Q.customer_orders().with_id_is(ids[0]).limit(1).comment('independent live query').purpose(
+ 'no ambient stream intent').execute_for_one(context)
+ assert len(service.active_cursors) == 2
+ assert len(service.active_driver_cursors) == 2
+ await other.aclose()
+ remaining = [row async for row in stream]
+ await stream.aclose()
+ rows = [first, *remaining]
+ assert [E.customer_order(row).id().eval() for row in rows] == ids
+ assert E.customer_order(other_first).id().eval() == ids[0]
+ assert len({id(row._entity_root) for row in rows}) == 3
+ assert service.active_cursors == set() and service.opened_cursors == service.closed_cursors == 2
+ assert service.active_driver_cursors == set() and service.closed_driver_cursors == 2
+ by_comment = {entry.comment: entry for entry in context.sql_logs()}
+ assert set(by_comment) == {'first captured stream', 'second captured stream', 'independent live query'}
+ assert by_comment['first captured stream'].result_count == 3
+ assert by_comment['first captured stream'].execution_outcome == 'success'
+ assert by_comment['second captured stream'].execution_outcome == 'cancelled'
+ assert by_comment['second captured stream'].result_count == 1
+ assert all(entry.trace_path[0].name == 'CustomerOrder' for entry in context.sql_logs())
+ evidence('streams-overlap', service, sink, context, policy, ids=ids, two_live_cursors=True,
+ two_live_driver_cursors=True, closed_driver_cursors=service.closed_driver_cursors,
+ opened=service.opened_cursors, closed=service.closed_cursors, independent_ledgers=True)
+ rows[0].update_description('saved streamed root')
+ rows[1].update_description('pending streamed root')
+ key = EntityKey('CustomerOrder', ids[1])
+ clear(service, sink, context, policy)
+ await rows[0].audit_as('save one streamed row').save(context)
+ assert_writes(service, sink, context, 1)
+ assert pending(rows[1]._entity_root, key) and service.requests[0][:2] == ('CustomerOrder', ids[0])
+ assert len(policy.plans) == 1 and len(policy.plans[0].operations) == 1
+ evidence('stream-save-one', service, sink, context, policy, id=ids[0], untouched_pending_id=ids[1])
+ persisted = await load(context, ids[1])
+ assert E.customer_order(persisted).description().eval() != 'pending streamed root'
+
+
+async def main():
+ context = UserContext.new().install(GENERATED_RUNTIME_MODULE)
+ service = PageStreamService(create_sqlite_service(os.environ['TEAQL_TRACE_CHAIN_PAGE_STREAM_DB']))
+ sink, policy = AuditSink(service), RecordingPolicy()
+ context.insert_resource('dataService', service).with_app_audit_event_sink(sink)
+ context.with_mutation_policy_registry(DelegatingMutationPolicyRegistry(lambda _: policy))
+ context.set_diagnostic_sql_log_sink(type('Silent', (), {'write': lambda self, entry: None})())
+ await context.ensure_schema()
+ platform = await Q.platforms().with_id_is(1).limit(1).comment('reuse root for pages').purpose(
+ 'attach generated page fixtures').execute_for_one(context)
+ ids = await pages(context, platform, service, sink, policy)
+ await count_privacy(context, service, sink, policy, ids)
+ await streams(context, service, sink, policy, ids)
+ print('PASS: Python generated page/stream 8 phases, real SQL/plans/audit, explicit cursor close')
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/examples/trace-chain/relation_aggregate.py b/examples/trace-chain/relation_aggregate.py
new file mode 100644
index 0000000..a02f168
--- /dev/null
+++ b/examples/trace-chain/relation_aggregate.py
@@ -0,0 +1,241 @@
+"""Assist-guided generated Q/E/save: root and nested aggregate provenance."""
+import asyncio
+import copy
+import json
+import os
+import uuid
+from types import SimpleNamespace
+
+from E import E
+from Q import Q
+from runtime_module import GENERATED_RUNTIME_MODULE
+from teaql.provider.sqlite import create_sqlite_service
+from teaql.runtime import UserContext, DelegatingMutationPolicyRegistry
+
+from main import AuditSink, new, reset
+from page_stream import RecordingPolicy, log_row
+from shared_reference import SharedService
+
+
+async def main():
+ context = UserContext.new().install(GENERATED_RUNTIME_MODULE)
+ native = create_sqlite_service(os.environ['TEAQL_TRACE_CHAIN_AGGREGATE_DB'])
+ service = SharedService(native)
+ sink, policy = AuditSink(service), RecordingPolicy()
+ context.insert_resource('dataService', service).with_app_audit_event_sink(sink)
+ context.with_mutation_policy_registry(DelegatingMutationPolicyRegistry(lambda _: policy))
+ diagnostic = []
+ context.set_diagnostic_sql_log_sink(SimpleNamespace(write=diagnostic.append))
+ await context.ensure_schema()
+ native.schema_provider.get_entity('OrderItem').audit_mask_fields(['name'])
+ platform = await (Q.platforms().with_id_is(1).limit(1).comment('reuse bootstrap root')
+ .purpose('prepare aggregate example').execute_for_one(context))
+ secret = 'PRIVATE-AGGREGATE-' + uuid.uuid4().hex
+ order = new(Q.customer_orders(), context).update_platform(platform)
+ order.update_order_number('AGG-' + uuid.uuid4().hex).update_description('aggregate original')
+ for name in (secret, 'public item'):
+ order.order_item_list().append(new(Q.order_items(), context)
+ .update_customer_order(order).update_name(name))
+ payment = (new(Q.payments(), context).update_customer_order(order)
+ .update_reference_code('AGG-PAY-' + uuid.uuid4().hex))
+ order.payment_list().append(payment)
+ await order.audit_as('seed aggregate graph').save(context)
+ order_id, payment_id = E.customer_order(order).id().eval(), E.payment(payment).id().eval()
+ reads, raw, fault = [], [], [False]
+ original_fetch = native.transport.fetch_all_sql
+
+ async def capture(compiled):
+ reads.append(compiled.sql)
+ if fault[0] and 'COUNT(' in compiled.sql.upper():
+ raise RuntimeError('synthetic aggregate failure')
+ return await original_fetch(compiled)
+
+ native.transport.fetch_all_sql = capture
+ original_record = context._record_metadata_log
+
+ def capture_metadata(metadata, *args, **kwargs):
+ # Read-only observation of runtime-produced frames, even with logs off.
+ raw.append(copy.deepcopy(metadata))
+ return original_record(metadata, *args, **kwargs)
+
+ context._record_metadata_log = capture_metadata
+
+ def raw_rows():
+ return [{'sql': entry.parameterized_sql, 'comment': entry.comment,
+ 'purpose': entry.purpose, 'outcome': entry.execution_outcome,
+ 'path': [(n.kind, n.name, n.comment) for n in entry.trace_chain]} for entry in raw]
+
+ def assert_raw(root, routes, comment, purpose, failed=False):
+ assert len(raw) == len(reads) == len(routes), 'every physical SQL needs metadata'
+ actual_routes = []
+ for entry, sql in zip(raw, reads):
+ assert entry.parameterized_sql == sql
+ assert entry.comment == comment and entry.purpose == purpose
+ route = [(node.name, node.comment) for node in entry.trace_chain if node.kind == 'relation']
+ actual_routes.append(route)
+ assert [(node.kind, node.name, node.comment) for node in entry.trace_chain] == [
+ ('operation', root, 'query'), ('request', root, ''),
+ *[('relation', name, detail) for name, detail in route],
+ ('provider', 'sqlite', ''), ('sql', 'select', '')]
+ assert all(name not in ('filtered_count', 'save') for name, _ in route)
+ assert sorted(actual_routes) == sorted(routes), (actual_routes, routes)
+ assert [entry.execution_outcome for entry in raw] == [
+ *['success'] * (len(raw) - 1), 'failure' if failed else 'success']
+
+ def query(nested, filtered=secret):
+ # Each aggregate has its own child builder; no hand-written application SQL.
+ request = (Q.customer_orders().with_id_is(order_id).limit(1)
+ .count_order_items_with('filtered_count', Q.order_items().with_name_is(filtered).limit(10))
+ .count_order_items_as('save')
+ .select_order_item_list_with(Q.order_items().limit(10))
+ .select_payment_list_with(Q.payments().limit(10).select_customer_order_with(
+ Q.customer_orders().with_id_is(0).limit(1))))
+ if nested:
+ request = (Q.payments().with_id_is(payment_id).limit(1)
+ .select_customer_order_with(request))
+ return request.comment('inspect ' + secret).purpose('verify aggregate provenance')
+
+ def clear():
+ reset(service, sink, context)
+ diagnostic.clear()
+ reads.clear()
+ raw.clear()
+ policy.plans.clear()
+
+ for logging in (True, False):
+ context.enable_all_sql_log() if logging else context.disable_sql_log()
+ hidden = (await (Q.payments().with_id_is(payment_id).limit(1)
+ .select_customer_order_with(Q.customer_orders().with_id_is(0).limit(1))
+ .comment('load filtered forward identity').purpose('distinguish unfetched detail from null')
+ .execute_for_list(context)))[0]
+ identity = E.payment(hidden).customer_order().eval()
+ assert identity is not None
+ assert E.customer_order(identity).id().eval() == order_id
+
+ def assert_unfetched():
+ try:
+ E.customer_order(identity).description().eval()
+ except RuntimeError as error:
+ assert type(error).__name__ == 'TeaQLNotLoadedError', error
+ assert 'description' in str(error), error
+ else:
+ raise AssertionError('unfetched description became loaded-null')
+
+ assert_unfetched()
+ visible = (await (Q.payments().with_id_is(payment_id).limit(1)
+ .select_customer_order_with(Q.customer_orders().with_id_is(order_id).limit(1))
+ .comment('load independent full reference').purpose('verify edge-owned load boundaries')
+ .execute_for_list(context)))[0]
+ full = E.payment(visible).customer_order().eval()
+ assert E.customer_order(full).description().eval() is not None
+ assert_unfetched()
+ print('FORWARD_NOTLOADED_OBSERVED ' + json.dumps({'logging': logging, 'id': order_id}))
+ for nested in (False, True):
+ clear()
+ rows = await query(nested).execute_for_list(context)
+ assert len(rows) == 1
+ loaded = E.payment(rows[0]).customer_order().eval() if nested else rows[0]
+ assert E.customer_order(loaded).id().eval() == order_id
+ assert E.customer_order(loaded).order_item_list().size().eval() == 2
+ assert E.customer_order(loaded).payment_list().size().eval() == 1
+ # Filtering detail must not erase either list membership or its real FK.
+ child = loaded.payment_list()[0]
+ unfetched = E.payment(child).customer_order().eval()
+ assert E.customer_order(unfetched).id().eval() == order_id
+ try:
+ E.customer_order(unfetched).description().eval()
+ except RuntimeError as error:
+ assert type(error).__name__ == 'TeaQLNotLoadedError', error
+ else:
+ raise AssertionError('aggregate child detail became loaded-null')
+ assert loaded.has_query_projection('filtered_count')
+ assert loaded.query_projection('filtered_count') == 1
+ assert loaded.query_projection('save') == 2 and callable(loaded.save)
+ assert not loaded.has_query_projection('id')
+ try:
+ loaded.query_projection('absent')
+ except KeyError:
+ pass
+ else:
+ raise AssertionError('missing aggregate silently became a value')
+ assert len(reads) == (7 if nested else 6), reads
+ prefix = [('customer_order', 'Payment.customer_order')] if nested else []
+ item_route = prefix + [('order_item_list', 'CustomerOrder.order_item_list')]
+ payment_route = prefix + [('payment_list', 'CustomerOrder.payment_list')]
+ routes = [[], *([prefix] if nested else []), item_route, payment_route,
+ payment_route + [('customer_order', 'Payment.customer_order')], item_route, item_route]
+ assert_raw('Payment' if nested else 'CustomerOrder', routes,
+ 'inspect ' + secret, 'verify aggregate provenance')
+ assert service.requests == [] and policy.plans == [] and sink.events == []
+ if logging:
+ assert len(context.sql_logs()) == len(diagnostic) == len(reads)
+ for entry, metadata in zip(context.sql_logs(), raw):
+ assert entry.trace_path[0].name == ('Payment' if nested else 'CustomerOrder')
+ assert entry.purpose == 'verify aggregate provenance'
+ assert secret not in json.dumps(log_row(entry))
+ assert [(n.kind, n.name, n.comment) for n in entry.trace_path] == [
+ (n.kind, n.name, n.comment) for n in metadata.trace_chain]
+ aggregates = [entry for entry in context.sql_logs() if 'COUNT(' in entry.sql.upper()]
+ assert len(aggregates) == 2
+ for entry in aggregates:
+ relations = [node.name for node in entry.trace_path if node.kind == 'relation']
+ assert relations == (['customer_order', 'order_item_list'] if nested else ['order_item_list']), relations
+ assert entry.trace_path[-1].kind == 'sql'
+ else:
+ assert context.sql_logs() == diagnostic == []
+ print('AGGREGATE_OBSERVED ' + json.dumps({'nested': nested, 'logging': logging,
+ 'count': loaded.query_projection('filtered_count'), 'provider_calls': len(reads),
+ 'sql': [log_row(entry) for entry in context.sql_logs()],
+ 'raw': raw_rows(),
+ 'membership': {'items': 2, 'payments': 1, 'foreign_id': order_id,
+ 'target_detail': 'NotLoaded'}}))
+ clear()
+ await loaded.update_description('aggregate saved ' + uuid.uuid4().hex).audit_as(
+ 'save model field without projection aliases').save(context)
+ assert len(service.requests) == len(sink.events) == len(policy.plans) == 1
+ operation = policy.plans[0].operations[0]
+ assert set(operation.changed_values) == {'description'}, operation.changed_values
+ assert loaded.query_projection('save') == 2
+ print('AGGREGATE_SAVE ' + json.dumps({'nested': nested, 'logging': logging,
+ 'commands': len(service.requests), 'audits': len(sink.events),
+ 'fields': sorted(operation.changed_values), 'projection': loaded.query_projection('save')}))
+ fresh = (await query(nested, 'NO-MATCH-' + uuid.uuid4().hex).execute_for_list(context))[0]
+ fresh = E.payment(fresh).customer_order().eval() if nested else fresh
+ assert E.customer_order(fresh).description().eval() == E.customer_order(loaded).description().eval()
+ assert fresh.has_query_projection('filtered_count') and fresh.query_projection('filtered_count') == 0
+
+ clear()
+ fault[0] = True
+ try:
+ await query(False).execute_for_list(context)
+ except Exception as error:
+ assert 'synthetic aggregate failure' in str(error), error
+ else:
+ raise AssertionError('aggregate provider failure was swallowed')
+ finally:
+ fault[0] = False
+ # Aggregates precede eager relation loads; failure must stop those later reads.
+ assert len(reads) == 2 and service.requests == [] and sink.events == []
+ assert_raw('CustomerOrder', [[], [('order_item_list', 'CustomerOrder.order_item_list')]],
+ 'inspect ' + secret, 'verify aggregate provenance', failed=True)
+ if logging:
+ assert context.sql_logs()[-1].execution_outcome == 'failure'
+ assert all(secret not in json.dumps(log_row(entry)) for entry in context.sql_logs())
+ else:
+ assert context.sql_logs() == diagnostic == []
+ print('AGGREGATE_FAILURE ' + json.dumps({'logging': logging, 'provider_calls': len(reads),
+ 'sql': [log_row(entry) for entry in context.sql_logs()], 'raw': raw_rows()}))
+ clear()
+ await (Q.platforms().with_id_is(1).limit(1).comment('independent next query')
+ .purpose('prove scope restored').execute_for_list(context))
+ assert len(reads) == 1
+ assert_raw('Platform', [[]], 'independent next query', 'prove scope restored')
+ assert all(entry.comment == 'independent next query' and entry.trace_path[0].name == 'Platform'
+ for entry in context.sql_logs())
+ print('AGGREGATE_INDEPENDENT ' + json.dumps({'logging': logging, 'raw': raw_rows(),
+ 'sql': [log_row(entry) for entry in context.sql_logs()]}))
+ print('PASS: Python generated aggregates; 4 root/nested/logging cases; isolated save; 2 failures restored')
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/examples/trace-chain/shared_reference.py b/examples/trace-chain/shared_reference.py
new file mode 100644
index 0000000..95cbbde
--- /dev/null
+++ b/examples/trace-chain/shared_reference.py
@@ -0,0 +1,264 @@
+"""App-owned generated Q/E/save acceptance; no generated-source API discovery."""
+import asyncio
+import copy
+import json
+import os
+import uuid
+
+from E import E
+from Q import Q
+from runtime_module import GENERATED_RUNTIME_MODULE
+from teaql.core.entity import EntityKey
+from teaql.provider.sqlite import create_sqlite_service
+from teaql.runtime import UserContext
+
+from main import AuditSink, ObservedService, ObservedTransaction, new, reset, scalar
+
+
+class VersionTransaction(ObservedTransaction):
+ async def mutate(self, context, request):
+ self.owner.versions.append(getattr(request._data, 'expected_version_val', None))
+ return await super().mutate(context, request)
+
+
+class SharedService(ObservedService):
+ def __init__(self, service):
+ super().__init__(service)
+ self.reuse_platform = False
+ self.platform_snapshot = None
+ self.snapshot_uses = 0
+ self.pause = None
+ self.versions = []
+
+ async def query(self, context, request):
+ result = await self.service.query(context, request)
+ if self.reuse_platform:
+ for row in result.rows:
+ candidate = row.get('platform')
+ if isinstance(candidate, dict):
+ if self.platform_snapshot is None:
+ self.platform_snapshot = candidate
+ else:
+ assert candidate == self.platform_snapshot
+ row['platform'] = self.platform_snapshot
+ self.snapshot_uses += 1
+ return result
+
+ async def begin(self, context):
+ pause, self.pause = self.pause, None
+ if pause:
+ entered, release = pause
+ entered.set()
+ await asyncio.wait_for(release.wait(), 10)
+ self.begins += 1
+ return VersionTransaction(self, await self.service.begin(context))
+
+
+def pending(ledger, key):
+ return key in dict(ledger.current_change_set().changes()) or key in ledger.new_keys() or key in ledger.deleted_keys()
+
+
+def observe(scenario, service, sink, context):
+ sql = [entry for entry in context.sql_logs() if entry.mutation_lineage]
+ print('OWNERSHIP EVIDENCE ' + json.dumps({
+ 'scenario': scenario,
+ 'commands': service.requests,
+ 'versions': service.versions[-len(service.requests):] if service.requests else [],
+ 'sql': [{'outcome': entry.execution_outcome,
+ 'comment': entry.comment, 'debugSQL': entry.debug_sql,
+ 'path': [(node.kind, node.name, node.entity_id, node.comment) for node in entry.trace_path],
+ 'lineage': [(node.kind, node.name, node.entity_id, node.comment) for node in entry.mutation_lineage]}
+ for entry in sql],
+ 'audit': [(event.entity, scalar(event.entity_id),
+ [(node.kind, node.name, node.entity_id, node.comment) for node in event.trace_chain])
+ for event in sink.events],
+ }))
+
+
+async def seed(context, platform, children=1):
+ label = 'OWN-' + uuid.uuid4().hex
+ order = new(Q.customer_orders(), context).update_platform(platform)
+ order.update_order_number(label).update_description('original ' + label)
+ for index in range(children):
+ child = new(Q.order_items(), context).update_customer_order(order).update_name(label + str(index))
+ order.order_item_list().append(child)
+ await order.audit_as('prepare ownership graph').save(context)
+ return order
+
+
+async def load(context, entity_id):
+ return await (Q.customer_orders().with_id_is(entity_id)
+ .select_order_item_list_with(Q.order_items().limit(10)).limit(1)
+ .comment('load ownership graph').purpose('verify graph mutation ownership')
+ .execute_for_one(context))
+
+
+def assert_writes(service, sink, context, count):
+ sql = [entry for entry in context.sql_logs() if entry.mutation_lineage and not entry.operation.is_select()]
+ readbacks = [entry for entry in context.sql_logs() if entry.mutation_lineage and entry.operation.is_select()]
+ assert len(readbacks) == count
+ for write, read in zip(sql, readbacks):
+ assert read.mutation_lineage == write.mutation_lineage
+ assert read.trace_path[1].kind == 'request'
+ assert read.trace_path[1].name == 'CustomerOrder'
+ assert len(service.requests) == len(sink.events) == len(sql) == count, 'only reached changed entities emit commands'
+ for request, entry, event in zip(service.requests, sql, sink.events):
+ entity, identity, lineage = request
+ actual = [(node.name or node.entity_type, node.entity_id, node.comment) for node in entry.mutation_lineage]
+ audit = [(node.name or node.entity_type, node.entity_id, node.comment) for node in event.trace_chain]
+ assert actual == audit == lineage
+ assert (event.entity, scalar(event.entity_id)) == (entity, identity)
+ assert entry.execution_outcome == 'success'
+ assert [node.kind for node in entry.trace_path] == ['operation', 'entity', 'provider', 'sql']
+ assert entry.trace_path[0].name == 'CustomerOrder'
+
+
+async def shared(context, platform, service, sink):
+ a, b = await seed(context, platform), await seed(context, platform)
+ revised = await load(context, E.customer_order(a).id().eval())
+ await revised.update_description('prior revision').audit_as('advance one root').save(context)
+ service.reuse_platform = True
+ try:
+ roots = await (Q.customer_orders().with_id_in(E.customer_order(a).id().eval(), E.customer_order(b).id().eval())
+ .order_by_id_ascending().select_platform_with(Q.platforms().limit(1))
+ .select_order_item_list_with(Q.order_items().limit(10)).limit(2)
+ .comment('load two independent roots').purpose('verify shared read-only provider record')
+ .execute_for_list(context))
+ finally:
+ service.reuse_platform = False
+ first, second = roots
+ assert service.snapshot_uses == 2, 'two roots must use the same actual provider-returned record'
+ before = copy.deepcopy(service.platform_snapshot)
+ first_platform, second_platform = E.customer_order(first).platform().eval(), E.customer_order(second).platform().eval()
+ assert first._entity_root is not second._entity_root, 'independent loaded roots must not share a mutable ledger'
+ assert first_platform is not second_platform and first_platform._entity_root is not second_platform._entity_root
+ versions = [E.customer_order(row).version().eval() for row in roots]
+ assert versions == [2, 1]
+ child_versions = [E.order_item(row.order_item_list()[0]).version().eval() for row in roots]
+ platform_version = E.platform(first_platform).version().eval()
+ first.update_description('saved first root')
+ second.update_description('saved second root')
+ reset(service, sink, context)
+ entered, release = asyncio.Event(), asyncio.Event()
+ service.pause = (entered, release)
+ first_task = asyncio.create_task(first.audit_as('save shared first').save(context))
+ await asyncio.wait_for(entered.wait(), 10)
+ second_task = asyncio.create_task(second.audit_as('save shared second').save(context))
+ await asyncio.sleep(0)
+ assert not first_task.done() and not second_task.done()
+ assert context._graph_save_lock.locked(), 'first save holds the actual Context graph gate'
+ release.set()
+ await asyncio.wait_for(asyncio.gather(first_task, second_task), 15)
+ observe('shared', service, sink, context)
+ assert_writes(service, sink, context, 2)
+ assert all(entity == 'CustomerOrder' for entity, _, _ in service.requests)
+ assert service.platform_snapshot == before
+ for index, row in enumerate(roots):
+ persisted = await load(context, E.customer_order(row).id().eval())
+ assert E.customer_order(persisted).version().eval() == versions[index] + 1
+ assert E.order_item(persisted.order_item_list()[0]).version().eval() == child_versions[index]
+ assert E.platform(first_platform).version().eval() == platform_version
+
+
+async def scoped(context, platform, service, sink):
+ foreign_seed, target_seed = await seed(context, platform, 2), await seed(context, platform, 0)
+ foreign = await load(context, E.customer_order(foreign_seed).id().eval())
+ # The public save composes the loaded reverse graph even on the old producer.
+ await foreign.audit_as('prepare loaded source graph').save(context)
+ target = await load(context, E.customer_order(target_seed).id().eval())
+ reached, sibling = foreign.order_item_list()
+ source = foreign._entity_root
+ assert reached._entity_root is source and sibling._entity_root is source
+ foreign.update_description('foreign pending root')
+ sibling.update_name('unreached pending sibling')
+ reached.update_name('reached changed child').audit_as('adopt reached child')
+ target.update_description('target changed root')
+ target.order_item_list().append(reached)
+ reset(service, sink, context)
+ await target.audit_as('save target graph').save(context)
+ observe('scoped', service, sink, context)
+ foreign_key = EntityKey('CustomerOrder', E.customer_order(foreign).id().eval())
+ sibling_key = EntityKey('OrderItem', E.order_item(sibling).id().eval())
+ reached_key = EntityKey('OrderItem', E.order_item(reached).id().eval())
+ assert not pending(target._entity_root, foreign_key), 'foreign root must not be imported'
+ assert not pending(target._entity_root, sibling_key), 'unreached sibling must not be imported'
+ assert all(pending(source, key) for key in [foreign_key, sibling_key, reached_key])
+ assert_writes(service, sink, context, 2)
+ persisted = await load(context, E.customer_order(target).id().eval())
+ assert E.order_item(persisted.order_item_list()[0]).name().eval() == 'reached changed child'
+ assert E.order_item(persisted.order_item_list()[0]).customer_order_id().eval() == E.customer_order(target).id().eval()
+ foreign_persisted = await load(context, E.customer_order(foreign).id().eval())
+ assert E.customer_order(foreign_persisted).description().eval() != 'foreign pending root'
+ assert E.order_item(foreign_persisted.order_item_list()[0]).name().eval() != 'unreached pending sibling'
+
+
+async def descendant(context, platform, service, sink):
+ seeded = await seed(context, platform)
+ order = await load(context, E.customer_order(seeded).id().eval())
+ child = order.order_item_list()[0]
+ parent_version = E.customer_order(order).version().eval()
+ child.update_name('changed descendant only').audit_as('repair descendant')
+ reset(service, sink, context)
+ await order.audit_as('save clean ancestor').save(context)
+ observe('descendant', service, sink, context)
+ assert_writes(service, sink, context, 1)
+ assert service.requests[0][2][-1][2] == 'repair descendant'
+ assert E.customer_order(order).version().eval() == parent_version
+ persisted = await load(context, E.customer_order(order).id().eval())
+ assert E.customer_order(persisted).version().eval() == parent_version
+ assert E.order_item(persisted.order_item_list()[0]).name().eval() == 'changed descendant only'
+
+
+async def conflict(context, platform, service, sink):
+ seeded = await seed(context, platform)
+ old = await load(context, E.customer_order(seeded).id().eval())
+ old_child = old.order_item_list()[0]
+ advance = await Q.order_items().with_id_is(E.order_item(old_child).id().eval()).limit(1).comment(
+ 'load child to create a new version').purpose('verify mixed loaded version rejection').execute_for_one(context)
+ await advance.update_name('committed newer child').audit_as('advance child independently').save(context)
+ current = await load(context, E.customer_order(seeded).id().eval())
+ current_child = current.order_item_list()[0]
+ assert E.order_item(old_child).version().eval() != E.order_item(current_child).version().eval()
+ old_ledger, current_ledger = old_child._entity_root, current_child._entity_root
+ old_child.update_name('old pending value')
+ current_child.update_name('current pending value')
+ old.order_item_list().append(current_child)
+ reset(service, sink, context)
+ try:
+ await old.audit_as('reject conflicting loaded graph').save(context)
+ except ValueError as error:
+ assert 'ENTITY_VERSION_CONFLICT' in str(error)
+ except BaseException:
+ observe('conflict', service, sink, context)
+ raise
+ else:
+ raise AssertionError('conflicting loaded versions must be rejected')
+ assert service.requests == [] and sink.events == []
+ key = EntityKey('OrderItem', E.order_item(old_child).id().eval())
+ assert pending(old_ledger, key) and pending(current_ledger, key)
+ assert scalar(old_ledger.get(key, 'name')) == 'old pending value'
+ assert scalar(current_ledger.get(key, 'name')) == 'current pending value'
+ observe('conflict', service, sink, context)
+
+
+async def main():
+ context = UserContext.new().install(GENERATED_RUNTIME_MODULE)
+ service = SharedService(create_sqlite_service(os.environ['TEAQL_TRACE_CHAIN_DB']))
+ sink = AuditSink(service)
+ context.insert_resource('dataService', service).with_app_audit_event_sink(sink)
+ context.set_diagnostic_sql_log_sink(type('Silent', (), {'write': lambda self, entry: None})())
+ await context.ensure_schema()
+ platform = await Q.platforms().with_id_is(1).limit(1).comment('reuse generated root').purpose('attach ownership fixtures').execute_for_one(context)
+ checks = {'shared': shared, 'scoped': scoped, 'descendant': descendant, 'conflict': conflict}
+ selected = os.environ.get('TEAQL_TRACE_CHAIN_SCENARIO')
+ assert selected is None or selected in checks
+ for name, check in checks.items():
+ if selected is None or selected == name:
+ await check(context, platform, service, sink)
+ print('PASS TC-OWN-' + name + ': generated Q/E/save, actual SQL and committed audit')
+ if selected is None:
+ print('PASS: Python shared ownership 4 scenarios')
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/examples/trace-chain/verify.sh b/examples/trace-chain/verify.sh
new file mode 100644
index 0000000..a4c7920
--- /dev/null
+++ b/examples/trace-chain/verify.sh
@@ -0,0 +1,112 @@
+#!/usr/bin/env bash
+set -euo pipefail
+example="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+repo="$(cd "$example/../.." && pwd)"
+if [[ -z "${TEAQL_TRACE_CHAIN_DB:-}" ]]; then
+ trace_test_directory="$(mktemp -d)"
+ export TEAQL_TRACE_CHAIN_DB="$trace_test_directory/trace-chain.sqlite"
+fi
+if [[ -z "${TEAQL_TRACE_CHAIN_SHARED_DB:-}" ]]; then
+ ownership_directory="$(mktemp -d -t teaql-python-shared.XXXXXX)"
+ export TEAQL_TRACE_CHAIN_SHARED_DB="$ownership_directory/shared.sqlite"
+fi
+if [[ -z "${TEAQL_TRACE_CHAIN_PAGE_STREAM_DB:-}" ]]; then
+ page_stream_directory="$(mktemp -d -t teaql-python-page-stream.XXXXXX)"
+ export TEAQL_TRACE_CHAIN_PAGE_STREAM_DB="$page_stream_directory/page-stream.sqlite"
+fi
+if [[ "$TEAQL_TRACE_CHAIN_PAGE_STREAM_DB" == "$TEAQL_TRACE_CHAIN_DB" || "$TEAQL_TRACE_CHAIN_PAGE_STREAM_DB" == "$TEAQL_TRACE_CHAIN_SHARED_DB" ]]; then
+ echo 'FAIL: page/stream requires its own retained database' >&2
+ exit 1
+fi
+if [[ "$TEAQL_TRACE_CHAIN_SHARED_DB" == "$TEAQL_TRACE_CHAIN_DB" ]]; then
+ echo 'FAIL: normative and ownership fixtures require separate databases' >&2
+ exit 1
+fi
+export PYTHONDONTWRITEBYTECODE=1
+if [[ -z "${TEAQL_TRACE_CHAIN_AGGREGATE_DB:-}" ]]; then
+ aggregate_directory="$(mktemp -d -t teaql-python-aggregate.XXXXXX)"
+ export TEAQL_TRACE_CHAIN_AGGREGATE_DB="$aggregate_directory/aggregate.sqlite"
+fi
+for other_db in "$TEAQL_TRACE_CHAIN_DB" "$TEAQL_TRACE_CHAIN_SHARED_DB" "$TEAQL_TRACE_CHAIN_PAGE_STREAM_DB"; do
+ if [[ "$TEAQL_TRACE_CHAIN_AGGREGATE_DB" == "$other_db" ]]; then
+ echo 'FAIL: aggregate fixture requires its own retained database' >&2
+ exit 1
+ fi
+done
+if [[ -z "${TEAQL_TRACE_CHAIN_CHECKER_DB:-}" ]]; then
+ checker_directory="$(mktemp -d -t teaql-python-checker.XXXXXX)"
+ export TEAQL_TRACE_CHAIN_CHECKER_DB="$checker_directory/checker.sqlite"
+fi
+for other_db in "$TEAQL_TRACE_CHAIN_DB" "$TEAQL_TRACE_CHAIN_SHARED_DB" "$TEAQL_TRACE_CHAIN_PAGE_STREAM_DB" "$TEAQL_TRACE_CHAIN_AGGREGATE_DB"; do
+ if [[ "$TEAQL_TRACE_CHAIN_CHECKER_DB" == "$other_db" ]]; then
+ echo 'FAIL: Checker overlap fixture requires its own retained database' >&2
+ exit 1
+ fi
+done
+export PYTHONPATH="$example/lib:$repo/src${PYTHONPATH:+:$PYTHONPATH}"
+snapshot="$(mktemp)"
+(cd "$example/lib" && rg --files --hidden -g '!**/__pycache__/**' | LC_ALL=C sort | xargs -d '\n' sha256sum) > "$snapshot"
+echo "Generated library manifest: $snapshot"
+for attempt in first second; do
+ intent_log="$(mktemp -t teaql-python-intent.XXXXXX.log)"
+ if ! python -m pytest -q "$repo/tests/provider/sqlite/test_trace_chain.py" \
+ -k 'request_intent_matrix or graph_intent_gate or explicit_mutation_comment_survives_blank_route' \
+ -s > "$intent_log" 2>&1; then
+ sed -n '1,200p' "$intent_log" >&2
+ echo "FAIL: native request intent gate; evidence $intent_log" >&2
+ exit 1
+ fi
+ rg -Fq 'INTENT_GATE_PASS' "$intent_log"
+ rg -Fq 'INTENT_TAIL_PASS' "$intent_log"
+ echo "PASS: native intent matrix $attempt; evidence $intent_log"
+ like_log="$(mktemp -t teaql-python-like.XXXXXX.log)"
+ if ! python -m pytest -q -p no:cacheprovider "$repo/tests/provider/sqlite/test_like_intent.py" \
+ > "$like_log" 2>&1; then
+ sed -n '1,200p' "$like_log" >&2
+ echo "FAIL: native LIKE intent privacy; evidence $like_log" >&2
+ exit 1
+ fi
+ rg -q '^66 passed in ' "$like_log"
+ echo "PASS: native LIKE privacy $attempt; evidence $like_log"
+ echo "Run $attempt on $TEAQL_TRACE_CHAIN_DB without cleanup"
+ main_log="$(mktemp -t teaql-python-main.XXXXXX.log)"
+ timeout --kill-after=5s 60s python "$example/main.py" | tee "$main_log"
+ rg -Fxq 'PASS: typed lineage oracle rejects correct-text Comment/Purpose/Entity/Sql nodes' "$main_log"
+ rg -Fxq 'PASS: identity controls reject duplicate, missing and equal-ID type collapse' "$main_log"
+ [[ "$(rg -c '^GRAPH IDENTITY EVIDENCE ' "$main_log")" == 1 ]]
+ [[ "$(rg -c '^TC-REQ-10 PYTHON READBACK EVIDENCE ' "$main_log")" == 1 ]]
+ [[ "$(rg -c '^BOOTSTRAP INTENT EVIDENCE ' "$main_log")" == 2 ]]
+ rg -Fxq 'PASS: generated library unchanged; all trace example checks passed' "$main_log"
+ echo "Mutation privacy $attempt on $TEAQL_TRACE_CHAIN_DB without cleanup"
+ privacy_log="$(mktemp -t teaql-python-privacy.XXXXXX.log)"
+ python "$example/mutation_privacy.py" | tee "$privacy_log"
+ rg -Fq 'PASS: Python generated loaded delete privacy; 2 writes/2 reads/2 audits; independent next request' "$privacy_log"
+ rg -Fq 'PASS: Python generated unchanged scalar privacy; 1 write/1 read/1 audit' "$privacy_log"
+ [[ "$(rg -c '^PRIVATE_LINEAGE_OBSERVED ' "$privacy_log")" == 4 ]]
+ [[ "$(rg -c '^PASS Python complete private lineage:' "$privacy_log")" == 4 ]]
+ echo "Assigned identities $attempt on $TEAQL_TRACE_CHAIN_SHARED_DB without cleanup"
+ assigned_log="$(mktemp -t teaql-python-assigned.XXXXXX.log)"
+ TEAQL_TRACE_CHAIN_DB="$TEAQL_TRACE_CHAIN_SHARED_DB" \
+ timeout --kill-after=5s 60s python "$example/assigned_identity.py" | tee "$assigned_log"
+ rg -Fxq 'PASS: Python generated assigned identity 2 scenarios; command/SQL/audit and independent reload' "$assigned_log"
+ echo "Ownership $attempt on $TEAQL_TRACE_CHAIN_SHARED_DB without cleanup"
+ run_log="$(mktemp -t teaql-python-shared.XXXXXX.log)"
+ env -u TEAQL_TRACE_CHAIN_SCENARIO TEAQL_TRACE_CHAIN_DB="$TEAQL_TRACE_CHAIN_SHARED_DB" \
+ python "$example/shared_reference.py" | tee "$run_log"
+ rg -Fq 'PASS: Python shared ownership 4 scenarios' "$run_log"
+ echo "Page/stream $attempt on $TEAQL_TRACE_CHAIN_PAGE_STREAM_DB without cleanup"
+ python "$example/page_stream.py"
+ echo "Aggregate $attempt on $TEAQL_TRACE_CHAIN_AGGREGATE_DB without cleanup"
+ aggregate_log="$(mktemp -t teaql-python-aggregate.XXXXXX.log)"
+ python "$example/relation_aggregate.py" | tee "$aggregate_log"
+ rg -Fq 'FORWARD_NOTLOADED_OBSERVED {"logging": true' "$aggregate_log"
+ rg -Fq 'FORWARD_NOTLOADED_OBSERVED {"logging": false' "$aggregate_log"
+ echo "Checker overlap $attempt on $TEAQL_TRACE_CHAIN_CHECKER_DB without cleanup"
+ checker_log="$(mktemp -t teaql-python-checker.XXXXXX.log)"
+ TEAQL_TRACE_CHAIN_DB="$TEAQL_TRACE_CHAIN_CHECKER_DB" \
+ python "$example/checker_overlap.py" | tee "$checker_log"
+ rg -Fq 'PASS: Python generated Checker overlap 4 scenarios; accepted-only command/SQL/audit; serialized callbacks' "$checker_log"
+ echo "PASS: generated Checker overlap $attempt; evidence $checker_log"
+done
+(cd "$example/lib" && sha256sum --check "$snapshot")
+echo "PASS: two runs on the same database; generated library hashes unchanged"
diff --git a/scripts/verify-examples.sh b/scripts/verify-examples.sh
index 9c5ae48..5199bd9 100755
--- a/scripts/verify-examples.sh
+++ b/scripts/verify-examples.sh
@@ -2,7 +2,7 @@
set -euo pipefail
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
-expected=(business-clock business-id conformance mutation-policy opaque-entity-reference order-management query-policy school-management task_board)
+expected=(business-clock business-id conformance facet-trace mutation-policy opaque-entity-reference order-management query-policy school-management task_board trace-chain)
mapfile -t actual < <(find "$repo/examples" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)
if [[ "${actual[*]}" != "${expected[*]}" ]]; then
echo "example inventory changed; update scripts/verify-examples.sh: ${actual[*]}" >&2
@@ -23,6 +23,7 @@ fi
PYTHONPATH="$repo/examples/conformance:$repo/src" python -m app.main
PYTHONPATH="$repo/src" python -m unittest discover -s "$repo/examples/conformance" -p 'test_sql_log_intent.py' -v
PYTHONPATH="$repo/examples/school-management:$repo/src" python -m app.main
+bash "$repo/scripts/verify-school-bootstrap-example.sh"
PYTHONPATH="$repo/src" python -m unittest discover -s "$repo/examples/school-management" -p 'test_sql_log_intent.py' -v
PYTHONPATH="$repo/src" python "$repo/examples/mutation-policy/main.py"
PYTHONPATH="$repo/src" python "$repo/examples/business-clock/main.py"
@@ -38,4 +39,6 @@ TEAQL_ORDER_MANAGEMENT_DB="$order_management_tmp/order.db" \
PYTHONPATH="$repo/src" python -m unittest discover -s "$repo/examples/order-management" -p 'test_sql_log_intent.py' -v
TEAQL_TASK_BOARD_DB="$task_board_tmp/task_board.db" PYTHONPATH="$repo/examples/task_board:$repo/src" python "$repo/examples/task_board/main.py"
PYTHONPATH="$repo/src" python -m unittest discover -s "$repo/examples/task_board" -p 'test_task_board.py' -v
+bash "$repo/examples/trace-chain/verify.sh"
+bash "$repo/examples/facet-trace/verify.sh"
echo "PASS: all Python examples"
diff --git a/scripts/verify-school-bootstrap-example.sh b/scripts/verify-school-bootstrap-example.sh
new file mode 100644
index 0000000..d98342c
--- /dev/null
+++ b/scripts/verify-school-bootstrap-example.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+set -euo pipefail
+repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+example="$repo/examples/school-management"
+evidence="$(mktemp -d -t teaql-python-bootstrap.XXXXXXXX)"
+export PYTHONDONTWRITEBYTECODE=1
+export PYTHONPATH="$example:$repo/src"
+fingerprint() {
+ { rg --files "$example/models" "$example/requests" -g '*.py'; printf '%s\n' "$example/Q.py" "$example/E.py" "$example/runtime_module.py"; } |
+ LC_ALL=C sort | xargs -d '\n' sha256sum
+}
+fingerprint > "$evidence/library-before.sha256"
+for logging in on off; do
+ for round in 1 2; do
+ TEAQL_SCHOOL_BOOTSTRAP_DB="$evidence/$logging.sqlite" TEAQL_SCHOOL_BOOTSTRAP_LOGGING="$logging" \
+ python -m app.bootstrap_trace | tee "$evidence/$logging-$round.log"
+ expected_logging=True; [[ "$logging" == off ]] && expected_logging=False
+ fresh=True; version=1
+ if [[ "$round" == 2 ]]; then fresh=False; version=3; fi
+ rg -Fq "PASS Python generated bootstrap trace logging=$expected_logging fresh=$fresh originalVersion=$version" "$evidence/$logging-$round.log"
+ fingerprint > "$evidence/library-after.sha256"
+ cmp "$evidence/library-before.sha256" "$evidence/library-after.sha256"
+ done
+done
+echo "PASS Python generated bootstrap twice per logging mode; retained evidence: $evidence"
diff --git a/src/teaql/core/__init__.py b/src/teaql/core/__init__.py
index c82407c..44ca5b3 100644
--- a/src/teaql/core/__init__.py
+++ b/src/teaql/core/__init__.py
@@ -18,6 +18,7 @@
BatchInsertCommand, BatchUpdateCommand, MutationRequest
)
from .graph import GraphNode
+from .request_intent import QueryIntent, MutationIntent, RequestIntentError
from .meta import EntityDescriptor, PropertyDescriptor
from .list import SmartList, TeaQLPage
@@ -43,6 +44,7 @@
)
__all__ = [
+ "QueryIntent", "MutationIntent", "RequestIntentError",
"Value", "DataType", "Timestamp",
"BaseEntityData", "EntityKey", "EntityChangeSet", "EntityRoot",
"Expr", "ExprBuilder", "BinaryOp", "ExprFunction",
diff --git a/src/teaql/core/entity.py b/src/teaql/core/entity.py
index 7514ecd..e1d79e2 100644
--- a/src/teaql/core/entity.py
+++ b/src/teaql/core/entity.py
@@ -1,4 +1,5 @@
from dataclasses import dataclass
+from copy import deepcopy
from threading import RLock
from typing import Dict, Optional, Any, Iterable, Mapping, Set, Tuple
from .value import Value
@@ -14,6 +15,36 @@ def __post_init__(self):
raise ValueError("entity type is required")
+class _LoadedScalarSnapshot:
+ """Generated plumbing: owned original scalars, not a mutable graph ledger."""
+
+ def __init__(self, values):
+ self.__values = deepcopy(values)
+
+ def select(self, fields):
+ return {name: deepcopy(self.__values[name]) for name in fields if name in self.__values}
+
+ def business_values(self):
+ """Private provenance includes unchanged loaded scalars, never structural version/ID."""
+ return self.select(name for name in self.__values if name not in ('id', 'version'))
+
+
+class _QueryProjectionSnapshot:
+ """Owned query-only aliases; never entity attributes or mutation payloads."""
+
+ def __init__(self, values, modeled_names):
+ excluded = frozenset(modeled_names)
+ self.__values = {name: deepcopy(value) for name, value in values.items()
+ if name not in excluded}
+
+ def contains(self, alias):
+ return alias in self.__values
+
+ def get(self, alias):
+ # Missing is not zero/None. Return an owned value, not a writable alias.
+ return deepcopy(self.__values[alias])
+
+
class EntityChangeSet:
"""Final pending field values grouped by stable entity identity."""
@@ -57,6 +88,20 @@ def __init__(self):
self._original_versions: Dict[EntityKey, int] = {}
self._new_keys: Set[EntityKey] = set()
self._deleted_keys: Set[EntityKey] = set()
+ self._trace_chains: Dict[EntityKey, tuple] = {}
+
+ def set_trace_chain(self, key: EntityKey, chain) -> None:
+ """Store a complete per-entity replacement, not an appended fragment."""
+ from .mutation import TraceNode
+ nodes = tuple(chain)
+ if any(not isinstance(node, TraceNode) or node.kind != 'auditReason' for node in nodes):
+ raise TypeError('ledger lineage must contain typed AuditReason nodes')
+ with self._lock:
+ self._trace_chains[key] = deepcopy(nodes)
+
+ def trace_chain(self, key: EntityKey) -> tuple:
+ with self._lock:
+ return deepcopy(self._trace_chains.get(key, ()))
def push_change_set(self) -> None:
with self._lock:
@@ -96,8 +141,21 @@ def mark_as_deleted(self, key: EntityKey) -> None:
def set_original_version(self, key: EntityKey, version: int) -> None:
with self._lock:
+ self._validate_version(key, version)
self._original_versions[key] = version
+ def accept_committed_version(self, key: EntityKey, version: int) -> None:
+ """Advance authoritative state only after this key's changes clear."""
+ with self._lock:
+ if self.has_pending(key):
+ raise ValueError('ENTITY_PENDING_CHANGES: clear committed changes before accepting a version')
+ self._original_versions[key] = version
+
+ def has_pending(self, key: EntityKey) -> bool:
+ with self._lock:
+ return (key in self._new_keys or key in self._deleted_keys or
+ any(key in change_set._changes for change_set in self._change_sets))
+
def original_version(self, key: EntityKey) -> Optional[int]:
with self._lock:
return self._original_versions.get(key)
@@ -115,18 +173,61 @@ def clear_committed(self) -> None:
self._change_sets[-1] = EntityChangeSet()
self._new_keys.clear()
self._deleted_keys.clear()
+ self._trace_chains.clear()
def merge_from(self, other: 'EntityRoot') -> None:
if other is self:
return
- with self._lock, other._lock:
- self._change_sets[-1].merge_from(other._change_sets[-1])
- self._original_versions.update(other._original_versions)
- self._new_keys.update(other._new_keys)
- self._deleted_keys.update(other._deleted_keys)
+ # Release the source lock before acquiring the target: opposing imports
+ # must not hold both graph locks in opposite order.
+ with other._lock:
+ keys = (set(other._original_versions) | other._new_keys | other._deleted_keys |
+ set(other._trace_chains) | set(other._change_sets[-1]._changes))
+ entries = [other._snapshot(key) for key in keys]
+ with self._lock:
+ for key, _, version, _, _, _ in entries:
+ self._validate_version(key, version)
+ for entry in entries:
+ self._import(entry)
+
+ def merge_entity_from(self, other: 'EntityRoot', key: EntityKey) -> None:
+ """Copy one reached typed key without draining its source graph."""
+ if other is self:
+ return
+ entry = other._snapshot(key)
+ with self._lock:
+ self._validate_version(key, entry[2])
+ self._import(entry)
+
+ def _snapshot(self, key):
+ with self._lock:
+ return (key, dict(self._change_sets[-1]._changes.get(key, {})),
+ self._original_versions.get(key), key in self._new_keys,
+ key in self._deleted_keys, deepcopy(self._trace_chains.get(key)))
+
+ def _validate_version(self, key, incoming):
+ original = self._original_versions.get(key)
+ if incoming is not None and original is not None and incoming != original:
+ raise ValueError(f'ENTITY_VERSION_CONFLICT: {key.entity} has loaded versions {original} and {incoming}')
+
+ def _import(self, entry):
+ key, values, version, added, removed, trace = entry
+ for field, value in values.items():
+ self._change_sets[-1].set(key, field, value)
+ if version is not None:
+ self._original_versions[key] = version
+ if added:
+ self._new_keys.add(key)
+ if removed:
+ self.mark_as_deleted(key)
+ if trace is not None:
+ self._trace_chains[key] = trace
def rekey(self, old_key: EntityKey, new_key: EntityKey) -> None:
+ if old_key == new_key:
+ return
with self._lock:
+ self._validate_version(new_key, self._original_versions.get(old_key))
for change_set in self._change_sets:
change_set.rekey(old_key, new_key)
if old_key in self._original_versions:
@@ -137,6 +238,12 @@ def rekey(self, old_key: EntityKey, new_key: EntityKey) -> None:
if old_key in self._deleted_keys:
self._deleted_keys.remove(old_key)
self._deleted_keys.add(new_key)
+ if old_key in self._trace_chains:
+ self._trace_chains[new_key] = self._trace_chains.pop(old_key)
+ for chain in self._trace_chains.values():
+ for node in chain:
+ if (node.name or node.entity_type, node.entity_id) == (old_key.entity, old_key.id):
+ node.entity_id = new_key.id
def clear_entity(self, key: EntityKey) -> None:
with self._lock:
@@ -144,6 +251,7 @@ def clear_entity(self, key: EntityKey) -> None:
change_set.clear_entity(key)
self._new_keys.discard(key)
self._deleted_keys.discard(key)
+ self._trace_chains.pop(key, None)
class BaseEntityData:
def __init__(self, id: int = 0, version: int = 0, dynamic: Optional[Dict[str, Value]] = None):
diff --git a/src/teaql/core/expr.py b/src/teaql/core/expr.py
index 808f74b..1e414bd 100644
--- a/src/teaql/core/expr.py
+++ b/src/teaql/core/expr.py
@@ -77,6 +77,14 @@ class ColumnExpr(Expr):
class ValueExpr(Expr):
value: Value
+def _like_pattern(original, prefix, suffix):
+ """Retain lowering provenance without changing dataclass/wire contracts."""
+ original = str(original)
+ pattern = prefix + original + suffix
+ result = ValueExpr(Value.Text(pattern))
+ result._like_operand = (Value.Text(original), Value.Text(pattern))
+ return result
+
@dataclass
class FunctionExpr(Expr):
function: ExprFunction
@@ -277,22 +285,22 @@ def not_like(field: str, val: Any) -> Expr:
return Expr.not_like(field, val)
def contain(field: str, val: Any) -> Expr:
- return Expr.like(field, f"%{val}%")
+ return Expr.like(field, _like_pattern(val, '%', '%'))
def not_contain(field: str, val: Any) -> Expr:
- return Expr.not_like(field, f"%{val}%")
+ return Expr.not_like(field, _like_pattern(val, '%', '%'))
def begin_with(field: str, val: Any) -> Expr:
- return Expr.like(field, f"{val}%")
+ return Expr.like(field, _like_pattern(val, '', '%'))
def not_begin_with(field: str, val: Any) -> Expr:
- return Expr.not_like(field, f"{val}%")
+ return Expr.not_like(field, _like_pattern(val, '', '%'))
def end_with(field: str, val: Any) -> Expr:
- return Expr.like(field, f"%{val}")
+ return Expr.like(field, _like_pattern(val, '%', ''))
def not_end_with(field: str, val: Any) -> Expr:
- return Expr.not_like(field, f"%{val}")
+ return Expr.not_like(field, _like_pattern(val, '%', ''))
def sound_like(field: str, val: Any) -> Expr:
return eq(soundex(column(field)), soundex(value(val)))
diff --git a/src/teaql/core/mutation.py b/src/teaql/core/mutation.py
index 7e588a1..1f3daa9 100644
--- a/src/teaql/core/mutation.py
+++ b/src/teaql/core/mutation.py
@@ -1,7 +1,9 @@
from enum import Enum, auto
from typing import Dict, Any, List, Optional
from dataclasses import dataclass, field
+from copy import deepcopy
from .value import Value
+from .request_intent import MutationIntent
@dataclass
class TraceNode:
@@ -77,6 +79,7 @@ class DeleteCommand:
expected_version_val: Optional[int] = None
soft_delete: bool = True
trace_chain: List[TraceNode] = field(default_factory=list)
+ old_values: Optional[Dict[str, Value]] = field(default=None, repr=False)
@classmethod
def new(cls, entity: str, id_val: Any) -> 'DeleteCommand':
@@ -96,6 +99,7 @@ class RecoverCommand:
id: Value
expected_version_val: int
trace_chain: List[TraceNode] = field(default_factory=list)
+ old_values: Optional[Dict[str, Value]] = field(default=None, repr=False)
@classmethod
def new(cls, entity: str, id_val: Any, expected_version: int) -> 'RecoverCommand':
@@ -113,38 +117,62 @@ class MutationKind(Enum):
BATCH = auto()
class MutationRequest:
- def __init__(self, data: Any):
+ __slots__ = ('_data', '__intent', '__mutation_lineage')
+
+ def __init__(self, data: Any, comment: Optional[str] = None):
+ self.__intent = MutationIntent(comment)
self._data = data
+ self.__mutation_lineage = ()
+
+ @property
+ def mutation_lineage(self) -> tuple[TraceNode, ...]:
+ """Detached business lineage, never the physical SQL trace path."""
+ return deepcopy(self.__mutation_lineage)
+
+ def with_mutation_lineage(self, chain) -> 'MutationRequest':
+ nodes = tuple(chain)
+ if any(not isinstance(node, TraceNode) or node.kind != 'auditReason' for node in nodes):
+ raise TypeError('mutation lineage must contain typed AuditReason nodes')
+ result = MutationRequest(self._data, comment=self.intent.comment)
+ result.__mutation_lineage = deepcopy(nodes)
+ return result
+
+ @property
+ def intent(self) -> MutationIntent:
+ return self.__intent
+
+ def validate(self) -> None:
+ MutationIntent(getattr(getattr(self, '_MutationRequest__intent', None), 'comment', None))
+ if any(not isinstance(node, TraceNode) for node in self.trace_chain()):
+ raise TypeError('MutationRequest trace must contain typed TraceNode values')
+
+ def with_root_intent(self, intent: MutationIntent) -> 'MutationRequest':
+ return MutationRequest(self._data, comment=intent.comment).with_mutation_lineage(self.mutation_lineage)
def trace_chain(self) -> List[TraceNode]:
if isinstance(self._data, list):
return []
return getattr(self._data, 'trace_chain', [])
- def comment(self) -> Optional[str]:
- if isinstance(self._data, list):
- return None
- traces = self.trace_chain()
- if traces:
- return traces[-1].comment
- return None
+ def comment(self) -> str:
+ return self.intent.comment
@classmethod
- def Insert(cls, cmd: InsertCommand) -> 'MutationRequest':
- return cls(cmd)
+ def Insert(cls, cmd: InsertCommand, comment: Optional[str] = None) -> 'MutationRequest':
+ return cls(cmd, comment)
@classmethod
- def Update(cls, cmd: UpdateCommand) -> 'MutationRequest':
- return cls(cmd)
+ def Update(cls, cmd: UpdateCommand, comment: Optional[str] = None) -> 'MutationRequest':
+ return cls(cmd, comment)
@classmethod
- def Delete(cls, cmd: DeleteCommand) -> 'MutationRequest':
- return cls(cmd)
+ def Delete(cls, cmd: DeleteCommand, comment: Optional[str] = None) -> 'MutationRequest':
+ return cls(cmd, comment)
@classmethod
- def Recover(cls, cmd: RecoverCommand) -> 'MutationRequest':
- return cls(cmd)
+ def Recover(cls, cmd: RecoverCommand, comment: Optional[str] = None) -> 'MutationRequest':
+ return cls(cmd, comment)
@classmethod
- def Batch(cls, cmds: List['MutationRequest']) -> 'MutationRequest':
- return cls(cmds)
+ def Batch(cls, cmds: List['MutationRequest'], comment: Optional[str] = None) -> 'MutationRequest':
+ return cls(cmds, comment)
diff --git a/src/teaql/core/request_intent.py b/src/teaql/core/request_intent.py
new file mode 100644
index 0000000..9c2a085
--- /dev/null
+++ b/src/teaql/core/request_intent.py
@@ -0,0 +1,54 @@
+"""Validated, request-owned intent shared by all execution adapters."""
+from dataclasses import dataclass, field
+from typing import Optional
+
+
+class RequestIntentError(ValueError):
+ def __init__(self, code: str, field_name: str, request_kind: str):
+ self.code = code
+ self.field = field_name
+ self.request_kind = request_kind
+ super().__init__(
+ f'{code}: {request_kind} request requires non-blank {field_name}; '
+ f'supply {field_name} at the request entry point')
+
+
+def _white_space(character: str) -> bool:
+ # Unicode White_Space, identical to Rust str::trim. Python str.isspace()
+ # additionally accepts U+001C..U+001F, which are NOT contract whitespace.
+ value = ord(character)
+ return (0x09 <= value <= 0x0D or value in (
+ 0x20, 0x85, 0xA0, 0x1680, 0x2028, 0x2029, 0x202F, 0x205F, 0x3000)
+ or 0x2000 <= value <= 0x200A)
+
+
+def require_intent(value: Optional[str], field_name: str, request_kind: str) -> str:
+ if not isinstance(value, str) or not value or all(_white_space(c) for c in value):
+ code = 'QUERY_PURPOSE_REQUIRED' if field_name == 'purpose' else 'REQUEST_COMMENT_REQUIRED'
+ raise RequestIntentError(code, field_name, request_kind)
+ return value # Preserve text verbatim; validation must not rewrite evidence.
+
+
+@dataclass(frozen=True, slots=True)
+class QueryIntent:
+ comment: str = field(repr=False)
+ purpose: str = field(repr=False)
+
+ def __post_init__(self):
+ require_intent(self.comment, 'comment', 'query')
+ require_intent(self.purpose, 'purpose', 'query')
+
+ @classmethod
+ def from_query(cls, query):
+ return cls(getattr(query, 'comment_text', None), getattr(query, 'purpose_text', None))
+
+
+@dataclass(frozen=True, slots=True)
+class MutationIntent:
+ comment: str = field(repr=False)
+
+ def __post_init__(self):
+ require_intent(self.comment, 'comment', 'mutation')
+
+ def readback(self) -> QueryIntent:
+ return QueryIntent(self.comment, 'verify the persisted mutation result')
diff --git a/src/teaql/core/trace.py b/src/teaql/core/trace.py
new file mode 100644
index 0000000..9c83bee
--- /dev/null
+++ b/src/teaql/core/trace.py
@@ -0,0 +1,61 @@
+"""Typed physical SQL paths, distinct from graph mutation audit lineage."""
+from copy import deepcopy
+from dataclasses import replace
+from typing import Iterable, List, Optional
+
+from .mutation import TraceNode
+
+
+def _kind(node: TraceNode) -> str:
+ return str(node.kind).replace('_', '').lower()
+
+
+def trace_name(node: TraceNode) -> str:
+ return node.name or node.entity_type
+
+
+def canonical_sql_trace_path(source: Iterable[TraceNode], backend: str,
+ operation: str) -> List[TraceNode]:
+ """Rust baseline algorithm; owns output and is idempotent for canonical input."""
+ nodes = list(source)
+ intent_kinds = {'comment', 'purpose', 'auditreason'}
+ kinds = {_kind(node) for node in nodes}
+ if {'operation', 'provider', 'sql'} <= kinds:
+ return deepcopy([node for node in nodes if _kind(node) not in intent_kinds])
+ root = next((trace_name(node) for node in nodes if trace_name(node).strip()), 'unknown')
+ query = operation.lower() == 'select'
+ entity = root if query else next((trace_name(node) for node in reversed(nodes)
+ if _kind(node) == 'entity' and trace_name(node).strip()), root)
+ return [TraceNode(kind='operation', name=root, comment='query' if query else 'mutation'),
+ TraceNode(kind='request' if query else 'entity', name=entity),
+ *deepcopy([node for node in nodes if _kind(node) == 'relation']),
+ TraceNode(kind='provider', name=backend if backend and backend.strip() else 'unknown'),
+ TraceNode(kind='sql', name=operation.lower())]
+
+
+def trace_intent(source: Iterable[TraceNode]) -> dict[str, Optional[str]]:
+ result = {'comment': None, 'purpose': None, 'auditReason': None}
+ names = {'comment': 'comment', 'purpose': 'purpose', 'auditreason': 'auditReason'}
+ for node in source:
+ name = names.get(_kind(node))
+ if name is not None:
+ result[name] = node.comment
+ return result
+
+
+def physical_readback_path(write_path: Iterable[TraceNode]) -> List[TraceNode]:
+ """A real SELECT keeps its mutation origin but uses a query request frame."""
+ nodes = list(write_path)
+ root = next((trace_name(node) for node in nodes if _kind(node) == 'operation'), 'unknown')
+ result = []
+ for node in nodes:
+ kind = _kind(node)
+ if kind == 'operation':
+ result.append(replace(node, comment='query'))
+ elif kind == 'entity':
+ result.append(TraceNode(kind='request', name=root))
+ elif kind == 'sql':
+ result.append(replace(node, name='select', comment=''))
+ else:
+ result.append(deepcopy(node))
+ return result
diff --git a/src/teaql/core/trace_scope.py b/src/teaql/core/trace_scope.py
new file mode 100644
index 0000000..71f66d4
--- /dev/null
+++ b/src/teaql/core/trace_scope.py
@@ -0,0 +1,57 @@
+"""Persistent, invocation-owned graph lineage, independent of SQL routes."""
+
+from dataclasses import dataclass
+from typing import Optional
+
+from .entity import EntityKey
+from .mutation import TraceNode
+
+
+@dataclass(frozen=True)
+class _AuditReason:
+ entity: str
+ entity_id: int
+ reason: str
+
+
+@dataclass(frozen=True)
+class TraceScope:
+ """O(1) immutable branch token; recover owned nodes only at a boundary."""
+
+ _owner: object
+ _node: _AuditReason
+ _parent: Optional['TraceScope'] = None
+
+ @classmethod
+ def root(cls, owner: object, key: EntityKey, reason: str) -> 'TraceScope':
+ from .request_intent import MutationIntent
+ intent = MutationIntent(reason)
+ cls._require_assigned(key)
+ return cls(owner, _AuditReason(key.entity, key.id, intent.comment))
+
+ def child(self, key: EntityKey, reason: Optional[str]) -> 'TraceScope':
+ from .request_intent import MutationIntent, _white_space
+ self._require_assigned(key)
+ # Match the public intent contract (Unicode White_Space), not Python
+ # strip(), which also discards the valid U+001C..U+001F separators.
+ if reason is None or (isinstance(reason, str) and all(_white_space(c) for c in reason)):
+ return self
+ intent = MutationIntent(reason)
+ return TraceScope(self._owner, _AuditReason(key.entity, key.id, intent.comment), self)
+
+ def recover(self) -> tuple[TraceNode, ...]:
+ nodes = []
+ scope = self
+ while scope is not None:
+ node = scope._node
+ nodes.append(TraceNode(entity_type=node.entity, entity_id=node.entity_id,
+ comment=node.reason, kind='auditReason', name=node.entity))
+ scope = scope._parent
+ nodes.reverse()
+ return tuple(nodes)
+
+ @staticmethod
+ def _require_assigned(key: EntityKey) -> None:
+ if not isinstance(key, EntityKey) or not isinstance(key.id, int) \
+ or isinstance(key.id, bool) or key.id <= 0:
+ raise ValueError('graph trace scope requires an assigned typed entity ID')
diff --git a/src/teaql/data_service/__init__.py b/src/teaql/data_service/__init__.py
index bb1c255..cc0d3ef 100644
--- a/src/teaql/data_service/__init__.py
+++ b/src/teaql/data_service/__init__.py
@@ -2,8 +2,10 @@
from datetime import datetime
from enum import Enum, auto
from typing import List, Dict, Any, Optional, Protocol, Union, AsyncIterator
+from copy import deepcopy
from teaql.core.query import SelectQuery
+from teaql.core.request_intent import QueryIntent
from teaql.core.mutation import (
MutationRequest,
InsertCommand as CoreInsertCommand,
@@ -25,33 +27,67 @@ class DataServiceCapabilities:
returning: bool = False
-@dataclass
class QueryRequest:
- query: SelectQuery
- trace_chain: List[TraceNode] = field(default_factory=list)
- _comment: Optional[str] = None
- _purpose: Optional[str] = None
-
- def __post_init__(self) -> None:
- # Older generated wrappers store intent on SelectQuery but construct
- # QueryRequest(query) without forwarding it. Keep the explicit request
- # values authoritative while preserving their diagnostic intent.
- if self._comment is None:
- self._comment = getattr(self.query, 'comment_text', None)
- if self._purpose is None:
- self._purpose = getattr(self.query, 'purpose_text', None)
+ __slots__ = ('query', 'trace_chain', '__intent', '__origin_entity',
+ '_log_intent_source', '_log_intent_queries')
+ _UNSET = object()
+
+ def __init__(self, query: SelectQuery, trace_chain=None, _comment=_UNSET, _purpose=_UNSET,
+ *, _origin_entity=None):
+ comment = getattr(query, 'comment_text', None) if _comment is self._UNSET else _comment
+ purpose = getattr(query, 'purpose_text', None) if _purpose is self._UNSET else _purpose
+ self.__intent = QueryIntent(comment, purpose)
+ self.__origin_entity = query.entity if _origin_entity is None else _origin_entity
+ self.query = deepcopy(query)
+ self.query.comment_text = self.intent.comment
+ self.query.purpose_text = self.intent.purpose
+ self.trace_chain = deepcopy(trace_chain) if trace_chain is not None else []
+
+ @property
+ def intent(self) -> QueryIntent:
+ return self.__intent
+
+ @property
+ def origin_entity(self) -> str:
+ return self.__origin_entity
+
+ @property
+ def _comment(self) -> str:
+ return self.intent.comment
+
+ @property
+ def _purpose(self) -> str:
+ return self.intent.purpose
+
+ def validate(self) -> None:
+ intent = getattr(self, '_QueryRequest__intent', None)
+ QueryIntent(getattr(intent, 'comment', None), getattr(intent, 'purpose', None))
+ if any(not isinstance(node, TraceNode) for node in self.trace_chain):
+ raise TypeError('QueryRequest trace must contain typed TraceNode values')
+
+ def with_query(self, query: SelectQuery) -> 'QueryRequest':
+ result = QueryRequest(query, self.trace_chain, self._comment, self._purpose,
+ _origin_entity=self.origin_entity)
+ if hasattr(self, '_log_intent_source'):
+ result._log_intent_source = deepcopy(self._log_intent_source)
+ # Derived work may drop a relation/projection whose private values are
+ # still mentioned by the originating intent. Retain owned source graphs
+ # solely for compiler classification, never on Context or log payloads.
+ result._log_intent_queries = (*deepcopy(getattr(self, '_log_intent_queries', ())),
+ deepcopy(self.query))
+ return result
def comment(self, text: str) -> 'QueryRequest':
- if not text:
- raise ValueError("comment cannot be empty")
- self._comment = text
- return self
+ result = self.with_query(self.query)
+ result.__intent = QueryIntent(text, self._purpose)
+ result.query.comment_text = result.intent.comment
+ return result
def purpose(self, text: str) -> 'QueryRequest':
- if not text:
- raise ValueError("purpose cannot be empty")
- self._purpose = text
- return self
+ result = self.with_query(self.query)
+ result.__intent = QueryIntent(self._comment, text)
+ result.query.purpose_text = result.intent.purpose
+ return result
class DataServiceOperation(Enum):
@@ -75,6 +111,7 @@ class ExecutionMetadata:
affected_rows: Optional[int] = None
result_count: Optional[int] = None
trace_chain: List[TraceNode] = field(default_factory=list)
+ mutation_lineage: tuple[TraceNode, ...] = field(default_factory=tuple)
comment: Optional[str] = None
purpose: Optional[str] = None
audit_reason: Optional[str] = None
@@ -85,6 +122,9 @@ class ExecutionMetadata:
sql_origin: Optional[str] = None
# Statement/cursor termination only, not transaction commit.
execution_outcome: Optional[str] = None
+ # Logical mutation summaries own ordered physical children. Batch summaries
+ # retain per-item grouping; leaves are empty and never point to a summary.
+ statements: tuple['ExecutionMetadata', ...] = field(default_factory=tuple)
@dataclass
@@ -93,17 +133,17 @@ class QueryResult:
metadata: ExecutionMetadata
facets: Dict[str, Any] = field(default_factory=dict)
-def InsertCommand(cmd):
- return MutationRequest(cmd)
+def InsertCommand(cmd, comment=None):
+ return MutationRequest(cmd, comment)
-def UpdateCommand(cmd):
- return MutationRequest(cmd)
+def UpdateCommand(cmd, comment=None):
+ return MutationRequest(cmd, comment)
-def DeleteCommand(cmd):
- return MutationRequest(cmd)
+def DeleteCommand(cmd, comment=None):
+ return MutationRequest(cmd, comment)
-def RecoverCommand(cmd):
- return MutationRequest(cmd)
+def RecoverCommand(cmd, comment=None):
+ return MutationRequest(cmd, comment)
diff --git a/src/teaql/provider/tfp_client/__init__.py b/src/teaql/provider/tfp_client/__init__.py
index 2926837..fc2e8ea 100644
--- a/src/teaql/provider/tfp_client/__init__.py
+++ b/src/teaql/provider/tfp_client/__init__.py
@@ -151,6 +151,7 @@ def capabilities(self) -> DataServiceCapabilities:
return DataServiceCapabilities(query=True, mutation=True)
async def query(self, context: Any, request: QueryRequest) -> QueryResult:
+ request.validate()
started_at = datetime.now()
federal = _query_request(request)
rows = await self.federal_client.execute_query(federal)
@@ -161,10 +162,12 @@ async def query(self, context: Any, request: QueryRequest) -> QueryResult:
backend="teaql-federal", operation=DataServiceOperation.Query,
started_at=started_at, ended_at=datetime.now(), result_count=len(rows),
comment=federal.comment,
+ purpose=federal.purpose,
),
)
async def mutate(self, context: Any, request: MutationRequest) -> MutationResult:
+ request.validate()
if context is not None and not context.consume_mutation_checked(request._data):
context.check_and_fix_mutation(request._data)
scope = context.mutation_policy_execution(request) if context is not None else nullcontext()
@@ -188,15 +191,14 @@ async def mutate(self, context: Any, request: MutationRequest) -> MutationResult
backend="teaql-federal", operation=operation,
started_at=started_at, ended_at=datetime.now(), affected_rows=affected,
comment=federal.comment,
+ audit_reason=federal.comment,
),
)
def _federal_query_payload(query: FederalQuery) -> Dict[str, Any]:
- if not query.comment or not query.comment.strip():
- raise TfpError("TFP_INVALID_REQUEST", "commentText is required")
- if not query.purpose or not query.purpose.strip():
- raise TfpError("TFP_POLICY_VIOLATION", "purposeText is required")
+ from teaql.core.request_intent import QueryIntent
+ intent = QueryIntent(query.comment, query.purpose)
if query.limit is not None and query.limit < 1:
raise TfpError("TFP_INVALID_REQUEST", "limitValue must be positive")
if query.offset is not None and query.offset < 0:
@@ -207,8 +209,8 @@ def _federal_query_payload(query: FederalQuery) -> Dict[str, Any]:
"selectItems": query.select_items,
"groupByItems": query.group_by_items,
"aggregateItems": query.aggregate_items,
- "commentText": query.comment.strip(),
- "purposeText": query.purpose.strip(),
+ "commentText": intent.comment,
+ "purposeText": intent.purpose,
}
if query.filter_condition is not None:
payload["filterCondition"] = query.filter_condition
@@ -221,13 +223,13 @@ def _federal_query_payload(query: FederalQuery) -> Dict[str, Any]:
def _federal_mutation_payload(mutation: FederalMutation) -> Dict[str, Any]:
+ from teaql.core.request_intent import MutationIntent
+ intent = MutationIntent(mutation.comment)
if mutation.action not in {"Create", "Update", "Delete", "Recover"}:
raise TfpError("TFP_INVALID_REQUEST", f"unsupported mutation action: {mutation.action}")
- if not mutation.comment or not mutation.comment.strip():
- raise TfpError("TFP_AUDIT_REASON_REQUIRED", "mutation audit reason is required")
payload: Dict[str, Any] = {
"entity": mutation.entity, "action": mutation.action,
- "payload": _json_value(mutation.payload), "comment": mutation.comment.strip(),
+ "payload": _json_value(mutation.payload), "comment": intent.comment,
}
if mutation.id is not None:
payload["id"] = _json_value(mutation.id)
@@ -256,7 +258,7 @@ def _query_request(request: QueryRequest) -> FederalQuery:
select_items=list(query.projection), group_by_items=list(query.group_by_items),
aggregate_items=[{"function": item.function.name, "field": item.field, "alias": item.alias}
for item in query.aggregates],
- comment=request._comment or query.comment_text,
+ comment=request.intent.comment,
purpose=request._purpose,
)
diff --git a/src/teaql/runtime/__init__.py b/src/teaql/runtime/__init__.py
index 44f2f21..c6d127e 100644
--- a/src/teaql/runtime/__init__.py
+++ b/src/teaql/runtime/__init__.py
@@ -8,6 +8,7 @@
Tools, ToolToken, ToolUnavailableError,
)
from .module import RuntimeModule, DefaultEntityDataServiceBehavior
+from .graph_session import GraphMutationSession, GraphCommittedError
from .store import DataStore
from .audit import RawAuditEvent, SafeAuditEvent, MutationAuditKind
from .business_clock import BusinessClock, FixedBusinessClock, SystemBusinessClock
@@ -59,6 +60,7 @@
__all__ = ["WireFieldMetadata", "WireEntityMetadata", "NormalizedWireInput", "WireInputError", "create_wire_entity_metadata", "normalize_wire_input", "encode_wire_output", "retain_submitted_paths", "EntityKey", "EntityChangeSet", "EntityRoot", "ContextEntityRef", "ContextRootError", "CheckException", "CheckResult", "I18nCatalog", "JsonFieldNamingProfile", "Locale", "ObjectLocation", "UnsupportedLocaleError", "UserContext", "TeaqlRuntime", "SqlLogEntry", "SqlLogOperation", "DiagnosticSqlLogSink", "TextDiagnosticSqlLogSink", "ServiceRuntimeFromEnv", "RuntimeModule", "DataStore", "RawAuditEvent", "SafeAuditEvent", "MutationAuditKind", "BusinessClock", "FixedBusinessClock", "SystemBusinessClock", "AeadEntityReferenceCodec", "EntityReferenceClaims", "EntityReferenceCodec", "EntityReferenceTokenError", "ENTITY_REFERENCE_AAD", "UNSAFE_RAW_ENTITY_REFERENCES_ACKNOWLEDGEMENT", "UNSAFE_RAW_ENTITY_REFERENCES_ENVIRONMENT", "BUSINESS_ID_PERMUTATION_V1_ALPHABET", "BUSINESS_ID_PERMUTATION_V1_DOMAIN_SIZE", "BUSINESS_ID_PERMUTATION_V1_MAX_SEQUENCE", "BUSINESS_ID_PERMUTATION_V1_WIDTH", "DefaultBusinessIdProfileFactory", "DefaultBusinessIdService", "InMemoryBusinessIdAllocator", "PermutedDailyBusinessIdProfile", "StaticBusinessIdKeyProvider", "encode_business_id_permutation_v1", "ContextTools", "ExecutableHttpTool", "HTTP_TOOL", "HttpIntentPhase", "HttpTool", "HttpToolProvider", "ToolDeniedError", "ToolError", "ToolPolicy", "ToolRisk", "Tools", "ToolToken", "ToolUnavailableError"]
__all__ += [
+ "GraphMutationSession", "GraphCommittedError",
"MISSING_APPROVAL", "MISSING_POLICY",
"DelegatingMutationGovernanceSink",
"DelegatingMutationPolicyApprovalProvider",
diff --git a/src/teaql/runtime/audit.py b/src/teaql/runtime/audit.py
index 1097a9a..7e90bce 100644
--- a/src/teaql/runtime/audit.py
+++ b/src/teaql/runtime/audit.py
@@ -28,6 +28,7 @@ class RawAuditEvent:
actor: Optional[str] = None
category: Optional[str] = None
mutation_governance: Any = None
+ _intent_privacy: Any = field(default=None, repr=False, compare=False)
def safe(self, mask_fields: List[str], max_length: Optional[int]) -> "SafeAuditEvent":
from .log_privacy import REDACTED, credential_name, payload_has_credentials, plaintext_enabled, scrub, value_strings
@@ -50,6 +51,8 @@ def safe(self, mask_fields: List[str], max_length: Optional[int]) -> "SafeAuditE
value = "*" * max_length if max_length <= 3 else value[:max_length - 3] + "..."
fields.append(SafeAuditField(change.field, value, masked, truncated))
intent_values = secrets + value_strings(self.entity_id)
+ if self._intent_privacy is not None:
+ intent_values.extend(self._intent_privacy.secrets(allow))
return SafeAuditEvent(
self.kind, self.entity, self.entity_id, scrub(tuple(fields), secrets), scrub(self.trace_chain, intent_values),
scrub(self.actor, intent_values), self.category, self.mutation_governance,
@@ -88,3 +91,43 @@ async def deliver(sink: Any, method: str, context: Any, event: Any) -> None:
result = callback(context, event)
if isawaitable(result):
await result
+
+
+class _CommittedAuditJournal:
+ """Private journal owned by one SQL transaction, never by shared Context."""
+
+ def __init__(self, context):
+ from copy import copy
+ self._events = []
+ self._active = True
+ view = copy(context)
+ context._resources.setdefault('sql_logs', [])
+ view._resources = dict(context._resources)
+ view._audit_journal = self
+ self.context = view
+
+ def require_active(self):
+ if not self._active:
+ raise RuntimeError('SQL audit transaction is no longer writable')
+
+ def queue(self, event, safe_event):
+ from copy import deepcopy
+ self.require_active()
+ self._events.append((deepcopy(event), deepcopy(safe_event)))
+
+ def discard(self):
+ self._active = False
+ self._events.clear()
+
+ async def committed(self):
+ self._active = False
+ failures = []
+ for event, safe in self._events:
+ try:
+ await self.context._deliver_audit_event(event, safe)
+ except BaseException as error:
+ failures.append(error)
+ self._events.clear()
+ if failures:
+ from .graph_session import GraphCommittedError
+ raise GraphCommittedError(failures)
diff --git a/src/teaql/runtime/context.py b/src/teaql/runtime/context.py
index a758daf..8e5d421 100644
--- a/src/teaql/runtime/context.py
+++ b/src/teaql/runtime/context.py
@@ -2,6 +2,7 @@
import asyncio
import contextvars
from copy import deepcopy
+from inspect import signature
from dataclasses import dataclass
from array import array
from datetime import date, datetime, timedelta, timezone
@@ -109,13 +110,14 @@ def __init__(self):
self._id_set_plan = "ID_SET_DISABLED"
self._id_set_count = 0
self._id_set_count_accuracy = "UNKNOWN"
- self._graph_save_active = False
self._graph_save_lock = asyncio.Lock()
- self._graph_save_owner = contextvars.ContextVar(
- f"teaql_graph_save_owner_{id(self)}", default=None
+ # This variable is a fail-fast re-entry guard, not an ambient session
+ # or a trace stack. Children receive the session explicitly.
+ self._graph_save_guard = contextvars.ContextVar(
+ f"teaql_graph_save_guard_{id(self)}", default=None
)
- self._graph_commit_actions: List[Any] = []
- self._graph_rollback_actions: List[Any] = []
+ self._graph_session = None
+ self._audit_journal = None
self._fix_evidence_current: List[FixEvidence] = []
self._fix_evidence_last: List[FixEvidence] = []
self._checked_mutations = set()
@@ -207,76 +209,59 @@ def decode_entity_reference(
)
return claims
- async def execute_graph_save(self, work):
- """Run one generated entity graph in one provider transaction."""
- if self._graph_save_owner.get() is not None:
- return await work()
+ async def execute_graph_save(self, work, *, comment=None):
+ """Run ``work(session)``; never implicitly join another root save."""
+ from teaql.core.request_intent import MutationIntent
+ from .graph_session import GraphMutationSession
+ intent = MutationIntent(comment)
+ guard = self._graph_save_guard.get()
+ if self._graph_session is not None or (guard is not None and guard['active']):
+ raise RuntimeError('independent graph save cannot re-enter an active graph; compose children explicitly')
async with self._graph_save_lock:
provider = self.require_resource("dataService")
begin = getattr(provider, "begin", None)
if not callable(begin):
raise RuntimeError("Configured dataService does not support graph transactions")
+ transaction = await self._call_transaction_method(begin, self)
+ guard = {'active': True}
+ guard_token = self._graph_save_guard.set(guard)
+ session = None
try:
- transaction = await begin(self)
- except TypeError:
- transaction = await begin()
- owner_token = self._graph_save_owner.set(object())
- self._graph_save_active = True
- self._mutation_policy.begin_graph()
- self._graph_commit_actions = []
- self._graph_rollback_actions = []
- self.insert_resource("fix_time", self.business_time())
- self.begin_fix_evidence()
- self.insert_resource("dataService", transaction)
- try:
- result = await work()
+ session = GraphMutationSession(self, transaction, intent)
+ result = await work(session)
+ session.context._mutation_policy.ensure_graph_complete()
+ await self._finish_graph_transaction(transaction, "commit", session.context)
except BaseException:
try:
- await self._finish_graph_transaction(transaction, "rollback")
- finally:
- for action in reversed(self._graph_rollback_actions):
- action()
+ await self._finish_graph_transaction(transaction, "rollback", session.context if session else self)
+ except BaseException:
+ pass # Preserve the original operation/commit failure.
+ if session is not None:
+ await session.rolled_back()
raise
else:
- try:
- self._mutation_policy.ensure_graph_complete()
- await self._finish_graph_transaction(transaction, "commit")
- except BaseException:
- try:
- await self._finish_graph_transaction(transaction, "rollback")
- finally:
- for action in reversed(self._graph_rollback_actions):
- action()
- raise
- for action in self._graph_commit_actions:
- action()
+ await session.committed()
return result
finally:
- self.insert_resource("dataService", provider)
- self._graph_save_active = False
- self._mutation_policy.end_graph()
- self._graph_commit_actions = []
- self._graph_rollback_actions = []
- self._resources.pop("fix_time", None)
- self.finish_fix_evidence()
- self._graph_save_owner.reset(owner_token)
-
- async def _finish_graph_transaction(self, transaction, operation: str) -> None:
- finish = getattr(transaction, operation)
+ if session is not None:
+ session.close()
+ self._fix_evidence_last = list(session.context.last_fix_evidence())
+ guard['active'] = False
+ self._graph_save_guard.reset(guard_token)
+
+ @staticmethod
+ async def _call_transaction_method(method, context):
+ # Select the signature before invocation. A TypeError thrown *inside*
+ # a provider must not trigger a second begin/commit/rollback call.
try:
- await finish(self)
+ signature(method).bind(context)
except TypeError:
- await finish()
+ return await method()
+ return await method(context)
- def after_graph_commit(self, work) -> None:
- if not self._graph_save_active:
- raise RuntimeError("No graph save is active")
- self._graph_commit_actions.append(work)
-
- def after_graph_rollback(self, work) -> None:
- if not self._graph_save_active:
- raise RuntimeError("No graph save is active")
- self._graph_rollback_actions.append(work)
+ async def _finish_graph_transaction(self, transaction, operation: str, context) -> None:
+ finish = getattr(transaction, operation)
+ await self._call_transaction_method(finish, context)
async def get_in_store(self, key: str) -> Optional[Any]:
store = self.get_resource("data_store")
@@ -385,6 +370,8 @@ def require_resource(self, resource_type: str) -> Any:
def prepare_query(self, query: Any) -> Any:
"""Clone a query graph and apply trusted policy once to every query node."""
+ from teaql.core.request_intent import QueryIntent
+ intent = QueryIntent.from_query(query)
prepared_root = deepcopy(query)
policy = self.get_resource("request_policy")
if policy is None:
@@ -420,7 +407,17 @@ def prepare_node(node: Any) -> Any:
prepared.child_enhancements = [prepare_node(child) for child in children]
return prepared
- return prepare_node(prepared_root)
+ result = prepare_node(prepared_root)
+ result.comment_text = intent.comment
+ result.purpose_text = intent.purpose
+ return result
+
+ def prepare_query_request(self, request):
+ request.validate()
+ query = deepcopy(request.query)
+ query.comment_text = request.intent.comment
+ query.purpose_text = request.intent.purpose
+ return request.with_query(self.prepare_query(query))
def set_user_identifier(self, identifier: str):
self._user_identifier = identifier
@@ -738,6 +735,19 @@ def translate_check_results(self, results: Any):
return results
async def send_audit_event(self, event: Any):
+ descriptor = self.entity(event.entity)
+ mask_fields = getattr(descriptor, "audit_mask_fields_val", []) if descriptor else []
+ max_len = getattr(descriptor, "audit_value_max_len_val", None) if descriptor else None
+ safe_event = event.safe(mask_fields, max_len)
+ if self._graph_session is not None:
+ self._graph_session.queue_audit(event, safe_event)
+ return
+ if self._audit_journal is not None:
+ self._audit_journal.queue(event, safe_event)
+ return
+ await self._deliver_audit_event(event, safe_event)
+
+ async def _deliver_audit_event(self, event: Any, safe_event: Any):
from .audit import deliver
from .telemetry import RuntimeOperation, start_runtime_operation
scope = start_runtime_operation(self._runtime_telemetry, RuntimeOperation(
@@ -748,13 +758,19 @@ async def send_audit_event(self, event: Any):
},
))
try:
+ failures = []
if self._standard_audit_sink is not None:
- await deliver(self._standard_audit_sink, "on_event", self, event)
+ try:
+ await deliver(self._standard_audit_sink, "on_event", self, event)
+ except BaseException as error:
+ failures.append(error)
if self._app_audit_sink is not None:
- descriptor = self.entity(event.entity)
- mask_fields = getattr(descriptor, "audit_mask_fields_val", []) if descriptor else []
- max_len = getattr(descriptor, "audit_value_max_len_val", None) if descriptor else None
- await deliver(self._app_audit_sink, "on_safe_event", self, event.safe(mask_fields, max_len))
+ try:
+ await deliver(self._app_audit_sink, "on_safe_event", self, safe_event)
+ except BaseException as error:
+ failures.append(error)
+ if failures:
+ raise failures[0]
scope.success()
except BaseException as error:
scope.failure(error)
@@ -797,11 +813,27 @@ def preflight_mutation(self, mutation: Any) -> None:
"""Check/fix and snapshot one operation for whole-graph policy review."""
self.check_and_fix_mutation(mutation)
self._mutation_policy.record_preflight(mutation)
+ if self._graph_session is not None:
+ from .log_privacy import _MutationIntentPrivacy
+ session = self._graph_session
+ session._intent_privacy = session._intent_privacy.merge(
+ _MutationIntentPrivacy.capture(mutation, self.entity))
def mutation_policy_execution(self, request: Any):
"""Provider boundary scope; entered after validation and before mutation."""
+ self._require_mutation_invocation()
return self._mutation_policy.enter_mutation(self, request)
+ def _require_mutation_invocation(self):
+ if self._audit_journal is not None:
+ self._audit_journal.require_active()
+ if self._graph_session is not None:
+ self._graph_session._require_active()
+ return
+ guard = self._graph_save_guard.get()
+ if guard is not None and guard['active']:
+ raise RuntimeError('mutation within an active graph requires its explicit session context')
+
def with_sql_log_options(self, options: 'SqlLogOptions') -> 'UserContext':
self.insert_resource("sql_log_options", options)
return self
@@ -870,7 +902,7 @@ def language(self) -> Any:
def record_metadata_log(self, metadata: Any):
self._record_metadata_log(metadata)
- def _record_metadata_log(self, metadata: Any, *, intent_source=None, intent_values=()):
+ def _record_metadata_log(self, metadata: Any, *, intent_source=None, intent_values=(), intent_privacy=None):
"""Internal statement plumbing: source bindings never reach sinks/buffers."""
op = SqlLogOperation.Select
op_str = str(getattr(metadata, 'operation', '')).lower()
@@ -889,6 +921,7 @@ def _record_metadata_log(self, metadata: Any, *, intent_source=None, intent_valu
purpose=getattr(metadata, 'purpose', None),
audit_reason=getattr(metadata, 'audit_reason', None),
trace_path=list(getattr(metadata, 'trace_chain', [])),
+ mutation_lineage=tuple(getattr(metadata, 'mutation_lineage', ())),
sql=getattr(metadata, 'parameterized_sql', ''),
params=list(getattr(metadata, 'parameters', [])),
debug_sql=getattr(metadata, 'debug_query', '') or '',
@@ -911,7 +944,8 @@ def _record_metadata_log(self, metadata: Any, *, intent_source=None, intent_valu
entry.result_summary = f"{entry.affected_rows} rows affected"
from .log_privacy import sql_log_projection
- entry = sql_log_projection(entry, _intent_source=intent_source, _intent_values=intent_values)
+ entry = sql_log_projection(entry, _intent_source=intent_source, _intent_values=intent_values,
+ _intent_privacy=intent_privacy)
logs = self.sql_logs()
logs.append(entry)
self._resources["sql_logs"] = logs
@@ -1224,6 +1258,7 @@ class SqlLogEntry:
log_mode: Optional[str] = None
omission_reason: Optional[str] = None
execution_outcome: Optional[str] = None
+ mutation_lineage: tuple[Any, ...] = ()
class DiagnosticSqlLogSink:
"""Policy-projected SQL destination; the text sink is installed by default."""
diff --git a/src/teaql/runtime/graph_session.py b/src/teaql/runtime/graph_session.py
new file mode 100644
index 0000000..15b36ce
--- /dev/null
+++ b/src/teaql/runtime/graph_session.py
@@ -0,0 +1,121 @@
+"""One explicit graph-save session. Context supplies configuration, not lineage."""
+
+from copy import copy, deepcopy
+from inspect import isawaitable
+
+from teaql.core.mutation import MutationRequest
+from teaql.core.request_intent import MutationIntent
+from teaql.core.trace_scope import TraceScope
+
+
+class GraphCommittedError(RuntimeError):
+ """Database commit succeeded; delivery/cleanup failed. Never retry the write."""
+
+ committed = True
+
+ def __init__(self, failures):
+ self.failures = tuple(failures)
+ super().__init__('graph committed; one or more completion actions failed')
+
+
+class GraphMutationSession:
+ def __init__(self, context, transaction, intent: MutationIntent):
+ from .log_privacy import _MutationIntentPrivacy
+ self._intent_privacy = _MutationIntentPrivacy()
+ self.intent = intent
+ self.transaction = transaction
+ self._owner = object()
+ self._active = True
+ self._committed = False
+ self._commit_actions = []
+ self._rollback_actions = []
+ self._audits = []
+ # Preserve customer UserContext overrides and configured services. Only
+ # mutable invocation state is detached; diagnostic destinations are shared.
+ view = copy(context)
+ context._resources.setdefault('sql_logs', [])
+ view._resources = dict(context._resources)
+ view._resources['dataService'] = transaction
+ view._resources['fix_time'] = context.business_time()
+ view._mutation_policy = context._mutation_policy.for_invocation()
+ view._mutation_policy.begin_graph(intent.comment)
+ view._checked_mutations = set()
+ view._fix_evidence_current = []
+ view._fix_evidence_last = []
+ view._graph_session = self
+ self.context = view
+
+ def _require_active(self):
+ if not self._active or self._committed:
+ raise RuntimeError('graph mutation session is no longer writable')
+
+ def scope(self, key, parent=None, local_reason=None):
+ self._require_active()
+ if parent is None:
+ return TraceScope.root(self._owner, key, self.intent.comment)
+ if not isinstance(parent, TraceScope) or parent._owner is not self._owner:
+ raise ValueError('graph trace scope belongs to another invocation')
+ return parent.child(key, local_reason)
+
+ def request(self, command, scope, ledger=None, key=None):
+ self._require_active()
+ if not isinstance(scope, TraceScope) or scope._owner is not self._owner:
+ raise ValueError('graph trace scope belongs to another invocation')
+ specific = ledger.trace_chain(key) if ledger is not None and key is not None else ()
+ return MutationRequest(command, comment=self.intent.comment).with_mutation_lineage(
+ specific or scope.recover())
+
+ def after_commit(self, action):
+ self._require_active()
+ self._commit_actions.append(action)
+
+ def after_rollback(self, action):
+ self._require_active()
+ self._rollback_actions.append(action)
+
+ def queue_audit(self, event, safe_event):
+ self._require_active()
+ self._audits.append((deepcopy(event), deepcopy(safe_event)))
+
+ async def committed(self):
+ self._committed = True
+ failures = []
+ # A failed sink must not prevent subsequent events or ledger cleanup.
+ for event, safe_event in self._audits:
+ try:
+ await self.context._deliver_audit_event(event, safe_event)
+ except BaseException as error:
+ failures.append(error)
+ for action in self._commit_actions:
+ try:
+ result = action()
+ if isawaitable(result):
+ await result
+ except BaseException as error:
+ failures.append(error)
+ if failures:
+ raise GraphCommittedError(failures)
+
+ async def rolled_back(self):
+ # Rollback evidence must never escape as a committed audit event.
+ self._audits.clear()
+ failures = []
+ for action in reversed(self._rollback_actions):
+ try:
+ result = action()
+ if isawaitable(result):
+ await result
+ except BaseException as error:
+ failures.append(error)
+ return tuple(failures)
+
+ def close(self):
+ from .log_privacy import _MutationIntentPrivacy
+ self._intent_privacy = _MutationIntentPrivacy()
+ self._active = False
+ self.context._mutation_policy.end_graph()
+ self.context.finish_fix_evidence()
+ self.context._resources.pop('fix_time', None)
+ self._audits.clear()
+ self._commit_actions.clear()
+ self._rollback_actions.clear()
diff --git a/src/teaql/runtime/log_privacy.py b/src/teaql/runtime/log_privacy.py
index eb02fdb..56026b6 100644
--- a/src/teaql/runtime/log_privacy.py
+++ b/src/teaql/runtime/log_privacy.py
@@ -5,7 +5,7 @@
import re
import weakref
from copy import deepcopy
-from dataclasses import fields, is_dataclass, replace
+from dataclasses import dataclass, fields, is_dataclass, replace
from functools import lru_cache
PLAINTEXT_ENV = "TEAQL_ALLOW_SENSITIVE_PLAINTEXT_LOGS"
@@ -117,7 +117,52 @@ def _is_masked(policy, allow):
return policy in ('credential', 'unknown') or (not allow and policy != 'plain')
-def sql_log_projection(entry, *, _intent_source=None, _intent_values=()):
+def field_log_policy(entity, name):
+ """One classification for SQL binds and invocation-owned intent snapshots."""
+ prop = entity.property_by_name(name) if entity else None
+ if credential_name(name) or (prop and credential_name(prop.column_name_val)):
+ return 'credential'
+ if entity is None or not entity.audit_mask_fields_declared:
+ return 'unknown'
+ if name in entity.audit_mask_fields_val:
+ return 'masked'
+ return getattr(prop, 'log_policy_val', 'unknown')
+
+
+@dataclass(frozen=True, repr=False)
+class _MutationIntentPrivacy:
+ # Immutable strings only, no caller-owned Value/map references. This object
+ # belongs to a native invocation or graph session, never to shared Context.
+ bindings: tuple = ()
+
+ def merge(self, other):
+ return _MutationIntentPrivacy(self.bindings + other.bindings)
+
+ def secrets(self, allow):
+ return [text for policy, text in self.bindings if _is_masked(policy, allow)]
+
+ @classmethod
+ def capture(cls, data, resolve_entity):
+ from teaql.core.mutation import MutationRequest
+ if isinstance(data, MutationRequest):
+ data = data._data
+ if isinstance(data, list):
+ result = cls()
+ for child in data:
+ result = result.merge(cls.capture(child, resolve_entity))
+ return result
+ descriptor = resolve_entity(data.entity)
+ bindings = []
+ for values in (getattr(data, 'values', {}), getattr(data, 'old_values', None) or {}):
+ for name, value in values.items():
+ policy = ('credential' if payload_has_credentials(value)
+ else field_log_policy(descriptor, name))
+ bindings.extend((policy, text) for text in value_strings(value))
+ bindings.extend(('unknown', text) for text in value_strings(getattr(data, 'id', None)))
+ return cls(tuple(bindings))
+
+
+def sql_log_projection(entry, *, _intent_source=None, _intent_values=(), _intent_privacy=None):
"""Source bindings are call-local runtime plumbing, never stored on a log entry."""
allow = plaintext_enabled() and entry.log_mode != 'masked'
prior = _projections.get(id(entry))
@@ -129,12 +174,12 @@ def sql_log_projection(entry, *, _intent_source=None, _intent_values=()):
# Entries are mutable. Do not expose the cached safe alternative itself.
if prior[3] is not None:
return _remember_projection(deepcopy(prior[3]), False)
- projected = _project_with_policy(entry, allow, _intent_source, _intent_values)
- alternative = _project_with_policy(entry, False, _intent_source, _intent_values) if allow else None
+ projected = _project_with_policy(entry, allow, _intent_source, _intent_values, _intent_privacy)
+ alternative = _project_with_policy(entry, False, _intent_source, _intent_values, _intent_privacy) if allow else None
return _remember_projection(projected, allow, alternative)
-def _project_with_policy(entry, allow, intent_source, intent_values):
+def _project_with_policy(entry, allow, intent_source, intent_values, intent_privacy=None):
from teaql.sql.types import DatabaseKind, render_log_sql, _sql_literal
from teaql.core.value import Value
supplied = entry.parameter_log_policies
@@ -155,6 +200,8 @@ def _project_with_policy(entry, allow, intent_source, intent_values):
secrets.extend(text for index, value in enumerate(intent_source.params)
if _is_masked(source_policies[index], allow) for text in value_strings(value))
intent_secrets = secrets + [text for value in intent_values for text in value_strings(value)]
+ if intent_privacy is not None:
+ intent_secrets.extend(intent_privacy.secrets(allow))
# A copied/changed debug record has lost its reliable private alternative.
# Its inherited intent may mention values absent from its own SQL bindings.
unknown_debug_intent = not allow and entry.log_mode == 'debug-plaintext' and intent_source is None
@@ -191,5 +238,6 @@ def literal(index):
result_summary=(f'{entry.result_count} rows returned' if entry.result_count is not None
else f'{entry.affected_rows} rows affected' if entry.affected_rows is not None
else scrub(entry.result_summary, secrets, hide_all=unknown_debug_intent)),
- trace_path=intent(entry.trace_path))
+ trace_path=intent(entry.trace_path),
+ mutation_lineage=intent(entry.mutation_lineage))
return projected
diff --git a/src/teaql/runtime/mutation_policy.py b/src/teaql/runtime/mutation_policy.py
index 78c68f0..0409460 100644
--- a/src/teaql/runtime/mutation_policy.py
+++ b/src/teaql/runtime/mutation_policy.py
@@ -244,12 +244,24 @@ def __init__(self) -> None:
def current(self) -> Optional[MutationGovernanceSnapshot]:
return self._active.get()
- def begin_graph(self) -> None:
+ def for_invocation(self) -> 'MutationPolicyRuntimeState':
+ """Share configuration/warning dedup, never a graph's mutable plan."""
+ state = MutationPolicyRuntimeState()
+ state.registry = self.registry
+ state.approval_provider = self.approval_provider
+ state.warning_sink = self.warning_sink
+ state._emitted_warnings = self._emitted_warnings
+ state._warning_lock = self._warning_lock
+ return state
+
+ def begin_graph(self, audit_reason: str) -> None:
+ from teaql.core.request_intent import MutationIntent
+ intent = MutationIntent(audit_reason)
self._graph_active = True
self._graph_reviewed = False
self._preflight.clear()
self._root_entity = None
- self._audit_reason = None
+ self._audit_reason = intent.comment
self._remaining.clear()
self._active.set(None)
@@ -273,10 +285,10 @@ def record_preflight(self, command: Any) -> None:
if not operations:
raise MutationPolicyError("mutation preflight must contain an operation")
self._root_entity = self._root_entity or operations[0].entity
- self._audit_reason = self._audit_reason or _comment_from_data(command)
self._preflight.extend(operations)
def enter_mutation(self, context: Any, request: MutationRequest):
+ request.validate()
operations = _operations_from_data(request._data)
if not operations:
raise MutationPolicyError("mutation request must contain an operation")
@@ -287,7 +299,7 @@ def enter_mutation(self, context: Any, request: MutationRequest):
# remains fail-closed.
if self._graph_active and self.registry is None and not self._preflight:
plan = self._build_plan(
- context, operations[0].entity, _request_comment(request), tuple(operations)
+ context, operations[0].entity, self._audit_reason, tuple(operations)
)
token = self._active.set(self.review(context, plan))
return _ResetScope(self._active, token)
@@ -300,7 +312,7 @@ def enter_mutation(self, context: Any, request: MutationRequest):
)
planned = tuple(self._preflight or operations)
root = self._root_entity or planned[0].entity
- reason = self._audit_reason or _request_comment(request)
+ reason = self._audit_reason
snapshot = self.review(context, self._build_plan(context, root, reason, planned))
self._active.set(snapshot)
self._remaining = Counter(_operation_signature(item) for item in planned)
diff --git a/src/teaql/sql/dialect.py b/src/teaql/sql/dialect.py
index b29d901..82e5af0 100644
--- a/src/teaql/sql/dialect.py
+++ b/src/teaql/sql/dialect.py
@@ -161,6 +161,17 @@ def compile_select_sql(self, entity: EntityDescriptor, query: SelectQuery, param
if where_parts:
sql += " WHERE " + " AND ".join(where_parts)
+ # Window ranks apply to the grouped, HAVING-filtered result. Returning
+ # the partition wrapper first silently collapses aggregate rows and
+ # drops HAVING bindings on both root and loaded-relation queries.
+ if query.group_by_items:
+ group_by = ", ".join(self.column_sql(entity, field) for field in query.group_by_items)
+ sql += f" GROUP BY {group_by}"
+
+ if query.having_expr is not None:
+ having_sql = self.compile_expr(entity, query.having_expr, params)
+ sql += f" HAVING {having_sql}"
+
if partitioned:
rank = self.quote_ident("__teaql_partition_rank")
predicates = [f"{rank} > {query.slice.offset}"]
@@ -169,14 +180,6 @@ def compile_select_sql(self, entity: EntityDescriptor, query: SelectQuery, param
alias = self.quote_ident("__teaql_partitioned")
return f"SELECT * FROM ({sql}) AS {alias} WHERE {' AND '.join(predicates)} ORDER BY {rank}"
- if query.group_by_items:
- group_by = ", ".join(self.column_sql(entity, field) for field in query.group_by_items)
- sql += f" GROUP BY {group_by}"
-
- if query.having_expr is not None:
- having_sql = self.compile_expr(entity, query.having_expr, params)
- sql += f" HAVING {having_sql}"
-
if query.order_by_items:
order_by = ", ".join(self.order_by_sql(entity, order, params) for order in query.order_by_items)
sql += f" ORDER BY {order_by}"
@@ -338,7 +341,17 @@ def property_projection(p):
return ", ".join(property_projection(p) for p in getattr(entity, 'properties', []))
parts = []
- for field in query.projection:
+ projected_fields = list(query.projection)
+ # Hydration must use an actual FK, not treat an omitted column as NULL.
+ # Keep caller-owned query/projection unchanged.
+ if not query.group_by_items:
+ relation_names = [load.name for load in query.relations]
+ relation_names += [aggregate.relation_name for aggregate in query.relation_aggregates]
+ for name in relation_names:
+ relation = entity.relation_by_name(name)
+ if relation and not relation.is_many and relation.local_key not in projected_fields:
+ projected_fields.append(relation.local_key)
+ for field in projected_fields:
prop = next((p for p in getattr(entity, 'properties', []) if getattr(p, 'name', None) == field), None)
if not prop:
raise UnknownFieldError(field)
@@ -399,15 +412,8 @@ def compile_projection(self, entity: EntityDescriptor, query: SelectQuery, param
return self.aggregate_projection(entity, query, params)
def field_log_policy(self, entity, field):
- from teaql.runtime.log_privacy import credential_name
- prop = entity.property_by_name(field)
- if credential_name(field) or (prop and credential_name(prop.column_name_val)):
- return 'credential'
- if not entity.audit_mask_fields_declared:
- return 'unknown'
- if field in entity.audit_mask_fields_val:
- return 'masked'
- return getattr(prop, 'log_policy_val', 'unknown')
+ from teaql.runtime.log_privacy import field_log_policy
+ return field_log_policy(entity, field)
def bind_field(self, params, value, entity, field):
if isinstance(params, SQLBindings):
@@ -443,6 +449,13 @@ def _compile_expr(self, entity: EntityDescriptor, expr: Expr, params: List[Value
elif isinstance(expr, BinaryExpr):
if expr.op in (BinaryOp.In, BinaryOp.NotIn, BinaryOp.InLarge, BinaryOp.NotInLarge):
return self.compile_in(entity, expr.left, expr.op, expr.right, params)
+ original = getattr(expr.right, '_like_operand', None)
+ if (isinstance(params, SQLBindings) and expr.op in (BinaryOp.Like, BinaryOp.NotLike)
+ and isinstance(expr.right, ValueExpr) and original is not None
+ and expr.right.value == original[1]):
+ # Retain only an intact typed lowering, never infer from raw SQL
+ # wildcards or reuse stale provenance after an AST rewrite.
+ params._retain_intent_operand(original[0])
lhs = self.compile_expr(entity, expr.left, params)
rhs = self.compile_expr(entity, expr.right, params)
op_str = {
diff --git a/src/teaql/sql/executor.py b/src/teaql/sql/executor.py
index 2f4207e..7b45aa9 100644
--- a/src/teaql/sql/executor.py
+++ b/src/teaql/sql/executor.py
@@ -27,6 +27,7 @@
)
from teaql.core.query import Aggregate, AggregateFunction, SelectQuery
from teaql.core.value import Value
+from teaql.core.trace import canonical_sql_trace_path, physical_readback_path, trace_name
from teaql.runtime.telemetry import RuntimeOperation, observe_runtime_operation, start_runtime_operation
from teaql.runtime.context import RetainedIdSet
@@ -36,21 +37,50 @@
class _QueryWithLogIntent(QueryRequest):
"""Invocation-local compiler plumbing, excluded from dataclass/wire fields."""
- def __init__(self, query, trace_chain, comment, purpose, source):
- super().__init__(query, trace_chain, comment, purpose)
+ def __init__(self, query, trace_chain, comment, purpose, source, origin_entity=None, assembly=None):
+ super().__init__(query, trace_chain, comment, purpose, _origin_entity=origin_entity)
self._log_intent_source = source
+ self._relation_assembly = assembly
+
+
+class _RelationAssembly:
+ """One derived load owns scalar keys; never attach them to result records."""
+ def __init__(self, field):
+ self.field = field
+ self.keys = {}
+
+ def capture(self, rows):
+ self.keys = {id(row): row.get(self.field) for row in rows}
+
+ def key(self, row):
+ # Hydration mutates these same row dictionaries. Reordering (ID-set
+ # paging) is fine, but a replacement must not silently drop membership.
+ return self.keys[id(row)]
def _intent_bindings(compiled, request):
# Resolve each source's policy before flattening: credential detection and
# malformed-policy handling depend on the original statement, not the child.
- from teaql.runtime.log_privacy import _binding_policies
inherited = getattr(request, '_log_intent_source', None)
sources = [inherited, compiled] if inherited is not None else [compiled]
- return CompiledQuery('', [deepcopy(value) for source in sources for value in source.params],
- parameter_log_policies=[policy for source in sources
- for policy in _binding_policies(source)],
- sql_origin='generated')
+ result = CompiledQuery('', [], parameter_log_policies=[], sql_origin='generated')
+ for source in sources:
+ _append_intent_bindings(result, source)
+ return result
+
+
+def _append_intent_bindings(target, source):
+ from teaql.runtime.log_privacy import _binding_policies
+ target.params.extend(deepcopy(source.params))
+ target.parameter_log_policies.extend(_binding_policies(source))
+ operands = getattr(source, '_intent_operands', ())
+ if operands:
+ # This is a private provenance copy, not the executable statement. Apply
+ # the same credential/unknown fail-closed policy before combining it.
+ original = CompiledQuery('', [deepcopy(value) for value, _ in operands],
+ parameter_log_policies=[policy for _, policy in operands], sql_origin='generated')
+ target.params.extend(original.params)
+ target.parameter_log_policies.extend(_binding_policies(original))
class _NoopContextManager:
@@ -132,6 +162,61 @@ def __init__(self, dialect: SqlDialect, transport: SqlTransport, schema_provider
self.transport = transport
self.schema_provider = schema_provider
+ def _backend_name(self) -> str:
+ kind = self.dialect.kind()
+ name = getattr(kind, 'name', str(kind)).lower()
+ return 'postgres' if name == 'postgresql' else name
+
+ def _query_intent_bindings(self, compiled, request):
+ """Capture declared descendant bindings before any physical parent log.
+
+ Compile copies only for policy provenance: no SQL, list preparation,
+ query-builder mutation or Context-owned redaction state.
+ """
+ source = _intent_bindings(compiled, request)
+ pending = [(request.query, request.query.entity)]
+ for original in getattr(request, '_log_intent_queries', ()):
+ descriptor = self.schema_provider.get_entity(original.entity)
+ if descriptor is None:
+ continue
+ candidate = deepcopy(original)
+ self._resolve_subquery_entities(candidate.filter_expr)
+ bindings = self.dialect.compile_select(descriptor, candidate)
+ _append_intent_bindings(source, bindings)
+ pending.append((original, original.entity))
+ visited = set()
+ while pending:
+ query, entity = pending.pop()
+ key = (id(query), entity)
+ if key in visited:
+ continue
+ visited.add(key)
+ descriptor = self.schema_provider.get_entity(entity)
+ children = []
+ for relation in query.relations:
+ model = descriptor.relation_by_name(relation.name) if descriptor else None
+ if relation.query is not None and model is not None:
+ children.append((relation.query, model.target_entity))
+ for aggregate in query.relation_aggregates:
+ model = descriptor.relation_by_name(aggregate.relation_name) if descriptor else None
+ if model is not None:
+ children.append((aggregate.query, model.target_entity))
+ for facet in query.facets:
+ children.append((facet.query, facet.query.entity))
+ for child, entity in children:
+ if (id(child), entity) in visited:
+ continue
+ child_descriptor = self.schema_provider.get_entity(entity)
+ if child_descriptor is None:
+ continue # execution still owns missing-schema diagnostics
+ candidate = deepcopy(child)
+ candidate.entity = entity
+ self._resolve_subquery_entities(candidate.filter_expr)
+ bindings = self.dialect.compile_select(child_descriptor, candidate)
+ _append_intent_bindings(source, bindings)
+ pending.append((child, entity))
+ return source
+
def _sync_generated_schema(self, context: 'UserContext') -> None:
register = getattr(self.schema_provider, "register_entity", None)
if callable(register) and context is not None:
@@ -189,15 +274,28 @@ def capabilities(self) -> DataServiceCapabilities:
)
def _record_statement(self, context, request, compiled, started_at, operation,
- outcome, result_count=None, affected_rows=None):
+ outcome, result_count=None, affected_rows=None, intent_privacy=None):
"""Project through context; never attach driver exceptions to diagnostics."""
query = operation == DataServiceOperation.Query
entity = request.query.entity if query else request._data.entity
comment = request._comment if query else getattr(request, 'comment', None)
if not query and callable(comment):
comment = comment()
- provider = str(self.dialect.kind()).lower()
+ provider = self._backend_name()
sql_operation = 'select' if query else operation.name.lower()
+ if query:
+ trace_source = [TraceNode(kind='comment', name=request.origin_entity, comment=comment),
+ TraceNode(kind='purpose', name=request.origin_entity, comment=request._purpose),
+ *request.trace_chain]
+ else:
+ chain = [*request.mutation_lineage, *request.trace_chain()]
+ root = next((trace_name(node) for node in chain if trace_name(node).strip()), entity)
+ target_id = getattr(request._data, 'id', None)
+ if target_id is None:
+ target_id = getattr(request._data, 'values', {}).get('id')
+ target_id = getattr(target_id, 'val', target_id)
+ trace_source = [TraceNode(kind='auditReason', name=root, comment=comment),
+ *chain, TraceNode(kind='entity', name=entity, entity_id=target_id)]
metadata = ExecutionMetadata(
backend=provider, operation=operation, started_at=started_at,
ended_at=datetime.now(), execution_outcome=outcome,
@@ -207,13 +305,8 @@ def _record_statement(self, context, request, compiled, started_at, operation,
result_count=result_count, affected_rows=affected_rows,
comment=comment, purpose=request._purpose if query else None,
audit_reason=None if query else comment,
- trace_chain=[
- TraceNode(kind='operation', name='query' if query else 'mutation', comment='query' if query else 'mutation'),
- TraceNode(kind='request' if query else 'entity', name=entity, comment=entity),
- *(request.trace_chain if query else request.trace_chain()),
- TraceNode(kind='provider', name=provider, comment=provider),
- TraceNode(kind='sql', name=sql_operation, comment=sql_operation),
- ],
+ trace_chain=canonical_sql_trace_path(trace_source, provider, sql_operation),
+ mutation_lineage=() if query else request.mutation_lineage,
)
if context is not None:
try:
@@ -227,11 +320,12 @@ def _record_statement(self, context, request, compiled, started_at, operation,
if getattr(prop, '_is_id', False) or getattr(prop, 'is_id_val', False)), None)
if id_property is not None:
target_id = getattr(request._data, 'values', {}).get(id_property.name)
- if source is None and target_id is None:
+ if source is None and target_id is None and intent_privacy is None:
context.record_metadata_log(metadata)
else:
context._record_metadata_log(metadata, intent_source=source,
- intent_values=() if target_id is None else (target_id,))
+ intent_values=() if target_id is None else (target_id,),
+ intent_privacy=intent_privacy)
except Exception:
# A broken diagnostic destination must not replace an in-flight
# driver failure, cancellation or generator close.
@@ -239,10 +333,16 @@ def _record_statement(self, context, request, compiled, started_at, operation,
raise
return metadata
- async def query_stream(self, context, request: QueryRequest, chunk_size: int):
+ def query_stream(self, context, request: QueryRequest, chunk_size: int):
+ """Own the request at cursor creation; opening the driver stays lazy."""
+ request.validate()
if chunk_size <= 0:
raise ValueError("chunk_size must be positive")
- if (request.query.relations or request.query.child_enhancements
+ owned = request.with_query(request.query)
+ return self._query_stream(context, owned, chunk_size)
+
+ async def _query_stream(self, context, request: QueryRequest, chunk_size: int):
+ if (request.query.relations or request.query.relation_aggregates or request.query.child_enhancements
or request.query.object_group_bys or request.query.facets):
raise ValueError(
"streaming relation or aggregate enhancement is not supported; "
@@ -253,6 +353,7 @@ async def query_stream(self, context, request: QueryRequest, chunk_size: int):
raise CompileError(SqlCompileError(f"unknown entity: {request.query.entity}"))
self._resolve_subquery_entities(request.query.filter_expr)
compiled = self.dialect.compile_select(entity_desc, request.query)
+ request._log_intent_source = self._query_intent_bindings(compiled, request)
pending = None
index = 0
delivered = 0
@@ -291,6 +392,7 @@ async def query_stream(self, context, request: QueryRequest, chunk_size: int):
DataServiceOperation.Query, outcome, result_count=delivered)
async def query(self, context: 'UserContext', request: QueryRequest) -> QueryResult:
+ request.validate()
telemetry = context.runtime_telemetry() if context is not None else None
return await observe_runtime_operation(
telemetry,
@@ -302,23 +404,21 @@ async def query(self, context: 'UserContext', request: QueryRequest) -> QueryRes
)
async def _query(self, context: 'UserContext', request: QueryRequest) -> QueryResult:
+ request.validate()
+ assembly = getattr(request, '_relation_assembly', None)
+ request = request.with_query(request.query)
self._sync_generated_schema(context)
request.query.prepare_for_list()
execution_query, retained_order, retained_empty = await self._prepare_id_set_page(context, request.query)
if retained_empty:
now = datetime.now()
- provider = str(self.dialect.kind()).lower()
+ provider = self._backend_name()
metadata = ExecutionMetadata(
backend=provider, operation=DataServiceOperation.Query,
started_at=now, ended_at=now, result_count=0,
- trace_chain=[
- TraceNode(kind="operation", name="query", comment="query"),
- TraceNode(kind="request", name=request.query.entity,
- comment=request.query.entity),
- *request.trace_chain,
- TraceNode(kind="provider", name=provider, comment=provider),
- TraceNode(kind="sql", name="select", comment="select"),
- ],
+ trace_chain=canonical_sql_trace_path(
+ [TraceNode(kind='comment', name=request.origin_entity, comment=request._comment),
+ *request.trace_chain], provider, 'select'),
comment=request._comment, purpose=request._purpose,
)
if context is not None:
@@ -326,8 +426,8 @@ async def _query(self, context: 'UserContext', request: QueryRequest) -> QueryRe
return QueryResult(rows=[], metadata=metadata)
source = getattr(request, '_log_intent_source', None)
request = (_QueryWithLogIntent(execution_query, request.trace_chain, request._comment,
- request._purpose, source) if source is not None else
- QueryRequest(execution_query, request.trace_chain, request._comment, request._purpose))
+ request._purpose, source, request.origin_entity) if source is not None else
+ request.with_query(execution_query))
entity_desc = self.schema_provider.get_entity(request.query.entity)
if not entity_desc and context:
entities = context.get_resource("entities")
@@ -344,6 +444,9 @@ async def _query(self, context: 'UserContext', request: QueryRequest) -> QueryRe
compiled = self.dialect.compile_select(entity_desc, request.query)
except SqlCompileError as e:
raise CompileError(e)
+
+ source = self._query_intent_bindings(compiled, request)
+ request._log_intent_source = source
start = datetime.now()
try:
@@ -365,58 +468,97 @@ async def _query(self, context: 'UserContext', request: QueryRequest) -> QueryRe
metadata = self._record_statement(context, request, compiled, start,
DataServiceOperation.Query, 'success', result_count=len(rows))
- source = (_intent_bindings(compiled, request) if rows and
- (request.query.relations or request.query.relation_aggregates) else None)
- await self._enhance_relations(context, rows, request, source)
+ if assembly is not None:
+ assembly.capture(rows)
+ # All local identities are captured before aggregate aliases or sibling
+ # hydration can replace record fields.
+ parent_keys = {relation.local_key: [row.get(relation.local_key) for row in rows]
+ for load in request.query.relations
+ if (relation := entity_desc.relation_by_name(load.name)) is not None}
+ # Forward hydration may replace a scalar membership key with an object.
+ # Compute related aggregates while those keys still identify their rows.
await self._enhance_relation_aggregates(context, rows, request, source)
+ await self._enhance_relations(context, rows, request, source, parent_keys)
if retained_order:
by_id = {int(row["id"]): row for row in rows if row.get("id") is not None}
rows = [by_id[entity_id] for entity_id in retained_order if entity_id in by_id]
+ facets = await self._query_facets(context, request, entity_desc)
+ return QueryResult(rows=rows, metadata=metadata, facets=facets)
+
+ async def _query_facets(self, context, request, entity_desc):
+ """Count the current membership, then traverse each owned Facet selection."""
+ from teaql.core.list import SmartList
facets = {}
for facet in getattr(request.query, 'facets', []):
+ relation = entity_desc.relation_by_name(facet.relation_name)
+ # Legacy numeric partitions still work, but only model metadata can
+ # identify a relationship or add a logical traversal to the trace.
+ member_field = relation.local_key if relation else facet.relation_name
+ target_field = relation.foreign_key if relation else 'id'
+ if relation and relation.target_entity != facet.query.entity:
+ raise CompileError(SqlCompileError(
+ f'facet target differs from relation: {request.query.entity}.{facet.relation_name}'))
membership_query = deepcopy(request.query)
membership_query.facets = []
membership_query.relations = []
+ membership_query.relation_aggregates = []
+ membership_query.child_enhancements = []
+ membership_query.object_group_bys = []
+ membership_query.dynamic_properties = []
+ membership_query.raw_projections = []
membership_query.order_by_items = []
membership_query.slice = None
+ membership_query.partition_by = None
membership_query.projection = []
+ membership_query.expr_projection = []
membership_query.aggregates = [Aggregate(
AggregateFunction.Count, "id", "__teaql_facet_count")]
- membership_query.group_by_items = [facet.relation_name]
- membership_result = await self._query(context, QueryRequest(membership_query))
+ membership_query.group_by_items = [member_field]
+ membership_result = await self._query(context, request.with_query(membership_query))
+ member_property = entity_desc.property_by_name(member_field)
+ member_column = member_property.column_name_val if member_property else member_field
counts = {
- str(row[facet.relation_name]): int(row["__teaql_facet_count"])
+ str(row[member_column]): int(row["__teaql_facet_count"])
for row in membership_result.rows
- if row.get(facet.relation_name) is not None
+ if row.get(member_column) is not None
}
nested_query = deepcopy(facet.query)
- nested_query.facets = []
count_aliases = [
aggregate.alias for aggregate in nested_query.aggregates
if aggregate.function == AggregateFunction.Count
]
nested_query.aggregates = []
nested_query.group_by_items = []
- nested_result = await self._query(context, QueryRequest(nested_query))
+ if not facet.include_all_facets:
+ # The target selection (and any Facets nested inside it) must
+ # see only matching candidates before pagination/recursion.
+ # Keep original key types and make empty membership explicit.
+ member_values = [Value.from_any(row[member_column])
+ for row in membership_result.rows
+ if row.get(member_column) is not None]
+ membership = (BinaryExpr(ColumnExpr(target_field), BinaryOp.In,
+ ValueExpr(Value.List(member_values)))
+ if member_values else ValueExpr(Value.Bool(False)))
+ nested_query.and_filter(membership)
+ nested_request = request.with_query(nested_query)
+ if relation:
+ nested_request.trace_chain.append(TraceNode(
+ kind='relation', name=facet.relation_name,
+ comment=f'{request.query.entity}.{facet.relation_name}'))
+ nested_result = await self._query(context, nested_request)
facet_rows = []
for row in nested_result.rows:
- count = counts.get(str(row.get("id")), 0)
+ count = counts.get(str(row.get(target_field)), 0)
if not facet.include_all_facets and count == 0:
continue
decorated = dict(row)
for alias in count_aliases or ["count"]:
decorated[alias] = count
facet_rows.append(decorated)
- from teaql.core.list import SmartList
- facets[facet.name] = SmartList(facet_rows)
-
- return QueryResult(
- rows=rows,
- metadata=metadata,
- facets=facets
- )
+ facets[facet.name] = SmartList(facet_rows, facets=nested_result.facets)
+ return facets
async def _prepare_id_set_page(self, context, query: SelectQuery):
options = getattr(query, "id_set_pagination", None)
@@ -518,7 +660,7 @@ def _id_set_query_key(context, query: SelectQuery, namespace: str) -> str:
return "teaql:id-set:v1:" + hashlib.sha256(repr(scope).encode("utf-8")).hexdigest()
async def _enhance_relations(self, context, parents: List[Dict[str, Any]], request: QueryRequest,
- intent_source=None) -> None:
+ intent_source, parent_keys) -> None:
query = request.query
if not parents or not query.relations:
return
@@ -537,33 +679,56 @@ async def _enhance_relations(self, context, parents: List[Dict[str, Any]], reque
relation = parent_desc.relation_by_name(load.name)
if relation is None:
raise CompileError(SqlCompileError(f"missing relation: {query.entity}.{load.name}"))
- parent_ids = [row[relation.local_key] for row in parents if relation.local_key in row]
+ local_keys = parent_keys[relation.local_key]
+ parent_ids = [key for key in local_keys if key is not None]
child_query = deepcopy(load.query) if load.query is not None else SelectQuery(relation.target_entity)
child_query.entity = relation.target_entity
+ # A batched relation SELECT can serve several parents, but its
+ # Facet counts belong to each parent's full filtered membership.
+ # Preserve the finite selection tree while deferring that work
+ # until scalar parent keys have been used to assemble the rows.
+ relation_facets = child_query.facets if relation.is_many else []
+ if relation_facets:
+ child_query.facets = []
if relation.foreign_key not in child_query.projection:
child_query.projection.append(relation.foreign_key)
limited = child_query.slice is not None and child_query.slice.limit is not None
- if limited and not any(order.field_name == "id" for order in child_query.order_by_items):
- child_query.order_asc("id")
+ if limited:
+ # Aggregate rows have group identities, not source-row IDs.
+ stable_fields = child_query.group_by_items or (
+ ['id'] if not child_query.aggregates else [])
+ for field in stable_fields:
+ if not any(order.field_name == field for order in child_query.order_by_items):
+ child_query.order_asc(field)
threshold = child_query.top_n_probe_threshold_value
provider_policy = self.dialect.relation_top_n_policy()
use_probes = limited and (
provider_policy == "always_probe" and threshold is None
or threshold is not None and threshold > 0 and len(parent_ids) <= threshold
)
- if use_probes:
+ child_trace = [*request.trace_chain, TraceNode(
+ kind="relation", name=load.name,
+ comment=f"{query.entity}.{load.name}")]
+ if not parent_ids:
+ # Missing/NULL local keys have no relation membership.
+ # Do not compile IN [] or accidentally load orphan rows.
+ children, child_keys = [], []
+ selected_plan, probe_count = "empty", 0
+ elif use_probes:
children = []
+ child_keys = []
for parent_id in parent_ids:
probe = deepcopy(child_query)
probe.partition_by = None
probe.and_filter(BinaryExpr(
ColumnExpr(relation.foreign_key), BinaryOp.Eq,
ValueExpr(Value.from_any(parent_id))))
- child_trace = [*request.trace_chain, TraceNode(
- kind="relation", name=f"{query.entity}.{load.name}",
- comment=load.name)]
- children.extend((await self.query(context, _QueryWithLogIntent(
- probe, child_trace, request._comment, request._purpose, intent_source))).rows)
+ assembly = _RelationAssembly(relation.foreign_key)
+ loaded = (await self.query(context, _QueryWithLogIntent(
+ probe, child_trace, request._comment, request._purpose, intent_source,
+ request.origin_entity, assembly))).rows
+ children.extend(loaded)
+ child_keys.extend(assembly.key(row) for row in loaded)
selected_plan = "bounded_probes"
probe_count = len(parent_ids)
else:
@@ -572,21 +737,38 @@ async def _enhance_relations(self, context, parents: List[Dict[str, Any]], reque
ColumnExpr(relation.foreign_key), BinaryOp.In, ValueExpr(values)))
if limited:
child_query.partition_by_field(relation.foreign_key)
- child_trace = [*request.trace_chain, TraceNode(
- kind="relation", name=f"{query.entity}.{load.name}",
- comment=load.name)]
+ assembly = _RelationAssembly(relation.foreign_key)
children = (await self.query(context, _QueryWithLogIntent(
- child_query, child_trace, request._comment, request._purpose, intent_source))).rows
+ child_query, child_trace, request._comment, request._purpose, intent_source,
+ request.origin_entity, assembly))).rows
+ child_keys = [assembly.key(row) for row in children]
selected_plan = "window" if limited else "batch"
probe_count = 0
for child in children:
child.pop("__teaql_partition_rank", None)
buckets: Dict[Any, List[Dict[str, Any]]] = {}
- for child in children:
- buckets.setdefault(child.get(relation.foreign_key), []).append(child)
- for parent in parents:
- related = buckets.get(parent.get(relation.local_key), [])
- parent[load.name] = related if relation.is_many else (related[0] if related else None)
+ for key, child in zip(child_keys, children):
+ buckets.setdefault(key, []).append(child)
+ for key, parent in zip(local_keys, parents):
+ related = buckets.get(key, [])
+ if relation_facets:
+ from teaql.core.list import SmartList
+ per_parent = deepcopy(child_query)
+ per_parent.facets = deepcopy(relation_facets)
+ per_parent.and_filter(BinaryExpr(
+ ColumnExpr(relation.foreign_key), BinaryOp.Eq,
+ ValueExpr(Value.from_any(key))))
+ facet_request = _QueryWithLogIntent(
+ per_parent, child_trace, request._comment, request._purpose,
+ intent_source, request.origin_entity)
+ child_desc = self.schema_provider.get_entity(relation.target_entity)
+ related = SmartList(related, facets=await self._query_facets(
+ context, facet_request, child_desc))
+ # Preserve the provider's known FK even if target detail is
+ # filtered out. Unfetched properties are absent/NotLoaded.
+ parent[load.name] = related if relation.is_many else (
+ related[0] if related else
+ {relation.foreign_key: key} if key is not None else None)
relation_scope.success({
"teaql.result.cardinality": len(children),
"teaql.relation.parent_count": len(parent_ids),
@@ -635,10 +817,11 @@ async def _enhance_relation_aggregates(self, context, parents: List[Dict[str, An
child_query.and_filter(BinaryExpr(
ColumnExpr(relation.foreign_key), BinaryOp.In, ValueExpr(values)))
child_trace = [*request.trace_chain, TraceNode(
- kind="relation", name=f"{query.entity}.{aggregate.relation_name}",
- comment=aggregate.relation_name)]
+ kind="relation", name=aggregate.relation_name,
+ comment=f"{query.entity}.{aggregate.relation_name}")]
rows = (await self.query(context, _QueryWithLogIntent(
- child_query, child_trace, request._comment, request._purpose, intent_source))).rows
+ child_query, child_trace, request._comment, request._purpose, intent_source,
+ request.origin_entity))).rows
child_desc = self.schema_provider.get_entity(relation.target_entity)
foreign_property = child_desc.property_by_name(relation.foreign_key) if child_desc else None
if foreign_property and foreign_property.column_name_val != relation.foreign_key:
@@ -673,12 +856,22 @@ def _attach_empty_relation_aggregate(self, parents, aggregate, query):
parent[aggregate.alias] = value
async def mutate(self, context: 'UserContext', request: MutationRequest) -> MutationResult:
+ request.validate()
+ if context is not None:
+ context._require_mutation_invocation()
self._sync_generated_schema(context)
entity = getattr(request._data, "entity", "unknown")
kind = type(request._data).__name__.replace("Command", "").lower()
if context is not None:
- if not context.consume_mutation_checked(request._data):
- context.check_and_fix_mutation(request._data)
+ def check(data):
+ if isinstance(data, MutationRequest):
+ check(data._data)
+ elif isinstance(data, list):
+ for child in data:
+ check(child)
+ elif not context.consume_mutation_checked(data):
+ context.check_and_fix_mutation(data)
+ check(request._data)
telemetry = context.runtime_telemetry() if context is not None else None
scope = (
context.mutation_policy_execution(request)
@@ -686,24 +879,35 @@ async def mutate(self, context: 'UserContext', request: MutationRequest) -> Muta
else _NoopContextManager()
)
with scope:
+ from teaql.runtime.log_privacy import _MutationIntentPrivacy
+ def descriptor(name):
+ return self.schema_provider.get_entity(name) or (context.entity(name) if context else None)
+ privacy = _MutationIntentPrivacy.capture(request, descriptor)
+ session = getattr(context, '_graph_session', None)
+ if session is not None:
+ privacy = session._intent_privacy.merge(privacy)
return await observe_runtime_operation(
telemetry,
RuntimeOperation("mutation", f"{entity}.{kind}", {
"teaql.entity.type": entity,
"teaql.mutation.kind": kind,
}),
- lambda: self._mutate(context, request),
+ lambda: self._mutate(context, request, privacy),
)
- async def _mutate(self, context: 'UserContext', request: MutationRequest) -> MutationResult:
+ async def _mutate(self, context: 'UserContext', request: MutationRequest, privacy=None) -> MutationResult:
+ request.validate()
if isinstance(self.transport, SqlTransactionTransport):
transaction = await self.transport.begin_sql()
executor = SqlDataServiceExecutor(self.dialect, transaction, self.schema_provider)
+ from teaql.runtime.audit import _CommittedAuditJournal
+ journal = _CommittedAuditJournal(context) if context is not None else None
try:
- result = await executor._mutate(context, request)
+ result = await executor._mutate(journal.context if journal else context, request, privacy)
await transaction.commit_sql()
- return result
except BaseException:
+ if journal:
+ journal.discard()
# CancelledError is not an Exception. Release the transaction
# on cancellation too, without replacing the original failure.
try:
@@ -711,8 +915,36 @@ async def _mutate(self, context: 'UserContext', request: MutationRequest) -> Mut
except BaseException:
pass
raise
+ else:
+ if journal:
+ await journal.committed()
+ return result
req_data = request._data
+ if isinstance(req_data, list):
+ start = datetime.now()
+ results = []
+ for child in req_data:
+ child_request = (child.with_root_intent(request.intent)
+ if isinstance(child, MutationRequest) else
+ MutationRequest(child, comment=request.intent.comment))
+ lineage = child_request.mutation_lineage
+ if (not isinstance(child_request._data, list) and lineage
+ and lineage[0].comment != request.intent.comment):
+ # A native batch has no aggregate entity/ID. Its reason
+ # prefixes each item's own responsibility type, without
+ # replacing the local chain or duplicating an existing root.
+ entity = lineage[0].entity_type or lineage[0].name or child_request._data.entity
+ child_request = child_request.with_mutation_lineage((TraceNode(
+ kind='auditReason', name=entity, entity_type=entity,
+ comment=request.intent.comment), *lineage))
+ results.append(await self._mutate(context, child_request, privacy))
+ affected = sum(result.affected_rows for result in results)
+ return MutationResult(affected, {}, ExecutionMetadata(
+ backend=self._backend_name(), operation=DataServiceOperation.Batch,
+ started_at=start, ended_at=datetime.now(), affected_rows=affected,
+ comment=request.intent.comment, audit_reason=request.intent.comment,
+ statements=tuple(result.metadata for result in results)))
entity_desc = self.schema_provider.get_entity(req_data.entity)
if not entity_desc and context:
entities = context.get_resource("entities")
@@ -745,6 +977,14 @@ async def _mutate(self, context: 'UserContext', request: MutationRequest) -> Mut
), None)
if version_prop is not None and version_prop.name not in req_data.values:
req_data.values[version_prop.name] = Value.from_any(1)
+
+ if not request.mutation_lineage:
+ target_id = getattr(req_data, 'id', None)
+ if target_id is None and id_prop is not None:
+ target_id = getattr(req_data, 'values', {}).get(id_prop.name)
+ request = request.with_mutation_lineage((TraceNode(
+ kind='auditReason', name=req_data.entity, entity_type=req_data.entity,
+ entity_id=getattr(target_id, 'val', target_id), comment=request.comment()),))
try:
if isinstance(req_data, InsertCommand):
@@ -780,12 +1020,13 @@ async def _mutate(self, context: 'UserContext', request: MutationRequest) -> Mut
)
except BaseException as e:
self._record_statement(context, request, compiled, start, operation,
- 'cancelled' if isinstance(e, asyncio.CancelledError) else 'failure')
+ 'cancelled' if isinstance(e, asyncio.CancelledError) else 'failure',
+ intent_privacy=privacy)
if isinstance(e, Exception):
raise TransportError(e) from e
raise
metadata = self._record_statement(context, request, compiled, start, operation,
- 'success', affected_rows=affected_rows)
+ 'success', affected_rows=affected_rows, intent_privacy=privacy)
generated_values = {}
if op == "insert" and last_insert_id:
@@ -838,9 +1079,12 @@ async def _mutate(self, context: 'UserContext', request: MutationRequest) -> Mut
f"expected one authoritative persisted row for {req_data.entity}, got {len(persisted_rows)}"))
except BaseException as error:
self._record_readback(context, readback, compiled, metadata, read_start,
- persisted_rows, error)
+ persisted_rows, error, privacy)
raise
persisted_record = persisted_rows[0]
+ read_metadata = self._record_readback(context, readback, compiled, metadata,
+ read_start, persisted_rows, None, privacy)
+ metadata = replace(metadata, statements=(metadata, read_metadata))
if affected_rows > 0 and context is not None:
from teaql.runtime.audit import AuditFieldChange, MutationAuditKind, RawAuditEvent
@@ -866,10 +1110,11 @@ async def _mutate(self, context: 'UserContext', request: MutationRequest) -> Mut
req_data.entity,
entity_id,
changes,
- tuple(request.trace_chain()),
+ request.mutation_lineage,
context.user_identifier(),
context.get_resource("bootstrapCategory"),
context.current_mutation_governance(),
+ _intent_privacy=privacy,
))
return MutationResult(
affected_rows=affected_rows,
@@ -878,25 +1123,27 @@ async def _mutate(self, context: 'UserContext', request: MutationRequest) -> Mut
persisted_record=persisted_record,
)
- def _record_readback(self, context, readback, source, write_metadata, started_at, rows, error):
- if context is None:
- return
+ def _record_readback(self, context, readback, source, write_metadata, started_at, rows, error, privacy=None):
# A driver returning zero/multiple rows succeeded as SQL; validation of
# the authoritative snapshot is a separate business failure.
outcome = ('success' if rows is not None else 'cancelled'
if isinstance(error, asyncio.CancelledError) else 'failure')
metadata = replace(write_metadata, operation=DataServiceOperation.Query,
+ purpose='verify the persisted mutation result',
started_at=started_at, ended_at=datetime.now(), execution_outcome=outcome,
parameterized_sql=readback.sql, parameters=list(readback.params),
parameter_log_policies=readback.parameter_log_policies, sql_origin=readback.sql_origin,
affected_rows=None, result_count=len(rows) if rows is not None else None,
- trace_chain=[*write_metadata.trace_chain, TraceNode(kind='sql', name='readback', comment='readback')])
+ trace_chain=physical_readback_path(write_metadata.trace_chain))
+ if context is None:
+ return metadata
try:
context._record_metadata_log(metadata, intent_source=source,
- intent_values=tuple(readback.params[:1]))
+ intent_values=tuple(readback.params[:1]), intent_privacy=privacy)
except BaseException:
# An in-flight readback error must survive a diagnostic sink failure.
pass
+ return metadata
async def next_id(self, entity: str) -> int:
await self.transport.execute_sql(CompiledQuery(
@@ -1050,6 +1297,9 @@ def __init__(self, dialect: SqlDialect, transport: SqlTransactionTransportTx, sc
self.dialect = dialect
self.transport = transport
self.schema_provider = schema_provider
+ self._audit_journal = None
+ self._audit_owner = None
+ self._completed = False
def capabilities(self) -> DataServiceCapabilities:
return DataServiceCapabilities(
@@ -1067,6 +1317,21 @@ async def query(self, context: 'UserContext', request: QueryRequest) -> QueryRes
return await executor.query(context, request)
async def mutate(self, context: 'UserContext', request: MutationRequest) -> MutationResult:
+ request.validate()
+ if self._completed:
+ raise RuntimeError('SQL transaction is already completed')
+ if context is not None:
+ context._require_mutation_invocation()
+ if context._graph_session is None:
+ if self._audit_journal is None:
+ from teaql.runtime.audit import _CommittedAuditJournal
+ self._audit_journal = _CommittedAuditJournal(context)
+ self._audit_owner = context
+ self._audit_journal.context._mutation_policy = context._mutation_policy.for_invocation()
+ self._audit_journal.context._checked_mutations = set()
+ elif self._audit_owner is not context:
+ raise RuntimeError('SQL transaction audit owner cannot change')
+ context = self._audit_journal.context
executor = SqlDataServiceExecutor(self.dialect, self.transport, self.schema_provider)
return await executor.mutate(context, request)
@@ -1079,7 +1344,15 @@ async def ensure_id_floor(self, entity: str, floor: int) -> None:
await executor.ensure_id_floor(entity, floor)
async def commit(self, context: 'UserContext') -> None:
+ if self._completed:
+ raise RuntimeError('SQL transaction is already completed')
await self.transport.commit_sql()
+ self._completed = True
+ if self._audit_journal:
+ await self._audit_journal.committed()
async def rollback(self, context: 'UserContext') -> None:
+ if self._audit_journal:
+ self._audit_journal.discard()
+ self._completed = True
await self.transport.rollback_sql()
diff --git a/src/teaql/sql/types.py b/src/teaql/sql/types.py
index abf93e2..ccd82c0 100644
--- a/src/teaql/sql/types.py
+++ b/src/teaql/sql/types.py
@@ -19,11 +19,15 @@ def __init__(self):
self.policies = []
self.current_policy = 'unknown'
self.trusted = True
+ self._intent_operands = []
def append(self, value, policy=None):
super().append(value)
self.policies.append(policy or self.current_policy)
+ def _retain_intent_operand(self, value):
+ self._intent_operands.append((value, self.current_policy))
+
@contextmanager
def policy(self, policy):
previous = self.current_policy
@@ -45,6 +49,7 @@ def __post_init__(self):
if isinstance(self.params, SQLBindings):
self.parameter_log_policies = list(self.params.policies)
self.sql_origin = 'generated' if self.params.trusted else None
+ self._intent_operands = tuple(self.params._intent_operands)
def sql_with_comment(self) -> str:
if self.comment:
diff --git a/tests/core/test_entity_root_ownership.py b/tests/core/test_entity_root_ownership.py
new file mode 100644
index 0000000..59b61ff
--- /dev/null
+++ b/tests/core/test_entity_root_ownership.py
@@ -0,0 +1,121 @@
+"""Reached-key imports must not compose another graph's pending mutations."""
+from concurrent.futures import ThreadPoolExecutor
+from threading import Barrier
+
+import pytest
+
+from teaql.core.entity import EntityKey, EntityRoot
+from teaql.core.mutation import TraceNode
+from teaql.core.value import Value
+
+
+def pending(root, key):
+ return key in dict(root.current_change_set().changes()) or key in root.new_keys() or key in root.deleted_keys()
+
+
+def test_scoped_import_keeps_unreached_and_source_changes():
+ source, target = EntityRoot(), EntityRoot()
+ parent, child, sibling = EntityKey('Order', 1), EntityKey('OrderItem', 1), EntityKey('OrderItem', 2)
+ source.set_original_version(parent, 9)
+ source.set_original_version(child, 2)
+ target.set_original_version(parent, 3)
+ source.set(parent, 'name', Value.Text('foreign root'))
+ source.set(child, 'name', Value.Text('reached child'))
+ source.set(sibling, 'name', Value.Text('foreign sibling'))
+ source.set_trace_chain(child, [TraceNode('Order', 1, 'import child', kind='auditReason', name='Order')])
+
+ target.merge_entity_from(source, child)
+
+ assert dict(target.current_change_set().changes()) == {child: {'name': Value.Text('reached child')}}
+ assert target.original_version(parent) == 3
+ assert target.original_version(child) == 2
+ assert not pending(target, parent) and not pending(target, sibling)
+ assert target.trace_chain(child)[0].comment == 'import child'
+ target.clear_entity(child)
+ assert pending(source, child) and pending(source, parent) and pending(source, sibling)
+
+
+def test_scoped_import_copies_lifecycle_without_pending_hydration():
+ source, target = EntityRoot(), EntityRoot()
+ loaded, added, removed = EntityKey('Platform', 1), EntityKey('OrderItem', -1), EntityKey('OrderItem', 2)
+ source.set_original_version(loaded, 1)
+ source.mark_as_new(added)
+ source.set(added, 'name', Value.Text('new'))
+ source.set_original_version(removed, 3)
+ source.mark_as_deleted(removed)
+ for key in (loaded, added, removed):
+ target.merge_entity_from(source, key)
+ assert not target.has_pending(loaded)
+ assert added in target.new_keys() and removed in target.deleted_keys()
+ assert added in source.new_keys() and removed in source.deleted_keys()
+
+
+def test_version_conflict_rejects_scoped_and_whole_import_before_copying():
+ source, target = EntityRoot(), EntityRoot()
+ key, unrelated = EntityKey('Order', 1), EntityKey('OrderItem', 2)
+ target.set_original_version(key, 1)
+ target.set(key, 'name', Value.Text('original'))
+ source.set_original_version(key, 2)
+ source.set(key, 'name', Value.Text('replacement'))
+ source.mark_as_deleted(key)
+ source.mark_as_new(unrelated)
+ source.set(unrelated, 'name', Value.Text('unreached'))
+ source.set_trace_chain(key, [TraceNode('Order', 1, 'foreign', kind='auditReason', name='Order')])
+ for operation in (lambda: target.merge_entity_from(source, key), lambda: target.merge_from(source)):
+ with pytest.raises(ValueError, match='ENTITY_VERSION_CONFLICT'):
+ operation()
+ assert target.original_version(key) == 1
+ assert target.get(key, 'name') == Value.Text('original')
+ assert not pending(target, unrelated) and key not in target.deleted_keys()
+ assert target.trace_chain(key) == ()
+ assert key in source.deleted_keys()
+
+
+def test_conflicting_rekey_preserves_both_keys():
+ root = EntityRoot()
+ old, new = EntityKey('Order', -1), EntityKey('Order', 1)
+ root.set_original_version(old, 2)
+ root.set_original_version(new, 1)
+ root.mark_as_new(old)
+ root.set(old, 'name', Value.Text('old'))
+ root.set(new, 'name', Value.Text('new'))
+ with pytest.raises(ValueError, match='ENTITY_VERSION_CONFLICT'):
+ root.rekey(old, new)
+ assert old in root.new_keys()
+ assert root.original_version(old) == 2 and root.original_version(new) == 1
+ assert root.get(old, 'name') == Value.Text('old') and root.get(new, 'name') == Value.Text('new')
+
+
+def test_committed_version_requires_cleared_changes_and_keeps_typed_keys():
+ root = EntityRoot()
+ order, payment = EntityKey('Order', 1), EntityKey('Payment', 1)
+ root.set_original_version(order, 7)
+ root.set_original_version(payment, 1)
+ root.set_original_version(order, 7)
+ with pytest.raises(ValueError, match='ENTITY_VERSION_CONFLICT'):
+ root.set_original_version(order, 8)
+ root.set(order, 'name', Value.Text('pending'))
+ with pytest.raises(ValueError, match='ENTITY_PENDING_CHANGES'):
+ root.accept_committed_version(order, 8)
+ root.clear_entity(order)
+ root.accept_committed_version(order, 8)
+ assert root.original_version(order) == 8 and root.original_version(payment) == 1
+
+
+def test_concurrent_conflicting_version_registration_has_one_winner():
+ root, key, start = EntityRoot(), EntityKey('Order', 1), Barrier(2)
+
+ def register(version):
+ start.wait(timeout=5)
+ try:
+ root.set_original_version(key, version)
+ return True
+ except ValueError as error:
+ assert 'ENTITY_VERSION_CONFLICT' in str(error)
+ return False
+
+ with ThreadPoolExecutor(max_workers=2) as executor:
+ first = executor.submit(register, 1)
+ second = executor.submit(register, 2)
+ assert sum([first.result(timeout=10), second.result(timeout=10)]) == 1
+ assert root.original_version(key) in (1, 2)
diff --git a/tests/core/test_mutation_coverage.py b/tests/core/test_mutation_coverage.py
index 1043808..4fd4dc5 100644
--- a/tests/core/test_mutation_coverage.py
+++ b/tests/core/test_mutation_coverage.py
@@ -35,15 +35,15 @@ def test_mutation_requests():
i = InsertCommand.new("User")
i.trace_chain = [t]
- req_i = MutationRequest.Insert(i)
- req_u = MutationRequest.Update(UpdateCommand.new("U", 1))
- req_d = MutationRequest.Delete(DeleteCommand.new("U", 1))
- req_r = MutationRequest.Recover(RecoverCommand.new("U", 1, 1))
+ req_i = MutationRequest.Insert(i, comment='what: runtime regression fixture')
+ req_u = MutationRequest.Update(UpdateCommand.new("U", 1), comment='what: runtime regression fixture')
+ req_d = MutationRequest.Delete(DeleteCommand.new("U", 1), comment='what: runtime regression fixture')
+ req_r = MutationRequest.Recover(RecoverCommand.new("U", 1, 1), comment='what: runtime regression fixture')
- req_b = MutationRequest.Batch([req_i, req_u])
+ req_b = MutationRequest.Batch([req_i, req_u], comment='what: runtime regression fixture')
assert len(req_i.trace_chain()) == 1
- assert req_i.comment() == "test"
+ assert req_i.comment() == 'what: runtime regression fixture'
assert req_b.trace_chain() == []
- assert req_b.comment() is None
+ assert req_b.comment() == 'what: runtime regression fixture'
diff --git a/tests/core/test_query_projection.py b/tests/core/test_query_projection.py
new file mode 100644
index 0000000..9c2a6c3
--- /dev/null
+++ b/tests/core/test_query_projection.py
@@ -0,0 +1,29 @@
+import pytest
+
+from teaql.core.entity import _QueryProjectionSnapshot
+
+
+def test_query_projections_are_owned_and_not_entity_attributes():
+ source = {'id': 7, 'order_number': 'private', 'orderNumber': 'private',
+ 'items': [{'id': 8}], 'summary': {'counts': [2]}, 'save': 3}
+ snapshot = _QueryProjectionSnapshot(source, ('id', 'order_number', 'orderNumber', 'items'))
+ source['summary']['counts'].append(99)
+ returned = snapshot.get('summary')
+ returned['counts'].append(100)
+ assert snapshot.get('summary') == {'counts': [2]}
+ assert snapshot.get('save') == 3
+ assert not hasattr(snapshot, 'save')
+ for field in ('id', 'order_number', 'orderNumber', 'items'):
+ assert not snapshot.contains(field)
+ with pytest.raises(KeyError):
+ snapshot.get(field)
+ assert 'private' not in repr(snapshot)
+
+
+def test_query_projection_missing_is_distinct_from_null_and_zero():
+ snapshot = _QueryProjectionSnapshot({'zero': 0, 'null': None}, ())
+ assert snapshot.contains('zero') and snapshot.get('zero') == 0
+ assert snapshot.contains('null') and snapshot.get('null') is None
+ assert not snapshot.contains('missing')
+ with pytest.raises(KeyError):
+ snapshot.get('missing')
diff --git a/tests/core/test_request_intent.py b/tests/core/test_request_intent.py
new file mode 100644
index 0000000..9d31837
--- /dev/null
+++ b/tests/core/test_request_intent.py
@@ -0,0 +1,154 @@
+import pytest
+import json
+from pathlib import Path
+from dataclasses import FrozenInstanceError
+
+from teaql.core import InsertCommand, MutationRequest, SelectQuery, TraceNode
+from teaql.data_service import QueryRequest
+from teaql.core import QueryIntent, MutationIntent, RequestIntentError
+
+VECTORS = json.loads((Path(__file__).parents[1] / 'fixtures/request-intent-v1.json').read_text())['cases']
+
+
+@pytest.mark.parametrize('case', VECTORS, ids=lambda case: case['id'])
+def test_shared_request_intent_contract(case):
+ data = case['input']
+ traces = [TraceNode(kind=node['kind'], comment=node['detail']) for node in data.get('trace', [])]
+
+ def build():
+ if case['kind'] == 'query':
+ return QueryRequest(SelectQuery('Order'), traces,
+ _comment=data.get('comment'), _purpose=data.get('purpose'))
+ if 'children' in data:
+ children = [MutationRequest(InsertCommand('OrderItem'), comment=child['comment'])
+ for child in data['children']]
+ return MutationRequest.Batch(children, comment=data.get('comment'))
+ return MutationRequest(InsertCommand('Order', trace_chain=traces), comment=data.get('comment'))
+
+ if 'error' in case:
+ with pytest.raises(RequestIntentError) as caught:
+ build()
+ assert caught.value.code == case['error']['code']
+ assert caught.value.field == case['error']['field']
+ assert caught.value.request_kind == case['kind']
+ else:
+ request = build()
+ assert request.intent.comment == case['expected']['comment']
+ if case['kind'] == 'query':
+ assert request.intent.purpose == case['expected']['purpose']
+
+
+@pytest.mark.parametrize('codepoint', [*range(9, 14), 0x20, 0x85, 0xA0, 0x1680,
+ *range(0x2000, 0x200B), 0x2028, 0x2029, 0x202F, 0x205F, 0x3000])
+def test_unicode_white_space_matches_rust(codepoint):
+ with pytest.raises(RequestIntentError, match='REQUEST_COMMENT_REQUIRED'):
+ MutationIntent(chr(codepoint))
+ with pytest.raises(RequestIntentError, match='QUERY_PURPOSE_REQUIRED'):
+ QueryIntent('load order', chr(codepoint))
+
+
+@pytest.mark.parametrize('text', ['\u001c', '\u001d', '\u001e', '\u001f', '\u200b', '\ufeff'])
+def test_non_white_space_is_not_silently_reclassified(text):
+ assert MutationIntent(text).comment == text
+
+
+def test_owned_intent_survives_builder_and_route_changes_without_leaking_in_repr():
+ query = SelectQuery('Order').comment(' load order ').purpose('render order')
+ request = QueryRequest(query)
+ query.comment('different request')
+ request.query.comment('policy must not rewrite intent')
+ assert request.intent.comment == ' load order '
+ assert request.with_query(SelectQuery('OrderItem')).intent == request.intent
+ with pytest.raises(AttributeError):
+ request._comment = ''
+ with pytest.raises(FrozenInstanceError):
+ request.intent.comment = ''
+ command = InsertCommand('Order', trace_chain=[TraceNode(comment='child reason')])
+ mutation = MutationRequest(command, comment='submit order')
+ command.trace_chain.append(TraceNode(kind='provider', comment=''))
+ assert mutation.comment() == 'submit order'
+ assert mutation.intent.readback().comment == 'submit order'
+ with pytest.raises(AttributeError):
+ mutation.comment = 'cannot shadow accessor'
+ assert 'submit order' not in repr(mutation.intent)
+ assert 'load order' not in repr(request.intent)
+
+
+def test_query_policy_cannot_replace_root_intent():
+ from teaql.runtime import UserContext
+ context = UserContext().with_request_policy(lambda _: SelectQuery('Order').comment('rewrite').purpose('rewrite'))
+ request = QueryRequest(SelectQuery('Order'), _comment='original comment', _purpose='original purpose')
+ prepared = context.prepare_query_request(request)
+ assert prepared.intent == request.intent
+ assert prepared.query.comment_text == 'original comment'
+ assert prepared.query.purpose_text == 'original purpose'
+
+
+@pytest.mark.asyncio
+async def test_graph_root_is_required_before_begin_even_with_a_valid_child():
+ from teaql.runtime import UserContext
+ calls = []
+ async def work():
+ calls.append('work')
+ return MutationRequest(InsertCommand('OrderItem'), comment='create item')
+ class Provider:
+ async def begin(self, _):
+ calls.append('begin')
+ raise AssertionError('provider must not be called')
+ context = UserContext().insert_resource('dataService', Provider())
+ with pytest.raises(RequestIntentError, match='REQUEST_COMMENT_REQUIRED'):
+ await context.execute_graph_save(work, comment='\u0085')
+ assert calls == []
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('logging', [False, True])
+async def test_direct_provider_gates_before_checker_schema_and_stream(logging):
+ from teaql.runtime import UserContext
+ from teaql.runtime.context import SqlLogOptions
+ from teaql.sql.executor import SqlDataServiceExecutor
+ context = UserContext()
+ if not logging:
+ context.with_sql_log_options(SqlLogOptions.disabled())
+ calls = []
+ context.check_and_fix_mutation = lambda _: calls.append('checker')
+ context.with_request_policy(lambda _: calls.append('policy'))
+ provider = object.__new__(SqlDataServiceExecutor)
+ provider._sync_generated_schema = lambda _: calls.append('schema')
+ query = object.__new__(QueryRequest)
+ query.query = SelectQuery('MissingTable')
+ mutation = object.__new__(MutationRequest)
+ mutation._data = InsertCommand('MissingTable').value('secret', 'PAYLOAD-CANARY')
+ def diagnostic(error, kind):
+ assert error.code == 'REQUEST_COMMENT_REQUIRED'
+ assert error.field == 'comment'
+ assert error.request_kind == kind
+ assert 'PAYLOAD-CANARY' not in str(error)
+ for execute, kind in [(provider.query(context, query), 'query'), (provider.mutate(context, mutation), 'mutation')]:
+ with pytest.raises(RequestIntentError) as caught:
+ await execute
+ diagnostic(caught.value, kind)
+ with pytest.raises(RequestIntentError) as caught:
+ await context.prepare_query_request(query)
+ diagnostic(caught.value, 'query')
+ with pytest.raises(RequestIntentError) as caught:
+ async for _ in provider.query_stream(context, query, 10):
+ raise AssertionError('stream must not open')
+ diagnostic(caught.value, 'query')
+ assert calls == []
+
+
+@pytest.mark.parametrize('comment', [None, '', ' \t\r\n', '\u0085', '\u2003'])
+def test_query_request_rejects_missing_or_blank_comment(comment):
+ with pytest.raises(ValueError, match='REQUEST_COMMENT_REQUIRED'):
+ QueryRequest(SelectQuery('Order'), _comment=comment, _purpose='render orders')
+
+
+@pytest.mark.parametrize('comment', [None, '', ' \t\r\n', '\u0085', '\u2003'])
+def test_mutation_request_does_not_infer_comment_from_trace(comment):
+ command = InsertCommand('Order', trace_chain=[TraceNode(comment='submit order')])
+ with pytest.raises(ValueError, match='REQUEST_COMMENT_REQUIRED'):
+ if comment is None:
+ MutationRequest(command)
+ else:
+ MutationRequest(command, comment=comment)
diff --git a/tests/core/test_trace.py b/tests/core/test_trace.py
new file mode 100644
index 0000000..f5f6e52
--- /dev/null
+++ b/tests/core/test_trace.py
@@ -0,0 +1,74 @@
+"""Shared frozen SQL vectors are pure algorithm evidence, not generated proof."""
+import json
+from pathlib import Path
+
+import pytest
+
+from teaql.core.mutation import TraceNode
+from teaql.core.trace import canonical_sql_trace_path, trace_intent
+
+
+CASES = json.loads((Path(__file__).parents[1] / 'fixtures' / 'sql-trace-path-v1.json').read_text())['cases']
+
+
+def decode(node):
+ return TraceNode(kind=node['kind'][0].lower() + node['kind'][1:], name=node['name'],
+ entity_id=node['entityId'], comment=node['detail'])
+
+
+def logical(node):
+ return {'kind': node.kind[0].upper() + node.kind[1:], 'name': node.name,
+ 'entityId': node.entity_id, 'detail': node.comment}
+
+
+@pytest.mark.parametrize('case', CASES, ids=[case['id'] for case in CASES])
+def test_frozen_sql_path(case):
+ source = [decode(node) for node in case['source']]
+ assert trace_intent(source) == case['expectedIntent']
+ result = canonical_sql_trace_path(source, case['backend'], case['operation'])
+ assert [logical(node) for node in result] == case['expectedPath']
+ assert canonical_sql_trace_path(result, case['backend'], case['operation']) == result
+ assert [logical(node) for node in source] == case['source']
+ if result:
+ result[0].name = 'consumer-owned change'
+ assert [logical(node) for node in source] == case['source']
+
+
+def test_derived_query_origin_is_owned_and_survives_intent_changes():
+ from teaql.core.query import SelectQuery
+ from teaql.data_service import QueryRequest
+ request = QueryRequest(SelectQuery('CustomerOrder'), _comment='load graph', _purpose='render graph')
+ derived = request.with_query(SelectQuery('Payment')).comment('updated intent').purpose('updated purpose')
+ assert derived.query.entity == 'Payment' and derived.origin_entity == 'CustomerOrder'
+ with pytest.raises(AttributeError):
+ derived.origin_entity = 'Shipment'
+
+
+def test_readback_path_keeps_origin_not_child_and_does_not_mutate_write():
+ from copy import deepcopy
+ from teaql.core.trace import physical_readback_path
+ source = canonical_sql_trace_path([
+ TraceNode(kind='auditReason', name='CustomerOrder', entity_id=7, comment='submit'),
+ TraceNode(kind='entity', name='PaymentAttempt', entity_id=7),
+ ], 'sqlite', 'update')
+ before = deepcopy(source)
+ read = physical_readback_path(source)
+ assert [(n.kind, n.name, n.comment) for n in read] == [
+ ('operation', 'CustomerOrder', 'query'), ('request', 'CustomerOrder', ''),
+ ('provider', 'sqlite', ''), ('sql', 'select', '')]
+ read[0].name = 'consumer change'
+ assert source == before
+
+
+@pytest.mark.parametrize('kind', ['query', 'mutation'])
+def test_untyped_trace_is_rejected_without_echoing_payload(kind):
+ from teaql.core.mutation import InsertCommand, MutationRequest
+ from teaql.core.query import SelectQuery
+ from teaql.data_service import QueryRequest
+ opaque = {'comment': 'SENSITIVE-FIXTURE-CANARY'}
+ request = (QueryRequest(SelectQuery('CustomerOrder'), [opaque], _comment='load graph', _purpose='render graph')
+ if kind == 'query' else MutationRequest(
+ InsertCommand('CustomerOrder', trace_chain=[opaque]), comment='save graph'))
+ with pytest.raises(TypeError, match='typed TraceNode') as caught:
+ request.validate()
+ assert 'SENSITIVE-FIXTURE-CANARY' not in str(caught.value)
diff --git a/tests/core/test_trace_scope.py b/tests/core/test_trace_scope.py
new file mode 100644
index 0000000..13415cd
--- /dev/null
+++ b/tests/core/test_trace_scope.py
@@ -0,0 +1,74 @@
+"""Shared graph fixture/token evidence; not generated traversal proof."""
+import json
+from dataclasses import FrozenInstanceError
+from pathlib import Path
+
+import pytest
+
+from teaql.core.entity import EntityKey, EntityRoot
+from teaql.core.mutation import InsertCommand, MutationRequest, TraceNode
+from teaql.core.trace_scope import TraceScope
+
+
+CASES = json.loads((Path(__file__).parents[1] / 'fixtures' / 'graph-mutation-lineage-v1.json').read_text())['cases']
+
+
+def logical(node):
+ return {'kind': 'AuditReason', 'name': node.name or node.entity_type,
+ 'entityId': node.entity_id, 'detail': node.comment}
+
+
+@pytest.mark.parametrize('case', CASES, ids=[case['id'] for case in CASES])
+def test_shared_graph_scope_recovery(case):
+ owner, scopes, recovered = object(), {}, {}
+ ledger = EntityRoot()
+ for node in case['nodes']:
+ key = EntityKey(node['entityType'], node.get('assignedEntityId') or node['entityId'])
+ scope = (TraceScope.root(owner, key, case['requestComment']) if node['parent'] is None
+ else scopes[node['parent']].child(key, node['localComment']))
+ scopes[node['nodeId']] = scope
+ if 'ledgerLineage' in node:
+ ledger.set_trace_chain(key, [TraceNode(kind='auditReason', name=item['name'],
+ entity_id=item['entityId'], comment=item['detail']) for item in node['ledgerLineage']])
+ request = MutationRequest(InsertCommand(key.entity), comment=case['requestComment']).with_mutation_lineage(
+ ledger.trace_chain(key) or scope.recover())
+ recovered[node['nodeId']] = [logical(item) for item in request.mutation_lineage]
+ copied = request.mutation_lineage
+ copied[0].comment = 'consumer changed the recovered snapshot'
+ assert [logical(item) for item in request.mutation_lineage] == recovered[node['nodeId']]
+ assert recovered == {item['nodeId']: item['lineage'] for item in case['expected']}
+
+
+def test_persistent_scope_is_immutable_and_blank_child_shares_parent():
+ root = TraceScope.root(object(), EntityKey('CustomerOrder', 1), 'submit order')
+ assert root.child(EntityKey('OrderItem', 2), ' \t') is root
+ with pytest.raises(FrozenInstanceError):
+ root._node.reason = 'changed ancestor'
+ with pytest.raises(FrozenInstanceError):
+ root._parent = root
+ child = root.child(EntityKey('Payment', 1), 'authorize payment')
+ assert child._parent is root
+ assert child.recover()[0].entity_type == 'CustomerOrder'
+
+
+def test_ledger_trace_is_typed_owned_and_rekeys_matching_ancestors_only():
+ ledger = EntityRoot()
+ old, new, payment = EntityKey('CustomerOrder', -1), EntityKey('CustomerOrder', 100), EntityKey('Payment', -1)
+ source = [TraceNode(kind='auditReason', name='CustomerOrder', entity_id=-1, comment='submit order'),
+ TraceNode(kind='auditReason', name='Payment', entity_id=-1, comment='authorize payment')]
+ ledger.set_trace_chain(payment, source)
+ source[0].comment = 'caller changed source'
+ ledger.rekey(old, new)
+ chain = ledger.trace_chain(payment)
+ assert [(node.name, node.entity_id) for node in chain] == [('CustomerOrder', 100), ('Payment', -1)]
+ assert chain[0].comment == 'submit order'
+ chain[0].entity_id = 999
+ assert ledger.trace_chain(payment)[0].entity_id == 100
+ merged = EntityRoot()
+ merged.merge_from(ledger)
+ ledger.clear_entity(payment)
+ assert len(merged.trace_chain(payment)) == 2
+ merged.clear_committed()
+ assert merged.trace_chain(payment) == ()
+ with pytest.raises(TypeError, match='typed AuditReason'):
+ ledger.set_trace_chain(payment, ['opaque sensitive input'])
diff --git a/tests/data_service/test_service.py b/tests/data_service/test_service.py
index f9773e4..21480fc 100644
--- a/tests/data_service/test_service.py
+++ b/tests/data_service/test_service.py
@@ -28,10 +28,10 @@ def test_mutation_request_trace_and_comment_accessors():
values={},
trace_chain=trace_chain.copy()
)
- req_insert = MutationRequest.Insert(insert_cmd)
+ req_insert = MutationRequest.Insert(insert_cmd, comment='what: runtime regression fixture')
assert len(req_insert.trace_chain()) == 2
assert req_insert.trace_chain()[1] == trace2
- assert req_insert.comment() == "Create Profile"
+ assert req_insert.comment() == 'what: runtime regression fixture'
# Test Update
update_cmd = CoreUpdateCommand(
@@ -42,9 +42,9 @@ def test_mutation_request_trace_and_comment_accessors():
old_values=None,
trace_chain=trace_chain.copy()
)
- req_update = MutationRequest.Update(update_cmd)
+ req_update = MutationRequest.Update(update_cmd, comment='what: runtime regression fixture')
assert len(req_update.trace_chain()) == 2
- assert req_update.comment() == "Create Profile"
+ assert req_update.comment() == 'what: runtime regression fixture'
# Test Delete
delete_cmd = CoreDeleteCommand(
@@ -54,9 +54,9 @@ def test_mutation_request_trace_and_comment_accessors():
soft_delete=True,
trace_chain=trace_chain.copy()
)
- req_delete = MutationRequest.Delete(delete_cmd)
+ req_delete = MutationRequest.Delete(delete_cmd, comment='what: runtime regression fixture')
assert len(req_delete.trace_chain()) == 2
- assert req_delete.comment() == "Create Profile"
+ assert req_delete.comment() == 'what: runtime regression fixture'
# Test Recover
recover_cmd = CoreRecoverCommand(
@@ -65,14 +65,14 @@ def test_mutation_request_trace_and_comment_accessors():
expected_version_val=1,
trace_chain=trace_chain.copy()
)
- req_recover = MutationRequest.Recover(recover_cmd)
+ req_recover = MutationRequest.Recover(recover_cmd, comment='what: runtime regression fixture')
assert len(req_recover.trace_chain()) == 2
- assert req_recover.comment() == "Create Profile"
+ assert req_recover.comment() == 'what: runtime regression fixture'
# Test Batch
- req_batch = MutationRequest.Batch([req_insert, req_update])
+ req_batch = MutationRequest.Batch([req_insert, req_update], comment='what: runtime regression fixture')
assert len(req_batch.trace_chain()) == 0
- assert req_batch.comment() is None
+ assert req_batch.comment() == 'what: runtime regression fixture'
# Test empty trace chain
insert_empty = CoreInsertCommand(
@@ -80,8 +80,8 @@ def test_mutation_request_trace_and_comment_accessors():
values={},
trace_chain=[]
)
- req_insert_empty = MutationRequest.Insert(insert_empty)
- assert req_insert_empty.comment() is None
+ req_insert_empty = MutationRequest.Insert(insert_empty, comment='what: runtime regression fixture')
+ assert req_insert_empty.comment() == 'what: runtime regression fixture'
def test_data_service_capabilities_default():
caps = DataServiceCapabilities()
@@ -96,8 +96,8 @@ def test_data_service_capabilities_default():
def test_query_request_coverage():
from teaql.data_service import QueryRequest
from teaql.core.query import SelectQuery
- q = QueryRequest(SelectQuery.new("User"))
- q.comment("c").purpose("p")
+ q = QueryRequest(SelectQuery.new("User"), _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior')
+ q = q.comment("c").purpose("p")
assert q._comment == "c"
assert q._purpose == "p"
@@ -110,7 +110,7 @@ def test_query_request_coverage():
def test_aliases():
from teaql.data_service import InsertCommand, UpdateCommand, DeleteCommand, RecoverCommand
from teaql.core.mutation import InsertCommand as CoreInsertCommand
- assert InsertCommand(CoreInsertCommand.new("A")) is not None
- assert UpdateCommand(None) is not None
- assert DeleteCommand(None) is not None
- assert RecoverCommand(None) is not None
+ assert InsertCommand(CoreInsertCommand.new("A"), comment='create alias fixture') is not None
+ assert UpdateCommand(None, comment='update alias fixture') is not None
+ assert DeleteCommand(None, comment='delete alias fixture') is not None
+ assert RecoverCommand(None, comment='recover alias fixture') is not None
diff --git a/tests/fixtures/graph-mutation-lineage-v1.json b/tests/fixtures/graph-mutation-lineage-v1.json
new file mode 100644
index 0000000..3ee6702
--- /dev/null
+++ b/tests/fixtures/graph-mutation-lineage-v1.json
@@ -0,0 +1,37 @@
+{
+ "contract": "teaql.graph-mutation-lineage.v1",
+ "owningIssue": "https://github.com/teaql/teaql-conformance/issues/99",
+ "evidenceLevel": "normative-fixture-not-runtime-proof",
+ "notes": [
+ "nodeId is a fixture-local handle only. Runtime identity remains (entityType, assignedEntityId).",
+ "The root comment is the Mutation Request comment and must appear once, not once for the request plus once for the root entity.",
+ "For unit token recovery, parentage and final assigned IDs are supplied by the fixture. Only real planner/provider/audit integration proves capture, allocation, deletion and commit ordering.",
+ "Compare keyed per-entity observations and ordered nodes within each lineage. Do not impose this fixture's sibling order on a runtime's SQL batch scheduler.",
+ "localComment=null or blank describes defensive graph state, not a permitted blank public audited-save request."
+ ],
+ "cases": [
+ {"id":"normative-order","coverage":["TC-MUT-01","TC-MUT-02","TC-MUT-03","TC-MUT-04","TC-MUT-05","TC-MUT-06","TC-MUT-09"],"requestComment":"submit order","nodes":[{"nodeId":"order","entityType":"Order","entityId":100,"parent":null,"localComment":"submit order","operation":"save"},{"nodeId":"item","entityType":"OrderItem","entityId":201,"parent":"order","localComment":null,"operation":"save"},{"nodeId":"payment","entityType":"Payment","entityId":301,"parent":"order","localComment":"authorize payment","operation":"save"},{"nodeId":"attempt","entityType":"PaymentAttempt","entityId":401,"parent":"payment","localComment":null,"operation":"save"},{"nodeId":"shipment","entityType":"Shipment","entityId":501,"parent":"order","localComment":"dispatch shipment","operation":"save"},{"nodeId":"deleted-item","entityType":"OrderItem","entityId":202,"parent":"order","localComment":"remove unavailable item","operation":"delete"}],"expected":[{"nodeId":"order","lineage":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"}]},{"nodeId":"item","lineage":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"}]},{"nodeId":"payment","lineage":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"},{"kind":"AuditReason","name":"Payment","entityId":301,"detail":"authorize payment"}]},{"nodeId":"attempt","lineage":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"},{"kind":"AuditReason","name":"Payment","entityId":301,"detail":"authorize payment"}]},{"nodeId":"shipment","lineage":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"},{"kind":"AuditReason","name":"Shipment","entityId":501,"detail":"dispatch shipment"}]},{"nodeId":"deleted-item","lineage":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"},{"kind":"AuditReason","name":"OrderItem","entityId":202,"detail":"remove unavailable item"}]}]},
+ {"id":"same-id-different-types","coverage":["TC-MUT-08"],"requestComment":"submit order","nodes":[{"nodeId":"order","entityType":"Order","entityId":1,"parent":null,"localComment":"submit order","operation":"save"},{"nodeId":"payment","entityType":"Payment","entityId":1,"parent":"order","localComment":"authorize payment","operation":"save"}],"expected":[{"nodeId":"order","lineage":[{"kind":"AuditReason","name":"Order","entityId":1,"detail":"submit order"}]},{"nodeId":"payment","lineage":[{"kind":"AuditReason","name":"Order","entityId":1,"detail":"submit order"},{"kind":"AuditReason","name":"Payment","entityId":1,"detail":"authorize payment"}]}]},
+ {"id":"complete-ledger-chain-replaces-fallback","coverage":["TC-MUT-10"],"requestComment":"submit order","nodes":[{"nodeId":"order","entityType":"Order","entityId":100,"parent":null,"localComment":"submit order","operation":"save"},{"nodeId":"payment","entityType":"Payment","entityId":301,"parent":"order","localComment":"authorize payment","operation":"save","ledgerLineage":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"},{"kind":"AuditReason","name":"Payment","entityId":301,"detail":"special approval"}]},{"nodeId":"item","entityType":"OrderItem","entityId":201,"parent":"order","localComment":null,"operation":"save","ledgerLineage":[]}],"expected":[{"nodeId":"order","lineage":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"}]},{"nodeId":"payment","lineage":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"},{"kind":"AuditReason","name":"Payment","entityId":301,"detail":"special approval"}]},{"nodeId":"item","lineage":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"}]}]},
+ {"id":"blank-local-defensive-inheritance","coverage":["TC-MUT-11"],"requestComment":"submit order","nodes":[{"nodeId":"order","entityType":"Order","entityId":100,"parent":null,"localComment":"submit order","operation":"save"},{"nodeId":"item","entityType":"OrderItem","entityId":201,"parent":"order","localComment":" \t ","operation":"save"}],"expected":[{"nodeId":"order","lineage":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"}]},{"nodeId":"item","lineage":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"}]}]},
+ {"id":"late-assigned-id","coverage":["TC-MUT-07"],"requestComment":"submit order","nodes":[{"nodeId":"order","entityType":"Order","entityId":100,"parent":null,"localComment":"submit order","operation":"save"},{"nodeId":"payment","entityType":"Payment","entityId":null,"parent":"order","localComment":"authorize payment","operation":"save","assignedEntityId":601}],"expected":[{"nodeId":"order","lineage":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"}]},{"nodeId":"payment","lineage":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"},{"kind":"AuditReason","name":"Payment","entityId":601,"detail":"authorize payment"}]}]}
+ ],
+ "requiredIntegrationCases": [
+ {
+ "id": "TC-MUT-12",
+ "requirement": "Execute two independent graphs concurrently in one real Context; observe isolated emitted lineages."
+ },
+ {
+ "id": "TC-MUT-13",
+ "requirement": "Fail the provider after planning; retain failed SQL lineage and emit no committed audit event."
+ },
+ {
+ "id": "TC-MUT-14",
+ "requirement": "Commit the write and fail readback; retain successful write and independently traced readback failure."
+ },
+ {
+ "id": "TC-MUT-15",
+ "requirement": "Drive normative-order with unchanged generated Mutation/Q/E APIs; assert emitted commands, SQL metadata and safe committed audit sinks; run twice without cleanup."
+ }
+ ]
+}
diff --git a/tests/fixtures/request-intent-v1.json b/tests/fixtures/request-intent-v1.json
new file mode 100644
index 0000000..b348aba
--- /dev/null
+++ b/tests/fixtures/request-intent-v1.json
@@ -0,0 +1,259 @@
+{
+ "contract": "teaql.request-intent.v1",
+ "cases": [
+ {
+ "id": "Q01-missing",
+ "kind": "query",
+ "input": {
+ "purpose": "render orders"
+ },
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ },
+ {
+ "id": "Q02-null",
+ "kind": "query",
+ "input": {
+ "comment": null,
+ "purpose": "render orders"
+ },
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ },
+ {
+ "id": "Q03-empty",
+ "kind": "query",
+ "input": {
+ "comment": "",
+ "purpose": "render orders"
+ },
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ },
+ {
+ "id": "Q04-ascii-blank",
+ "kind": "query",
+ "input": {
+ "comment": " \t\r\n",
+ "purpose": "render orders"
+ },
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ },
+ {
+ "id": "Q05-unicode-blank",
+ "kind": "query",
+ "input": {
+ "comment": " ",
+ "purpose": "render orders"
+ },
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ },
+ {
+ "id": "Q06-purpose-missing",
+ "kind": "query",
+ "input": {
+ "comment": "load orders"
+ },
+ "error": {
+ "code": "QUERY_PURPOSE_REQUIRED",
+ "field": "purpose"
+ }
+ },
+ {
+ "id": "Q07-purpose-blank",
+ "kind": "query",
+ "input": {
+ "comment": "load orders",
+ "purpose": " "
+ },
+ "error": {
+ "code": "QUERY_PURPOSE_REQUIRED",
+ "field": "purpose"
+ }
+ },
+ {
+ "id": "Q08-preserve",
+ "kind": "query",
+ "input": {
+ "comment": " load orders ",
+ "purpose": "render orders"
+ },
+ "expected": {
+ "comment": " load orders ",
+ "purpose": "render orders"
+ }
+ },
+ {
+ "id": "Q09-trace-only",
+ "kind": "query",
+ "input": {
+ "trace": [
+ {
+ "kind": "Comment",
+ "detail": "load orders"
+ },
+ {
+ "kind": "Purpose",
+ "detail": "render orders"
+ }
+ ]
+ },
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ },
+ {
+ "id": "Q10-logs-disabled",
+ "kind": "query",
+ "input": {
+ "purpose": "render orders",
+ "logging": false
+ },
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ },
+ {
+ "id": "M01-missing",
+ "kind": "mutation",
+ "input": {},
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ },
+ {
+ "id": "M02-null",
+ "kind": "mutation",
+ "input": {
+ "comment": null
+ },
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ },
+ {
+ "id": "M03-empty",
+ "kind": "mutation",
+ "input": {
+ "comment": ""
+ },
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ },
+ {
+ "id": "M04-ascii-blank",
+ "kind": "mutation",
+ "input": {
+ "comment": " \t\r\n"
+ },
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ },
+ {
+ "id": "M05-unicode-blank",
+ "kind": "mutation",
+ "input": {
+ "comment": " "
+ },
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ },
+ {
+ "id": "M06-preserve",
+ "kind": "mutation",
+ "input": {
+ "comment": " submit order "
+ },
+ "expected": {
+ "comment": " submit order "
+ }
+ },
+ {
+ "id": "M07-trace-only",
+ "kind": "mutation",
+ "input": {
+ "trace": [
+ {
+ "kind": "AuditReason",
+ "detail": "submit order"
+ }
+ ]
+ },
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ },
+ {
+ "id": "M08-batch-child-only",
+ "kind": "mutation",
+ "input": {
+ "children": [
+ {
+ "comment": "create item"
+ },
+ {
+ "comment": "delete item"
+ }
+ ]
+ },
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ },
+ {
+ "id": "M09-blank-route-tail",
+ "kind": "mutation",
+ "input": {
+ "comment": "submit order",
+ "trace": [
+ {
+ "kind": "AuditReason",
+ "detail": "authorize payment"
+ },
+ {
+ "kind": "Entity",
+ "detail": ""
+ }
+ ]
+ },
+ "expected": {
+ "comment": "submit order"
+ }
+ },
+ {
+ "id": "M10-logs-disabled",
+ "kind": "mutation",
+ "input": {
+ "logging": false
+ },
+ "error": {
+ "code": "REQUEST_COMMENT_REQUIRED",
+ "field": "comment"
+ }
+ }
+ ]
+}
diff --git a/tests/fixtures/sql-trace-path-v1.json b/tests/fixtures/sql-trace-path-v1.json
new file mode 100644
index 0000000..9035418
--- /dev/null
+++ b/tests/fixtures/sql-trace-path-v1.json
@@ -0,0 +1,35 @@
+{
+ "contract": "teaql.sql-trace-path.v1",
+ "owningIssue": "https://github.com/teaql/teaql-conformance/issues/99",
+ "evidenceLevel": "pure-algorithm",
+ "notes": [
+ "Input source frames here are authored unit-test inputs, not proof of generated Q or Mutation execution.",
+ "Every case must be idempotent. Intent values are compared separately; source text is preserved verbatim.",
+ "A rebuilt noncanonical mutation path currently has no Entity ID; an already canonical path preserves its existing IDs.",
+ "Empty-source is a helper boundary, not permission to execute a request without required intent."
+ ],
+ "cases": [
+ {"id":"TC-SQL-01","operation":"select","backend":"sqlite","source":[{"kind":"Comment","name":"School","entityId":null,"detail":"what: load school graph"},{"kind":"Purpose","name":"School","entityId":null,"detail":"why: render details"},{"kind":"Relation","name":"platform","entityId":null,"detail":"School.platform"},{"kind":"Relation","name":"organization","entityId":null,"detail":"Platform.organization"}],"expectedIntent":{"comment":"what: load school graph","purpose":"why: render details","auditReason":null},"expectedPath":[{"kind":"Operation","name":"School","entityId":null,"detail":"query"},{"kind":"Request","name":"School","entityId":null,"detail":""},{"kind":"Relation","name":"platform","entityId":null,"detail":"School.platform"},{"kind":"Relation","name":"organization","entityId":null,"detail":"Platform.organization"},{"kind":"Provider","name":"sqlite","entityId":null,"detail":""},{"kind":"Sql","name":"select","entityId":null,"detail":""}]},
+ {"id":"TC-SQL-02","operation":"update","backend":"ignored-new-backend","source":[{"kind":"Operation","name":"Order","entityId":null,"detail":"mutation"},{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"},{"kind":"Entity","name":"OrderItem","entityId":201,"detail":""},{"kind":"Relation","name":"items","entityId":null,"detail":"Order.items"},{"kind":"Comment","name":"Order","entityId":null,"detail":"update graph"},{"kind":"Provider","name":"postgres","entityId":null,"detail":""},{"kind":"Sql","name":"update","entityId":null,"detail":""}],"expectedIntent":{"comment":"update graph","purpose":null,"auditReason":"submit order"},"expectedPath":[{"kind":"Operation","name":"Order","entityId":null,"detail":"mutation"},{"kind":"Entity","name":"OrderItem","entityId":201,"detail":""},{"kind":"Relation","name":"items","entityId":null,"detail":"Order.items"},{"kind":"Provider","name":"postgres","entityId":null,"detail":""},{"kind":"Sql","name":"update","entityId":null,"detail":""}]},
+ {"id":"TC-SQL-03","operation":"update","backend":"postgres","source":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"},{"kind":"Entity","name":"Order","entityId":100,"detail":""},{"kind":"Entity","name":"OrderItem","entityId":201,"detail":""}],"expectedIntent":{"comment":null,"purpose":null,"auditReason":"submit order"},"expectedPath":[{"kind":"Operation","name":"Order","entityId":null,"detail":"mutation"},{"kind":"Entity","name":"OrderItem","entityId":null,"detail":""},{"kind":"Provider","name":"postgres","entityId":null,"detail":""},{"kind":"Sql","name":"update","entityId":null,"detail":""}]},
+ {"id":"TC-SQL-04-insert","operation":"insert","backend":"postgres","source":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"insert order"},{"kind":"Entity","name":"Order","entityId":100,"detail":""}],"expectedIntent":{"comment":null,"purpose":null,"auditReason":"insert order"},"expectedPath":[{"kind":"Operation","name":"Order","entityId":null,"detail":"mutation"},{"kind":"Entity","name":"Order","entityId":null,"detail":""},{"kind":"Provider","name":"postgres","entityId":null,"detail":""},{"kind":"Sql","name":"insert","entityId":null,"detail":""}]},
+ {"id":"TC-SQL-04-update","operation":"update","backend":"postgres","source":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"update order"},{"kind":"Entity","name":"Order","entityId":100,"detail":""}],"expectedIntent":{"comment":null,"purpose":null,"auditReason":"update order"},"expectedPath":[{"kind":"Operation","name":"Order","entityId":null,"detail":"mutation"},{"kind":"Entity","name":"Order","entityId":null,"detail":""},{"kind":"Provider","name":"postgres","entityId":null,"detail":""},{"kind":"Sql","name":"update","entityId":null,"detail":""}]},
+ {"id":"TC-SQL-04-delete","operation":"delete","backend":"postgres","source":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"delete order"},{"kind":"Entity","name":"Order","entityId":100,"detail":""}],"expectedIntent":{"comment":null,"purpose":null,"auditReason":"delete order"},"expectedPath":[{"kind":"Operation","name":"Order","entityId":null,"detail":"mutation"},{"kind":"Entity","name":"Order","entityId":null,"detail":""},{"kind":"Provider","name":"postgres","entityId":null,"detail":""},{"kind":"Sql","name":"delete","entityId":null,"detail":""}]},
+ {"id":"TC-SQL-04-recover","operation":"recover","backend":"postgres","source":[{"kind":"AuditReason","name":"Order","entityId":100,"detail":"recover order"},{"kind":"Entity","name":"Order","entityId":100,"detail":""}],"expectedIntent":{"comment":null,"purpose":null,"auditReason":"recover order"},"expectedPath":[{"kind":"Operation","name":"Order","entityId":null,"detail":"mutation"},{"kind":"Entity","name":"Order","entityId":null,"detail":""},{"kind":"Provider","name":"postgres","entityId":null,"detail":""},{"kind":"Sql","name":"recover","entityId":null,"detail":""}]},
+ {"id":"TC-SQL-05","operation":"select","backend":" \t ","source":[{"kind":"Comment","name":"School","entityId":null,"detail":"load school"},{"kind":"Purpose","name":"School","entityId":null,"detail":"render school"}],"expectedIntent":{"comment":"load school","purpose":"render school","auditReason":null},"expectedPath":[{"kind":"Operation","name":"School","entityId":null,"detail":"query"},{"kind":"Request","name":"School","entityId":null,"detail":""},{"kind":"Provider","name":"unknown","entityId":null,"detail":""},{"kind":"Sql","name":"select","entityId":null,"detail":""}]},
+ {"id":"TC-SQL-06","operation":"select","backend":"sqlite","source":[{"kind":"Comment","name":"School","entityId":null,"detail":"old comment"},{"kind":"Purpose","name":"School","entityId":null,"detail":"old purpose"},{"kind":"AuditReason","name":"School","entityId":1,"detail":"old reason"},{"kind":"Comment","name":"School","entityId":null,"detail":" new comment "},{"kind":"Purpose","name":"School","entityId":null,"detail":"new purpose"},{"kind":"AuditReason","name":"School","entityId":1,"detail":"new reason"},{"kind":"Entity","name":"School","entityId":1,"detail":""}],"expectedIntent":{"comment":" new comment ","purpose":"new purpose","auditReason":"new reason"},"expectedPath":[{"kind":"Operation","name":"School","entityId":null,"detail":"query"},{"kind":"Request","name":"School","entityId":null,"detail":""},{"kind":"Provider","name":"sqlite","entityId":null,"detail":""},{"kind":"Sql","name":"select","entityId":null,"detail":""}]},
+ {"id":"BOUNDARY-empty-source","operation":"select","backend":"","source":[],"expectedIntent":{"comment":null,"purpose":null,"auditReason":null},"expectedPath":[{"kind":"Operation","name":"unknown","entityId":null,"detail":"query"},{"kind":"Request","name":"unknown","entityId":null,"detail":""},{"kind":"Provider","name":"unknown","entityId":null,"detail":""},{"kind":"Sql","name":"select","entityId":null,"detail":""}]},
+ {"id":"BOUNDARY-blank-first-name","operation":"update","backend":"postgres","source":[{"kind":"Entity","name":" ","entityId":null,"detail":"not intent"},{"kind":"AuditReason","name":"Order","entityId":100,"detail":"submit order"},{"kind":"Entity","name":"OrderItem","entityId":201,"detail":""}],"expectedIntent":{"comment":null,"purpose":null,"auditReason":"submit order"},"expectedPath":[{"kind":"Operation","name":"Order","entityId":null,"detail":"mutation"},{"kind":"Entity","name":"OrderItem","entityId":null,"detail":""},{"kind":"Provider","name":"postgres","entityId":null,"detail":""},{"kind":"Sql","name":"update","entityId":null,"detail":""}]},
+ {"id":"BOUNDARY-three-level-shape","operation":"select","backend":"sqlite","source":[{"kind":"Comment","name":"School","entityId":null,"detail":"load school graph"},{"kind":"Purpose","name":"School","entityId":null,"detail":"render school"},{"kind":"Relation","name":"platform","entityId":null,"detail":"School.platform"},{"kind":"Relation","name":"organization","entityId":null,"detail":"Platform.organization"},{"kind":"Relation","name":"region","entityId":null,"detail":"Organization.region"}],"expectedIntent":{"comment":"load school graph","purpose":"render school","auditReason":null},"expectedPath":[{"kind":"Operation","name":"School","entityId":null,"detail":"query"},{"kind":"Request","name":"School","entityId":null,"detail":""},{"kind":"Relation","name":"platform","entityId":null,"detail":"School.platform"},{"kind":"Relation","name":"organization","entityId":null,"detail":"Platform.organization"},{"kind":"Relation","name":"region","entityId":null,"detail":"Organization.region"},{"kind":"Provider","name":"sqlite","entityId":null,"detail":""},{"kind":"Sql","name":"select","entityId":null,"detail":""}]}
+ ],
+ "requiredIntegrationCases": [
+ {
+ "id": "TC-SQL-07",
+ "requirement": "Generated Q constructs three relation levels; observe real provider metadata without test-injected frames."
+ },
+ {
+ "id": "TC-SQL-08",
+ "requirement": "Existing privacy projection masks secret intent before every sink without changing the path structure."
+ }
+ ]
+}
diff --git a/tests/provider/sqlite/test_ledger_trace_override.py b/tests/provider/sqlite/test_ledger_trace_override.py
new file mode 100644
index 0000000..3edda31
--- /dev/null
+++ b/tests/provider/sqlite/test_ledger_trace_override.py
@@ -0,0 +1,139 @@
+"""Explicit native ledger input, not a generated automatic-writer assertion."""
+from copy import deepcopy
+from types import SimpleNamespace
+
+import aiosqlite
+import pytest
+
+from test_trace_chain import fixture
+from teaql.core.entity import EntityKey, EntityRoot
+from teaql.core.mutation import UpdateCommand
+
+
+def scalar(value):
+ return getattr(value, 'val', value)
+
+
+def lineage(nodes):
+ return [(node.name or node.entity_type, node.entity_id, node.comment) for node in nodes]
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('logging', [False, True])
+@pytest.mark.parametrize('empty', [False, True])
+async def test_native_ledger_preference_reaches_sql_and_committed_audit(
+ tmp_path, monkeypatch, logging, empty):
+ monkeypatch.delenv('TEAQL_ALLOW_SENSITIVE_PLAINTEXT_LOGS', raising=False)
+ context, service, transport, diagnostics = await fixture(tmp_path)
+ context.insert_resource('dataService', service)
+ if not logging:
+ context.disable_sql_log()
+ context.entity('Payment').audit_mask_fields(['name'])
+ service.schema_provider.get_entity('Payment').audit_mask_fields(['name'])
+ secret = 'OVERRIDEPRIVATEPAYMENT'
+ reason = 'apply ' + secret
+ ledger = EntityRoot()
+ keys = [EntityKey(entity, 1) for entity in ('CustomerOrder', 'Payment', 'Shipment')]
+ names = ['changed root', secret, 'changed shipment']
+ commands, results, events, phases = [], [], [], []
+ begin = transport.begin_sql
+
+ async def observed_begin():
+ tx = await begin()
+ phases.append('begin')
+ commit = tx.commit_sql
+
+ async def observed_commit():
+ await commit()
+ phases.append('commit')
+ tx.commit_sql = observed_commit
+ return tx
+
+ monkeypatch.setattr(transport, 'begin_sql', observed_begin)
+
+ def audit(_context, event):
+ assert phases[-1] == 'commit'
+ events.append(event)
+ context.with_app_audit_event_sink(SimpleNamespace(on_safe_event=audit))
+
+ async def work(graph):
+ root = graph.scope(keys[0])
+ # A native caller supplies the complete chain using real scope recovery.
+ # No observer inserts expected frames into requests or execution results.
+ supplied = graph.scope(keys[1], root, 'review ' + secret).recover()
+ before = deepcopy(supplied)
+ ledger.set_trace_chain(keys[1], () if empty else supplied)
+ ledger.set_trace_chain(keys[2], ())
+ scope = [root, graph.scope(keys[1], root, 'fallback charge'), root]
+ updates = [UpdateCommand.new(key.entity, key.id).expected_version(1).value('name', name)
+ for key, name in zip(keys, names)]
+ for command in updates:
+ graph.context.preflight_mutation(command)
+ for key, command, local in zip(keys, updates, scope):
+ request = graph.request(command, local, ledger, key)
+ commands.append((key, deepcopy(request.mutation_lineage)))
+ results.append(await graph.transaction.mutate(graph.context, request))
+ assert not events
+ assert supplied == before
+ assert ledger.trace_chain(keys[1]) == (() if empty else before)
+
+ await context.execute_graph_save(work, comment=reason)
+ root_node = ('CustomerOrder', 1, reason)
+ local_reason = 'fallback charge' if empty else 'review ' + secret
+ expected = [[root_node], [root_node, ('Payment', 1, local_reason)], [root_node]]
+ assert phases == ['begin', 'commit']
+ assert [key for key, _ in commands] == keys
+ assert [lineage(nodes) for _, nodes in commands] == expected
+ assert len(transport.writes) == len(transport.reads) == len(results) == len(events) == 3
+ for index, result in enumerate(results):
+ assert scalar(result.persisted_record['name']) == names[index]
+ assert scalar(result.persisted_record['version']) == 2
+ assert result.affected_rows == 1
+ assert lineage(result.metadata.mutation_lineage) == expected[index]
+ assert len(result.metadata.statements) == 2
+ for read, metadata in enumerate(result.metadata.statements):
+ compiled = (transport.reads if read else transport.writes)[index]
+ assert metadata.parameterized_sql == compiled.sql
+ assert metadata.parameters == compiled.params
+ assert metadata.comment == reason
+ assert metadata.execution_outcome == 'success'
+ assert lineage(metadata.mutation_lineage) == expected[index]
+ assert metadata.result_count == 1 if read else metadata.affected_rows == 1
+ assert [node.kind for node in metadata.trace_chain] == [
+ 'operation', 'request' if read else 'entity', 'provider', 'sql']
+ assert metadata.trace_chain[0].name == 'CustomerOrder'
+ safe = [[(kind, identity, comment.replace(secret, '[REDACTED]'))
+ for kind, identity, comment in nodes] for nodes in expected]
+ assert [(event.entity, scalar(event.entity_id)) for event in events] == [(key.entity, key.id) for key in keys]
+ assert [lineage(event.trace_chain) for event in events] == safe
+ assert secret not in repr(events)
+ assert len(diagnostics) == (6 if logging else 0)
+ if logging:
+ assert [lineage(entry.mutation_lineage) for entry in diagnostics] == [nodes for nodes in safe for _ in range(2)]
+ assert secret not in repr(diagnostics)
+ async with aiosqlite.connect(transport.db_path) as db:
+ for key, name in zip(keys, names):
+ row = await (await db.execute(
+ f'SELECT name, version FROM {key.entity.lower()}_data WHERE id = ?', (key.id,))).fetchone()
+ assert row == (name, 2)
+
+ # Native ownership is explicit; this does not claim generated auto-cleanup.
+ ledger.clear_committed()
+ assert ledger.trace_chain(keys[1]) == ()
+ later_reason = 'independent mention ' + secret
+
+ async def later(graph):
+ command = UpdateCommand.new('Payment', 1).expected_version(2).value('name', 'next value')
+ graph.context.preflight_mutation(command)
+ request = graph.request(command, graph.scope(keys[1]), ledger, keys[1])
+ result = await graph.transaction.mutate(graph.context, request)
+ assert lineage(result.metadata.mutation_lineage) == [('Payment', 1, later_reason)]
+ assert scalar(result.persisted_record['version']) == 3
+ await context.execute_graph_save(later, comment=later_reason)
+ assert phases == ['begin', 'commit', 'begin', 'commit']
+ assert len(events) == 4
+ assert lineage(events[-1].trace_chain) == [('Payment', 1, later_reason)]
+ assert len(transport.writes) == len(transport.reads) == 4
+ if logging:
+ assert len(diagnostics) == 8
+ assert all(entry.comment == later_reason for entry in diagnostics[-2:])
diff --git a/tests/provider/sqlite/test_like_intent.py b/tests/provider/sqlite/test_like_intent.py
new file mode 100644
index 0000000..ada6893
--- /dev/null
+++ b/tests/provider/sqlite/test_like_intent.py
@@ -0,0 +1,243 @@
+"""Exact typed LIKE operands at real SQLite boundaries; never strip wildcards."""
+from copy import deepcopy
+from dataclasses import asdict, replace
+
+import pytest
+
+from test_trace_chain import fixture
+from teaql.core import expr
+from teaql.core.mutation import MutationRequest, UpdateCommand
+from teaql.core.query import SelectQuery
+from teaql.core.value import Value
+from teaql.data_service import QueryRequest
+from teaql.runtime.context import SqlLogOptions
+from teaql.sql.executor import TransportError
+
+
+OPERATIONS = [
+ (expr.contain, '%', '%', False),
+ (expr.not_contain, '%', '%', True),
+ (expr.begin_with, '', '%', False),
+ (expr.not_begin_with, '', '%', True),
+ (expr.end_with, '%', '', False),
+ (expr.not_end_with, '%', '', True),
+]
+
+
+async def set_name(context, service, transport, entries, entity, value):
+ await service.mutate(context, MutationRequest(UpdateCommand.new(entity, 1)
+ .expected_version(1).value('name', value), comment='prepare LIKE operand fixture'))
+ entries.clear()
+ context.clear_sql_logs()
+ transport.reads.clear()
+
+
+def assert_logs(entries, logging, private, operand, root):
+ if not logging:
+ assert entries == []
+ return
+ assert entries
+ for entry in entries:
+ assert entry.trace_path[0].name == root
+ assert entry.trace_path[-1].kind == 'sql'
+ if private:
+ assert operand not in repr(entry), 'original operand leaked to safe SQL evidence'
+ assert '[REDACTED]' in entry.comment
+ assert '[REDACTED]' in entry.purpose
+ else:
+ assert operand in entry.comment and operand in entry.purpose
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('operation,prefix,suffix,negated', OPERATIONS,
+ ids=[op[0].__name__ for op in OPERATIONS])
+@pytest.mark.parametrize('logging', [False, True])
+@pytest.mark.parametrize('private', [False, True])
+@pytest.mark.parametrize('transaction', [False, True])
+async def test_like_original_operand_at_sqlite_boundaries(
+ tmp_path, monkeypatch, operation, prefix, suffix, negated, logging, private, transaction):
+ monkeypatch.delenv('TEAQL_ALLOW_SENSITIVE_PLAINTEXT_LOGS', raising=False)
+ context, service, transport, entries = await fixture(tmp_path)
+ descriptor = service.schema_provider.get_entity('CustomerOrder')
+ descriptor.audit_mask_fields(['name'] if private else [])
+ descriptor.property_by_name('name').log_policy('plain')
+ operand = 'PRIVATE-ROOT-OPERAND'
+ await set_name(context, service, transport, entries, 'CustomerOrder', operand)
+ if not logging:
+ context.with_sql_log_options(SqlLogOptions.disabled())
+ comment, purpose = 'inspect ' + operand, 'justify ' + operand
+ policies = []
+ context.with_request_policy(lambda query: policies.append(deepcopy(query)) or query)
+ tx = await service.begin(context) if transaction else None
+ execution = tx or service
+ try:
+ for count in [False, True]:
+ query = SelectQuery('CustomerOrder').filter(operation('name', operand)).limit(2)
+ query = query.count('n') if count else query.project('id')
+ request = QueryRequest(query, _comment=comment, _purpose=purpose)
+ before = deepcopy(request.query)
+ result = await execution.query(context, context.prepare_query_request(request))
+ assert (result.rows[0]['n'] if count else len(result.rows)) == (0 if negated else 1)
+ assert request.query == before
+ assert (request.intent.comment, request.intent.purpose) == (comment, purpose)
+ assert policies[-1].filter_expr == query.filter_expr
+ assert policies[-1].comment_text == comment and policies[-1].purpose_text == purpose
+ assert [value.val for value in transport.reads[-1].params] == [prefix + operand + suffix]
+ assert transport.reads[-1].parameter_log_policies == ['masked' if private else 'plain']
+ assert_logs(entries, logging, private, operand, 'CustomerOrder')
+ assert len(transport.reads) == 2
+ # An independent invocation on the same Context must not retain this secret.
+ await execution.query(context, QueryRequest(SelectQuery('CustomerOrder').project('id').limit(1),
+ _comment=comment, _purpose=purpose))
+ if logging:
+ assert entries[-1].comment == comment and entries[-1].purpose == purpose
+ finally:
+ if tx:
+ await tx.rollback(context)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('logging', [False, True])
+@pytest.mark.parametrize('transaction', [False, True])
+@pytest.mark.parametrize('failure', [False, True])
+async def test_like_future_child_masks_first_root_and_failure(
+ tmp_path, monkeypatch, logging, transaction, failure):
+ monkeypatch.delenv('TEAQL_ALLOW_SENSITIVE_PLAINTEXT_LOGS', raising=False)
+ context, service, transport, entries = await fixture(tmp_path)
+ service.schema_provider.get_entity('Payment').audit_mask_fields(['name'])
+ operand = 'PRIVATE-CHILD-OPERAND'
+ await set_name(context, service, transport, entries, 'Payment', operand)
+ if not logging:
+ context.with_sql_log_options(SqlLogOptions.disabled())
+ child = SelectQuery('Payment').project('id', 'parent_id').filter(expr.contain('name', operand)).limit(1)
+ query = SelectQuery('CustomerOrder').project('id').limit(1).relation_query('children', child)
+ request = QueryRequest(query, _comment='inspect ' + operand, _purpose='justify ' + operand)
+ before = deepcopy(request.query)
+ tx = await service.begin(context) if transaction else None
+ execution = tx or service
+ try:
+ if failure:
+ transport.fail_table = 'payment_data'
+ with pytest.raises(TransportError):
+ await execution.query(context, request)
+ else:
+ result = await execution.query(context, request)
+ assert len(result.rows[0]['children']) == 1
+ assert len(transport.reads) == 2
+ assert any(value.val == '%' + operand + '%' for value in transport.reads[1].params)
+ assert request.query == before
+ assert_logs(entries, logging, True, operand, 'CustomerOrder')
+ if logging:
+ assert len(entries) == 2
+ assert [(n.name, n.comment) for n in entries[-1].trace_path if n.kind == 'relation'] == [
+ ('children', 'CustomerOrder.children')]
+ assert entries[-1].execution_outcome == ('failure' if failure else 'success')
+ transport.fail_table = None
+ await execution.query(context, QueryRequest(SelectQuery('CustomerOrder').project('id').limit(1),
+ _comment='independent ' + operand, _purpose='no inherited source'))
+ if logging:
+ assert entries[-1].comment == 'independent ' + operand
+ finally:
+ if tx:
+ await tx.rollback(context)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('logging', [False, True])
+async def test_like_literal_operands_and_raw_patterns_are_not_inferred(tmp_path, monkeypatch, logging):
+ monkeypatch.delenv('TEAQL_ALLOW_SENSITIVE_PLAINTEXT_LOGS', raising=False)
+ context, service, transport, entries = await fixture(tmp_path)
+ service.schema_provider.get_entity('CustomerOrder').audit_mask_fields(['name'])
+ operand = '%_\\fragment%'
+ await service.mutate(context, MutationRequest(UpdateCommand.new('CustomerOrder', 1)
+ .expected_version(1).value('name', operand), comment='prepare literal fixture'))
+ entries.clear()
+ transport.reads.clear()
+ if not logging:
+ context.with_sql_log_options(SqlLogOptions.disabled())
+ result = await service.query(context, QueryRequest(
+ SelectQuery('CustomerOrder').project('id').filter(expr.begin_with('name', operand)).limit(1),
+ _comment='inspect ' + operand + ' public fragment', _purpose='justify ' + operand))
+ assert len(result.rows) == 1
+ assert [value.val for value in transport.reads[-1].params] == [operand + '%']
+ assert_logs(entries, logging, True, operand, 'CustomerOrder')
+ if logging:
+ assert 'public fragment' in entries[-1].comment
+ # Raw LIKE accepts a complete pattern. It has no original unwrapped operand.
+ pattern = '%fragment%'
+ result = await service.query(context, QueryRequest(
+ SelectQuery('CustomerOrder').project('id').filter(expr.like('name', pattern)).limit(1),
+ _comment='literal fragment; pattern ' + pattern, _purpose='raw pattern ' + pattern))
+ assert len(result.rows) == 1
+ assert [value.val for value in transport.reads[-1].params] == [pattern]
+ if logging:
+ assert entries[-1].comment == 'literal fragment; pattern [REDACTED]'
+ assert entries[-1].purpose == 'raw pattern [REDACTED]'
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('logging', [False, True])
+@pytest.mark.parametrize('rewrite', ['operator', 'pattern', 'replace'])
+async def test_like_rewritten_ast_does_not_retain_stale_original(tmp_path, monkeypatch, logging, rewrite):
+ monkeypatch.delenv('TEAQL_ALLOW_SENSITIVE_PLAINTEXT_LOGS', raising=False)
+ context, service, transport, entries = await fixture(tmp_path)
+ service.schema_provider.get_entity('CustomerOrder').audit_mask_fields(['name'])
+ original = 'STALE-ORIGINAL-OPERAND'
+ condition = expr.contain('name', original)
+ assert set(asdict(condition.right)) == {'value'}, 'private provenance became a wire field'
+ assert condition.right == expr.ValueExpr(Value.Text('%' + original + '%'))
+ assert deepcopy(condition.right) == condition.right
+ if rewrite == 'operator':
+ condition.op = expr.BinaryOp.Eq
+ pattern = '%' + original + '%'
+ elif rewrite == 'pattern':
+ pattern = '%CURRENT-PATTERN%'
+ condition.right.value = Value.Text(pattern)
+ else:
+ pattern = '%REPLACEMENT-PATTERN%'
+ condition.right = replace(condition.right, value=Value.Text(pattern))
+ assert condition.right == replace(expr.ValueExpr(Value.Text('old')), value=Value.Text(pattern))
+ assert not hasattr(condition.right, '_like_operand')
+ if not logging:
+ context.with_sql_log_options(SqlLogOptions.disabled())
+ comment = 'old ' + original + ' current ' + pattern
+ request = QueryRequest(SelectQuery('CustomerOrder').project('id').filter(condition).limit(1),
+ _comment=comment, _purpose='do not infer a rewritten operand')
+ result = await service.query(context, request)
+ assert result.rows == []
+ assert [value.val for value in transport.reads[-1].params] == [pattern]
+ if logging:
+ assert entries[-1].comment == 'old ' + original + ' current [REDACTED]'
+ else:
+ assert entries == []
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('policy', ['masked', 'credential'])
+async def test_like_stream_debug_opt_in_and_revocation(tmp_path, monkeypatch, policy):
+ from teaql.runtime.log_privacy import PLAINTEXT_ENV, PLAINTEXT_ACK
+ monkeypatch.delenv(PLAINTEXT_ENV, raising=False)
+ context, service, transport, entries = await fixture(tmp_path)
+ descriptor = service.schema_provider.get_entity('CustomerOrder')
+ descriptor.audit_mask_fields([])
+ descriptor.property_by_name('name').log_policy(policy)
+ operand = 'PRIVATE-STREAM-OPERAND'
+ await set_name(context, service, transport, entries, 'CustomerOrder', operand)
+ request = QueryRequest(SelectQuery('CustomerOrder').project('id')
+ .filter(expr.contain('name', operand)).limit(1),
+ _comment='inspect ' + operand, _purpose='justify ' + operand)
+ for mode in ['safe', 'debug', 'revoked']:
+ if mode == 'debug':
+ monkeypatch.setenv(PLAINTEXT_ENV, PLAINTEXT_ACK)
+ else:
+ monkeypatch.delenv(PLAINTEXT_ENV, raising=False)
+ entries.clear()
+ chunks = [chunk async for chunk in service.query_stream(context, request, 1)]
+ assert sum(len(chunk.rows) for chunk in chunks) == 1
+ assert [value.val for value in transport.streams[-1].params] == ['%' + operand + '%']
+ assert len(entries) == 1
+ private = mode != 'debug' or policy == 'credential'
+ assert_logs(entries, True, private, operand, 'CustomerOrder')
+ if not private:
+ assert 'DEBUG PLAINTEXT' in entries[0].debug_sql
+ assert request.intent.comment == 'inspect ' + operand
diff --git a/tests/provider/sqlite/test_numeric_partition_trace.py b/tests/provider/sqlite/test_numeric_partition_trace.py
new file mode 100644
index 0000000..d4219cf
--- /dev/null
+++ b/tests/provider/sqlite/test_numeric_partition_trace.py
@@ -0,0 +1,200 @@
+"""Native numeric grouping/window evidence, not generated aggregation acceptance."""
+from copy import deepcopy
+import json
+import re
+from types import SimpleNamespace
+
+import aiosqlite
+import pytest
+
+from test_trace_chain import RecordingTransport
+from teaql.core.expr import Expr, begin_with
+from teaql.core.meta import EntityDescriptor, PropertyDescriptor, RelationDescriptor
+from teaql.core.query import SelectQuery
+from teaql.core.value import DataType
+from teaql.data_service import QueryRequest
+from teaql.provider.sqlite import SimpleSchemaProvider
+from teaql.provider.sqlite.dialect import SqliteDialect
+from teaql.runtime import RuntimeModule
+from teaql.runtime.context import SqlLogOptions
+from teaql.sql.executor import SqlDataServiceExecutor
+
+
+OPERANDS = ('%NUMERIC_FIRST_', '%NUMERIC_SECOND_')
+
+
+async def numeric_fixture(tmp_path, monkeypatch, logging):
+ monkeypatch.delenv('TEAQL_ALLOW_SENSITIVE_PLAINTEXT_LOGS', raising=False)
+ module, schema = RuntimeModule.new(), SimpleSchemaProvider()
+ for name, fields in (
+ ('NumericRoot', ()), ('NumericParent', ('root_id',)),
+ ('NumericItem', ('parent_id', 'bucket', 'private_name', 'public_name'))):
+ descriptor = EntityDescriptor(name).table_name(name.lower())
+ descriptor.property(PropertyDescriptor('id', DataType.I64).is_id().log_policy('plain'))
+ descriptor.property(PropertyDescriptor('version', DataType.I64).is_version().log_policy('plain'))
+ for field in fields:
+ descriptor.property(PropertyDescriptor(field,
+ DataType.Text if field.endswith('_name') else DataType.I64).log_policy('plain'))
+ descriptor.audit_mask_fields(['private_name'] if name == 'NumericItem' else [])
+ if name == 'NumericRoot':
+ descriptor.relation(RelationDescriptor('parents', 'NumericParent')
+ .local('id').foreign('root_id').many())
+ elif name == 'NumericParent':
+ descriptor.relation(RelationDescriptor('items', 'NumericItem')
+ .local('id').foreign('parent_id').many())
+ module.entity(descriptor)
+ schema.register_entity(descriptor)
+ context = module.into_context()
+ entries, raw = [], []
+ context.set_diagnostic_sql_log_sink(SimpleNamespace(write=entries.append))
+ if not logging:
+ context.with_sql_log_options(SqlLogOptions.disabled())
+ record = context._record_metadata_log
+
+ def observe(metadata, **kwargs):
+ raw.append(deepcopy(metadata))
+ return record(metadata, **kwargs)
+
+ monkeypatch.setattr(context, '_record_metadata_log', observe)
+ transport = RecordingTransport(str(tmp_path / 'numeric.sqlite'))
+ service = SqlDataServiceExecutor(SqliteDialect(), transport, schema)
+ async with aiosqlite.connect(transport.db_path) as db:
+ await db.execute('CREATE TABLE numericroot (id INTEGER PRIMARY KEY, version INTEGER)')
+ await db.execute('CREATE TABLE numericparent '
+ '(id INTEGER PRIMARY KEY, version INTEGER, root_id INTEGER)')
+ await db.execute('CREATE TABLE numericitem (id INTEGER PRIMARY KEY, version INTEGER, '
+ 'parent_id INTEGER, bucket INTEGER, private_name TEXT, public_name TEXT)')
+ await db.execute('INSERT INTO numericroot VALUES (1, 1)')
+ await db.executemany('INSERT INTO numericparent VALUES (?, 1, 1)', [(1,), (2,), (3,)])
+ for index, operand in enumerate(OPERANDS):
+ rows = [(identity + index * 100, 1, parent, bucket,
+ operand + str(identity), operand + str(identity))
+ for identity, parent, bucket in (
+ (11, 1, 10), (12, 1, 10), (13, 1, 20),
+ (21, 2, 10), (22, 2, 30), (23, 2, 30))]
+ await db.executemany('INSERT INTO numericitem VALUES (?, ?, ?, ?, ?, ?)', rows)
+ await db.commit()
+ return context, service, transport, entries, raw
+
+
+def assert_path(nodes, root, relations):
+ assert [node.kind for node in nodes] == [
+ 'operation', 'request', *['relation'] * len(relations), 'provider', 'sql']
+ assert [node.name for node in nodes] == [root, root, *relations, 'sqlite', 'select']
+ assert not any(node.kind == 'relation' and node.name in ('bucket', 'n', 'parent_id')
+ for node in nodes)
+
+
+def assert_observations(request, before, transport, entries, raw, logging, field, operand,
+ root, routes, counts, expected_params):
+ assert request.query == before and request.query.trace_chain == []
+ assert len(transport.reads) == len(raw) == len(routes)
+ assert [item.result_count for item in raw] == counts
+ assert [[value.val for value in query.params] for query in transport.reads] == expected_params
+ assert [item.parameters for item in raw] == [list(query.params) for query in transport.reads]
+ assert all((item.comment, item.purpose) == (request.intent.comment, request.intent.purpose)
+ for item in raw)
+ for item, relations in zip(raw, routes):
+ assert_path(item.trace_chain, root, relations)
+ assert len(entries) == (len(raw) if logging else 0)
+ for entry, relations in zip(entries, routes):
+ assert_path(entry.trace_path, root, relations)
+ if field == 'private_name':
+ assert operand not in repr(entry)
+ assert entry.comment == request.intent.comment.replace(operand, '[REDACTED]')
+ assert entry.purpose == request.intent.purpose.replace(operand, '[REDACTED]')
+ else:
+ assert entry.comment == request.intent.comment and entry.purpose == request.intent.purpose
+ print('NUMERIC_OBSERVATION ' + json.dumps({
+ 'root': root, 'logging': logging, 'field': field, 'routes': routes, 'counts': counts,
+ 'sql': [query.sql for query in transport.reads], 'parameters': expected_params,
+ 'safeIntent': [(entry.comment, entry.purpose) for entry in entries],
+ }))
+
+
+async def independent(context, service, entries, logging):
+ reason = 'independent ' + ' and '.join(OPERANDS)
+ result = await service.query(context, QueryRequest(SelectQuery('NumericRoot').project('id').limit(1),
+ _comment=reason, _purpose='no numeric query provenance'))
+ assert [row['id'] for row in result.rows] == [1]
+ assert_path(result.metadata.trace_chain, 'NumericRoot', [])
+ if logging:
+ assert entries[-1].comment == reason
+ assert entries[-1].purpose == 'no numeric query provenance'
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('logging', [False, True])
+@pytest.mark.parametrize('field', ['private_name', 'public_name'])
+@pytest.mark.parametrize('shape', ['group', 'window', 'grouped-window'])
+async def test_root_numeric_grouping_and_partition_keep_real_sql_semantics(
+ tmp_path, monkeypatch, logging, field, shape):
+ context, service, transport, entries, raw = await numeric_fixture(tmp_path, monkeypatch, logging)
+ for index, operand in enumerate(OPERANDS):
+ entries.clear(); raw.clear(); transport.reads.clear()
+ minimum = 15 if index == 0 else 25
+ query = SelectQuery('NumericItem').filter(begin_with(field, operand))
+ if shape == 'window':
+ query.project('id', 'parent_id', 'bucket').partition_by_field('bucket').order_asc('id').limit(1)
+ else:
+ query.group_by('parent_id', 'bucket').count('n').having(Expr.gte('bucket', minimum))
+ query.order_desc('bucket').limit(10 if shape == 'group' else 1)
+ if shape == 'grouped-window':
+ query.partition_by_field('parent_id')
+ request = QueryRequest(query, _comment='inspect ' + operand, _purpose='justify ' + operand)
+ before = deepcopy(request.query)
+ result = await service.query(context, request)
+ if shape == 'window':
+ assert sorted((row['id'], row['parent_id'], row['bucket']) for row in result.rows) == [
+ (11 + index * 100, 1, 10), (13 + index * 100, 1, 20), (22 + index * 100, 2, 30)]
+ else:
+ expected = [(1, 20, 1), (2, 30, 2)] if index == 0 else [(2, 30, 2)]
+ assert sorted((row['parent_id'], row['bucket'], row['n']) for row in result.rows) == expected
+ sql = transport.reads[0].sql
+ assert ('ROW_NUMBER()' in sql) == (shape != 'group')
+ assert ('GROUP BY' in sql and 'HAVING' in sql) == (shape != 'window')
+ assert transport.reads[0].parameter_log_policies == [
+ 'masked' if field == 'private_name' else 'plain', *(['plain'] if shape != 'window' else [])]
+ assert_observations(request, before, transport, entries, raw, logging, field, operand,
+ 'NumericItem', [[]], [len(result.rows)], [[operand + '%'] + ([] if shape == 'window' else [minimum])])
+ await independent(context, service, entries, logging)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('logging', [False, True])
+@pytest.mark.parametrize('field', ['private_name', 'public_name'])
+@pytest.mark.parametrize('plan', ['window', 'probe'])
+async def test_loaded_grouped_relation_keeps_ancestors_membership_and_future_privacy(
+ tmp_path, monkeypatch, logging, field, plan):
+ context, service, transport, entries, raw = await numeric_fixture(tmp_path, monkeypatch, logging)
+ for index, operand in enumerate(OPERANDS):
+ entries.clear(); raw.clear(); transport.reads.clear()
+ minimum = 15 if index == 0 else 25
+ child = (SelectQuery('NumericItem').filter(begin_with(field, operand))
+ .group_by('parent_id', 'bucket').count('n').having(Expr.gte('bucket', minimum))
+ .order_desc('bucket').limit(1).top_n_probe_parent_threshold(0 if plan == 'window' else 10))
+ parents = (SelectQuery('NumericParent').project('id').order_asc('id').limit(3)
+ .top_n_probe_parent_threshold(0).relation_query('items', child))
+ query = SelectQuery('NumericRoot').project('id').limit(1).relation_query('parents', parents)
+ request = QueryRequest(query, _comment='inspect future ' + operand, _purpose='justify future ' + operand)
+ before = deepcopy(request.query)
+ result = await service.query(context, request)
+ assert len(result.rows) == 1
+ loaded = result.rows[0]['parents']
+ assert [parent['id'] for parent in loaded] == [1, 2, 3]
+ assert {parent['id']: [(row['parent_id'], row['bucket'], row['n']) for row in parent['items']]
+ for parent in loaded} == {
+ 1: [(1, 20, 1)] if index == 0 else [], 2: [(2, 30, 2)], 3: []}
+ assert all('__teaql_partition_rank' not in row for parent in loaded for row in parent['items'])
+ child_reads = transport.reads[2:]
+ assert all('GROUP BY' in item.sql and 'HAVING' in item.sql for item in child_reads)
+ assert all(not re.search(r'(?= {"mutation", "provider", "audit", "query"}
@@ -611,7 +646,7 @@ async def test_mutation_returns_external_database_default_in_same_transaction(te
service = create_sqlite_service(temp_db, provider)
result = await service.mutate(
- None, MutationRequest(InsertCommand("Widget", {"version": Value.I64(1)})))
+ None, MutationRequest(InsertCommand("Widget", {"version": Value.I64(1)}), comment='what: runtime regression fixture'))
assert result.persisted_record["id"] > 0
assert result.persisted_record["name"] == "database-default"
@@ -628,7 +663,7 @@ async def test_structured_sql_evidence_is_parameterized_and_filterable(temp_db,
secret = "secret-customer-value"
insert = MutationRequest(InsertCommand("User", {
"id": Value.I64(1), "name": Value.Text(secret), "version": Value.I64(1)
- }))
+ }), comment='what: runtime regression fixture')
await service.mutate(context, insert)
query = SelectQuery("User").filter(
BinaryExpr(ColumnExpr("name"), BinaryOp.Eq, ValueExpr(Value.Text(secret))))
@@ -636,16 +671,17 @@ async def test_structured_sql_evidence_is_parameterized_and_filterable(temp_db,
TraceNode(kind="relation", name="User.organization", comment="organization"),
TraceNode(kind="relation", name="Organization.region", comment="region"),
TraceNode(kind="relation", name="Region.country", comment="country"),
- ]).comment("what: load governed users").purpose("why: verify trace inheritance")
+ ], _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior').comment("what: load governed users").purpose("why: verify trace inheritance")
await service.query(context, request)
entries = context.sql_logs()
- assert len(entries) == 2
+ assert len(entries) == 3 # INSERT, authoritative readback, explicit query
assert all(entry.sql and secret not in entry.sql for entry in entries)
assert all(entry.params for entry in entries)
assert any(entry.result_count is not None for entry in entries)
assert any(entry.affected_rows is not None for entry in entries)
- select_entry = next(entry for entry in entries if entry.operation.is_select())
+ select_entry = entries[-1]
+ assert select_entry.operation.is_select()
assert select_entry.comment == "what: load governed users"
assert select_entry.purpose == "why: verify trace inheritance"
assert [node.kind for node in select_entry.trace_path] == [
@@ -655,10 +691,11 @@ async def test_structured_sql_evidence_is_parameterized_and_filterable(temp_db,
context.enable_select_sql_log()
await service.mutate(context, MutationRequest(InsertCommand("User", {
"id": Value.I64(2), "name": Value.Text("ignored"), "version": Value.I64(1)
- })))
- assert context.sql_logs() == []
+ }), comment='what: runtime regression fixture'))
+ assert len(context.sql_logs()) == 1
+ assert context.sql_logs()[0].operation.is_select() # the real readback
context.enable_mutation_sql_log()
- await service.query(context, QueryRequest(SelectQuery("User")))
+ await service.query(context, QueryRequest(SelectQuery("User"), _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior'))
assert context.sql_logs() == []
context.disable_sql_log()
assert context.sql_logs() == []
@@ -736,12 +773,17 @@ async def on_safe_event(self, context, event): self.events.append(event)
raw, app = RawSink(), AppSink()
context = RuntimeModule.new().entity(entity).audit_event_sink(raw).into_context().with_app_audit_event_sink(app)
command = InsertCommand("User", {"id": Value.I64(1), "name": Value.Text("Alice Example"), "version": Value.I64(1)})
- command.trace_chain.append(type("Trace", (), {"comment": "approved change"})())
- result = await service.mutate(context, MutationRequest(command))
+ command.trace_chain.append(TraceNode(kind='auditReason', entity_type='User',
+ entity_id=1, comment='approved change'))
+ result = await service.mutate(context, MutationRequest(command, comment='what: runtime regression fixture'))
assert result.affected_rows == 1
assert len(raw.events) == 1 and raw.events[0].changes[1].new_value.val == "Alice Example"
- assert raw.events[0].trace_chain[0].comment == "approved change"
+ assert raw.events[0].trace_chain[0].comment == "what: runtime regression fixture"
+ assert len(raw.events[0].trace_chain) == 1
+ assert raw.events[0].trace_chain[0].entity_id == 1
+ # Legacy command trace text is not the request-owned root audit reason.
+ assert command.trace_chain[0].comment == "approved change"
assert len(app.events) == 1
name_field = next(field for field in app.events[0].fields if field.field == "name")
assert name_field.masked and name_field.value != "Alice Example"
@@ -809,7 +851,7 @@ def relation_ids(rows):
return {parent["id"]: [child["id"] for child in parent["lines"]]
for parent in rows}
- result = await service.query(context, QueryRequest(nested()))
+ result = await service.query(context, QueryRequest(nested(), _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior'))
assert len(result.rows) == 3
assert [len(parent["lines"]) for parent in result.rows] == [3, 3, 0]
assert len(queries) == 4
@@ -822,21 +864,24 @@ def relation_ids(rows):
assert relation_entries
assert [node.kind for node in relation_entries[0].trace_path] == [
"operation", "request", "relation", "provider", "sql"]
- assert relation_entries[0].trace_path[2].name == "Order.lines"
+ assert relation_entries[0].trace_path[2].name == "lines"
+ assert relation_entries[0].trace_path[2].comment == "Order.lines"
+ assert relation_entries[0].trace_path[0].name == "Order"
+ assert relation_entries[0].trace_path[1].name == "Order"
probe_ids = relation_ids(result.rows)
queries.clear()
- window = await service.query(context, QueryRequest(nested(0)))
+ window = await service.query(context, QueryRequest(nested(0), _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior'))
assert len(queries) == 2 and "ROW_NUMBER() OVER" in queries[1]
assert relation_ids(window.rows) == probe_ids
assert "state" in queries[1] and "version" in queries[1]
for threshold, expected_queries in ((3, 4), (2, 2)):
queries.clear()
- first = await service.query(context, QueryRequest(nested(threshold)))
+ first = await service.query(context, QueryRequest(nested(threshold), _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior'))
first_sql = list(queries)
queries.clear()
- second = await service.query(context, QueryRequest(nested(threshold)))
+ second = await service.query(context, QueryRequest(nested(threshold), _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior'))
assert relation_ids(first.rows) == relation_ids(second.rows) == probe_ids
assert queries == first_sql
assert len(queries) == expected_queries
@@ -901,10 +946,10 @@ async def test_relation_facet_merges_outer_filter_and_supports_include_all(temp_
outer = (SelectQuery("School")
.and_filter(contain("name", "Riverside"))
.facet_by("types", "school_type", nested))
- result = await service.query(None, QueryRequest(outer))
+ result = await service.query(None, QueryRequest(outer, _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior'))
assert [(row["code"], row["school_count"]) for row in result.facets["types"]] == [
("PRIMARY", 2), ("SECONDARY", 0), ("VOCATIONAL", 0)]
outer.facets[0].include_all_facets = False
- matched = await service.query(None, QueryRequest(outer))
+ matched = await service.query(None, QueryRequest(outer, _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior'))
assert [(row["code"], row["school_count"]) for row in matched.facets["types"]] == [("PRIMARY", 2)]
diff --git a/tests/provider/sqlite/test_trace_chain.py b/tests/provider/sqlite/test_trace_chain.py
new file mode 100644
index 0000000..8190b61
--- /dev/null
+++ b/tests/provider/sqlite/test_trace_chain.py
@@ -0,0 +1,1380 @@
+"""Native SQLite observations; no expected trace frames are supplied to requests."""
+import asyncio
+import json
+import re
+from pathlib import Path
+from types import SimpleNamespace
+
+import aiosqlite
+import pytest
+
+from teaql.core.expr import Expr
+from teaql.core.meta import EntityDescriptor, PropertyDescriptor, RelationDescriptor
+from teaql.core.mutation import InsertCommand, MutationRequest
+from teaql.core.query import SelectQuery
+from teaql.core.value import DataType
+from teaql.data_service import QueryRequest
+from teaql.provider.sqlite import SimpleSchemaProvider
+from teaql.provider.sqlite.dialect import SqliteDialect
+from teaql.provider.sqlite.transport import SqliteTransport
+from teaql.runtime import RuntimeModule
+from teaql.sql.executor import SqlDataServiceExecutor, TransportError
+
+
+class RecordingTransport(SqliteTransport):
+ def __init__(self, path):
+ super().__init__(path)
+ self.reads = []
+ self.streams = []
+ self.writes = []
+ self.begins = 0
+ self.fail_table = None
+ self.failure = RuntimeError('synthetic readback failure')
+
+ async def fetch_all_sql(self, compiled):
+ self.reads.append(compiled)
+ if self.fail_table and self.fail_table in compiled.sql:
+ raise self.failure
+ return await super().fetch_all_sql(compiled)
+
+ async def stream_sql(self, compiled, chunk_size):
+ self.streams.append(compiled)
+ async for chunk in super().stream_sql(compiled, chunk_size):
+ yield chunk
+
+ async def execute_sql(self, compiled):
+ self.writes.append(compiled)
+ return await super().execute_sql(compiled)
+
+ async def begin_sql(self):
+ self.begins += 1
+ transaction = await super().begin_sql()
+ fetch = transaction.fetch_all_sql
+ execute = transaction.execute_sql
+
+ async def capture(compiled):
+ self.reads.append(compiled)
+ if self.fail_table and self.fail_table in compiled.sql:
+ raise self.failure
+ return await fetch(compiled)
+
+ transaction.fetch_all_sql = capture
+ async def capture_write(compiled):
+ self.writes.append(compiled)
+ return await execute(compiled)
+ transaction.execute_sql = capture_write
+ return transaction
+
+
+async def fixture(tmp_path):
+ module, schema = RuntimeModule.new(), SimpleSchemaProvider()
+ names = ('CustomerOrder', 'Payment', 'PaymentAttempt', 'Shipment')
+ for index, name in enumerate(names):
+ descriptor = EntityDescriptor(name).table_name(name.lower() + '_data')
+ descriptor.property(PropertyDescriptor('id', DataType.I64).is_id())
+ descriptor.property(PropertyDescriptor('version', DataType.I64).is_version())
+ descriptor.property(PropertyDescriptor('name', DataType.Text))
+ if index < 3:
+ descriptor.relation(RelationDescriptor('children', names[index + 1])
+ .local('id').foreign('parent_id').many())
+ if index:
+ descriptor.property(PropertyDescriptor('parent_id', DataType.I64))
+ module.entity(descriptor)
+ schema.register_entity(descriptor)
+ context = module.into_context()
+ entries = []
+ context.set_diagnostic_sql_log_sink(SimpleNamespace(write=entries.append))
+ transport = RecordingTransport(str(tmp_path / 'trace.db'))
+ service = SqlDataServiceExecutor(SqliteDialect(), transport, schema)
+ # Native provider fixture, not generated-example or bootstrap proof.
+ async with aiosqlite.connect(transport.db_path) as database:
+ for index, name in enumerate(names):
+ tail = ', parent_id INTEGER' if index else ''
+ await database.execute(f'CREATE TABLE {name.lower()}_data '
+ f'(id INTEGER PRIMARY KEY, version INTEGER, name TEXT{tail})')
+ await database.commit()
+ for index, name in enumerate(names):
+ command = InsertCommand.new(name).value('name', name)
+ if index:
+ command.value('parent_id', 1)
+ await service.mutate(context, MutationRequest(command, comment='seed native trace fixture'))
+ entries.clear()
+ context.clear_sql_logs()
+ transport.reads.clear()
+ transport.writes.clear()
+ transport.begins = 0
+ return context, service, transport, entries
+
+
+def three_levels():
+ return (SelectQuery('CustomerOrder').project('id', 'name').limit(1)
+ .relation_query('children', SelectQuery('Payment').project('id').limit(1)
+ .relation_query('children', SelectQuery('PaymentAttempt').project('id').limit(1)
+ .relation_query('children', SelectQuery('Shipment').project('id').limit(1)))))
+
+
+INTENT_FAILURES = [case for case in json.loads(
+ (Path(__file__).parents[2] / 'fixtures/request-intent-v1.json').read_text())['cases']
+ if 'error' in case]
+
+
+def unchecked_request(case):
+ """Exercise defensive adapter validation, not a public construction bypass.
+
+ A decoder/custom adapter can present a malformed object without having run
+ its constructor. Trace-only and child-only inputs must still be rejected.
+ These supplied frames are invalid-input stimuli, not generated trace proof.
+ """
+ from teaql.core import TraceNode
+ data = case['input']
+ nodes = [TraceNode(kind=node['kind'], comment=node['detail'])
+ for node in data.get('trace', [])]
+ if case['kind'] == 'query':
+ request = object.__new__(QueryRequest)
+ request.query = SelectQuery('CustomerOrder').limit(1).filter(
+ Expr.eq('name', 'INTENT-PAYLOAD-CANARY'))
+ request.trace_chain = nodes
+ request._QueryRequest__intent = SimpleNamespace(**data)
+ else:
+ request = object.__new__(MutationRequest)
+ request._data = InsertCommand.new('CustomerOrder').value('name', 'INTENT-PAYLOAD-CANARY')
+ request._data.trace_chain = nodes
+ if 'children' in data:
+ request._data = [MutationRequest(InsertCommand.new('Payment').value('name', 'INTENT-PAYLOAD-CANARY'),
+ comment=child['comment']) for child in data['children']]
+ request._MutationRequest__intent = SimpleNamespace(**data)
+ return request
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('case', INTENT_FAILURES, ids=lambda case: case['id'])
+@pytest.mark.parametrize('logging', [False, True])
+@pytest.mark.parametrize('transaction', [False, True])
+async def test_request_intent_matrix_rejects_before_real_sqlite_policy_and_io(
+ tmp_path, case, logging, transaction):
+ from teaql.core import RequestIntentError
+ from teaql.runtime import DelegatingMutationPolicyRegistry
+ from teaql.runtime.context import SqlLogOptions
+ context, service, transport, entries = await fixture(tmp_path)
+ context.insert_resource('dataService', service)
+ if not logging:
+ context.with_sql_log_options(SqlLogOptions.disabled())
+ calls, audits = [], []
+ def query_policy(query):
+ calls.append('query-policy')
+ return query
+ def mutation_policy(key):
+ calls.append('mutation-policy')
+ return None
+ checker = context.check_and_fix_mutation
+ def check(command):
+ calls.append('checker')
+ return checker(command)
+ context.with_request_policy(query_policy)
+ context.with_mutation_policy_registry(DelegatingMutationPolicyRegistry(mutation_policy))
+ context.check_and_fix_mutation = check
+ context.with_app_audit_event_sink(SimpleNamespace(on_safe_event=lambda _, event: audits.append(event)))
+ tx = await service.begin(context) if transaction else None
+ execution = tx if tx is not None else service
+ # An explicitly opened transaction is setup, not IO caused by invalid input.
+ transport.begins = 0
+ request = unchecked_request(case)
+ def assert_diagnostic(error):
+ assert (error.code, error.field, error.request_kind) == (
+ case['error']['code'], case['error']['field'], case['kind'])
+ assert 'supply ' + error.field in str(error)
+ assert 'INTENT-PAYLOAD-CANARY' not in str(error)
+ try:
+ if case['kind'] == 'query':
+ with pytest.raises(RequestIntentError) as caught:
+ context.prepare_query_request(request)
+ assert_diagnostic(caught.value)
+ with pytest.raises(RequestIntentError) as caught:
+ await execution.query(context, request)
+ assert_diagnostic(caught.value)
+ # Cursor creation itself must reject; no polling is required.
+ with pytest.raises(RequestIntentError) as caught:
+ service.query_stream(context, request, 1)
+ assert_diagnostic(caught.value)
+ else:
+ with pytest.raises(RequestIntentError) as caught:
+ await execution.mutate(context, request)
+ assert_diagnostic(caught.value)
+ assert calls == []
+ assert transport.reads == transport.streams == transport.writes == []
+ assert transport.begins == 0
+ assert entries == audits == context.sql_logs() == []
+ finally:
+ if tx is not None:
+ await tx.rollback(context)
+
+ # Positive controls prove these are live hooks and a functioning SQLite
+ # provider, not a fixture that unconditionally refuses all operations.
+ valid = context.prepare_query_request(QueryRequest(SelectQuery('CustomerOrder').limit(1),
+ _comment='read validation control', _purpose='verify real provider and policy probes'))
+ result = await service.query(context, valid)
+ assert len(result.rows) == 1 and 'query-policy' in calls and transport.reads
+ await service.mutate(context, MutationRequest(InsertCommand.new('CustomerOrder').value('name', 'validation control'),
+ comment='persist validation control'))
+ assert 'checker' in calls and 'mutation-policy' in calls
+ assert transport.writes and transport.begins and len(audits) == 1
+ assert bool(entries) == logging
+ print(f"INTENT_GATE_PASS {case['id']} logging={logging} transaction={transaction} invalid_io=0 invalid_policy=0")
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('comment', [None, '', ' \t\r\n', '\u0085', '\u2003'])
+@pytest.mark.parametrize('logging', [False, True])
+async def test_graph_intent_gate_precedes_begin_and_annotated_child(tmp_path, comment, logging):
+ from teaql.core import RequestIntentError
+ from teaql.runtime import DelegatingMutationPolicyRegistry
+ from teaql.runtime.context import SqlLogOptions
+ context, service, transport, entries = await fixture(tmp_path)
+ context.insert_resource('dataService', service)
+ if not logging:
+ context.with_sql_log_options(SqlLogOptions.disabled())
+ calls, audits = [], []
+ context.with_mutation_policy_registry(DelegatingMutationPolicyRegistry(lambda _: calls.append('policy')))
+ context.check_and_fix_mutation = lambda _: calls.append('checker')
+ context.with_app_audit_event_sink(SimpleNamespace(on_safe_event=lambda _, event: audits.append(event)))
+ child = MutationRequest(InsertCommand.new('Payment').value('name', 'GRAPH-PAYLOAD-CANARY'),
+ comment='valid local child reason')
+ async def work(session):
+ calls.append('work')
+ return await session.transaction.mutate(session.context, child)
+ with pytest.raises(RequestIntentError) as caught:
+ await context.execute_graph_save(work, comment=comment)
+ error = caught.value
+ assert (error.code, error.field, error.request_kind) == ('REQUEST_COMMENT_REQUIRED', 'comment', 'mutation')
+ assert 'supply comment' in str(error) and 'GRAPH-PAYLOAD-CANARY' not in str(error)
+ assert calls == [] and transport.begins == 0
+ assert transport.reads == transport.streams == transport.writes == []
+ assert entries == audits == context.sql_logs() == []
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('tail', ['entity', 'provider', 'sql'])
+@pytest.mark.parametrize('logging', [False, True])
+@pytest.mark.parametrize('transaction', [False, True])
+async def test_explicit_mutation_comment_survives_blank_route_tail_at_real_sinks(
+ tmp_path, tail, logging, transaction):
+ from teaql.core import TraceNode
+ from teaql.runtime.context import SqlLogOptions
+ context, service, transport, entries = await fixture(tmp_path)
+ if not logging:
+ context.with_sql_log_options(SqlLogOptions.disabled())
+ audits = []
+ context.with_app_audit_event_sink(SimpleNamespace(on_safe_event=lambda _, event: audits.append(event)))
+ command = InsertCommand.new('CustomerOrder').value('name', 'explicit reason control')
+ command.trace_chain = [TraceNode(kind=tail, name='CustomerOrder', comment='')]
+ request = MutationRequest(command, comment=' explicit root reason ')
+ tx = await service.begin(context) if transaction else None
+ try:
+ result = await (tx if tx is not None else service).mutate(context, request)
+ if tx is not None:
+ assert audits == [], 'explicit transaction must not publish precommit'
+ await tx.commit(context)
+ except BaseException:
+ if tx is not None:
+ await tx.rollback(context)
+ raise
+ assert result.affected_rows == 1 and len(audits) == 1
+ assert request.comment() == result.metadata.audit_reason == ' explicit root reason '
+ assert result.metadata.statements
+ for statement in result.metadata.statements:
+ assert statement.comment == statement.audit_reason == request.comment()
+ assert [node.comment for node in audits[0].trace_chain] == [request.comment()]
+ assert audits[0].trace_chain[0].entity_id is not None
+ assert transport.writes and transport.reads
+ if logging:
+ assert len(entries) == 2, 'physical INSERT plus authoritative readback'
+ assert all(entry.audit_reason == request.comment() for entry in entries)
+ else:
+ assert entries == context.sql_logs() == []
+ print(f"INTENT_TAIL_PASS {tail} logging={logging} transaction={transaction} committed_audit=1")
+
+
+def assert_facet_paths(entries, origin, routes, comment='inspect native facets'):
+ assert len(entries) == len(routes)
+ for entry, route in zip(entries, routes):
+ assert entry.comment == comment
+ assert entry.purpose == 'verify original facet ancestry'
+ assert entry.execution_outcome == 'success'
+ assert [(node.kind, node.name, node.comment) for node in entry.trace_path] == [
+ ('operation', origin, 'query'), ('request', origin, ''),
+ *[('relation', name, detail) for name, detail in route],
+ ('provider', 'sqlite', ''), ('sql', 'select', '')]
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('relation_name', ['parent_id', 'owner'])
+async def test_nested_facets_keep_original_route_and_nested_materialization(tmp_path, relation_name):
+ context, service, transport, entries = await fixture(tmp_path)
+ for owner, target in [('PaymentAttempt', 'Payment'), ('Payment', 'CustomerOrder')]:
+ service.schema_provider.get_entity(owner).relation(
+ RelationDescriptor(relation_name, target).local('parent_id').foreign('id'))
+ orders = SelectQuery('CustomerOrder').project('id').limit(10).count('order_count')
+ payments = (SelectQuery('Payment').project('id', 'parent_id').limit(10).count('attempt_count')
+ .facet_by('orders', relation_name, orders))
+ query = (SelectQuery('PaymentAttempt').project('id', 'parent_id').limit(10)
+ .facet_by('payments', relation_name, payments))
+ result = await service.query(context, QueryRequest(query, _comment='inspect native facets',
+ _purpose='verify original facet ancestry'))
+ payment_route = [(relation_name, 'PaymentAttempt.' + relation_name)]
+ order_route = [*payment_route, (relation_name, 'Payment.' + relation_name)]
+ assert len(transport.reads) == 5, 'the nested Facet must actually count and materialize'
+ assert_facet_paths(entries, 'PaymentAttempt', [[], [], payment_route, payment_route, order_route])
+ assert [(row['id'], row['attempt_count']) for row in result.facets['payments']] == [(1, 1)]
+ assert [(row['id'], row['order_count']) for row in
+ result.facets['payments'].facet('orders')] == [(1, 1)]
+ assert query.facets[0].query.facets[0].name == 'orders', 'execution changed the caller Facet tree'
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('include_all', [False, True])
+@pytest.mark.parametrize('empty', [False, True])
+@pytest.mark.parametrize('logging', [False, True])
+async def test_matched_only_nested_facets_filter_membership_before_recursion(tmp_path, include_all, empty, logging):
+ context, service, transport, entries = await fixture(tmp_path)
+ for owner, target in [('PaymentAttempt', 'Payment'), ('Payment', 'CustomerOrder')]:
+ service.schema_provider.get_entity(owner).relation(
+ RelationDescriptor('owner', target).local('parent_id').foreign('id'))
+ # This candidate belongs to the same order but has no matching attempt.
+ await service.mutate(context, MutationRequest(InsertCommand.new('Payment')
+ .value('name', 'unused candidate').value('parent_id', 1), comment='seed unmatched facet candidate'))
+ orders = SelectQuery('CustomerOrder').project('id').limit(10).count('payment_count')
+ payments = (SelectQuery('Payment').project('id', 'parent_id').limit(10).count('attempt_count')
+ .facet_by('orders', 'owner', orders, include_all))
+ query = (SelectQuery('PaymentAttempt').project('id', 'parent_id').limit(1)
+ .facet_by('payments', 'owner', payments, include_all))
+ if empty:
+ query.filter(Expr.eq('id', 999))
+ entries.clear()
+ transport.reads.clear()
+ if not logging:
+ context.disable_sql_log()
+ result = await service.query(context, QueryRequest(query, _comment='inspect native facets',
+ _purpose='verify original facet ancestry'))
+ facets = result.facets['payments']
+ expected = [(1, int(not empty)), (2, 0)] if include_all else ([] if empty else [(1, 1)])
+ assert [(row['id'], row['attempt_count']) for row in facets] == expected
+ expected_nested = [(1, 2)] if include_all else ([] if empty else [(1, 1)])
+ assert [(row['id'], row['payment_count']) for row in facets.facet('orders')] == expected_nested
+ assert len(transport.reads) == 5
+ if logging:
+ assert_facet_paths(entries, 'PaymentAttempt', [[], [], [('owner', 'PaymentAttempt.owner')],
+ [('owner', 'PaymentAttempt.owner')],
+ [('owner', 'PaymentAttempt.owner'), ('owner', 'Payment.owner')]])
+ else:
+ assert entries == []
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('logging', [False, True])
+@pytest.mark.parametrize('probes', [False, True])
+async def test_loaded_relation_facets_keep_parent_counts_and_empty_results(tmp_path, logging, probes):
+ context, service, transport, entries = await fixture(tmp_path)
+ service.schema_provider.get_entity('PaymentAttempt').relation(
+ RelationDescriptor('parent_id', 'Payment').local('parent_id').foreign('id'))
+ for entity, name, parent in [('Payment', 'empty payment', 1), ('PaymentAttempt', 'second attempt', 1)]:
+ await service.mutate(context, MutationRequest(
+ InsertCommand.new(entity).value('name', name).value('parent_id', parent), comment='seed facet counts'))
+ entries.clear()
+ transport.reads.clear()
+ if not logging:
+ context.disable_sql_log()
+ attempts = (SelectQuery('PaymentAttempt').project('id', 'parent_id').limit(1)
+ .facet_by('payments', 'parent_id', SelectQuery('Payment').project('id').limit(10).count('attempt_count')))
+ attempts.top_n_probe_parent_threshold(10 if probes else 0)
+ query = SelectQuery('Payment').project('id').limit(10).relation_query('children', attempts)
+ result = await service.query(context, QueryRequest(query, _comment='inspect native facets',
+ _purpose='verify original facet ancestry'))
+ assert [row['id'] for row in result.rows] == [1, 2]
+ first, empty = [row['children'] for row in result.rows]
+ assert [row['id'] for row in first] == [1] and empty == []
+ assert hasattr(first, 'facet') and hasattr(empty, 'facet'), 'loaded lists must retain their Facet results'
+ assert [(row['id'], row['attempt_count']) for row in first.facet('payments')] == [(1, 2), (2, 0)]
+ assert [(row['id'], row['attempt_count']) for row in empty.facet('payments')] == [(1, 0), (2, 0)]
+ assert first.facets is not empty.facets
+ child_route = [('children', 'Payment.children')]
+ facet_route = [*child_route, ('parent_id', 'PaymentAttempt.parent_id')]
+ # One relation load (or two bounded probes), then count/materialize once per parent.
+ routes = [[], *([child_route] * (2 if probes else 1)),
+ child_route, facet_route, child_route, facet_route]
+ assert len(transport.reads) == len(routes)
+ if logging:
+ assert_facet_paths(entries, 'Payment', routes)
+ else:
+ assert entries == []
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('failure', [False, True])
+async def test_nested_facet_intent_privacy_survives_failure_and_next_request(tmp_path, failure):
+ from teaql.core.mutation import UpdateCommand
+ context, service, transport, entries = await fixture(tmp_path)
+ secret = 'PRIVATE-NESTED-FACET'
+ service.schema_provider.get_entity('CustomerOrder').audit_mask_fields(['name'])
+ await service.mutate(context, MutationRequest(UpdateCommand.new('CustomerOrder', 1)
+ .expected_version(1).value('name', secret), comment='seed private facet value'))
+ for owner, target in [('PaymentAttempt', 'Payment'), ('Payment', 'CustomerOrder')]:
+ service.schema_provider.get_entity(owner).relation(
+ RelationDescriptor('owner', target).local('parent_id').foreign('id'))
+ nested = (SelectQuery('Payment').project('id', 'parent_id').limit(10)
+ .facet_by('orders', 'owner', SelectQuery('CustomerOrder').project('id', 'name')
+ .filter(Expr.eq('name', secret)).limit(10)))
+ query = SelectQuery('PaymentAttempt').project('id').limit(10).facet_by('payments', 'owner', nested)
+ request = QueryRequest(query, _comment='inspect ' + secret, _purpose='verify facet privacy')
+ entries.clear()
+ transport.reads.clear()
+ if failure:
+ transport.fail_table = 'customerorder_data'
+ with pytest.raises(TransportError):
+ await service.query(context, request)
+ else:
+ result = await service.query(context, request)
+ assert result.facets['payments'].facet('orders')[0]['name'] == secret
+ assert len(transport.reads) == len(entries) == 5
+ assert all(secret not in repr(entry) for entry in entries)
+ assert all(entry.trace_path[0].name == 'PaymentAttempt' for entry in entries)
+ assert [(node.name, node.comment) for node in entries[-1].trace_path if node.kind == 'relation'] == [
+ ('owner', 'PaymentAttempt.owner'), ('owner', 'Payment.owner')]
+ assert entries[-1].execution_outcome == ('failure' if failure else 'success')
+ assert request.intent.comment == 'inspect ' + secret
+ assert [value.val for value in transport.reads[-1].params] == [secret], 'diagnostic masking changed a database binding'
+ transport.fail_table = None
+ await service.query(context, QueryRequest(SelectQuery('CustomerOrder').project('id').limit(1),
+ _comment='independent ' + secret, _purpose='verify no inherited Facet privacy'))
+ assert entries[-1].comment == 'independent ' + secret
+ assert len(entries[-1].trace_path) == 4
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('missing_key', [False, True])
+@pytest.mark.parametrize('probes', [False, True])
+async def test_loaded_relation_facets_do_not_count_orphans_for_absent_parent_key(tmp_path, missing_key, probes):
+ context, service, transport, entries = await fixture(tmp_path)
+ service.schema_provider.get_entity('Payment').relation(
+ RelationDescriptor('attempts_by_owner', 'PaymentAttempt').many()
+ .local('parent_id').foreign('parent_id'))
+ # A separate, non-null facet key ensures an incorrectly matched orphan
+ # would contribute a visible count, rather than a discarded NULL group.
+ service.schema_provider.get_entity('PaymentAttempt').relation(
+ RelationDescriptor('category', 'CustomerOrder').local('version').foreign('id'))
+ for entity, name in [('Payment', 'parent without owner'), ('PaymentAttempt', 'orphan attempt')]:
+ await service.mutate(context, MutationRequest(
+ InsertCommand.new(entity).value('name', name), comment='seed absent relation key'))
+ attempts = (SelectQuery('PaymentAttempt').project('id', 'parent_id').limit(10)
+ .top_n_probe_parent_threshold(10 if probes else 0)
+ .facet_by('categories', 'category',
+ SelectQuery('CustomerOrder').project('id').limit(10).count('attempt_count')))
+ query = (SelectQuery('Payment').project('id').limit(10).order_asc('id')
+ .relation_query('attempts_by_owner', attempts))
+ if not missing_key:
+ query.project('parent_id')
+ entries.clear()
+ transport.reads.clear()
+ result = await service.query(context, QueryRequest(query, _comment='inspect absent parent keys',
+ _purpose='orphan rows never belong to a missing relation'))
+ assert [row['id'] for row in result.rows] == [1, 2]
+ if missing_key:
+ assert all('parent_id' not in row for row in result.rows)
+ else:
+ assert [row['parent_id'] for row in result.rows] == [1, None]
+ for index, row in enumerate(result.rows):
+ related = row['attempts_by_owner']
+ expected_count = int(index == 0 and not missing_key)
+ assert [child['id'] for child in related] == ([1] if expected_count else [])
+ assert [(facet['id'], facet['attempt_count']) for facet in related.facet('categories')] == [
+ (1, expected_count)]
+ assert len(transport.reads) == (5 if missing_key else 6)
+ assert sum('COUNT(' in statement.sql.upper() for statement in transport.reads) == 2
+ assert all(entry.trace_path[0].name == 'Payment' for entry in entries)
+ assert all(entry.execution_outcome == 'success' for entry in entries)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('with_context', [False, True])
+async def test_successful_readback_retains_ordered_physical_metadata(tmp_path, with_context):
+ from teaql.data_service import DataServiceOperation
+ context, service, transport, entries = await fixture(tmp_path)
+ request = MutationRequest(
+ InsertCommand.new('Payment').value('name', 'new payment').value('parent_id', 1),
+ comment='persist payment and verify its authoritative snapshot')
+ result = await service.mutate(context if with_context else None, request)
+ assert result.affected_rows == result.metadata.affected_rows == 1
+ assert result.metadata.operation == DataServiceOperation.Insert
+ assert result.persisted_record['name'] == 'new payment'
+ assert len(result.metadata.statements) == 2
+ write, read = result.metadata.statements
+ assert write.operation == DataServiceOperation.Insert
+ assert read.operation == DataServiceOperation.Query
+ assert write.statements == read.statements == ()
+ assert write.affected_rows == 1 and read.affected_rows is None
+ assert read.result_count == 1 and read.execution_outcome == 'success'
+ assert read.comment == write.comment == request.comment()
+ assert read.audit_reason == write.audit_reason == request.comment()
+ assert read.mutation_lineage == write.mutation_lineage
+ assert read.purpose == 'verify the persisted mutation result'
+ assert [node.kind for node in read.trace_chain] == ['operation', 'request', 'provider', 'sql']
+ assert [node.name for node in read.trace_chain] == ['Payment', 'Payment', 'sqlite', 'select']
+ assert read.trace_chain[0].comment == 'query'
+ assert len(entries) == (2 if with_context else 0)
+ if with_context:
+ assert [entry.execution_outcome for entry in entries] == ['success', 'success']
+ assert len(context.sql_logs()) == 2
+
+
+@pytest.mark.asyncio
+async def test_batch_keeps_each_write_readback_pair(tmp_path):
+ from teaql.data_service import DataServiceOperation
+ context, service, transport, entries = await fixture(tmp_path)
+ result = await service.mutate(context, MutationRequest.Batch([
+ InsertCommand.new('Payment').value('name', 'batch payment').value('parent_id', 1),
+ InsertCommand.new('PaymentAttempt').value('name', 'batch attempt').value('parent_id', 1),
+ ], comment='persist atomic payment batch'))
+ assert result.affected_rows == result.metadata.affected_rows == 2
+ assert result.metadata.operation == DataServiceOperation.Batch
+ assert len(result.metadata.statements) == 2
+ for item in result.metadata.statements:
+ assert item.operation == DataServiceOperation.Insert
+ assert [part.operation for part in item.statements] == [
+ DataServiceOperation.Insert, DataServiceOperation.Query]
+ assert len(entries) == 4
+
+
+async def ordered_batch_fixture(tmp_path, monkeypatch, logging):
+ """Observe real native leaf execution and driver commit, never supply SQL traces."""
+ from copy import deepcopy
+ from teaql.runtime.context import SqlLogOptions
+ monkeypatch.delenv('TEAQL_ALLOW_SENSITIVE_PLAINTEXT_LOGS', raising=False)
+ context, service, transport, entries = await fixture(tmp_path)
+ for entity in ('Payment', 'PaymentAttempt'):
+ context.entity(entity).audit_mask_fields(['name'])
+ if not logging:
+ context.with_sql_log_options(SqlLogOptions.disabled())
+ observed = SimpleNamespace(commands=[], physical=[], audits=[], lifecycle=[])
+ original_mutate = SqlDataServiceExecutor._mutate
+ original_record = type(context)._record_metadata_log
+ original_begin = transport.begin_sql
+
+ async def mutate(executor, invocation, request, privacy=None):
+ if not isinstance(request._data, list):
+ observed.commands.append(deepcopy(request))
+ return await original_mutate(executor, invocation, request, privacy)
+
+ def record(invocation, metadata, **kwargs):
+ observed.physical.append(deepcopy(metadata))
+ return original_record(invocation, metadata, **kwargs)
+
+ async def begin():
+ transaction = await original_begin()
+ observed.lifecycle.append('begin')
+ commit, rollback = transaction.commit_sql, transaction.rollback_sql
+
+ async def committed():
+ assert observed.audits == [], 'no committed audit before driver commit'
+ await commit()
+ observed.lifecycle.append('commit')
+
+ async def rolled_back():
+ await rollback()
+ observed.lifecycle.append('rollback')
+
+ transaction.commit_sql, transaction.rollback_sql = committed, rolled_back
+ return transaction
+
+ def audit(invocation, event):
+ assert observed.lifecycle[-1] == 'commit'
+ observed.audits.append(event)
+
+ monkeypatch.setattr(SqlDataServiceExecutor, '_mutate', mutate)
+ monkeypatch.setattr(type(context), '_record_metadata_log', record)
+ monkeypatch.setattr(transport, 'begin_sql', begin)
+ context.with_app_audit_event_sink(SimpleNamespace(on_safe_event=audit))
+ return context, service, transport, entries, observed
+
+
+def native_batch_lineage(nodes):
+ return [(node.kind, node.name or node.entity_type, node.entity_id, node.comment) for node in nodes]
+
+
+def native_batch_identity(request):
+ target = getattr(request._data, 'id', None)
+ if target is None:
+ target = request._data.values['id']
+ return request._data.entity, target.val
+
+
+def native_batch_statements(metadata):
+ if not metadata.statements:
+ return [metadata]
+ return [leaf for child in metadata.statements for leaf in native_batch_statements(child)]
+
+
+def native_batch_item(entity, identity, value, reason, update=False, old=None):
+ from teaql.core.entity import EntityKey
+ from teaql.core.mutation import UpdateCommand
+ from teaql.core.trace_scope import TraceScope
+ from teaql.core.value import Value
+ command = (UpdateCommand.new(entity, identity).expected_version(1).value('name', value)
+ if update else InsertCommand.new(entity).value('id', identity).value('version', 1)
+ .value('name', value).value('parent_id', 1))
+ if update:
+ command.old_values = {'name': Value.from_any(old)} if old is not None else {}
+ scope = TraceScope.root(object(), EntityKey(entity, identity), reason)
+ return MutationRequest(command, comment=reason).with_mutation_lineage(scope.recover())
+
+
+def nested_native_batch(items, comment, nested):
+ return MutationRequest.Batch([
+ MutationRequest.Batch(items[:2], comment='inner first container'),
+ MutationRequest.Batch(items[2:], comment='inner second container'),
+ ] if nested else items, comment=comment)
+
+
+def native_batch_witness(observed, logging, nested, phase):
+ print('NATIVE_BATCH_OBSERVED ' + json.dumps({
+ 'logging': logging, 'nested': nested, 'phase': phase, 'lifecycle': observed.lifecycle,
+ 'commands': [{'entity': item._data.entity, 'id': native_batch_identity(item)[1], 'comment': item.comment(),
+ 'lineage': native_batch_lineage(item.mutation_lineage)} for item in observed.commands],
+ 'physical': [{'sql': item.parameterized_sql, 'parameters': [value.val for value in item.parameters],
+ 'affected': item.affected_rows, 'count': item.result_count,
+ 'outcome': item.execution_outcome, 'lineage': native_batch_lineage(item.mutation_lineage)}
+ for item in observed.physical],
+ 'committedAudit': [{'entity': item.entity, 'id': getattr(item.entity_id, 'val', item.entity_id),
+ 'lineage': native_batch_lineage(item.trace_chain)} for item in observed.audits],
+ }))
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('logging', [False, True])
+async def test_blank_local_reasons_inherit_at_command_sql_and_committed_audit(
+ tmp_path, monkeypatch, logging):
+ """TC-MUT-11: real graph scopes and SQLite execution, not supplied SQL frames."""
+ from teaql.core import RequestIntentError
+ from teaql.core.entity import EntityKey
+ context, service, transport, entries, seen = await ordered_batch_fixture(tmp_path, monkeypatch, logging)
+ context.insert_resource('dataService', service)
+ blanks = [None, '', ' \t\r\n', '\u0085', '\u00a0', '\u2003']
+ for blank in blanks:
+ with pytest.raises(RequestIntentError) as caught:
+ await service.mutate(context, MutationRequest(
+ InsertCommand.new('PaymentAttempt').value('name', 'rejected public write'), comment=blank))
+ error = caught.value
+ assert (error.code, error.field, error.request_kind) == ('REQUEST_COMMENT_REQUIRED', 'comment', 'mutation')
+ assert seen.commands == seen.physical == seen.audits == seen.lifecycle == entries == []
+ assert transport.begins == 0 and transport.reads == transport.writes == []
+ expected_payment = [('auditReason', 'CustomerOrder', 100, 'submit order'),
+ ('auditReason', 'Payment', 201, 'authorize payment')]
+ expected_shipment = [('auditReason', 'CustomerOrder', 100, 'submit order'),
+ ('auditReason', 'Shipment', 301, 'prepare shipment')]
+ expected = [expected_payment] * len(blanks) + [expected_shipment]
+ identities = [('PaymentAttempt', 400 + index) for index in range(len(blanks))] + [('Shipment', 301)]
+
+ async def work(graph):
+ root = graph.scope(EntityKey('CustomerOrder', 100))
+ payment = graph.scope(EntityKey('Payment', 201), parent=root, local_reason='authorize payment')
+ shipment = graph.scope(EntityKey('Shipment', 301), parent=root, local_reason='prepare shipment')
+ for index, blank in enumerate(blanks):
+ key = EntityKey('PaymentAttempt', 400 + index)
+ inherited = graph.scope(key, parent=payment, local_reason=blank)
+ assert inherited is payment, 'blank local reason must not add an entity reason'
+ command = (InsertCommand.new(key.entity).value('id', key.id).value('version', 1)
+ .value('name', 'native attempt').value('parent_id', 201))
+ result = await graph.transaction.mutate(graph.context, graph.request(command, inherited))
+ assert result.affected_rows == 1 and result.persisted_record is not None
+ assert seen.audits == [], 'graph audit escaped before commit'
+ command = (InsertCommand.new('Shipment').value('id', 301).value('version', 1)
+ .value('name', 'native shipment').value('parent_id', 100))
+ await graph.transaction.mutate(graph.context, graph.request(command, shipment))
+ assert seen.audits == [], 'sibling audit escaped before commit'
+
+ await context.execute_graph_save(work, comment='submit order')
+ assert seen.lifecycle == ['begin', 'commit']
+ assert len(seen.commands) == len(seen.audits) == 7 and len(seen.physical) == 14
+ assert [native_batch_identity(request) for request in seen.commands] == identities
+ assert [native_batch_lineage(request.mutation_lineage) for request in seen.commands] == expected
+ assert [(event.entity, event.entity_id.val) for event in seen.audits] == identities
+ assert [native_batch_lineage(event.trace_chain) for event in seen.audits] == expected
+ for index, ((entity, identity), chain) in enumerate(zip(identities, expected)):
+ write, read = seen.physical[index * 2:index * 2 + 2]
+ assert write.affected_rows == 1 and read.result_count == 1
+ assert [value.val for value in read.parameters] == [identity]
+ for statement, operation in [(write, 'insert'), (read, 'select')]:
+ assert native_batch_lineage(statement.mutation_lineage) == chain
+ assert statement.comment == statement.audit_reason == 'submit order'
+ assert statement.trace_chain[-1].name == operation
+ assert statement.trace_chain[-2].name == 'sqlite'
+ assert len(entries) == (14 if logging else 0)
+ assert all(native_batch_lineage(entry.mutation_lineage) == expected[index // 2]
+ for index, entry in enumerate(entries))
+ async with aiosqlite.connect(transport.db_path) as database:
+ for entity, identity in identities:
+ row = await (await database.execute(
+ f'SELECT id, version, parent_id FROM {entity.lower()}_data WHERE id=?', (identity,))).fetchone()
+ assert row == (identity, 1, 201 if entity == 'PaymentAttempt' else 100)
+ native_batch_witness(seen, logging, False, 'blank-local-reason')
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('logging', [False, True])
+@pytest.mark.parametrize('local_reason', ['\u001c', '\u001d', '\u001e', '\u001f'])
+async def test_non_contract_whitespace_local_reason_is_preserved_at_real_sinks(
+ tmp_path, monkeypatch, logging, local_reason):
+ """Python strip() must not silently discard reasons accepted by the public contract."""
+ from teaql.core.entity import EntityKey
+ context, service, _, entries, seen = await ordered_batch_fixture(tmp_path, monkeypatch, logging)
+ context.insert_resource('dataService', service)
+
+ async def work(graph):
+ root = graph.scope(EntityKey('CustomerOrder', 100))
+ key = EntityKey('PaymentAttempt', 400)
+ scope = graph.scope(key, parent=root, local_reason=local_reason)
+ assert scope is not root, 'not Unicode White_Space; this is a valid local reason'
+ command = (InsertCommand.new(key.entity).value('id', key.id).value('version', 1)
+ .value('name', 'native separator control').value('parent_id', 100))
+ await graph.transaction.mutate(graph.context, graph.request(command, scope))
+ assert seen.audits == []
+
+ await context.execute_graph_save(work, comment='submit order')
+ expected = [('auditReason', 'CustomerOrder', 100, 'submit order'),
+ ('auditReason', 'PaymentAttempt', 400, local_reason)]
+ assert seen.lifecycle == ['begin', 'commit']
+ assert len(seen.commands) == len(seen.audits) == 1 and len(seen.physical) == 2
+ assert native_batch_lineage(seen.commands[0].mutation_lineage) == expected
+ assert all(native_batch_lineage(statement.mutation_lineage) == expected for statement in seen.physical)
+ assert native_batch_lineage(seen.audits[0].trace_chain) == expected
+ assert (seen.audits[0].entity, seen.audits[0].entity_id.val) == ('PaymentAttempt', 400)
+ assert len(entries) == (2 if logging else 0)
+ assert all(native_batch_lineage(entry.mutation_lineage) == expected for entry in entries)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('logging', [False, True])
+@pytest.mark.parametrize('nested', [False, True])
+@pytest.mark.parametrize('same_root', [False, True])
+async def test_native_batch_ordered_typed_commands_sql_and_committed_audit(
+ tmp_path, monkeypatch, logging, nested, same_root):
+ from copy import deepcopy
+ from teaql.core.mutation import UpdateCommand
+ from teaql.data_service import DataServiceOperation
+ context, service, transport, entries, seen = await ordered_batch_fixture(tmp_path, monkeypatch, logging)
+ identities = [('Payment', 3), ('Payment', 2), ('PaymentAttempt', 3)]
+ previous = ['FIRST-PRIVATE', 'SECOND-PRIVATE', 'FUTURE-PRIVATE']
+ for update in (False, True):
+ values = ['UPDATED-' + value for value in previous] if update else previous
+ reason = 'batch ' + ' '.join(values) + ' public marker'
+ local = [reason] * 4 if same_root else ['alpha responsibility', 'beta responsibility',
+ 'gamma responsibility', 'zero responsibility']
+ items = [native_batch_item(entity, identity, value, local[index], update, previous[index])
+ for index, ((entity, identity), value) in enumerate(zip(identities, values))]
+ items.append(native_batch_item('Payment', 999, 'unmatched private name', local[3], True))
+ before = [(deepcopy(item._data), item.comment(), item.mutation_lineage) for item in items]
+ seen.commands.clear(); seen.physical.clear(); seen.audits.clear(); seen.lifecycle.clear()
+ entries.clear(); transport.reads.clear(); transport.writes.clear()
+ result = await service.mutate(context, nested_native_batch(items, reason, nested))
+ native_batch_witness(seen, logging, nested, 'update' if update else 'insert')
+ assert result.affected_rows == result.metadata.affected_rows == 3
+ assert result.metadata.operation == DataServiceOperation.Batch
+ assert result.generated_values == {} and result.persisted_record is None
+ assert seen.lifecycle == ['begin', 'commit']
+ assert len(seen.commands) == 4 and len(seen.physical) == 7 and len(seen.audits) == 3
+ assert [native_batch_identity(item) for item in seen.commands] == identities + [('Payment', 999)]
+ returned = native_batch_statements(result.metadata)
+ assert [(item.operation, item.parameters, item.mutation_lineage, item.affected_rows, item.result_count)
+ for item in returned] == [
+ (item.operation, item.parameters, item.mutation_lineage, item.affected_rows, item.result_count)
+ for item in seen.physical]
+ assert [(event.entity, event.entity_id.val) for event in seen.audits] == identities
+ assert [event.kind.value for event in seen.audits] == ['updated' if update else 'created'] * 3
+ for index, ((entity, identity), value) in enumerate(zip(identities, values)):
+ expected = ([('auditReason', entity, identity, reason)] if same_root else
+ [('auditReason', entity, None, reason), ('auditReason', entity, identity, local[index])])
+ assert seen.commands[index].comment() == reason
+ assert seen.commands[index]._data.values['name'].val == value
+ assert native_batch_lineage(seen.commands[index].mutation_lineage) == expected
+ write, read = seen.physical[index * 2:index * 2 + 2]
+ assert write.operation == (DataServiceOperation.Update if update else DataServiceOperation.Insert)
+ assert write.affected_rows == 1 and read.operation == DataServiceOperation.Query and read.result_count == 1
+ assert value in [param.val for param in write.parameters]
+ assert [param.val for param in read.parameters] == [identity]
+ for statement in (write, read):
+ assert native_batch_lineage(statement.mutation_lineage) == expected
+ assert statement.comment == statement.audit_reason == reason
+ assert [node.kind for node in statement.trace_chain] == [
+ 'operation', 'request' if statement is read else 'entity', 'provider', 'sql']
+ assert [node.name for node in statement.trace_chain] == [
+ entity, entity, 'sqlite', 'select' if statement is read else 'update' if update else 'insert']
+ safe_reason = 'batch [REDACTED] [REDACTED] [REDACTED] public marker'
+ safe_expected = [(kind, name, key, safe_reason if text == reason else text)
+ for kind, name, key, text in expected]
+ assert native_batch_lineage(seen.audits[index].trace_chain) == safe_expected
+ assert seen.physical[-1].affected_rows == 0 and seen.physical[-1].statements == ()
+ assert len(entries) == (7 if logging else 0)
+ assert all(value not in repr(entries) + repr(seen.audits) for value in values)
+ assert [(item._data, item.comment(), item.mutation_lineage) for item in items] == before
+ # Compare actual transport bindings only for modeled business statements;
+ # explicit INSERT IDs also execute genuine teaql_id_space maintenance.
+ writes = [item for item in transport.writes if 'teaql_id_space' not in item.sql]
+ reads = [item for item in transport.reads if 'teaql_id_space' not in item.sql]
+ assert [item.params for item in writes] == [item.parameters for item in seen.physical[::2]]
+ assert [item.params for item in reads] == [item.parameters for item in seen.physical[1:6:2]]
+ async with aiosqlite.connect(transport.db_path) as db:
+ for (entity, identity), value in zip(identities, values):
+ row = await (await db.execute(f'SELECT name, version FROM {entity.lower()}_data WHERE id=?',
+ (identity,))).fetchone()
+ assert row == (value, 2 if update else 1)
+ seen.commands.clear(); seen.physical.clear(); seen.audits.clear(); seen.lifecycle.clear(); entries.clear()
+ independent = 'independent ' + ' '.join(previous + values)
+ await service.mutate(context, MutationRequest(UpdateCommand.new('CustomerOrder', 1).expected_version(1)
+ .value('name', 'ordinary next root'), comment=independent))
+ assert len(seen.audits) == 1 and len(seen.physical) == 2
+ assert native_batch_lineage(seen.audits[0].trace_chain) == [('auditReason', 'CustomerOrder', 1, independent)]
+ if logging:
+ assert all(entry.audit_reason == independent for entry in entries)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('logging', [False, True])
+@pytest.mark.parametrize('nested', [False, True])
+async def test_native_batch_failed_write_retains_item_lineage_but_no_committed_audit(
+ tmp_path, monkeypatch, logging, nested):
+ context, service, transport, entries, seen = await ordered_batch_fixture(tmp_path, monkeypatch, logging)
+ items = [native_batch_item('Payment', 3, 'FIRST-PRIVATE', 'alpha responsibility'),
+ native_batch_item('Payment', 2, 'SECOND-PRIVATE', 'beta responsibility'),
+ native_batch_item('PaymentAttempt', 3, 'FUTURE-PRIVATE', 'gamma responsibility'),
+ native_batch_item('Payment', 3, 'duplicate private value', 'failed responsibility')]
+ reason = 'batch FIRST-PRIVATE SECOND-PRIVATE FUTURE-PRIVATE public marker'
+ with pytest.raises(TransportError, match='UNIQUE constraint failed'):
+ await service.mutate(context, nested_native_batch(items, reason, nested))
+ native_batch_witness(seen, logging, nested, 'rollback')
+ assert seen.lifecycle == ['begin', 'rollback'] and seen.audits == []
+ assert len(seen.commands) == 4 and len(seen.physical) == 7
+ assert [native_batch_identity(item) for item in seen.commands] == [
+ ('Payment', 3), ('Payment', 2), ('PaymentAttempt', 3), ('Payment', 3)]
+ assert [item.execution_outcome for item in seen.physical] == ['success'] * 6 + ['failure']
+ assert native_batch_lineage(seen.physical[-1].mutation_lineage) == [
+ ('auditReason', 'Payment', None, reason), ('auditReason', 'Payment', 3, 'failed responsibility')]
+ assert len(entries) == (7 if logging else 0)
+ assert all(value not in repr(entries) for value in ('FIRST-PRIVATE', 'SECOND-PRIVATE', 'FUTURE-PRIVATE'))
+ async with aiosqlite.connect(transport.db_path) as db:
+ for table in ('payment_data', 'paymentattempt_data'):
+ assert (await (await db.execute(f'SELECT COUNT(*) FROM {table} WHERE id IN (2,3)')).fetchone())[0] == 0
+ seen.commands.clear(); seen.physical.clear(); seen.audits.clear(); seen.lifecycle.clear(); entries.clear()
+ independent = 'independent FIRST-PRIVATE SECOND-PRIVATE FUTURE-PRIVATE'
+ await service.mutate(context, MutationRequest(InsertCommand.new('Payment').value('name', 'independent row')
+ .value('parent_id', 1), comment=independent))
+ assert len(seen.audits) == 1 and seen.lifecycle == ['begin', 'commit']
+ assert seen.audits[0].trace_chain[0].comment == independent
+ if logging:
+ assert all(entry.audit_reason == independent for entry in entries)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('mode', ['no-match', 'hard-delete'])
+async def test_no_synthetic_readback_when_none_executed(tmp_path, mode):
+ from teaql.core.mutation import UpdateCommand, DeleteCommand
+ context, service, transport, entries = await fixture(tmp_path)
+ command = (UpdateCommand.new('Payment', 99999).expected_version(1).value('name', 'missing')
+ if mode == 'no-match' else DeleteCommand.new('Payment', 1).hard_delete())
+ result = await service.mutate(context, MutationRequest(command, comment='verify physical work only'))
+ assert result.affected_rows == (0 if mode == 'no-match' else 1)
+ assert result.persisted_record is None and result.metadata.statements == ()
+ assert len(entries) == 1
+ assert transport.reads == []
+
+
+@pytest.mark.asyncio
+async def test_query_log_switch_does_not_remove_physical_readback_result(tmp_path):
+ context, service, transport, entries = await fixture(tmp_path)
+ context.enable_mutation_sql_log()
+ result = await service.mutate(context, MutationRequest(
+ InsertCommand.new('Payment').value('name', 'log switch payment').value('parent_id', 1),
+ comment='persist despite query log switch'))
+ assert len(result.metadata.statements) == 2
+ assert len(entries) == 1 and not entries[0].operation.is_select()
+ assert len(context.sql_logs()) == 1
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('graph', [False, True])
+@pytest.mark.parametrize('failure', [None, 'read-error', 'cancelled'])
+async def test_future_child_values_cannot_escape_root_intent(tmp_path, graph, failure):
+ import asyncio
+ from teaql.core.mutation import UpdateCommand
+ from teaql.core.value import Value
+ context, service, transport, entries = await fixture(tmp_path)
+ context.entity('Payment').audit_mask_fields(['name'])
+ service.schema_provider.get_entity('Payment').audit_mask_fields(['name'])
+ audits = []
+ context.with_app_audit_event_sink(SimpleNamespace(on_safe_event=lambda _, event: audits.append(event)))
+ secret, old = 'PRIVATE-FUTURE-CHILD-VALUE', 'PRIVATE-OLD-CHILD-VALUE'
+ comment = f'authorize graph containing {secret} replacing {old}'
+ root = InsertCommand.new('CustomerOrder').value('name', 'public root')
+ child = UpdateCommand.new('Payment', 1).expected_version(1).value('name', secret)
+ child.old_values = {'name': Value.from_any(old)}
+ async with aiosqlite.connect(transport.db_path) as database:
+ await database.execute('UPDATE payment_data SET name=? WHERE id=1', (old,))
+ await database.commit()
+ if failure:
+ transport.fail_table = 'payment_data'
+ if failure == 'cancelled':
+ transport.failure = asyncio.CancelledError()
+ async def execute():
+ if graph:
+ context.insert_resource('dataService', service)
+ async def work(session):
+ for command in (root, child):
+ session.context.preflight_mutation(command)
+ for command in (root, child):
+ await session.transaction.mutate(session.context, MutationRequest(command, comment=comment))
+ await context.execute_graph_save(work, comment=comment)
+ else:
+ await service.mutate(context, MutationRequest.Batch([root, child], comment=comment))
+ if failure:
+ with pytest.raises(type(transport.failure)) as caught:
+ await execute()
+ assert caught.value is transport.failure
+ else:
+ await execute()
+ assert len(entries) == 4 and len(audits) == (0 if failure else 2)
+ assert entries[-1].execution_outcome == (
+ 'cancelled' if failure == 'cancelled' else 'failure' if failure else 'success')
+ for value in (secret, old):
+ assert value not in repr(entries)
+ assert value not in repr(context.sql_logs())
+ assert value not in repr(audits)
+ async with aiosqlite.connect(transport.db_path) as database:
+ row = await (await database.execute('SELECT name, version FROM payment_data WHERE id=1')).fetchone()
+ assert row == ((old, 1) if failure else (secret, 2))
+ await service.query(context, QueryRequest(SelectQuery('CustomerOrder').limit(1),
+ _comment=f'independent {secret} {old}', _purpose='verify privacy does not outlive its invocation'))
+ assert secret in entries[-1].comment and old in entries[-1].comment
+
+
+@pytest.mark.asyncio
+async def test_concurrent_native_batches_own_distinct_privacy(tmp_path):
+ import asyncio
+ context, service, transport, entries = await fixture(tmp_path)
+ context.configure_audit_policy('Payment', ['name'])
+ secrets = ['FIRST-PRIVATE-PAYMENT', 'SECOND-PRIVATE-PAYMENT']
+ groups = ['alpha', 'beta']
+ async def save(index):
+ own, other = secrets[index], secrets[1 - index]
+ return await service.mutate(context, MutationRequest.Batch([
+ InsertCommand.new('CustomerOrder').value('name', f'public root {index}'),
+ InsertCommand.new('Payment').value('name', own).value('parent_id', 1),
+ ], comment=f'group-{groups[index]} protects {own} unrelated {other}'))
+ results = await asyncio.gather(save(0), save(1))
+ assert all(result.affected_rows == 2 for result in results)
+ assert len(entries) == 8
+ for index in range(2):
+ own_logs = [entry for entry in entries if entry.comment.startswith(f'group-{groups[index]}')]
+ assert len(own_logs) == 4
+ assert all(secrets[index] not in entry.comment and secrets[1-index] in entry.comment for entry in own_logs)
+
+
+@pytest.mark.asyncio
+async def test_inherited_debug_opt_in_keeps_credentials_hidden_and_can_reproject(tmp_path, monkeypatch):
+ from teaql.runtime.log_privacy import PLAINTEXT_ENV, PLAINTEXT_ACK, sql_log_projection
+ context, service, transport, entries = await fixture(tmp_path)
+ descriptor = context.entity('Payment')
+ descriptor.audit_mask_fields(['name'])
+ descriptor.property(PropertyDescriptor('password_hash', DataType.Text))
+ async with aiosqlite.connect(transport.db_path) as database:
+ await database.execute('ALTER TABLE payment_data ADD COLUMN password_hash TEXT')
+ await database.commit()
+ audits = []
+ context.with_app_audit_event_sink(SimpleNamespace(on_safe_event=lambda _, event: audits.append(event)))
+ business, credential = 'DEBUG-BUSINESS-CANARY', 'CREDENTIAL-NEVER-PUBLIC'
+ monkeypatch.setenv(PLAINTEXT_ENV, PLAINTEXT_ACK)
+ await service.mutate(context, MutationRequest.Batch([
+ InsertCommand.new('CustomerOrder').value('name', 'root'),
+ InsertCommand.new('Payment').value('name', business).value('password_hash', credential).value('parent_id', 1),
+ ], comment=f'authorize {business} with {credential}'))
+ assert len(entries) == 4 and len(audits) == 2
+ assert all(business in entry.comment and 'EXPLICIT OPT-IN' in entry.debug_sql for entry in entries)
+ assert credential not in repr(entries) + repr(audits)
+ monkeypatch.delenv(PLAINTEXT_ENV)
+ for entry in entries:
+ safe = sql_log_projection(entry)
+ assert business not in repr(safe) and credential not in repr(safe)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('logging', [False, True])
+async def test_two_live_query_graphs_keep_independent_intent(tmp_path, monkeypatch, logging):
+ context, service, transport, entries = await fixture(tmp_path)
+ if not logging:
+ context.disable_select_sql_log()
+ entered, release = asyncio.Event(), asyncio.Event()
+ roots = 0
+ metadata = []
+ fetch = transport.fetch_all_sql
+ record = context._record_metadata_log
+
+ async def pause_real_roots(compiled):
+ nonlocal roots
+ rows = await fetch(compiled)
+ if re.search(r'\bFROM\s+customerorder_data\b', compiled.sql.replace('"', ''), re.IGNORECASE):
+ roots += 1
+ if roots == 2:
+ entered.set()
+ await release.wait()
+ return rows
+
+ def observe(actual, **kwargs):
+ # Observe the runtime's physical-statement metadata unchanged, including
+ # when the diagnostic sink is disabled. Do not supply expected frames.
+ metadata.append(actual)
+ record(actual, **kwargs)
+
+ monkeypatch.setattr(transport, 'fetch_all_sql', pause_real_roots)
+ monkeypatch.setattr(context, '_record_metadata_log', observe)
+ requests = [QueryRequest(three_levels(), _comment=f'load {label} graph',
+ _purpose=f'render {label} graph') for label in ('alpha', 'beta')]
+ # Native test-only inspection: query frames must not become ambient Context
+ # fields/resources. Keep legitimate SQL-log appends observable separately.
+ context_storage_before = {key: id(value) for key, value in vars(context).items()}
+ resource_storage_before = {key: id(value) for key, value in context._resources.items()}
+
+ def assert_context_unchanged():
+ assert {key: id(value) for key, value in vars(context).items()} == context_storage_before, \
+ 'shared Context fields changed during independent queries'
+ assert {key: id(value) for key, value in context._resources.items()} == resource_storage_before, \
+ 'shared Context resources changed during independent queries'
+ assert context._graph_save_guard.get() is None
+ assert context._graph_session is None
+
+ pending = [asyncio.create_task(service.query(context, request)) for request in requests]
+ ready = asyncio.create_task(entered.wait())
+ try:
+ done, _ = await asyncio.wait([ready, *pending], timeout=5,
+ return_when=asyncio.FIRST_COMPLETED)
+ assert ready in done, 'both root SQL calls must overlap before either query finishes'
+ assert roots == 2 and not any(task.done() for task in pending)
+ assert metadata == entries == []
+ assert [request.intent.comment for request in requests] == ['load alpha graph', 'load beta graph']
+ assert_context_unchanged()
+ release.set()
+ results = await asyncio.wait_for(asyncio.gather(*pending), timeout=5)
+ assert all(result.rows[0]['children'][0]['children'][0]['children'][0]['id'] == 1
+ for result in results)
+ assert len(transport.reads) == len(metadata) == 8
+ assert len(entries) == (8 if logging else 0)
+ assert_context_unchanged()
+ for observed, path_attr in [(metadata, 'trace_chain'), (entries, 'trace_path')]:
+ for label in ('alpha', 'beta'):
+ own = [entry for entry in observed if entry.comment == f'load {label} graph']
+ assert len(own) == (4 if observed is metadata or logging else 0)
+ for depth, entry in enumerate(own):
+ assert entry.purpose == f'render {label} graph'
+ assert entry.execution_outcome == 'success'
+ nodes = getattr(entry, path_attr)
+ assert [(node.kind, node.name) for node in nodes] == [
+ ('operation', 'CustomerOrder'), ('request', 'CustomerOrder'),
+ *[('relation', 'children')] * depth,
+ ('provider', 'sqlite'), ('sql', 'select')]
+ assert [node.comment for node in nodes[2:-2]] == [
+ 'CustomerOrder.children', 'Payment.children', 'PaymentAttempt.children'][:depth]
+ finally:
+ release.set()
+ for task in [ready, *pending]:
+ if not task.done():
+ task.cancel()
+ await asyncio.gather(ready, *pending, return_exceptions=True)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('failure', [False, True])
+async def test_three_real_relation_levels_keep_origin_and_qualified_frames(tmp_path, failure):
+ context, service, transport, entries = await fixture(tmp_path)
+ request = QueryRequest(three_levels(), _comment='load payment execution graph',
+ _purpose='observe business trace without injected frames')
+ if failure:
+ transport.fail_table = 'shipment_data'
+ with pytest.raises(TransportError) as caught:
+ await service.query(context, request)
+ assert caught.value.error is transport.failure
+ else:
+ result = await service.query(context, request)
+ assert result.rows[0]['children'][0]['children'][0]['children'][0]['id'] == 1
+ assert len(transport.reads) == len(entries) == 4
+ qualified = ['CustomerOrder.children', 'Payment.children', 'PaymentAttempt.children']
+ for depth, entry in enumerate(entries):
+ assert entry.comment == request.intent.comment
+ assert entry.purpose == request.intent.purpose
+ nodes = entry.trace_path
+ assert [node.kind for node in nodes] == ['operation', 'request'] + ['relation'] * depth + ['provider', 'sql']
+ assert [node.name for node in nodes[:2]] == ['CustomerOrder', 'CustomerOrder']
+ assert nodes[0].comment == 'query'
+ assert [node.name for node in nodes[2:-2]] == ['children'] * depth
+ assert [node.comment for node in nodes[2:-2]] == qualified[:depth]
+ assert [(node.name, node.comment) for node in nodes[-2:]] == [('sqlite', ''), ('select', '')]
+ assert entry.execution_outcome == ('failure' if failure and depth == 3 else 'success')
+
+
+@pytest.mark.asyncio
+async def test_successful_write_and_failed_readback_have_separate_canonical_paths(tmp_path):
+ context, service, transport, entries = await fixture(tmp_path)
+ transport.fail_table = 'payment_data'
+ command = InsertCommand.new('Payment').value('name', 'pending payment').value('parent_id', 1)
+ request = MutationRequest(command, comment='create payment before verifying authoritative row')
+ with pytest.raises(RuntimeError) as caught:
+ await service.mutate(context, request)
+ assert caught.value is transport.failure
+ assert len(entries) == 2
+ write, read = entries
+ assert write.execution_outcome == 'success' and read.execution_outcome == 'failure'
+ assert write.audit_reason == read.audit_reason == request.comment()
+ for entry, operation in ((write, 'insert'), (read, 'select')):
+ assert [node.kind for node in entry.trace_path] == [
+ 'operation', 'request' if operation == 'select' else 'entity', 'provider', 'sql']
+ assert entry.trace_path[0].name == 'Payment'
+ assert entry.trace_path[-1].name == operation
+ async with aiosqlite.connect(transport.db_path) as database:
+ cursor = await database.execute('SELECT COUNT(*) FROM payment_data')
+ assert (await cursor.fetchone())[0] == 1 # failed graph transaction rolled back
+
+
+@pytest.mark.asyncio
+async def test_query_trace_source_does_not_override_request_owned_intent(tmp_path):
+ context, service, transport, entries = await fixture(tmp_path)
+ from teaql.core.mutation import TraceNode
+ request = QueryRequest(SelectQuery('CustomerOrder').filter(Expr.eq('id', 1)).limit(1),
+ trace_chain=[TraceNode(kind='comment', name='CustomerOrder', comment='untrusted old text'),
+ TraceNode(kind='purpose', name='CustomerOrder', comment='old purpose')],
+ _comment='explicit request comment', _purpose='explicit request purpose')
+ await service.query(context, request)
+ assert entries[0].comment == 'explicit request comment'
+ assert entries[0].purpose == 'explicit request purpose'
+ assert not {'comment', 'purpose', 'auditReason'}.intersection(node.kind for node in entries[0].trace_path)
+
+
+@pytest.mark.asyncio
+async def test_unexecuted_aggregate_provenance_does_not_modify_query_builders(tmp_path):
+ from copy import deepcopy
+ from teaql.core.query import RelationAggregate
+ context, service, transport, entries = await fixture(tmp_path)
+ child = SelectQuery('Payment').count('count').relation_query('children', SelectQuery('PaymentAttempt'))
+ parent = SelectQuery('CustomerOrder').project('id').filter(Expr.eq('name', 'missing')).limit(1)
+ parent.relation_aggregates.append(RelationAggregate('children', 'child_count', child, True))
+ request = QueryRequest(parent, _comment='inspect absent parent', _purpose='test provenance isolation')
+ before = deepcopy(request.query)
+ result = await service.query(context, request)
+ assert result.rows == [] and len(entries) == len(transport.reads) == 1
+ assert request.query == before and child.slice is None
+ assert child.relations[0].query.slice is None
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('nested', [False, True])
+@pytest.mark.parametrize('logging', [False, True])
+@pytest.mark.parametrize('reference', ['visible', 'filtered', 'filtered_with_sibling'])
+async def test_loaded_forward_key_remains_usable_by_relation_aggregate(tmp_path, nested, logging, reference):
+ from teaql.core.query import RelationAggregate
+ from teaql.core.mutation import UpdateCommand
+ context, service, transport, entries = await fixture(tmp_path)
+ descriptor = service.schema_provider.get_entity('Payment')
+ descriptor.relation(RelationDescriptor('parent_id', 'CustomerOrder').local('parent_id').foreign('id'))
+ descriptor.relation(RelationDescriptor('parent_again', 'CustomerOrder').local('parent_id').foreign('id'))
+ service.schema_provider.get_entity('CustomerOrder').audit_mask_fields(['name'])
+ secret = 'PRIVATE-AGGREGATE-PARENT'
+ await service.mutate(context, MutationRequest(UpdateCommand.new('CustomerOrder', 1)
+ .expected_version(1).value('name', secret), comment='prepare private relation fixture'))
+ entries.clear()
+ context.clear_sql_logs()
+ transport.reads.clear()
+ if not logging:
+ context.disable_select_sql_log()
+ forward = SelectQuery('CustomerOrder').project('id', 'name').limit(1)
+ if reference != 'visible':
+ forward.filter(Expr.eq('name', 'not-visible'))
+ child = (SelectQuery('Payment').project('id', 'parent_id').limit(1)
+ .relation_query('parent_id', forward))
+ if reference == 'filtered_with_sibling':
+ child.relation_query('parent_again', SelectQuery('CustomerOrder').project('id', 'name').limit(1))
+ child.relation_aggregates.append(RelationAggregate('parent_id', 'parent_count',
+ SelectQuery('CustomerOrder').filter(Expr.eq('name', secret)).count('n'), True))
+ query = (SelectQuery('CustomerOrder').project('id').limit(1).relation_query('children', child)
+ if nested else child)
+ result = await service.query(context, QueryRequest(query, _comment='inspect ' + secret,
+ _purpose='verify composed aggregate ancestry'))
+ row = result.rows[0]['children'][0] if nested else result.rows[0]
+ assert row['parent_count'] == 1
+ if reference == 'visible':
+ assert row['parent_id']['name'] == secret
+ else:
+ assert row['parent_id'] == {'id': 1}
+ assert 'name' not in row['parent_id']
+ if reference == 'filtered_with_sibling':
+ assert row['parent_again']['name'] == secret
+ assert len(transport.reads) == (4 if nested else 3) + (reference == 'filtered_with_sibling')
+ if logging:
+ assert len(entries) == len(transport.reads)
+ assert all(secret not in entry.comment for entry in entries)
+ assert all(entry.purpose == 'verify composed aggregate ancestry' for entry in entries)
+ expected_root = 'CustomerOrder' if nested else 'Payment'
+ assert all(entry.trace_path[0].name == expected_root for entry in entries)
+ aggregate = next(entry for entry in entries if 'COUNT(' in entry.sql.upper())
+ assert [node.name for node in aggregate.trace_path if node.kind == 'relation'] == (
+ ['children', 'parent_id'] if nested else ['parent_id'])
+ else:
+ assert entries == []
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('logging', [False, True])
+async def test_scalar_stream_rejects_relation_aggregate_before_provider(tmp_path, logging):
+ from teaql.core.query import RelationAggregate
+ context, service, transport, entries = await fixture(tmp_path)
+ if not logging:
+ context.disable_select_sql_log()
+ query = SelectQuery('CustomerOrder').project('id').limit(1)
+ query.relation_aggregates.append(RelationAggregate('children', 'child_count', SelectQuery('Payment').count('n'), True))
+ stream = service.query_stream(context, QueryRequest(query, _comment='stream child counts',
+ _purpose='reject unsupported enhancement'), 1)
+ with pytest.raises(ValueError, match='streaming relation or aggregate enhancement is not supported'):
+ async for _ in stream:
+ pytest.fail('unsupported aggregate stream must not yield incomplete rows')
+ assert transport.streams == [] and transport.reads == [] and entries == []
+
+
+@pytest.mark.asyncio
+async def test_forward_id_detail_is_distinct_from_actual_null_and_unrequested_relation(tmp_path):
+ from teaql.core.mutation import UpdateCommand
+ context, service, transport, entries = await fixture(tmp_path)
+ descriptor = service.schema_provider.get_entity('Payment')
+ descriptor.relation(RelationDescriptor('parent', 'CustomerOrder').local('parent_id').foreign('id'))
+ plain = await service.query(context, QueryRequest(SelectQuery('Payment').project('id').limit(1),
+ _comment='identity only', _purpose='do not fabricate an unrequested relation'))
+ assert 'parent' not in plain.rows[0]
+ query = (SelectQuery('Payment').project('id').limit(1)
+ .relation_query('parent', SelectQuery('CustomerOrder').filter(Expr.eq('name', 'absent'))))
+ loaded = await service.query(context, QueryRequest(query, _comment='filtered parent',
+ _purpose='keep actual ID with unloaded details'))
+ assert loaded.rows[0]['parent'] == {'id': 1}
+ await service.mutate(context, MutationRequest(UpdateCommand.new('Payment', 1)
+ .expected_version(1).value('parent_id', None), comment='clear nullable fixture FK'))
+ cleared = await service.query(context, QueryRequest(query, _comment='nullable parent',
+ _purpose='SQL NULL is not a hidden target'))
+ assert cleared.rows[0]['parent'] is None
+
+
+def test_relation_assembly_keeps_original_text_key_after_hydration_or_filtering():
+ from teaql.sql.executor import _RelationAssembly
+ row = {'sku': 'SKU-A'}
+ capture = _RelationAssembly('sku')
+ capture.capture([row])
+ row['sku'] = {'id': 999, 'code': 'SKU-A'}
+ assert capture.key(row) == 'SKU-A'
+ row['sku'] = None
+ assert capture.key(row) == 'SKU-A'
+ assert row == {'sku': None}
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('remove_in_policy', [False, True])
+@pytest.mark.parametrize('fail', [False, True])
+async def test_exact_count_retains_privacy_of_removed_relations_without_ambient_state(tmp_path, remove_in_policy, fail):
+ context, service, transport, entries = await fixture(tmp_path)
+ service.schema_provider.get_entity('CustomerOrder').audit_mask_fields([])
+ service.schema_provider.get_entity('CustomerOrder').property_by_name('name').log_policy('plain')
+ service.schema_provider.get_entity('Payment').audit_mask_fields(['name'])
+ private = 'CHILD-COUNT-PRIVACY-CANARY'
+ original = QueryRequest(SelectQuery('CustomerOrder').project('id').limit(1)
+ .relation_query('children', SelectQuery('Payment').filter(Expr.eq('name', private)).limit(1)),
+ _comment='count graph mentioning ' + private,
+ _purpose='inspect total for ' + private)
+ if remove_in_policy:
+ def discard_relation(query):
+ query.relations = []
+ return query
+ context.with_request_policy(discard_relation)
+ prepared = context.prepare_query_request(original)
+ counted = prepared.with_query(prepared.query.for_exact_count('__total')).comment(
+ original.intent.comment).purpose(original.intent.purpose)
+ if fail:
+ transport.fail_table = 'customerorder_data'
+ with pytest.raises(TransportError) as caught:
+ await service.query(context, counted)
+ assert caught.value.error is transport.failure
+ transport.fail_table = None
+ else:
+ result = await service.query(context, counted)
+ assert result.rows == [{'__total': 1}]
+ assert len(transport.reads) == len(entries) == 1
+ assert 'COUNT(' in transport.reads[0].sql.upper()
+ assert 'payment_data' not in transport.reads[0].sql
+ assert entries[0].comment == 'count graph mentioning [REDACTED]'
+ assert entries[0].purpose == 'inspect total for [REDACTED]'
+ assert entries[0].execution_outcome == ('failure' if fail else 'success')
+ assert private not in repr(context.sql_logs())
+ assert original.intent.comment.endswith(private) and len(original.query.relations) == 1
+ await service.query(context, QueryRequest(SelectQuery('CustomerOrder')
+ .filter(Expr.eq('name', private)).limit(1),
+ _comment='independent query mentioning ' + private, _purpose='no inherited privacy'))
+ assert entries[-1].comment.endswith(private)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('rollback', [False, True])
+async def test_explicit_transaction_audit_waits_for_commit_and_rollback_discards(tmp_path, rollback):
+ context, service, transport, entries = await fixture(tmp_path)
+ audits = []
+ context.with_app_audit_event_sink(SimpleNamespace(on_safe_event=lambda context, event: audits.append(event)))
+ transaction = await service.begin(context)
+ command = InsertCommand.new('Payment').value('name', 'new payment').value('parent_id', 1)
+ await transaction.mutate(context, MutationRequest(command, comment='authorize transaction payment'))
+ assert audits == []
+ if rollback:
+ await transaction.rollback(context)
+ else:
+ await transaction.commit(context)
+ assert len(audits) == 1
+ assert audits[0].trace_chain[0].comment == 'authorize transaction payment'
+ assert audits[0].trace_chain[0].entity_id == 2
+ async with aiosqlite.connect(transport.db_path) as database:
+ cursor = await database.execute('SELECT COUNT(*) FROM payment_data')
+ assert (await cursor.fetchone())[0] == (1 if rollback else 2)
+ assert context._audit_journal is None
+
+
+@pytest.mark.asyncio
+async def test_automatic_batch_readback_failure_discards_prior_successful_audits(tmp_path):
+ context, service, transport, entries = await fixture(tmp_path)
+ audits = []
+ context.with_app_audit_event_sink(SimpleNamespace(on_safe_event=lambda context, event: audits.append(event)))
+ transport.fail_table = 'paymentattempt_data'
+ first = InsertCommand.new('Payment').value('name', 'first payment').value('parent_id', 1)
+ second = InsertCommand.new('PaymentAttempt').value('name', 'second attempt').value('parent_id', 1)
+ batch = MutationRequest.Batch([first, second], comment='authorize atomic payment batch')
+ with pytest.raises(RuntimeError) as caught:
+ await service.mutate(context, batch)
+ assert caught.value is transport.failure
+ assert audits == []
+ assert [entry.execution_outcome for entry in entries] == ['success', 'success', 'success', 'failure']
+ assert [entry.trace_path[-1].name for entry in entries] == ['insert', 'select', 'insert', 'select']
+ async with aiosqlite.connect(transport.db_path) as database:
+ for table in ('payment_data', 'paymentattempt_data'):
+ cursor = await database.execute(f'SELECT COUNT(*) FROM {table}')
+ assert (await cursor.fetchone())[0] == 1
+ assert context._audit_journal is None
+
+
+@pytest.mark.asyncio
+async def test_automatic_transaction_sink_failure_reports_committed_and_does_not_rollback(tmp_path):
+ context, service, transport, entries = await fixture(tmp_path)
+ calls = []
+ def unavailable(context, event):
+ calls.append(event)
+ raise RuntimeError('postcommit audit unavailable')
+ context.with_app_audit_event_sink(SimpleNamespace(on_safe_event=unavailable))
+ command = InsertCommand.new('Payment').value('name', 'committed payment').value('parent_id', 1)
+ with pytest.raises(RuntimeError) as caught:
+ await service.mutate(context, MutationRequest(command, comment='create committed payment'))
+ assert caught.value.committed is True and len(calls) == 1
+ async with aiosqlite.connect(transport.db_path) as database:
+ cursor = await database.execute('SELECT COUNT(*) FROM payment_data')
+ assert (await cursor.fetchone())[0] == 2
diff --git a/tests/provider/test_sql_masking_real_databases.py b/tests/provider/test_sql_masking_real_databases.py
index daed979..16dc4dc 100644
--- a/tests/provider/test_sql_masking_real_databases.py
+++ b/tests/provider/test_sql_masking_real_databases.py
@@ -84,7 +84,7 @@ def capture(entry):
.value("password_hash", "PASSWORD-CANARY")
)
command.trace_chain = [TraceNode(comment="what: create masked customer")]
- await service.mutate(context, MutationRequest(command))
+ await service.mutate(context, MutationRequest(command, comment='what: runtime regression fixture'))
query = SelectQuery("Customer").filter(
Expr.new_and(
Expr.eq("display_name", "Riverside"),
@@ -94,7 +94,7 @@ def capture(entry):
rows = (
await service.query(
context,
- QueryRequest(query)
+ QueryRequest(query, _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior')
.comment("what: read masked customer")
.purpose("why: verify live-provider SQL masking"),
)
diff --git a/tests/provider/test_tfp_client.py b/tests/provider/test_tfp_client.py
index 2709941..b225d3a 100644
--- a/tests/provider/test_tfp_client.py
+++ b/tests/provider/test_tfp_client.py
@@ -117,13 +117,13 @@ async def handler(request):
provider = TfpHttpProvider("https://tfp.test", client=http)
query = SelectQuery.new("CustomerOrder").filter(Expr.eq("status", "NEW"))
query.slice = Slice(offset=0, limit=20)
- result = await provider.query(None, QueryRequest(query).comment("List orders").purpose("Render queue"))
+ result = await provider.query(None, QueryRequest(query, _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior').comment("List orders").purpose("Render queue"))
assert isinstance(result.rows, SmartList)
assert payloads[0]["filterCondition"] == {"status": {"$eq": "NEW"}}
command = UpdateCommand.new("CustomerOrder", 42).expected_version(3).value("status", "PAID")
command.trace_chain.append(TraceNode(comment="Mark paid"))
- mutation = await provider.mutate(None, MutationRequest.Update(command))
+ mutation = await provider.mutate(None, MutationRequest.Update(command, comment='what: runtime regression fixture'))
assert mutation.affected_rows == 1
assert payloads[1]["expectedVersion"] == 3
@@ -163,7 +163,7 @@ def review(self, context, plan):
command.trace_chain.append(TraceNode(comment="Mark paid"))
with pytest.raises(MutationPolicyError, match="REMOTE_MUTATION_DENIED"):
- await provider.mutate(context, MutationRequest.Update(command))
+ await provider.mutate(context, MutationRequest.Update(command, comment='what: runtime regression fixture'))
assert calls == 0
diff --git a/tests/runtime/test_graph_audit.py b/tests/runtime/test_graph_audit.py
new file mode 100644
index 0000000..77a9824
--- /dev/null
+++ b/tests/runtime/test_graph_audit.py
@@ -0,0 +1,141 @@
+"""Transaction lifecycle assertions, independent of generated library fixtures."""
+import pytest
+
+from teaql.runtime import UserContext
+from teaql.runtime.audit import MutationAuditKind, RawAuditEvent
+from teaql.core.entity import EntityKey
+
+
+class Provider:
+ def __init__(self, events):
+ self.events = events
+
+ async def begin(self, context):
+ self.events.append('begin')
+ return self
+
+ async def commit(self, context):
+ self.events.append('commit')
+
+ async def rollback(self, context):
+ self.events.append('rollback')
+
+
+def fixture():
+ events = []
+ provider = Provider(events)
+ context = UserContext().insert_resource('dataService', provider)
+ class Sink:
+ def on_safe_event(self, context, event):
+ events.append('audit')
+ context.with_app_audit_event_sink(Sink())
+ return context, provider, events
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize('failure', [False, True])
+async def test_graph_audit_waits_for_commit_and_rollback_discards(failure):
+ context, provider, events = fixture()
+ async def work(graph):
+ await graph.context.send_audit_event(RawAuditEvent(MutationAuditKind.CREATED, 'CustomerOrder', 1, ()))
+ assert events == ['begin']
+ if failure:
+ raise RuntimeError('failed graph')
+ if failure:
+ with pytest.raises(RuntimeError, match='failed graph'):
+ await context.execute_graph_save(work, comment='create order')
+ assert events == ['begin', 'rollback']
+ else:
+ await context.execute_graph_save(work, comment='create order')
+ assert events == ['begin', 'commit', 'audit']
+
+
+@pytest.mark.asyncio
+async def test_postcommit_failure_still_attempts_remaining_completion_actions():
+ context, provider, events = fixture()
+ def broken():
+ events.append('broken completion')
+ raise RuntimeError('completion unavailable')
+ async def work(graph):
+ graph.after_commit(broken)
+ graph.after_commit(lambda: events.append('ledger clear'))
+ with pytest.raises(RuntimeError) as caught:
+ await context.execute_graph_save(work, comment='complete order')
+ assert getattr(caught.value, 'committed', False) is True
+ assert events == ['begin', 'commit', 'broken completion', 'ledger clear']
+
+
+@pytest.mark.asyncio
+async def test_reentrant_root_fails_before_begin_and_explicit_child_uses_one_session():
+ context, provider, events = fixture()
+ async def root(graph):
+ assert context.require_resource('dataService') is provider
+ with pytest.raises(RuntimeError, match='re-enter'):
+ await context.execute_graph_save(root, comment='another independent root')
+ async def spawned():
+ with pytest.raises(RuntimeError, match='re-enter'):
+ await context.execute_graph_save(root, comment='spawned independent root')
+ await __import__('asyncio').create_task(spawned())
+ with pytest.raises(RuntimeError, match='explicit session context'):
+ context._require_mutation_invocation()
+ graph.context._require_mutation_invocation()
+ return graph
+ graph = await context.execute_graph_save(root, comment='explicit root')
+ assert events == ['begin', 'commit']
+ with pytest.raises(RuntimeError, match='no longer writable'):
+ graph.scope(EntityKey('CustomerOrder', 1))
+ with pytest.raises(RuntimeError, match='no longer writable'):
+ graph.context._require_mutation_invocation()
+
+
+@pytest.mark.asyncio
+async def test_foreign_scope_rejected_and_next_invocation_can_reuse_context():
+ context, provider, events = fixture()
+ async def first(graph):
+ return graph.scope(EntityKey('CustomerOrder', 1))
+ previous = await context.execute_graph_save(first, comment='first graph')
+ async def second(graph):
+ with pytest.raises(ValueError, match='another invocation'):
+ graph.scope(EntityKey('Payment', 1), previous, 'authorize payment')
+ return graph.scope(EntityKey('CustomerOrder', 1)).recover()
+ nodes = await context.execute_graph_save(second, comment='second graph')
+ assert [node.comment for node in nodes] == ['second graph']
+ assert events == ['begin', 'commit', 'begin', 'commit']
+
+
+@pytest.mark.asyncio
+async def test_audit_snapshot_is_owned_and_failing_sink_does_not_skip_other_sink_or_cleanup():
+ context, provider, events = fixture()
+ raw = []
+ class Broken:
+ def on_event(self, context, event):
+ raw.append(event)
+ events.append('raw attempt')
+ raise RuntimeError('sink failure')
+ context._set_standard_audit_sink(Broken())
+ async def work(graph):
+ from teaql.core.mutation import TraceNode
+ source = TraceNode(kind='auditReason', name='CustomerOrder', entity_id=1, comment='create order')
+ await graph.context.send_audit_event(RawAuditEvent(
+ MutationAuditKind.CREATED, 'CustomerOrder', 1, (), (source,)))
+ source.comment = 'changed after queue'
+ graph.after_commit(lambda: events.append('ledger clear'))
+ with pytest.raises(RuntimeError) as caught:
+ await context.execute_graph_save(work, comment='create order')
+ assert caught.value.committed is True
+ assert raw[0].trace_chain[0].comment == 'create order'
+ assert events == ['begin', 'commit', 'raw attempt', 'audit', 'ledger clear']
+
+
+@pytest.mark.asyncio
+async def test_provider_type_error_does_not_retry_transaction_begin():
+ context, provider, events = fixture()
+ async def broken(context):
+ events.append('broken begin')
+ raise TypeError('provider implementation error')
+ provider.begin = broken
+ async def work(graph):
+ raise AssertionError('work must not run')
+ with pytest.raises(TypeError, match='provider implementation error'):
+ await context.execute_graph_save(work, comment='create order')
+ assert events == ['broken begin']
diff --git a/tests/runtime/test_mutation_intent_privacy.py b/tests/runtime/test_mutation_intent_privacy.py
new file mode 100644
index 0000000..1464380
--- /dev/null
+++ b/tests/runtime/test_mutation_intent_privacy.py
@@ -0,0 +1,62 @@
+"""Owned provenance and debug rules, independent from graph acceptance."""
+from teaql.core.meta import EntityDescriptor, PropertyDescriptor
+from teaql.core.mutation import UpdateCommand
+from teaql.core.value import DataType, Value
+from teaql.runtime.log_privacy import _MutationIntentPrivacy
+
+
+def test_snapshot_owns_old_and_new_values_and_debug_never_reveals_credentials():
+ descriptor = EntityDescriptor('Payment').audit_mask_fields(['name'])
+ descriptor.property(PropertyDescriptor('name', DataType.Text).log_policy('plain'))
+ descriptor.property(PropertyDescriptor('password_hash', DataType.Text).log_policy('plain'))
+ command = UpdateCommand.new('Payment', 73).value('name', 'PRIVATE-NEW')
+ command.value('password_hash', 'CREDENTIAL-CANARY')
+ command.old_values = {'name': Value.from_any('PRIVATE-OLD')}
+ privacy = _MutationIntentPrivacy.capture(command, lambda _: descriptor)
+ command.values['name'] = Value.from_any('changed after snapshot')
+ command.old_values.clear()
+ descriptor.audit_mask_fields([])
+ assert set(privacy.secrets(False)) == {'PRIVATE-NEW', 'PRIVATE-OLD', 'CREDENTIAL-CANARY', '73'}
+ assert set(privacy.secrets(True)) == {'CREDENTIAL-CANARY', '73'}
+ assert 'PRIVATE-NEW' not in repr(privacy)
+
+
+def test_loaded_scalar_snapshot_owns_values_and_returns_detached_changed_fields():
+ from teaql.core.entity import _LoadedScalarSnapshot
+ source = {'name': Value.from_any('original'), 'config': Value.from_any({'nested': ['original']})}
+ snapshot = _LoadedScalarSnapshot(source)
+ source['name'] = Value.from_any('caller changed')
+ source['config'].val['nested'].append('caller changed')
+ selected = snapshot.select(['name', 'config', 'not_loaded'])
+ assert selected['name'].val == 'original'
+ assert selected['config'].val == {'nested': ['original']}
+ assert 'not_loaded' not in selected
+ selected['config'].val['nested'].clear()
+ assert snapshot.select(['config'])['config'].val == {'nested': ['original']}
+
+
+def test_business_snapshot_includes_unchanged_scalars_without_identity_or_version():
+ from teaql.core.entity import _LoadedScalarSnapshot
+ snapshot = _LoadedScalarSnapshot({'id': Value.from_any(1), 'version': Value.from_any(2),
+ 'name': Value.from_any('PRIVATE-UNCHANGED'),
+ 'config': Value.from_any({'secret': ['original']})})
+ values = snapshot.business_values()
+ assert set(values) == {'name', 'config'}
+ values['config'].val['secret'].clear()
+ assert snapshot.business_values()['config'].val == {'secret': ['original']}
+
+
+def test_delete_and_recover_capture_old_private_values_without_altering_bindings():
+ from teaql.core.mutation import DeleteCommand, RecoverCommand
+ descriptor = EntityDescriptor('Payment').audit_mask_fields(['name'])
+ descriptor.property(PropertyDescriptor('name', DataType.Text).log_policy('plain'))
+ descriptor.property(PropertyDescriptor('public_note', DataType.Text).log_policy('plain'))
+ for command in (DeleteCommand.new('Payment', 73), RecoverCommand.new('Payment', 73, -2)):
+ command.old_values = {'name': Value.from_any('PRIVATE-OLD'),
+ 'public_note': Value.from_any('ordinary prose')}
+ privacy = _MutationIntentPrivacy.capture(command, lambda _: descriptor)
+ command.old_values.clear()
+ assert set(privacy.secrets(False)) == {'PRIVATE-OLD', '73'}
+ assert set(privacy.secrets(True)) == {'73'}
+ assert command.id.val == 73
+ assert not hasattr(command, 'values')
diff --git a/tests/runtime/test_mutation_policy.py b/tests/runtime/test_mutation_policy.py
index ef516b6..ab26b06 100644
--- a/tests/runtime/test_mutation_policy.py
+++ b/tests/runtime/test_mutation_policy.py
@@ -65,13 +65,15 @@ class RecordingTransaction:
def __init__(self, owner):
self.owner = owner
self.executor = object.__new__(SqlDataServiceExecutor)
+ from teaql.provider.sqlite import SimpleSchemaProvider
+ self.executor.schema_provider = SimpleSchemaProvider()
self.executor._sync_generated_schema = lambda context: None
self.executor._mutate = self._mutate
async def mutate(self, context, request):
return await self.executor.mutate(context, request)
- async def _mutate(self, context, request):
+ async def _mutate(self, context, request, privacy=None):
self.owner.mutations += 1
command = request._data
await context.send_audit_event(RawAuditEvent(
@@ -179,18 +181,19 @@ def review(plan):
order = _insert("Order", 42, "DRAFT")
line = _insert("OrderLine", 99, "LINE")
- async def save_graph():
- context.preflight_mutation(order)
- context.preflight_mutation(line)
+ async def save_graph(graph):
+ local = graph.context
+ local.preflight_mutation(order)
+ local.preflight_mutation(line)
order.values["name"] = Value.Text("APPROVED")
# The policy still receives DRAFT, and an execution may only consume the
# original reviewed operation rather than the changed command.
- transaction = context.require_resource("dataService")
+ transaction = local.require_resource("dataService")
await transaction.mutate(
- context, MutationRequest(_insert("Order", 42, "DRAFT")))
- return await transaction.mutate(context, MutationRequest(line))
+ local, MutationRequest(_insert("Order", 42, "DRAFT"), comment='what: runtime regression fixture'))
+ return await transaction.mutate(local, MutationRequest(line, comment='what: runtime regression fixture'))
- await context.execute_graph_save(save_graph)
+ await context.execute_graph_save(save_graph, comment='what: runtime regression fixture')
assert observed == {"count": 2, "name": "DRAFT"}
assert provider.mutations == 2
assert provider.commits == 1
@@ -218,14 +221,14 @@ async def test_denial_and_missing_preflight_leave_zero_provider_mutations():
order = _insert("Order", 43, "DENIED")
line = _insert("OrderLine", 100, "DENIED-LINE")
- async def denied_graph():
- denied.preflight_mutation(order)
- denied.preflight_mutation(line)
- return await denied.require_resource("dataService").mutate(
- denied, MutationRequest(order))
+ async def denied_graph(graph):
+ graph.context.preflight_mutation(order)
+ graph.context.preflight_mutation(line)
+ return await graph.transaction.mutate(
+ graph.context, MutationRequest(order, comment='what: runtime regression fixture'))
with pytest.raises(MutationPolicyError, match="ORDER_DENIED"):
- await denied.execute_graph_save(denied_graph)
+ await denied.execute_graph_save(denied_graph, comment='what: runtime regression fixture')
assert denied_provider.begins == 1
assert denied_provider.mutations == 0
assert denied_provider.rollbacks == 1
@@ -237,8 +240,8 @@ async def denied_graph():
)
with pytest.raises(MutationPolicyError, match="complete graph preflight"):
await missing.execute_graph_save(
- lambda: missing.require_resource("dataService").mutate(
- missing, MutationRequest(_insert("Order", 44, "MISSING"))))
+ lambda graph: graph.transaction.mutate(
+ graph.context, MutationRequest(_insert("Order", 44, "MISSING"), comment='what: runtime regression fixture')), comment='what: runtime regression fixture')
assert missing_provider.mutations == 0
assert missing_provider.rollbacks == 1
@@ -251,14 +254,14 @@ async def test_generated_default_allows_legacy_graph_without_complete_preflight(
.insert_resource("dataService", provider)
.with_mutation_governance_sink(warnings))
- async def legacy_graph():
- transaction = context.require_resource("dataService")
+ async def legacy_graph(graph):
+ transaction = graph.transaction
await transaction.mutate(
- context, MutationRequest(_insert("Order", 47, "FIRST")))
+ graph.context, MutationRequest(_insert("Order", 47, "FIRST"), comment='what: runtime regression fixture'))
await transaction.mutate(
- context, MutationRequest(_insert("OrderLine", 102, "SECOND")))
+ graph.context, MutationRequest(_insert("OrderLine", 102, "SECOND"), comment='what: runtime regression fixture'))
- await context.execute_graph_save(legacy_graph)
+ await context.execute_graph_save(legacy_graph, comment='what: runtime regression fixture')
assert provider.mutations == 2
assert provider.commits == 1
assert provider.rollbacks == 0
@@ -274,13 +277,13 @@ async def test_unplanned_and_incomplete_operations_fail_closed():
TestPolicy(identity, lambda plan: MutationDecision.allowed()),
)
- async def unplanned_graph():
- unplanned.preflight_mutation(_insert("Order", 45, "PLANNED"))
- return await unplanned.require_resource("dataService").mutate(
- unplanned, MutationRequest(_insert("Order", 45, "DIFFERENT")))
+ async def unplanned_graph(graph):
+ graph.context.preflight_mutation(_insert("Order", 45, "PLANNED"))
+ return await graph.transaction.mutate(
+ graph.context, MutationRequest(_insert("Order", 45, "DIFFERENT"), comment='what: runtime regression fixture'))
with pytest.raises(MutationPolicyError, match="not present"):
- await unplanned.execute_graph_save(unplanned_graph)
+ await unplanned.execute_graph_save(unplanned_graph, comment='what: runtime regression fixture')
assert unplanned_provider.mutations == 0
assert unplanned_provider.rollbacks == 1
@@ -292,14 +295,14 @@ async def unplanned_graph():
first = _insert("Order", 46, "FIRST")
second = _insert("OrderLine", 101, "SECOND")
- async def incomplete_graph():
- incomplete.preflight_mutation(first)
- incomplete.preflight_mutation(second)
- return await incomplete.require_resource("dataService").mutate(
- incomplete, MutationRequest(first))
+ async def incomplete_graph(graph):
+ graph.context.preflight_mutation(first)
+ graph.context.preflight_mutation(second)
+ return await graph.transaction.mutate(
+ graph.context, MutationRequest(first, comment='what: runtime regression fixture'))
with pytest.raises(MutationPolicyError, match="were not executed"):
- await incomplete.execute_graph_save(incomplete_graph)
+ await incomplete.execute_graph_save(incomplete_graph, comment='what: runtime regression fixture')
assert incomplete_provider.mutations == 1
assert incomplete_provider.commits == 0
assert incomplete_provider.rollbacks == 1
@@ -317,12 +320,12 @@ async def test_warning_sink_failure_is_fail_open():
)
order = _insert("Order", 47, "ALLOWED")
- async def graph():
- context.preflight_mutation(order)
- return await context.require_resource("dataService").mutate(
- context, MutationRequest(order))
+ async def graph(session):
+ session.context.preflight_mutation(order)
+ return await session.transaction.mutate(
+ session.context, MutationRequest(order, comment='what: runtime regression fixture'))
- await context.execute_graph_save(graph)
+ await context.execute_graph_save(graph, comment='what: runtime regression fixture')
assert warnings.events[0].warning_code == MISSING_APPROVAL
assert provider.mutations == 1
assert provider.commits == 1
diff --git a/tests/runtime/test_query_policy.py b/tests/runtime/test_query_policy.py
index 0fe0ec7..d20f3d2 100644
--- a/tests/runtime/test_query_policy.py
+++ b/tests/runtime/test_query_policy.py
@@ -4,7 +4,7 @@
def test_prepare_query_clones_and_governs_every_query_node_once():
shared = SelectQuery("SchoolType").project("id")
- root = SelectQuery("School").project("id")
+ root = SelectQuery("School").project("id").comment('govern query graph').purpose('verify policy')
root.relations.append(RelationLoad("schoolType", shared))
root.facets.append(FacetRequest("types", "schoolType", shared))
calls = []
@@ -27,7 +27,7 @@ def policy(query):
def test_prepare_query_without_policy_still_returns_independent_graph():
child = SelectQuery("SchoolType").project("id")
- root = SelectQuery("School")
+ root = SelectQuery("School").comment('clone query graph').purpose('verify policy')
root.relations.append(RelationLoad("schoolType", child))
prepared = UserContext().prepare_query(root)
@@ -43,7 +43,7 @@ def deny(query):
if query.entity == "SchoolType":
raise PermissionError("query policy denied SchoolType")
- root = SelectQuery("School")
+ root = SelectQuery("School").comment('deny query graph').purpose('verify policy')
root.relations.append(RelationLoad("schoolType", SelectQuery("SchoolType")))
context = UserContext().with_request_policy(deny)
@@ -57,7 +57,7 @@ def deny(query):
def test_prepare_query_preserves_shared_nodes_when_policy_returns_replacement():
shared = SelectQuery("SchoolType")
- root = SelectQuery("School")
+ root = SelectQuery("School").comment('replace query graph').purpose('verify policy')
root.relations.append(RelationLoad("schoolType", shared))
root.facets.append(FacetRequest("types", "schoolType", shared))
diff --git a/tests/runtime/test_relation_masking.py b/tests/runtime/test_relation_masking.py
index a99fa86..352a633 100644
--- a/tests/runtime/test_relation_masking.py
+++ b/tests/runtime/test_relation_masking.py
@@ -43,7 +43,7 @@ async def query(self, context, request):
@pytest.mark.asyncio
-@pytest.mark.parametrize('shape', ['batch', 'probe', 'window', 'aggregate', 'nested'])
+@pytest.mark.parametrize('shape', ['batch', 'probe', 'window', 'aggregate', 'nested', 'nested-aggregate'])
@pytest.mark.parametrize('debug', [False, True])
@pytest.mark.parametrize('failure', [False, True])
async def test_derived_relation_intent(tmp_path, monkeypatch, shape, debug, failure):
@@ -88,7 +88,7 @@ def capture(entry):
if debug:
monkeypatch.setenv(PLAINTEXT_ENV, PLAINTEXT_ACK)
if failure:
- transport.failing_table = 'line_data' if shape == 'nested' else 'order_data'
+ transport.failing_table = 'line_data' if shape in ('nested', 'nested-aggregate') else 'order_data'
query = (SelectQuery('Customer').project('id', 'name')
.filter(Expr.eq('name', 'Riverside')).and_filter(Expr.eq('password', 'PASSWORD-CANARY')).limit(1))
child = SelectQuery('Order').project('id', 'name')
@@ -98,13 +98,16 @@ def capture(entry):
child.limit(1).top_n_probe_parent_threshold(0)
if shape == 'nested':
child.filter(Expr.eq('name', 'Lakeside')).relation_query('lines', SelectQuery('Line').project('id').limit(2))
+ if shape == 'nested-aggregate':
+ child.filter(Expr.eq('name', 'Lakeside'))
+ child.relation_aggregates.append(RelationAggregate('lines', 'line_count', SelectQuery('Line').count('n'), True))
if shape == 'aggregate':
query.relation_aggregates.append(RelationAggregate('orders', 'record_count', SelectQuery('Order').count('n'), True))
elif shape == 'batch':
query.relation('orders')
else:
query.relation_query('orders', child)
- request = (QueryRequest(query).comment('what: load Riverside PASSWORD-CANARY Lakeside graph')
+ request = (QueryRequest(query, _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior').comment('what: load Riverside PASSWORD-CANARY Lakeside graph')
.purpose('why: verify inherited intent'))
if failure:
with pytest.raises(TransportError) as caught:
@@ -122,7 +125,9 @@ def capture(entry):
assert result.rows[0]['orders'][0]['id'] == 1
if shape == 'nested':
assert result.rows[0]['orders'][0]['lines'][0]['id'] == 1
- assert service.dispatched == (['Customer', 'Order', 'Line'] if shape == 'nested' else ['Customer', 'Order'])
+ if shape == 'nested-aggregate':
+ assert result.rows[0]['orders'][0]['line_count'] == 1
+ assert service.dispatched == (['Customer', 'Order', 'Line'] if shape in ('nested', 'nested-aggregate') else ['Customer', 'Order'])
assert len(logs) == len(service.dispatched)
entry = logs[-1]
assert entry.execution_outcome == ('failure' if failure else 'success')
@@ -132,8 +137,13 @@ def capture(entry):
assert 'PASSWORD-CANARY' not in repr(logs) + '\n'.join(output) + repr(context.sql_logs())
if not debug:
assert 'Riverside' not in repr(logs) + '\n'.join(output)
- if shape == 'nested':
+ if shape in ('nested', 'nested-aggregate'):
assert 'Lakeside' not in entry.comment
+ assert 'Lakeside' not in repr(logs) + '\n'.join(output) + repr(context.sql_logs())
+ if shape == 'nested-aggregate':
+ assert [(node.kind, node.name) for node in entry.trace_path] == [
+ ('operation', 'Customer'), ('request', 'Customer'), ('relation', 'orders'),
+ ('relation', 'lines'), ('provider', 'sqlite'), ('sql', 'select')]
assert len(entry.params) == len(transport.reads[-1].params)
assert 'PASSWORD-CANARY' in [v.val for v in transport.reads[0].params]
if shape == 'batch':
@@ -145,6 +155,6 @@ def capture(entry):
monkeypatch.delenv(PLAINTEXT_ENV, raising=False)
assert 'Riverside' not in repr(sql_log_projection(entry))
transport.failing_table = None
- await service.query(context, QueryRequest(SelectQuery('Customer').limit(1))
+ await service.query(context, QueryRequest(SelectQuery('Customer').limit(1), _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior')
.comment('what: independent Riverside').purpose('why: source isolation'))
assert logs[-1].comment == 'what: independent Riverside'
diff --git a/tests/runtime/test_runtime.py b/tests/runtime/test_runtime.py
index 32124f5..1a38968 100644
--- a/tests/runtime/test_runtime.py
+++ b/tests/runtime/test_runtime.py
@@ -143,18 +143,19 @@ async def test_graph_save_uses_one_transaction_and_retains_rollback_order():
provider = RecordingTransactionProvider(events)
context = UserContext.new().insert_resource("dataService", provider)
- async def failing_graph():
- assert context.require_resource("dataService") is provider.transaction
- context.after_graph_rollback(lambda: events.append("parent rollback"))
+ async def failing_graph(graph):
+ assert context.require_resource("dataService") is provider
+ assert graph.context.require_resource("dataService") is provider.transaction
+ graph.after_rollback(lambda: events.append("parent rollback"))
- async def nested_save():
- context.after_graph_rollback(lambda: events.append("child rollback"))
+ async def child_work(graph):
+ graph.after_rollback(lambda: events.append("child rollback"))
- await context.execute_graph_save(nested_save)
+ await child_work(graph)
raise RuntimeError("injected graph failure")
with pytest.raises(RuntimeError, match="injected graph failure"):
- await context.execute_graph_save(failing_graph)
+ await context.execute_graph_save(failing_graph, comment='what: runtime regression fixture')
assert events == ["begin", "rollback", "child rollback", "parent rollback"]
assert context.require_resource("dataService") is provider
@@ -166,11 +167,11 @@ async def test_graph_save_runs_commit_actions_only_after_provider_commit():
provider = RecordingTransactionProvider(events)
context = UserContext.new().insert_resource("dataService", provider)
- async def successful_graph():
- context.after_graph_commit(lambda: events.append("ledger clear"))
+ async def successful_graph(graph):
+ graph.after_commit(lambda: events.append("ledger clear"))
return "saved"
- assert await context.execute_graph_save(successful_graph) == "saved"
+ assert await context.execute_graph_save(successful_graph, comment='what: runtime regression fixture') == "saved"
assert events == ["begin", "commit", "ledger clear"]
assert context.require_resource("dataService") is provider
@@ -191,12 +192,12 @@ def check_and_fix(self, context, record, location, results):
.insert_resource("dataService", provider)
.with_checker_registry(DummyCheckerRegistry(ClockChecker())))
- async def graph():
- context.check_and_fix_mutation(InsertCommand.new("Dummy"))
+ async def graph(session):
+ session.context.check_and_fix_mutation(InsertCommand.new("Dummy"))
await __import__("asyncio").sleep(0.005)
- context.check_and_fix_mutation(InsertCommand.new("Dummy"))
+ session.context.check_and_fix_mutation(InsertCommand.new("Dummy"))
- await context.execute_graph_save(graph)
+ await context.execute_graph_save(graph, comment='what: runtime regression fixture')
assert len(observed) == 2 and observed[0] is observed[1]
assert observed[0] == expected
assert context.get_resource("fix_time") is None
@@ -226,11 +227,11 @@ async def test_independent_concurrent_graph_saves_do_not_join_transaction():
context = UserContext.new().insert_resource("dataService", provider)
async def save(name, delay):
- async def graph():
+ async def graph(session):
events.append(f"{name}:start")
await __import__("asyncio").sleep(delay)
events.append(f"{name}:end")
- await context.execute_graph_save(graph)
+ await context.execute_graph_save(graph, comment='what: runtime regression fixture')
await __import__("asyncio").gather(save("first", 0.01), save("second", 0))
assert events == [
diff --git a/tests/runtime/test_sql_mask_lifecycle.py b/tests/runtime/test_sql_mask_lifecycle.py
index a90b60e..fce1b6d 100644
--- a/tests/runtime/test_sql_mask_lifecycle.py
+++ b/tests/runtime/test_sql_mask_lifecycle.py
@@ -82,7 +82,7 @@ def request():
query = (SelectQuery('Customer').filter(Expr.eq('display_name', 'Riverside'))
.and_filter(Expr.eq('public_address', '1 Runtime Road'))
.and_filter(Expr.eq('password_hash', 'PASSWORD-CANARY')).limit(5))
- return QueryRequest(query).comment('what: inspect customers').purpose('why: lifecycle test')
+ return QueryRequest(query, _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior').comment('what: inspect customers').purpose('why: lifecycle test')
@pytest.mark.asyncio
@@ -137,7 +137,7 @@ async def test_failed_statement_logs_and_preserves_error(fixture, operation):
}
cmd = commands[operation]
cmd.trace_chain = [TraceNode(comment='what: lifecycle mutation')]
- await executor.mutate(context, MutationRequest(cmd))
+ await executor.mutate(context, MutationRequest(cmd, comment='what: runtime regression fixture'))
assert caught.value.error is failure
assert_log(fixture, 'failure')
assert entries[0].affected_rows is None
@@ -308,6 +308,37 @@ async def test_consumer_athrow_closes_stream(fixture):
assert_log(fixture, 'failure', 1)
+@pytest.mark.asyncio
+async def test_stream_owns_request_before_delayed_consumption(fixture):
+ context, provider, _, entries = fixture
+ transport = FaultTransport(3)
+ supplied = request()
+ supplied.trace_chain.append(TraceNode(kind='relation', name='original', comment='Customer.original'))
+ original_params = ['Riverside', '1 Runtime Road', 'PASSWORD-CANARY']
+ stream = SqlDataServiceExecutor(SqliteDialect(), transport, provider).query_stream(context, supplied, 1)
+ supplied.query.filter(Expr.eq('public_address', 'LATE-BUILDER-CANARY'))
+ supplied.trace_chain.clear()
+ chunks = [chunk async for chunk in stream]
+ assert len(chunks) == 3 and transport.closed
+ assert [getattr(value, 'val', value) for value in transport.params] == original_params
+ assert any(node.name == 'original' for node in entries[0].trace_path)
+ assert 'LATE-BUILDER-CANARY' not in repr(context.sql_logs())
+
+
+@pytest.mark.asyncio
+async def test_live_stream_path_cannot_be_rewritten_before_close(fixture):
+ context, provider, _, entries = fixture
+ transport = FaultTransport(3)
+ supplied = request()
+ supplied.trace_chain.append(TraceNode(kind='relation', name='original', comment='Customer.original'))
+ stream = SqlDataServiceExecutor(SqliteDialect(), transport, provider).query_stream(context, supplied, 1)
+ await anext(stream)
+ supplied.trace_chain[:] = [TraceNode(kind='relation', name='late', comment='Customer.late')]
+ await stream.aclose()
+ assert transport.closed and entries[0].execution_outcome == 'cancelled'
+ assert [node.name for node in entries[0].trace_path if node.kind == 'relation'] == ['original']
+
+
@pytest.mark.asyncio
async def test_cancelled_mutation_rolls_back_transaction(fixture):
context, provider, _, _ = fixture
@@ -327,7 +358,7 @@ async def begin_sql(self):
cmd = DeleteCommand('Customer', Value.I64(1)).expected_version(1)
cmd.trace_chain = [TraceNode(comment='what: cancel mutation')]
with pytest.raises(asyncio.CancelledError) as caught:
- await executor.mutate(context, MutationRequest(cmd))
+ await executor.mutate(context, MutationRequest(cmd, comment='what: runtime regression fixture'))
assert caught.value is failure
assert tx.rolled_back and not tx.committed
assert_log(fixture, 'cancelled')
@@ -338,11 +369,10 @@ async def test_generated_mutation_string_comment_is_preserved(fixture):
context, provider, _, entries = fixture
executor = DomainStatementExecutor(SqliteDialect(), FaultTransport(), provider)
command = InsertCommand('Customer').value('display_name', 'Riverside')
- mutation = MutationRequest(command)
- mutation.comment = 'what: generated audited save'
+ mutation = MutationRequest(command, comment='what: generated audited save')
await executor.mutate(context, mutation)
- assert entries[0].comment == mutation.comment
- assert entries[0].audit_reason == mutation.comment
+ assert entries[0].comment == mutation.comment()
+ assert entries[0].audit_reason == mutation.comment()
assert_log(fixture, 'success')
@@ -362,8 +392,7 @@ async def test_sql_intent_scrubs_target_id_without_changing_bindings(fixture, op
}
command = commands[operation]
command.trace_chain = [TraceNode(comment='what: mutate target 1001')]
- mutation = MutationRequest(command)
- mutation.comment = 'what: mutate target 1001'
+ mutation = MutationRequest(command, comment='what: mutate target 1001')
if failure:
with pytest.raises(TransportError):
await executor.mutate(context, mutation)
@@ -373,7 +402,7 @@ async def test_sql_intent_scrubs_target_id_without_changing_bindings(fixture, op
assert '1001' not in repr(entries[0].trace_path)
assert '1001' not in output[0].split('auditReason=', 1)[1].split('Debug SQL:', 1)[0]
assert any(getattr(value, 'val', value) == 1001 for value in transport.params)
- assert mutation.comment == 'what: mutate target 1001'
+ assert mutation.comment() == 'what: mutate target 1001'
def test_short_target_id_does_not_redact_structural_row_count(fixture):
@@ -419,7 +448,7 @@ def readback_request():
command = (UpdateCommand('Customer', Value.I64(1)).expected_version(1)
.value('display_name', 'Riverside').value('password_hash', 'PASSWORD-CANARY'))
command.trace_chain = [TraceNode(comment='what: update Riverside PASSWORD-CANARY')]
- return MutationRequest(command)
+ return MutationRequest(command, comment='what: update Riverside PASSWORD-CANARY')
@pytest.mark.asyncio
@@ -445,7 +474,11 @@ async def begin_sql(self):
assert tx.commits == 0
await target.commit(context)
assert tx.commits == 1 and tx.rollbacks == 0
- assert len(entries) == 1
+ assert len(entries) == 2
+ assert len(result.metadata.statements) == 2
+ assert entries[1].execution_outcome == 'success'
+ assert entries[1].result_count == 1
+ assert entries[1].trace_path[1].kind == 'request'
else:
with pytest.raises(BaseException) as caught:
await target.mutate(context, readback_request())
@@ -520,15 +553,17 @@ async def begin_sql(self):
return tx
context.insert_resource('dataService', SqlDataServiceExecutor(SqliteDialect(), Transport(), provider))
failure = RuntimeError('SECOND-READBACK-FAILURE')
- async def work():
- service = context.require_resource('dataService')
- await service.mutate(context, readback_request())
+ async def work(graph):
+ local = graph.context
+ service = local.require_resource('dataService')
+ await service.mutate(local, readback_request())
tx.failure = failure
- await service.mutate(context, readback_request())
- await service.mutate(context, readback_request())
+ await service.mutate(local, readback_request())
+ await service.mutate(local, readback_request())
with pytest.raises(RuntimeError) as caught:
- await context.execute_graph_save(work)
+ await context.execute_graph_save(work, comment='what: runtime regression fixture')
assert caught.value is failure
- assert [entry.execution_outcome for entry in entries] == ['success','success','failure']
+ assert [entry.execution_outcome for entry in entries] == ['success','success','success','failure']
+ assert [entry.trace_path[-1].name for entry in entries] == ['update','select','update','select']
assert tx.writes == tx.reads == 2 and tx.rollbacks == 1 and tx.commits == 0
assert 'Riverside' not in repr(context.sql_logs())
diff --git a/tests/runtime/test_sql_masking_policy.py b/tests/runtime/test_sql_masking_policy.py
index 8dc0462..b51800b 100644
--- a/tests/runtime/test_sql_masking_policy.py
+++ b/tests/runtime/test_sql_masking_policy.py
@@ -220,12 +220,12 @@ def capture(log):
context.set_diagnostic_sql_log_sink(SimpleNamespace(write=capture))
async def mutate(command):
command.trace_chain = [TraceNode(comment='what: verify mutation log policy')]
- return await service.mutate(context, MutationRequest(command))
+ return await service.mutate(context, MutationRequest(command, comment='what: runtime regression fixture'))
await mutate(InsertCommand('Customer').value('id', 1).value('version', 1)
.value('display_name', 'Riverside').value('active', True))
query = SelectQuery('Customer').filter(Expr.new_and(
Expr.eq('display_name', 'Riverside'), Expr.eq('active', True))).limit(1)
- rows = (await service.query(context, QueryRequest(query)
+ rows = (await service.query(context, QueryRequest(query, _comment='what: runtime regression fixture', _purpose='why: verify runtime behavior')
.comment('what: read Riverside').purpose('why: check field policy'))).rows
assert rows[0]['display_name'] == 'Riverside'
assert entries[-1].parameter_log_policies == ['masked', 'plain']
@@ -238,4 +238,6 @@ async def mutate(command):
assert 'LIMIT 1' in logs and '1 rows returned' in logs
assert 'Parameterized SQL:' not in logs and 'REDACTED SQL' not in logs
assert 'Riverside' not in repr(context.sql_logs())
- assert len(entries) == 4
+ assert len(entries) == 7
+ assert [entry.trace_path[-1].name for entry in entries] == [
+ 'insert', 'select', 'select', 'update', 'select', 'delete', 'select']