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.
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/PKG-INFO +1 -1
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/filters.py +12 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/runtime.py +33 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/transactions.py +10 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime.egg-info/PKG-INFO +1 -1
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/pyproject.toml +1 -1
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_atomic_add_command.py +16 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_unit.py +8 -4
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/README.md +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/__init__.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/async_runtime.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/batch.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/behavior_exec.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/concurrency.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/cursor.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/errors.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/handler_registry.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/hydration.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/limits.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/middleware.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/per_key_cursor.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime/templates.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime.egg-info/SOURCES.txt +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime.egg-info/dependency_links.txt +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime.egg-info/requires.txt +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime.egg-info/top_level.txt +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/setup.cfg +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_concurrency.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_contract_runtime.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_crosslang_python_cells.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_description.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_description_followups.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_description_jsdoc.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_error_policy.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_generated_module.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_guard_eval.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_handler_registry.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_batch_write.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_binding_contract.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_command.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_compose.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_contract.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_edge_derive.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_edge_write.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_events.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_maintain.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_middleware.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_referential.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_relations.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_unique.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_maintain.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_middleware.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_relations.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_spec_version_guard.py +0 -0
- {graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_ttl.py +0 -0
|
@@ -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"):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "graphddb-runtime"
|
|
7
|
-
version = "1.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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/graphddb_runtime.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graphddb_runtime-1.0.0 → graphddb_runtime-1.0.2}/tests/test_integration_binding_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|