graphddb-runtime 0.2.1__tar.gz → 0.2.2__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 (37) hide show
  1. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/PKG-INFO +1 -1
  2. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime/runtime.py +71 -0
  3. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime.egg-info/PKG-INFO +1 -1
  4. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/pyproject.toml +1 -1
  5. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/tests/test_unit.py +48 -0
  6. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/README.md +0 -0
  7. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime/__init__.py +0 -0
  8. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime/async_runtime.py +0 -0
  9. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime/batch.py +0 -0
  10. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime/concurrency.py +0 -0
  11. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime/cursor.py +0 -0
  12. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime/errors.py +0 -0
  13. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime/filters.py +0 -0
  14. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime/hydration.py +0 -0
  15. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime/limits.py +0 -0
  16. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime/per_key_cursor.py +0 -0
  17. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime/relations.py +0 -0
  18. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime/templates.py +0 -0
  19. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime/transactions.py +0 -0
  20. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime.egg-info/SOURCES.txt +0 -0
  21. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime.egg-info/dependency_links.txt +0 -0
  22. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime.egg-info/requires.txt +0 -0
  23. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/graphddb_runtime.egg-info/top_level.txt +0 -0
  24. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/setup.cfg +0 -0
  25. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/tests/test_concurrency.py +0 -0
  26. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/tests/test_contract_runtime.py +0 -0
  27. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/tests/test_integration.py +0 -0
  28. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/tests/test_integration_command.py +0 -0
  29. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/tests/test_integration_compose.py +0 -0
  30. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/tests/test_integration_contract.py +0 -0
  31. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/tests/test_integration_edge_derive.py +0 -0
  32. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/tests/test_integration_edge_write.py +0 -0
  33. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/tests/test_integration_events.py +0 -0
  34. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/tests/test_integration_referential.py +0 -0
  35. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/tests/test_integration_relations.py +0 -0
  36. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/tests/test_integration_unique.py +0 -0
  37. {graphddb_runtime-0.2.1 → graphddb_runtime-0.2.2}/tests/test_relations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphddb-runtime
3
- Version: 0.2.1
3
+ Version: 0.2.2
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
@@ -1649,6 +1649,17 @@ class GraphDDBRuntime:
1649
1649
  values[v] = self._serializer.serialize(resolve_template(tmpl, params))
1650
1650
  sets.append(f"{n} = {v}")
1651
1651
 
1652
+ # GSI key re-derivation (issue #115): an UpdateItem that changes a GSI's
1653
+ # composing field MUST re-derive its `<index>PK`/`<index>SK` or the index
1654
+ # silently rots — exactly as the TS `buildUpdateInput` does. The available
1655
+ # values are the caller `params` (key fields ∪ change fields); a GSI is
1656
+ # affected when at least one of its template placeholders is a changed
1657
+ # field. An affected GSI whose composing fields are not all available
1658
+ # throws (mirroring the base-table partial-key throw).
1659
+ self._append_gsi_rederive_sets(
1660
+ command_id, spec, params, names, values, sets
1661
+ )
1662
+
1652
1663
  request: Dict[str, Any] = {
1653
1664
  "TableName": self._physical_table(spec["tableName"]),
1654
1665
  "Key": key,
@@ -1660,6 +1671,66 @@ class GraphDDBRuntime:
1660
1671
  self._apply_condition(spec, request, params)
1661
1672
  self._call(command_id, self._client.update_item, request)
1662
1673
 
1674
+ def _append_gsi_rederive_sets(
1675
+ self,
1676
+ command_id: str,
1677
+ spec: Mapping[str, Any],
1678
+ params: Mapping[str, Any],
1679
+ names: Dict[str, str],
1680
+ values: Dict[str, Any],
1681
+ sets: List[str],
1682
+ ) -> None:
1683
+ """Re-derive every affected GSI key into the UpdateExpression (issue #115).
1684
+
1685
+ Mirrors the TS ``buildUpdateInput``: a GSI is *affected* when at least one
1686
+ of its ``inputFields`` is a changed field; an affected GSI re-derives its
1687
+ ``<index>PK``/``<index>SK`` from the available caller params (key fields ∪
1688
+ change fields). An affected GSI whose ``inputFields`` are not all available
1689
+ throws — the SAME explicit error the TS path raises — rather than letting
1690
+ the index silently rot.
1691
+ """
1692
+ entity_meta = self._entities.get(spec.get("entity"), {})
1693
+ gsis = entity_meta.get("gsis", [])
1694
+ if not gsis:
1695
+ return
1696
+ changed = set(spec.get("changes", {}).keys())
1697
+ if not changed:
1698
+ return
1699
+ # Available value map: caller params with a non-None value.
1700
+ available = {k: v for k, v in params.items() if v is not None}
1701
+ gsi_index = 0
1702
+ for gsi in gsis:
1703
+ input_fields = list(gsi.get("inputFields", []))
1704
+ if not any(f in changed for f in input_fields):
1705
+ continue # unaffected GSI — never touched (non-regression).
1706
+ missing = [f for f in input_fields if f not in available]
1707
+ if missing:
1708
+ changed_in_gsi = [f for f in input_fields if f in changed]
1709
+ raise GraphDDBError(
1710
+ f"{command_id}: updating "
1711
+ f"{', '.join(repr(f) for f in changed_in_gsi)} affects index "
1712
+ f"'{gsi['indexName']}' (also depends on "
1713
+ f"{', '.join(repr(f) for f in missing)}); provide "
1714
+ f"{'them' if len(missing) > 1 else 'it'}, or use "
1715
+ f"read-modify-write."
1716
+ )
1717
+ index = gsi["indexName"]
1718
+ pk_tmpl = gsi.get("pkTemplate")
1719
+ sk_tmpl = gsi.get("skTemplate")
1720
+ if pk_tmpl is not None:
1721
+ n = f"#gsi{gsi_index}pk"
1722
+ v = f":gsi{gsi_index}pk"
1723
+ names[n] = f"{index}PK"
1724
+ values[v] = self._serializer.serialize(self._fill(pk_tmpl, available))
1725
+ sets.append(f"{n} = {v}")
1726
+ if sk_tmpl is not None:
1727
+ n = f"#gsi{gsi_index}sk"
1728
+ v = f":gsi{gsi_index}sk"
1729
+ names[n] = f"{index}SK"
1730
+ values[v] = self._serializer.serialize(self._fill(sk_tmpl, available))
1731
+ sets.append(f"{n} = {v}")
1732
+ gsi_index += 1
1733
+
1663
1734
  def _run_delete_item(
1664
1735
  self, command_id: str, spec: Mapping[str, Any], params: Mapping[str, Any]
1665
1736
  ) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphddb-runtime
3
- Version: 0.2.1
3
+ Version: 0.2.2
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.2.1"
7
+ version = "0.2.2"
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"
@@ -231,6 +231,54 @@ def test_update_item_builds_set_expression():
231
231
  assert plain(req["ExpressionAttributeValues"]) == {":c0": "disabled"}
232
232
 
233
233
 
234
+ def test_update_item_rederives_affected_gsi_key():
235
+ # #115: setOrderStatus changes `status` (a GSI1 composing field). With
236
+ # `tenantId` also supplied, GSI1's composing fields {tenantId, status, orderId}
237
+ # are all available, so the update re-derives GSI1PK/GSI1SK in the SAME
238
+ # UpdateExpression — the index follows the row instead of rotting.
239
+ client = FakeClient()
240
+ rt = make_runtime(client)
241
+ rt.execute_command(
242
+ "setOrderStatus",
243
+ {"orderId": "o1", "tenantId": "t1", "status": "SHIPPED"},
244
+ )
245
+ method, req = client.calls[0]
246
+ assert method == "update_item"
247
+ names = req["ExpressionAttributeNames"]
248
+ values = plain(req["ExpressionAttributeValues"])
249
+ # The status SET plus the re-derived GSI key attributes.
250
+ assert "status" in names.values()
251
+ gsi_pk_alias = next(a for a, n in names.items() if n == "GSI1PK")
252
+ gsi_sk_alias = next(a for a, n in names.items() if n == "GSI1SK")
253
+ assert f"{gsi_pk_alias} = " in req["UpdateExpression"]
254
+ pk_val_alias = req["UpdateExpression"].split(f"{gsi_pk_alias} = ")[1].split()[0].rstrip(",")
255
+ sk_val_alias = req["UpdateExpression"].split(f"{gsi_sk_alias} = ")[1].split()[0].rstrip(",")
256
+ assert values[pk_val_alias] == "TENANT#t1#STATUS#SHIPPED"
257
+ assert values[sk_val_alias] == "ORDER#o1"
258
+
259
+
260
+ def test_update_item_throws_when_gsi_field_unavailable():
261
+ # #115: status alone affects GSI1 but tenantId is missing → explicit throw.
262
+ from graphddb_runtime.errors import GraphDDBError
263
+
264
+ client = FakeClient()
265
+ rt = make_runtime(client)
266
+ with pytest.raises(GraphDDBError, match=r"affects index 'GSI1'.*depends on 'tenantId'"):
267
+ rt.execute_command("setOrderStatusBad", {"orderId": "o1", "status": "SHIPPED"})
268
+ assert client.calls == [] # nothing written
269
+
270
+
271
+ def test_update_item_does_not_touch_unaffected_gsi():
272
+ # #115 non-regression: disableUser changes `status` on a User whose GSI1 is
273
+ # keyed by `email` (status is NOT a composing field), so NO GSI re-derivation.
274
+ client = FakeClient()
275
+ rt = make_runtime(client)
276
+ rt.execute_command("disableUser", {"userId": "alice", "status": "disabled"})
277
+ _method, req = client.calls[0]
278
+ assert "GSI1PK" not in req["ExpressionAttributeNames"].values()
279
+ assert req["UpdateExpression"] == "SET #c0 = :c0"
280
+
281
+
234
282
  def test_put_item_injects_key_attributes():
235
283
  client = FakeClient()
236
284
  rt = make_runtime(client)