graphddb-runtime 0.7.10__tar.gz → 0.8.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/PKG-INFO +1 -1
  2. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/runtime.py +44 -12
  3. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime.egg-info/PKG-INFO +1 -1
  4. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/pyproject.toml +1 -1
  5. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_concurrency.py +7 -7
  6. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_description.py +2 -2
  7. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_description_jsdoc.py +1 -1
  8. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_integration.py +29 -29
  9. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_integration_edge_write.py +2 -2
  10. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_integration_middleware.py +11 -11
  11. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_integration_relations.py +9 -9
  12. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_middleware.py +44 -44
  13. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_relations.py +9 -9
  14. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_unit.py +46 -46
  15. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/README.md +0 -0
  16. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/__init__.py +0 -0
  17. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/async_runtime.py +0 -0
  18. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/batch.py +0 -0
  19. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/concurrency.py +0 -0
  20. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/cursor.py +0 -0
  21. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/errors.py +0 -0
  22. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/filters.py +0 -0
  23. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/hydration.py +0 -0
  24. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/limits.py +0 -0
  25. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/middleware.py +0 -0
  26. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/per_key_cursor.py +0 -0
  27. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/relations.py +0 -0
  28. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/templates.py +0 -0
  29. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime/transactions.py +0 -0
  30. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime.egg-info/SOURCES.txt +0 -0
  31. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime.egg-info/dependency_links.txt +0 -0
  32. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime.egg-info/requires.txt +0 -0
  33. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/graphddb_runtime.egg-info/top_level.txt +0 -0
  34. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/setup.cfg +0 -0
  35. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_contract_runtime.py +0 -0
  36. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_crosslang_python_cells.py +0 -0
  37. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_description_followups.py +0 -0
  38. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_guard_eval.py +0 -0
  39. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_integration_binding_contract.py +0 -0
  40. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_integration_command.py +0 -0
  41. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_integration_compose.py +0 -0
  42. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_integration_contract.py +0 -0
  43. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_integration_edge_derive.py +0 -0
  44. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_integration_events.py +0 -0
  45. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_integration_maintain.py +0 -0
  46. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_integration_referential.py +0 -0
  47. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_integration_unique.py +0 -0
  48. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_maintain.py +0 -0
  49. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_spec_version_guard.py +0 -0
  50. {graphddb_runtime-0.7.10 → graphddb_runtime-0.8.1}/tests/test_ttl.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphddb-runtime
3
- Version: 0.7.10
3
+ Version: 0.8.1
4
4
  Summary: Thin DynamoDB executor for GraphDDB-generated Python repositories (single-operation core, issue #44).
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -268,15 +268,15 @@ class GraphDDBRuntime:
268
268
  def use(self, mw: Any) -> None:
269
269
  """Register a host-side middleware (read R1–R5 / write W1–W5 hooks).
270
270
 
271
- Mirrors the TS ``DDBModel.use``: appended last, so it runs last in the
272
- FIFO ``before*`` order and first in the LIFO ``after`` / ``onError``
271
+ Mirrors the TS ``graphddb.config.use``: appended last, so it runs last in
272
+ the FIFO ``before*`` order and first in the LIFO ``after`` / ``onError``
273
273
  order. The middleware is never serialized.
274
274
  """
275
275
  self._middleware.use(mw)
276
276
 
277
277
  def clear_middleware(self) -> None:
278
278
  """Remove every registered middleware (teardown / tests). Mirrors the TS
279
- ``DDBModel.clearMiddleware``."""
279
+ ``graphddb.config.clearMiddleware``."""
280
280
  self._middleware.clear()
281
281
 
282
282
  def get_middleware(self) -> Tuple[Any, ...]:
@@ -589,6 +589,33 @@ class GraphDDBRuntime:
589
589
  opts = dict(params or {})
590
590
 
591
591
  is_array = isinstance(key_or_keys, (list, tuple))
592
+
593
+ # A **relation-traversal** contract read lowers to >1 operation (the root
594
+ # read + one leg per resolved relation; e.g. getUserByEmail → 4 ops,
595
+ # getGroupAccess → 3 ops). The single-op point/range executors below cannot
596
+ # assemble a multi-op result — that is the job of :meth:`execute_query`,
597
+ # which walks the whole ``operations`` list and merges the relation legs.
598
+ # So delegate a multi-op single-key read to the relation-assembly path (the
599
+ # exact path ``execute_query`` uses), keyed by the method's referenced
600
+ # operation id. An array of keys against such a method is an N+1 fan-out
601
+ # (one full traversal per key), forbidden by the same N+1 rule that rejects
602
+ # a range array — a single-key relation read is the only reachable shape.
603
+ if len(op_spec.get("operations", [])) > 1:
604
+ if is_array:
605
+ raise ContractArityError(
606
+ f"{contract_name}.{method_name}: a relation-traversal method "
607
+ f"(it lowers to multiple operations) was called with an array of "
608
+ f"keys. Each key is a full traversal, so an array would be an N+1 "
609
+ f"fan-out — forbidden by the contract's N+1 rule. Loop in "
610
+ f"application code for N independent reads."
611
+ )
612
+ validate_params(
613
+ key_or_keys,
614
+ op_spec.get("params", {}),
615
+ operation_id=f"{contract_name}.{method_name}",
616
+ )
617
+ return self.execute_query(method["operation"], key_or_keys, opts)
618
+
592
619
  if is_array:
593
620
  # Only a method whose ``inputArity`` is ``'either'`` coalesces a key
594
621
  # array into one batched call. A ``'single'`` method must reject an
@@ -676,12 +703,17 @@ class GraphDDBRuntime:
676
703
  def _contract_query_op(
677
704
  self, contract_name: str, method_name: str, method: Mapping[str, Any]
678
705
  ) -> Mapping[str, Any]:
679
- """The single referenced read op spec for a contract query method.
680
-
681
- A contract method resolves to exactly one declarative read op (#58). Any
682
- External Query compositions (#63) it declares are carried separately on
683
- the method spec (``compose``) and resolved by :meth:`_resolve_compositions`
684
- after the parent step they do **not** add operations to this op spec.
706
+ """The referenced read op spec for a contract query method.
707
+
708
+ A method's referenced operation carries **one or more** read operations: a
709
+ plain point/range read is a single operation, whereas a relation-traversal
710
+ read lowers to the root read PLUS one leg per resolved relation (e.g.
711
+ getUserByEmail 4 ops). ``execute_query_method`` routes a multi-op spec
712
+ through the relation-assembly path (:meth:`execute_query`); the single-op
713
+ point/range executors handle the ``len == 1`` case. Any External Query
714
+ compositions (#63) a method declares are carried separately on the method
715
+ spec (``compose``) — they do **not** add operations to this op spec (and a
716
+ composed child is always a ``point`` read, i.e. a single operation).
685
717
  """
686
718
  op_name = method["operation"]
687
719
  spec = self._queries.get(op_name)
@@ -691,10 +723,10 @@ class GraphDDBRuntime:
691
723
  f"not present in `queries`."
692
724
  )
693
725
  operations = spec.get("operations", [])
694
- if len(operations) != 1:
726
+ if len(operations) < 1:
695
727
  raise GraphDDBError(
696
- f"{contract_name}.{method_name}: a single-contract query method must "
697
- f"resolve to exactly one read operation, found {len(operations)}."
728
+ f"{contract_name}.{method_name}: a query method must resolve to at "
729
+ f"least one read operation, found {len(operations)}."
698
730
  )
699
731
  return spec
700
732
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphddb-runtime
3
- Version: 0.7.10
3
+ Version: 0.8.1
4
4
  Summary: Thin DynamoDB executor for GraphDDB-generated Python repositories (single-operation core, issue #44).
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "graphddb-runtime"
7
- version = "0.7.10"
7
+ version = "0.8.1"
8
8
  description = "Thin DynamoDB executor for GraphDDB-generated Python repositories (single-operation core, issue #44)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -166,7 +166,7 @@ def test_independent_relation_stage_runs_concurrently():
166
166
 
167
167
  # If the two sibling queries did NOT overlap, the barrier (timeout=3) would
168
168
  # raise BrokenBarrierError here. Reaching the assertions proves concurrency.
169
- out = rt.execute_query("getGroupAccess", {"groupId": "eng"})
169
+ out = rt.execute_query("GroupAccessMulti__getGroupAccess", {"groupId": "eng"})
170
170
 
171
171
  assert out["members"]["items"][0]["userId"] == "alice"
172
172
  assert out["permissions"]["items"][0]["resource"] == "codebase"
@@ -192,9 +192,9 @@ def test_stage_concurrency_is_bounded_by_the_declared_limit():
192
192
  client = _CountingGroupAccessClient()
193
193
  rt = GraphDDBRuntime(client, MULTI_MANIFEST, MULTI_OPS)
194
194
  # Force the declared bound to 1 on the loaded spec.
195
- rt._queries["getGroupAccess"]["executionPlan"]["concurrency"] = 1
195
+ rt._queries["GroupAccessMulti__getGroupAccess"]["executionPlan"]["concurrency"] = 1
196
196
 
197
- rt.execute_query("getGroupAccess", {"groupId": "eng"})
197
+ rt.execute_query("GroupAccessMulti__getGroupAccess", {"groupId": "eng"})
198
198
  # Bound 1 → strictly one query at a time.
199
199
  assert client.peak_in_flight == 1
200
200
  assert client.query_count == 2
@@ -215,9 +215,9 @@ def test_plan_absent_falls_back_to_sequential():
215
215
  client.barrier = threading.Barrier(1, timeout=3) # no overlap expected
216
216
  rt = GraphDDBRuntime(client, MULTI_MANIFEST, MULTI_OPS)
217
217
  # Strip the plan → exercise the backward-compatible (sequential) fallback.
218
- rt._queries["getGroupAccess"].pop("executionPlan", None)
218
+ rt._queries["GroupAccessMulti__getGroupAccess"].pop("executionPlan", None)
219
219
 
220
- out = rt.execute_query("getGroupAccess", {"groupId": "eng"})
220
+ out = rt.execute_query("GroupAccessMulti__getGroupAccess", {"groupId": "eng"})
221
221
  assert out["members"]["items"][0]["userId"] == "alice"
222
222
  assert out["permissions"]["items"][0]["resource"] == "codebase"
223
223
  # Plan-less → each op its own stage → strictly one query in flight at a time.
@@ -227,7 +227,7 @@ def test_plan_absent_falls_back_to_sequential():
227
227
  def test_relation_stages_reads_plan_groups():
228
228
  """Unit: `_relation_stages` derives stages from the plan (root dropped)."""
229
229
  rt = GraphDDBRuntime(_BarrierGroupAccessClient(), MULTI_MANIFEST, MULTI_OPS)
230
- spec = rt._queries["getGroupAccess"]
230
+ spec = rt._queries["GroupAccessMulti__getGroupAccess"]
231
231
  operations = spec["operations"]
232
232
  # Plan present: [[0],[1,2]] → relation stages [[1, 2]] (root index 0 dropped).
233
233
  assert rt._relation_stages(spec, operations) == [[1, 2]]
@@ -243,7 +243,7 @@ def test_relation_stages_reads_plan_groups():
243
243
  def test_deep_chain_plan_stages_are_sequential():
244
244
  """A result-dependent chain (getUserByEmail) stages into ordered single-op stages."""
245
245
  rt = GraphDDBRuntime(_BarrierGroupAccessClient(), MULTI_MANIFEST, MULTI_OPS)
246
- spec = rt._queries["getUserByEmail"]
246
+ spec = rt._queries["UserByEmailMulti__getUserByEmail"]
247
247
  # Plan [[0],[1],[2],[3]] → relation stages [[1],[2],[3]] (each depends on prior).
248
248
  assert rt._relation_stages(spec, spec["operations"]) == [[1], [2], [3]]
249
249
 
@@ -38,7 +38,7 @@ def test_manifest_carries_entity_and_field_description() -> None:
38
38
 
39
39
  def test_operations_carries_query_param_and_contract_description() -> None:
40
40
  ops = _operations()
41
- get_group = ops["queries"]["getGroup"]
41
+ get_group = ops["queries"]["GroupByIdRead__get"]
42
42
  assert get_group["description"] == "Fetch a single group by its id."
43
43
  assert get_group["params"]["groupId"]["description"] == "The group id to fetch."
44
44
  # The contract method description rides the `contracts` map.
@@ -48,7 +48,7 @@ def test_operations_carries_query_param_and_contract_description() -> None:
48
48
 
49
49
  def test_undescribed_query_has_no_description_key() -> None:
50
50
  # `getUser` declares no description — backward compatible.
51
- get_user = _operations()["queries"]["getUser"]
51
+ get_user = _operations()["queries"]["UserByIdRead__get"]
52
52
  assert "description" not in get_user
53
53
 
54
54
 
@@ -63,7 +63,7 @@ def test_manifest_carries_jsdoc_gsi_description() -> None:
63
63
 
64
64
 
65
65
  def test_operations_carry_jsdoc_query_and_param_description() -> None:
66
- get_user = _operations()["queries"]["getUserDocumented"]
66
+ get_user = _operations()["queries"]["UserDocumentedRead__getUserDocumented"]
67
67
  assert get_user["description"] == (
68
68
  "#165 JSDoc-as-default (query): fetch a single user by id (JSDoc source)."
69
69
  )
@@ -155,11 +155,11 @@ def test_command_put_then_query_then_delete(client, repos):
155
155
  membership = repos["membership"]
156
156
 
157
157
  # put (generated command, key attributes injected by the runtime)
158
- membership.add_group_member(group_id="eng", user_id="alice", role="admin")
159
- membership.add_group_member(group_id="eng", user_id="bob", role="member")
158
+ membership.membership_conf_commands__add_group_member(group_id="eng", user_id="alice", role="admin")
159
+ membership.membership_conf_commands__add_group_member(group_id="eng", user_id="bob", role="member")
160
160
 
161
161
  # query (generated list query, single Query op on the base partition)
162
- result = repos["membership"].list_group_members(group_id="eng")
162
+ result = repos["membership"].members_by_group_read__list(group_id="eng")
163
163
  assert result is not None
164
164
  by_user = {m["userId"]: m for m in result["items"]}
165
165
  assert set(by_user) == {"alice", "bob"}
@@ -168,17 +168,17 @@ def test_command_put_then_query_then_delete(client, repos):
168
168
  assert by_user["alice"]["joinedAt"] == datetime(1970, 1, 1, tzinfo=timezone.utc)
169
169
 
170
170
  # delete one member, confirm it is gone
171
- membership.delete_group_member(group_id="eng", user_id="bob")
172
- after = repos["membership"].list_group_members(group_id="eng")
171
+ membership.membership_conf_commands__delete_group_member(group_id="eng", user_id="bob")
172
+ after = repos["membership"].members_by_group_read__list(group_id="eng")
173
173
  assert {m["userId"] for m in after["items"]} == {"alice"}
174
174
 
175
175
  # cleanup
176
- membership.delete_group_member(group_id="eng", user_id="alice")
176
+ membership.membership_conf_commands__delete_group_member(group_id="eng", user_id="alice")
177
177
 
178
178
 
179
179
  def test_get_item_on_base_table(client, repos):
180
180
  _put_group(client, "product", "Product")
181
- out = repos["group"].get_group(group_id="product")
181
+ out = repos["group"].group_by_id_read__get(group_id="product")
182
182
  assert out is not None
183
183
  assert out["groupId"] == "product"
184
184
  assert out["name"] == "Product"
@@ -188,12 +188,12 @@ def test_get_item_on_base_table(client, repos):
188
188
 
189
189
 
190
190
  def test_get_item_missing_returns_none(repos):
191
- assert repos["group"].get_group(group_id="does-not-exist") is None
191
+ assert repos["group"].group_by_id_read__get(group_id="does-not-exist") is None
192
192
 
193
193
 
194
194
  def test_query_on_unique_gsi(client, repos):
195
195
  _put_user(client, "carol", "carol@example.com")
196
- out = repos["user"].get_user_by_email(email="carol@example.com")
196
+ out = repos["user"].user_by_email_read__get(email="carol@example.com")
197
197
  # getUserByEmail is a relation-free cardinality-one Query: a single object
198
198
  # (matching the generated flat GetUserByEmailResult and the TS executor),
199
199
  # not a {items, cursor} connection.
@@ -204,8 +204,8 @@ def test_query_on_unique_gsi(client, repos):
204
204
 
205
205
  def test_update_item_round_trip(client, repos):
206
206
  _put_user(client, "dave", "dave@example.com", status="active")
207
- repos["user"].disable_user(status="disabled", user_id="dave")
208
- out = repos["user"].get_user(user_id="dave")
207
+ repos["user"].user_conf_commands__disable_user(status="disabled", user_id="dave")
208
+ out = repos["user"].user_by_id_read__get(user_id="dave")
209
209
  assert out["status"] == "disabled"
210
210
 
211
211
 
@@ -220,18 +220,18 @@ def test_conditional_put_succeeds_then_fails_on_existing(client, repos):
220
220
  from graphddb_runtime import OperationExecutionError
221
221
 
222
222
  runtime = repos["runtime"]
223
- runtime._commands["addGroupMember"]["condition"] = {"kind": "notExists"}
223
+ runtime._commands["MembershipConfCommands__addGroupMember"]["condition"] = {"kind": "notExists"}
224
224
  try:
225
225
  runtime.execute_command(
226
- "addGroupMember", {"groupId": "cond", "userId": "once", "role": "member"}
226
+ "MembershipConfCommands__addGroupMember", {"groupId": "cond", "userId": "once", "role": "member"}
227
227
  )
228
228
  with pytest.raises(OperationExecutionError):
229
229
  runtime.execute_command(
230
- "addGroupMember",
230
+ "MembershipConfCommands__addGroupMember",
231
231
  {"groupId": "cond", "userId": "once", "role": "member"},
232
232
  )
233
233
  finally:
234
- runtime._commands["addGroupMember"].pop("condition", None)
234
+ runtime._commands["MembershipConfCommands__addGroupMember"].pop("condition", None)
235
235
  client.delete_item(
236
236
  TableName=TABLE,
237
237
  Key={"PK": {"S": "GROUP#cond"}, "SK": {"S": "USER#once"}},
@@ -250,32 +250,32 @@ def test_conditional_update_equality_matches_then_mismatches(client, repos):
250
250
  runtime = repos["runtime"]
251
251
  _put_user(client, "condu", "condu@example.com", status="active")
252
252
  # Condition: only disable when the current status equals the param `expect`.
253
- runtime._commands["disableUser"]["condition"] = {
253
+ runtime._commands["UserConfCommands__disableUser"]["condition"] = {
254
254
  "kind": "equals",
255
255
  "fields": {"status": "{expect}"},
256
256
  }
257
257
  # Declare the extra param so validate_params accepts `expect`.
258
- runtime._commands["disableUser"]["params"]["expect"] = {
258
+ runtime._commands["UserConfCommands__disableUser"]["params"]["expect"] = {
259
259
  "type": "string",
260
260
  "required": True,
261
261
  }
262
262
  try:
263
263
  # Condition expects current status 'active' → matches → succeeds.
264
264
  runtime.execute_command(
265
- "disableUser",
265
+ "UserConfCommands__disableUser",
266
266
  {"userId": "condu", "status": "disabled", "expect": "active"},
267
267
  )
268
- out = repos["user"].get_user(user_id="condu")
268
+ out = repos["user"].user_by_id_read__get(user_id="condu")
269
269
  assert out["status"] == "disabled"
270
270
  # The row's status is now 'disabled', so expecting 'active' must fail.
271
271
  with pytest.raises(OperationExecutionError):
272
272
  runtime.execute_command(
273
- "disableUser",
273
+ "UserConfCommands__disableUser",
274
274
  {"userId": "condu", "status": "disabled", "expect": "active"},
275
275
  )
276
276
  finally:
277
- runtime._commands["disableUser"].pop("condition", None)
278
- runtime._commands["disableUser"]["params"].pop("expect", None)
277
+ runtime._commands["UserConfCommands__disableUser"].pop("condition", None)
278
+ runtime._commands["UserConfCommands__disableUser"]["params"].pop("expect", None)
279
279
  client.delete_item(
280
280
  TableName=TABLE,
281
281
  Key={"PK": {"S": "USER#condu"}, "SK": {"S": "PROFILE"}},
@@ -297,7 +297,7 @@ def test_transaction_static_round_trip(client, repos):
297
297
  ConsistentRead=True,
298
298
  )["Item"]
299
299
  assert member["role"]["S"] == "admin"
300
- group = repos["group"].get_group(group_id="txg")
300
+ group = repos["group"].group_by_id_read__get(group_id="txg")
301
301
  assert group["name"] == "admin"
302
302
  # cleanup
303
303
  client.delete_item(
@@ -322,7 +322,7 @@ def test_transaction_foreach_round_trip(client, repos):
322
322
  ],
323
323
  },
324
324
  )
325
- members = repos["membership"].list_group_members(group_id="txm")
325
+ members = repos["membership"].members_by_group_read__list(group_id="txm")
326
326
  assert {m["userId"] for m in members["items"]} == {"a", "c"}
327
327
  for uid in ("a", "c"):
328
328
  client.delete_item(
@@ -366,7 +366,7 @@ def test_transaction_rolls_back_on_condition_failure(client, repos):
366
366
  {"groupId": "txr", "userId": "txu", "role": "admin"},
367
367
  )
368
368
  # The group name must be unchanged (whole tx rolled back).
369
- group = repos["group"].get_group(group_id="txr")
369
+ group = repos["group"].group_by_id_read__get(group_id="txr")
370
370
  assert group["name"] == "Original"
371
371
  finally:
372
372
  spec["items"][0].pop("condition", None)
@@ -396,7 +396,7 @@ def test_transaction_condition_check_commits_when_target_exists(client, repos):
396
396
  assert member is not None
397
397
  assert member["role"]["S"] == "member"
398
398
  # The asserted group row is untouched (ConditionCheck mutates nothing).
399
- group = repos["group"].get_group(group_id="ccok")
399
+ group = repos["group"].group_by_id_read__get(group_id="ccok")
400
400
  assert group["name"] == "CcOkGroup"
401
401
  # cleanup
402
402
  client.delete_item(
@@ -442,12 +442,12 @@ def test_query_pagination_cursor(client, repos):
442
442
  # Seed three memberships under a fresh group and page with limit-less query;
443
443
  # then page explicitly using the runtime cursor to confirm round-trip.
444
444
  for uid in ("u1", "u2", "u3"):
445
- repos["membership"].add_group_member(group_id="pag", user_id=uid, role="member")
445
+ repos["membership"].membership_conf_commands__add_group_member(group_id="pag", user_id=uid, role="member")
446
446
 
447
447
  runtime = repos["runtime"]
448
- page1 = runtime.execute_query("listGroupMembers", {"groupId": "pag"})
448
+ page1 = runtime.execute_query("MembersByGroupRead__list", {"groupId": "pag"})
449
449
  # DynamoDB Local returns all small items in one page; cursor may be None.
450
450
  assert {m["userId"] for m in page1["items"]} == {"u1", "u2", "u3"}
451
451
 
452
452
  for uid in ("u1", "u2", "u3"):
453
- repos["membership"].delete_group_member(group_id="pag", user_id=uid)
453
+ repos["membership"].membership_conf_commands__delete_group_member(group_id="pag", user_id=uid)
@@ -178,12 +178,12 @@ def _raw(client, pk, sk):
178
178
 
179
179
 
180
180
  def _members(runtime, group_id):
181
- out = runtime.execute_query("getGroupMembers", {"groupId": group_id})
181
+ out = runtime.execute_query("GroupMembersMulti__getGroupMembers", {"groupId": group_id})
182
182
  return {m["userId"] for m in out["members"]["items"]}
183
183
 
184
184
 
185
185
  def _groups(runtime, user_id):
186
- out = runtime.execute_query("getUserGroups", {"userId": user_id})
186
+ out = runtime.execute_query("UserGroupsMulti__getUserGroups", {"userId": user_id})
187
187
  return {g["groupId"] for g in out["groups"]["items"]}
188
188
 
189
189
 
@@ -167,7 +167,7 @@ def test_r2_filter_narrows_permissions_fan_out_end_to_end(multi_rt):
167
167
  ctx.operation["ExpressionAttributeValues"][":mwa"] = {"S": "read"}
168
168
 
169
169
  multi_rt.use({"read": {"op": {"before": op_before}}})
170
- out = multi_rt.execute_query("getGroupAccess", {"groupId": "eng"})
170
+ out = multi_rt.execute_query("GroupAccessMulti__getGroupAccess", {"groupId": "eng"})
171
171
  actions = sorted(p["action"] for p in out["permissions"]["items"])
172
172
  assert actions == ["read"] # 'write' filtered out by DynamoDB
173
173
  # Members are untouched (the filter scoped only the permissions leg).
@@ -176,7 +176,7 @@ def test_r2_filter_narrows_permissions_fan_out_end_to_end(multi_rt):
176
176
 
177
177
  def test_r4_transforms_final_result_end_to_end(multi_rt):
178
178
  multi_rt.use({"read": {"afterFetch": lambda ctx, r: {**r, "redacted": True}}})
179
- out = multi_rt.execute_query("getGroupAccess", {"groupId": "eng"})
179
+ out = multi_rt.execute_query("GroupAccessMulti__getGroupAccess", {"groupId": "eng"})
180
180
  assert out["redacted"] is True
181
181
  assert out["groupId"] == "eng"
182
182
 
@@ -192,7 +192,7 @@ def test_r5_op_recovery_substitutes_failed_fan_out_end_to_end(multi_rt):
192
192
  return None
193
193
 
194
194
  multi_rt.use({"read": {"op": {"before": op_before, "onError": op_error}}})
195
- out = multi_rt.execute_query("getGroupAccess", {"groupId": "eng"})
195
+ out = multi_rt.execute_query("GroupAccessMulti__getGroupAccess", {"groupId": "eng"})
196
196
  assert [m["userId"] for m in out["members"]["items"]] == ["fallback"]
197
197
 
198
198
 
@@ -202,7 +202,7 @@ def test_r5_op_recovery_substitutes_failed_fan_out_end_to_end(multi_rt):
202
202
  def test_w1_soft_delete_rewrite_stores_update_row_survives(std_rt):
203
203
  # Seed a membership to soft-delete.
204
204
  std_rt.execute_command(
205
- "addGroupMember", {"groupId": "eng", "userId": "carol", "role": "member"}
205
+ "MembershipConfCommands__addGroupMember", {"groupId": "eng", "userId": "carol", "role": "member"}
206
206
  )
207
207
 
208
208
  def before(ctx):
@@ -211,7 +211,7 @@ def test_w1_soft_delete_rewrite_stores_update_row_survives(std_rt):
211
211
  ctx.input["changes"] = {"role": "SOFT-DELETED"}
212
212
 
213
213
  std_rt.use({"write": {"before": before}})
214
- std_rt.execute_command("deleteGroupMember", {"groupId": "eng", "userId": "carol"})
214
+ std_rt.execute_command("MembershipConfCommands__deleteGroupMember", {"groupId": "eng", "userId": "carol"})
215
215
 
216
216
  # The row SURVIVES (soft-deleted): a delete would have removed it.
217
217
  raw = std_rt._client.get_item(
@@ -233,7 +233,7 @@ def test_w3_mutation_persisted_on_real_write(std_rt):
233
233
 
234
234
  std_rt.use({"write": {"persist": {"before": persist_before}}})
235
235
  std_rt.execute_command(
236
- "addGroupMember", {"groupId": "eng", "userId": "dave", "role": "member"}
236
+ "MembershipConfCommands__addGroupMember", {"groupId": "eng", "userId": "dave", "role": "member"}
237
237
  )
238
238
  raw = std_rt._client.get_item(
239
239
  TableName=TABLE,
@@ -255,7 +255,7 @@ def test_w5_persist_recovery_suppresses_failure(std_rt):
255
255
  }}})
256
256
  # Recovery suppresses the throw; the row is NOT written (send never ran).
257
257
  std_rt.execute_command(
258
- "addGroupMember", {"groupId": "eng", "userId": "erin", "role": "member"}
258
+ "MembershipConfCommands__addGroupMember", {"groupId": "eng", "userId": "erin", "role": "member"}
259
259
  )
260
260
  raw = std_rt._client.get_item(
261
261
  TableName=TABLE,
@@ -272,12 +272,12 @@ def test_w2_put_real_old_image_end_to_end(std_rt):
272
272
  # overwrites it. W2 sees the exact prior row as oldImage (real ALL_OLD from
273
273
  # DynamoDB Local) and the written item as newImage.
274
274
  std_rt.execute_command(
275
- "addGroupMember", {"groupId": "eng", "userId": "frank", "role": "member"}
275
+ "MembershipConfCommands__addGroupMember", {"groupId": "eng", "userId": "frank", "role": "member"}
276
276
  )
277
277
  captured = {}
278
278
  std_rt.use({"write": {"after": lambda ctx, change: captured.update(change)}})
279
279
  std_rt.execute_command(
280
- "addGroupMember", {"groupId": "eng", "userId": "frank", "role": "admin"}
280
+ "MembershipConfCommands__addGroupMember", {"groupId": "eng", "userId": "frank", "role": "admin"}
281
281
  )
282
282
  assert captured["oldImage"]["role"] == "member" # real pre-write row
283
283
  assert captured["newImage"]["role"] == "admin" # the written item
@@ -285,11 +285,11 @@ def test_w2_put_real_old_image_end_to_end(std_rt):
285
285
 
286
286
  def test_w2_delete_real_old_image_end_to_end(std_rt):
287
287
  std_rt.execute_command(
288
- "addGroupMember", {"groupId": "eng", "userId": "gina", "role": "member"}
288
+ "MembershipConfCommands__addGroupMember", {"groupId": "eng", "userId": "gina", "role": "member"}
289
289
  )
290
290
  captured = {}
291
291
  std_rt.use({"write": {"after": lambda ctx, change: captured.update(seen=change)}})
292
- std_rt.execute_command("deleteGroupMember", {"groupId": "eng", "userId": "gina"})
292
+ std_rt.execute_command("MembershipConfCommands__deleteGroupMember", {"groupId": "eng", "userId": "gina"})
293
293
  change = captured["seen"]
294
294
  assert change["oldImage"]["userId"] == "gina"
295
295
  assert change["oldImage"]["role"] == "member"
@@ -227,7 +227,7 @@ def env(seeded):
227
227
 
228
228
 
229
229
  def test_get_group_access_members_and_permissions(env):
230
- out = env["group"].get_group_access(group_id="eng")
230
+ out = env["group"].group_access_multi__get_group_access(group_id="eng")
231
231
  assert out is not None
232
232
  assert out["groupId"] == "eng"
233
233
  assert out["name"] == "Engineering"
@@ -249,7 +249,7 @@ def test_get_group_access_members_and_permissions(env):
249
249
 
250
250
 
251
251
  def test_get_user_by_email_deep_traversal(env):
252
- out = env["user"].get_user_by_email(email="alice@example.com")
252
+ out = env["user"].user_by_email_multi__get_user_by_email(email="alice@example.com")
253
253
  assert out is not None
254
254
  # cardinality 'one' → root is the User object, not a connection.
255
255
  assert out["userId"] == "alice"
@@ -276,7 +276,7 @@ def test_get_user_by_email_deep_traversal(env):
276
276
  def test_belongs_to_resolution_is_single_batch_get_no_n_plus_one(env):
277
277
  spy = env["spy"]
278
278
  spy.reset()
279
- env["user"].get_user_by_email(email="alice@example.com")
279
+ env["user"].user_by_email_multi__get_user_by_email(email="alice@example.com")
280
280
 
281
281
  # The belongsTo parent-Group resolution for the 2 memberships is ONE
282
282
  # BatchGetItem, and NOT a single individual GetItem (N+1 not issued).
@@ -287,7 +287,7 @@ def test_belongs_to_resolution_is_single_batch_get_no_n_plus_one(env):
287
287
  def test_user_with_no_groups_returns_empty_connection(env):
288
288
  # bob is seeded; a fresh user with no memberships still resolves cleanly.
289
289
  env["runtime"] # warm
290
- out = env["user"].get_user_by_email(email="bob@example.com")
290
+ out = env["user"].user_by_email_multi__get_user_by_email(email="bob@example.com")
291
291
  groups = {g["groupId"]: g for g in out["groups"]["items"]}
292
292
  assert set(groups) == {"eng", "product"}
293
293
 
@@ -304,7 +304,7 @@ def test_depth_limit_exceeded_is_runtime_error(seeded):
304
304
  )
305
305
  bridge = _load_generated_package()
306
306
  with pytest.raises(LimitExceededError):
307
- bridge.UserRepository(runtime).get_user_by_email(email="alice@example.com")
307
+ bridge.UserRepository(runtime).user_by_email_multi__get_user_by_email(email="alice@example.com")
308
308
 
309
309
 
310
310
  def test_operations_limit_exceeded_is_runtime_error(seeded):
@@ -316,7 +316,7 @@ def test_operations_limit_exceeded_is_runtime_error(seeded):
316
316
  )
317
317
  bridge = _load_generated_package()
318
318
  with pytest.raises(LimitExceededError):
319
- bridge.UserRepository(runtime).get_user_by_email(email="alice@example.com")
319
+ bridge.UserRepository(runtime).user_by_email_multi__get_user_by_email(email="alice@example.com")
320
320
 
321
321
 
322
322
  def test_explain_lists_operations_without_touching_dynamodb(seeded):
@@ -327,7 +327,7 @@ def test_explain_lists_operations_without_touching_dynamodb(seeded):
327
327
  operations_path=MULTI_OPS,
328
328
  limits=RuntimeLimits(max_depth=3),
329
329
  )
330
- plan = runtime.explain("getUserByEmail", {"email": "alice@example.com"})
330
+ plan = runtime.explain("UserByEmailMulti__getUserByEmail", {"email": "alice@example.com"})
331
331
  assert len(plan["operations"]) == 4
332
332
  assert plan["operations"][0]["keyCondition"]["GSI1PK"] == "EMAIL#alice@example.com"
333
333
  assert plan["operations"][2]["type"] == "BatchGetItem"
@@ -342,7 +342,7 @@ def test_refs_fan_out_resolves_inline_id_list_in_one_batch_get(env):
342
342
  repo = bridge.ArticleRepository(env["runtime"])
343
343
  env["spy"].reset()
344
344
 
345
- out = repo.get_article_with_tags(article_id="a1")
345
+ out = repo.article_with_tags_multi__get_article_with_tags(article_id="a1")
346
346
  assert out is not None
347
347
  assert out["articleId"] == "a1"
348
348
  assert out["title"] == "Refs!"
@@ -368,7 +368,7 @@ def test_refs_empty_list_resolves_empty_connection_without_batch_get(env):
368
368
  repo = bridge.ArticleRepository(env["runtime"])
369
369
  env["spy"].reset()
370
370
 
371
- out = repo.get_article_with_tags(article_id="a2")
371
+ out = repo.article_with_tags_multi__get_article_with_tags(article_id="a2")
372
372
  assert out is not None
373
373
  assert out["tags"] == {"items": [], "cursor": None}
374
374
  assert "tagRefs" not in out