graphddb-runtime 1.0.0__tar.gz → 1.0.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 (56) hide show
  1. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/PKG-INFO +1 -1
  2. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/filters.py +12 -0
  3. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/runtime.py +33 -0
  4. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/transactions.py +10 -0
  5. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime.egg-info/PKG-INFO +1 -1
  6. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/pyproject.toml +1 -1
  7. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_atomic_add_command.py +16 -0
  8. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_unit.py +8 -4
  9. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/README.md +0 -0
  10. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/__init__.py +0 -0
  11. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/async_runtime.py +0 -0
  12. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/batch.py +0 -0
  13. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/behavior_exec.py +0 -0
  14. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/concurrency.py +0 -0
  15. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/cursor.py +0 -0
  16. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/errors.py +0 -0
  17. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/handler_registry.py +0 -0
  18. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/hydration.py +0 -0
  19. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/limits.py +0 -0
  20. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/middleware.py +0 -0
  21. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/per_key_cursor.py +0 -0
  22. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/templates.py +0 -0
  23. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime.egg-info/SOURCES.txt +0 -0
  24. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime.egg-info/dependency_links.txt +0 -0
  25. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime.egg-info/requires.txt +0 -0
  26. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime.egg-info/top_level.txt +0 -0
  27. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/setup.cfg +0 -0
  28. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_concurrency.py +0 -0
  29. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_contract_runtime.py +0 -0
  30. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_crosslang_python_cells.py +0 -0
  31. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_description.py +0 -0
  32. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_description_followups.py +0 -0
  33. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_description_jsdoc.py +0 -0
  34. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_error_policy.py +0 -0
  35. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_generated_module.py +0 -0
  36. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_guard_eval.py +0 -0
  37. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_handler_registry.py +0 -0
  38. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration.py +0 -0
  39. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_batch_write.py +0 -0
  40. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_binding_contract.py +0 -0
  41. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_command.py +0 -0
  42. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_compose.py +0 -0
  43. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_contract.py +0 -0
  44. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_edge_derive.py +0 -0
  45. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_edge_write.py +0 -0
  46. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_events.py +0 -0
  47. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_maintain.py +0 -0
  48. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_middleware.py +0 -0
  49. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_referential.py +0 -0
  50. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_relations.py +0 -0
  51. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_unique.py +0 -0
  52. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_maintain.py +0 -0
  53. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_middleware.py +0 -0
  54. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_relations.py +0 -0
  55. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_spec_version_guard.py +0 -0
  56. {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_ttl.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphddb-runtime
3
- Version: 1.0.0
3
+ Version: 1.0.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
@@ -148,6 +148,18 @@ def _compile_field(ctx: _Ctx, field: str, condition: Any) -> str:
148
148
 
149
149
 
150
150
  def _compile_node(ctx: _Ctx, node: Any) -> str:
151
+ # Structural primitives (notExists / attributeExists / attributeNotExists) may be
152
+ # nested inside an and/or/not group, not only at the top level (issue #330). Render
153
+ # them here; otherwise the loop treats them as a field named "attributeNotExists"
154
+ # with an equality operand — a never-satisfied clause.
155
+ if node.get("notExists") is True:
156
+ return f"attribute_not_exists({ctx.name_alias('PK')})"
157
+ ae = node.get("attributeExists")
158
+ if isinstance(ae, str) and ae:
159
+ return f"attribute_exists({ctx.name_alias(ae)})"
160
+ ane = node.get("attributeNotExists")
161
+ if isinstance(ane, str) and ane:
162
+ return f"attribute_not_exists({ctx.name_alias(ane)})"
151
163
  clauses: List[str] = []
152
164
  for key, value in node.items():
153
165
  if value is None:
@@ -218,6 +218,14 @@ def _resolve_condition_tree(node: Any, params: Mapping[str, Any]) -> Any:
218
218
  typed caller-param values, and returns a tree shaped exactly like a
219
219
  declarative ``filter`` so :func:`compile_filter` compiles it identically.
220
220
  """
221
+ # Structural primitives (notExists / attributeExists / attributeNotExists) may be
222
+ # nested in an and/or/not group (issue #330); pass them through verbatim.
223
+ if node.get("notExists") is True:
224
+ return {"notExists": True}
225
+ if isinstance(node.get("attributeExists"), str):
226
+ return {"attributeExists": node["attributeExists"]}
227
+ if isinstance(node.get("attributeNotExists"), str):
228
+ return {"attributeNotExists": node["attributeNotExists"]}
221
229
  out: Dict[str, Any] = {}
222
230
  for key, value in node.items():
223
231
  if key in ("and", "or"):
@@ -2319,6 +2327,31 @@ class GraphDDBRuntime:
2319
2327
  command_id, spec, params, names, values, sets
2320
2328
  )
2321
2329
 
2330
+ # #333: an UpdateItem that CREATES a row (create-if-absent) must carry the
2331
+ # model's key-source field attributes, like PutItem does. SET each field a
2332
+ # `keyCondition` template references from the command's key params. A
2333
+ # key-source field is immutable for a given key, so a plain SET to the
2334
+ # key-derived value is a safe no-op on existing rows and populates it on
2335
+ # create. Without this, a row first created by an update lacks e.g. `child`,
2336
+ # so a projection of that field returns empty. (Skipped when add_only — the
2337
+ # #301 fallback only fires on rows that already exist.)
2338
+ seen_ks: set = set()
2339
+ for tmpl in spec["keyCondition"].values():
2340
+ for m in _PLACEHOLDER_RE.finditer(str(tmpl)):
2341
+ field = m.group(0)[1:-1]
2342
+ if "." in field or field in seen_ks or field not in params:
2343
+ continue
2344
+ raw = params[field]
2345
+ if raw is None:
2346
+ continue
2347
+ seen_ks.add(field)
2348
+ idx = len(seen_ks) - 1
2349
+ n = f"#ks{idx}"
2350
+ v = f":ks{idx}"
2351
+ names[n] = field
2352
+ values[v] = self._serializer.serialize(raw)
2353
+ sets.append(f"{n} = {v}")
2354
+
2322
2355
  # #301: atomic-`ADD` clauses (`ADD #a :d`, a caller `param.number()` delta).
2323
2356
  # Built AFTER the SET so an ADD-only expression (`add_only`, or an update
2324
2357
  # whose SET change set is empty) is legal — DynamoDB's `SET … ADD …` is one
@@ -108,6 +108,16 @@ def _resolve_condition_tree(
108
108
  Mirrors the runtime's resolver but element-aware, producing a tree shaped
109
109
  like a declarative ``filter`` for :func:`compile_filter`.
110
110
  """
111
+ # Structural primitives (notExists / attributeExists / attributeNotExists) carry a
112
+ # static field name / flag and no param leaf; they may be nested in an and/or/not
113
+ # group (issue #330). Pass them through verbatim, else the default branch iterates
114
+ # the string field name as operator entries.
115
+ if node.get("notExists") is True:
116
+ return {"notExists": True}
117
+ if isinstance(node.get("attributeExists"), str):
118
+ return {"attributeExists": node["attributeExists"]}
119
+ if isinstance(node.get("attributeNotExists"), str):
120
+ return {"attributeNotExists": node["attributeNotExists"]}
111
121
  out: Dict[str, Any] = {}
112
122
  for key, value in node.items():
113
123
  if key in ("and", "or"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphddb-runtime
3
- Version: 1.0.0
3
+ Version: 1.0.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 = "1.0.0"
7
+ version = "1.0.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"
@@ -168,6 +168,22 @@ def test_mark_fallback_add_only_on_condition_fail():
168
168
  assert any(v == {"N": "1"} for v in fvals.values()), fvals
169
169
 
170
170
 
171
+ def test_mark_stores_key_source_fields():
172
+ """#333: an UpdateItem that CREATES the row must persist the key-source field
173
+ attributes (part/child) via a plain SET — like PutItem — so a row first created
174
+ by the mark carries them and a projection of e.g. ``child`` is not empty."""
175
+ client = _MarkClient(fail_first=False)
176
+ rt = _mark_runtime(client)
177
+
178
+ rt.execute_command("StatsTreeCmd__mark", _MARK_PARAMS)
179
+
180
+ req = client.updates[0]
181
+ assert _has_set_clause(req["UpdateExpression"]), req["UpdateExpression"]
182
+ names = set(req["ExpressionAttributeNames"].values())
183
+ for field in ("part", "child"):
184
+ assert field in names, f"key-source field {field!r} must be SET (#333); {names}"
185
+
186
+
171
187
  def test_mark_non_ccf_error_propagates():
172
188
  """A NON-condition error on the primary must NOT be swallowed by the two-stage —
173
189
  it propagates and no ADD-only fallback fires. Parity with the Rust
@@ -253,9 +253,12 @@ def test_update_item_builds_set_expression():
253
253
  method, req = client.calls[0]
254
254
  assert method == "update_item"
255
255
  assert plain(req["Key"]) == {"PK": "USER#alice", "SK": "PROFILE"}
256
- assert req["UpdateExpression"] == "SET #c0 = :c0"
257
- assert req["ExpressionAttributeNames"] == {"#c0": "status"}
258
- assert plain(req["ExpressionAttributeValues"]) == {":c0": "disabled"}
256
+ # #333: the update also SETs the key-source field(s) the PK/SK templates
257
+ # reference (here `userId`, from `USER#{userId}`) so a row first created by this
258
+ # update carries them like a PutItem would — a no-op on an existing row.
259
+ assert req["UpdateExpression"] == "SET #c0 = :c0, #ks0 = :ks0"
260
+ assert req["ExpressionAttributeNames"] == {"#c0": "status", "#ks0": "userId"}
261
+ assert plain(req["ExpressionAttributeValues"]) == {":c0": "disabled", ":ks0": "alice"}
259
262
 
260
263
 
261
264
  def test_update_item_rederives_affected_gsi_key():
@@ -303,7 +306,8 @@ def test_update_item_does_not_touch_unaffected_gsi():
303
306
  rt.execute_command("UserConfCommands__disableUser", {"userId": "alice", "status": "disabled"})
304
307
  _method, req = client.calls[0]
305
308
  assert "GSI1PK" not in req["ExpressionAttributeNames"].values()
306
- assert req["UpdateExpression"] == "SET #c0 = :c0"
309
+ # #333: the change SET plus the key-source SET (`userId`); no GSI re-derive.
310
+ assert req["UpdateExpression"] == "SET #c0 = :c0, #ks0 = :ks0"
307
311
 
308
312
 
309
313
  def test_put_item_injects_key_attributes():