graphddb-runtime 0.2.0__tar.gz → 0.2.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.
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/PKG-INFO +1 -1
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime/filters.py +5 -3
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime/runtime.py +76 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime/transactions.py +84 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime.egg-info/PKG-INFO +1 -1
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/pyproject.toml +1 -1
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/tests/test_unit.py +139 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/README.md +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime/__init__.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime/async_runtime.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime/batch.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime/concurrency.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime/cursor.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime/errors.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime/hydration.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime/limits.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime/per_key_cursor.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime/relations.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime/templates.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime.egg-info/SOURCES.txt +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime.egg-info/dependency_links.txt +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime.egg-info/requires.txt +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/graphddb_runtime.egg-info/top_level.txt +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/setup.cfg +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/tests/test_concurrency.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/tests/test_contract_runtime.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/tests/test_integration.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/tests/test_integration_command.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/tests/test_integration_compose.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/tests/test_integration_contract.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/tests/test_integration_edge_derive.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/tests/test_integration_edge_write.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/tests/test_integration_events.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/tests/test_integration_referential.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/tests/test_integration_relations.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/tests/test_integration_unique.py +0 -0
- {graphddb_runtime-0.2.0 → graphddb_runtime-0.2.1}/tests/test_relations.py +0 -0
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Port of the TypeScript ``compileFilterExpression``
|
|
4
4
|
(``src/expression/filter-expression.ts``). The declarative tree carried in a
|
|
5
|
-
query spec's ``filter.declarative`` is JSON-safe
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
query spec's ``filter.declarative`` is JSON-safe, so only the operator/logical
|
|
6
|
+
forms are handled here. (A read-side ``cond`` escape hatch is not serialized into
|
|
7
|
+
``filter.declarative``; a *write*-side ``cond`` rides the separate ``raw``
|
|
8
|
+
condition spec — issue #114-B — rendered in ``runtime.py`` / ``transactions.py``,
|
|
9
|
+
not here.)
|
|
8
10
|
|
|
9
11
|
Names are ``#``-aliased columns (reused per distinct column); values are
|
|
10
12
|
``:``-aliased parameters — no literal interpolation. The result is returned in
|
|
@@ -64,6 +64,53 @@ def _normalize_template(template: Optional[str]) -> Optional[str]:
|
|
|
64
64
|
return _PLACEHOLDER_RE.sub("\x00", template)
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def _resolve_condition_leaf(value: Any, params: Mapping[str, Any]) -> Any:
|
|
68
|
+
"""Resolve one declarative-condition operand leaf (issue #114-A).
|
|
69
|
+
|
|
70
|
+
A ``{"$param": name}`` marker is replaced by the **typed** caller-param value
|
|
71
|
+
(so a numeric comparison stays numeric); a native literal is returned as-is.
|
|
72
|
+
"""
|
|
73
|
+
if isinstance(value, dict) and isinstance(value.get("$param"), str):
|
|
74
|
+
name = value["$param"]
|
|
75
|
+
if name not in params or params[name] is None:
|
|
76
|
+
raise KeyError(f"condition references unbound parameter '{name}'")
|
|
77
|
+
return params[name]
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _resolve_condition_tree(node: Any, params: Mapping[str, Any]) -> Any:
|
|
82
|
+
"""Resolve every ``{"$param"}`` marker in a serialized condition tree.
|
|
83
|
+
|
|
84
|
+
Walks the JSON-safe operator tree the TS planner emits (field operator
|
|
85
|
+
objects + ``and`` / ``or`` / ``not`` groups), substituting param markers with
|
|
86
|
+
typed caller-param values, and returns a tree shaped exactly like a
|
|
87
|
+
declarative ``filter`` so :func:`compile_filter` compiles it identically.
|
|
88
|
+
"""
|
|
89
|
+
out: Dict[str, Any] = {}
|
|
90
|
+
for key, value in node.items():
|
|
91
|
+
if key in ("and", "or"):
|
|
92
|
+
out[key] = [_resolve_condition_tree(s, params) for s in value]
|
|
93
|
+
elif key == "not":
|
|
94
|
+
out[key] = _resolve_condition_tree(value, params)
|
|
95
|
+
else:
|
|
96
|
+
ops: Dict[str, Any] = {}
|
|
97
|
+
for op, op_val in value.items():
|
|
98
|
+
if op == "between":
|
|
99
|
+
lo, hi = op_val
|
|
100
|
+
ops[op] = [
|
|
101
|
+
_resolve_condition_leaf(lo, params),
|
|
102
|
+
_resolve_condition_leaf(hi, params),
|
|
103
|
+
]
|
|
104
|
+
elif op == "in":
|
|
105
|
+
ops[op] = [_resolve_condition_leaf(v, params) for v in op_val]
|
|
106
|
+
elif op == "attributeExists":
|
|
107
|
+
ops[op] = op_val
|
|
108
|
+
else:
|
|
109
|
+
ops[op] = _resolve_condition_leaf(op_val, params)
|
|
110
|
+
out[key] = ops
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
|
|
67
114
|
def _present(records: Any) -> List[Dict[str, Any]]:
|
|
68
115
|
"""The present (non-``None``, dict) parent records from a result collection.
|
|
69
116
|
|
|
@@ -1668,6 +1715,35 @@ class GraphDDBRuntime:
|
|
|
1668
1715
|
values[v] = self._serializer.serialize(resolved)
|
|
1669
1716
|
clauses.append(f"{n} = {v}")
|
|
1670
1717
|
request["ConditionExpression"] = " AND ".join(clauses)
|
|
1718
|
+
elif kind == "expr":
|
|
1719
|
+
# The declarative operator tree (issue #114-A): resolve each
|
|
1720
|
+
# ``{"$param": name}`` marker to its typed caller-param value, then
|
|
1721
|
+
# compile the tree to a ConditionExpression with the SAME mechanics as
|
|
1722
|
+
# the read-side filter (``compile_filter``) so TS and Python emit an
|
|
1723
|
+
# identical expression / semantics. ``compile_filter`` allocates
|
|
1724
|
+
# ``#f`` / ``:vf`` aliases that never collide with the update ``#c`` /
|
|
1725
|
+
# ``:c`` namespace, so the compiled names/values merge cleanly.
|
|
1726
|
+
concrete = _resolve_condition_tree(condition["declarative"], params)
|
|
1727
|
+
compiled = compile_filter(concrete, self._serializer)
|
|
1728
|
+
if compiled is None:
|
|
1729
|
+
return
|
|
1730
|
+
names.update(compiled["ExpressionAttributeNames"])
|
|
1731
|
+
values = request.setdefault("ExpressionAttributeValues", {})
|
|
1732
|
+
values.update(compiled["ExpressionAttributeValues"])
|
|
1733
|
+
request["ConditionExpression"] = compiled["FilterExpression"]
|
|
1734
|
+
elif kind == "raw":
|
|
1735
|
+
# A raw ``cond`` write condition (issue #114-B): the expression and
|
|
1736
|
+
# the ``#cr_*`` name aliases are finished; bind each ``:crN`` value to
|
|
1737
|
+
# its literal, or — when it is a ``{"$param": name}`` marker — to the
|
|
1738
|
+
# typed caller-param value, then serialize. ``#cr_*`` / ``:cr*`` never
|
|
1739
|
+
# collide with the update namespace, so they merge cleanly. The
|
|
1740
|
+
# expression mirrors the TS ``serializeRawCondition`` output verbatim.
|
|
1741
|
+
names.update(condition["names"])
|
|
1742
|
+
values = request.setdefault("ExpressionAttributeValues", {})
|
|
1743
|
+
for alias, raw_val in condition["values"].items():
|
|
1744
|
+
resolved = _resolve_condition_leaf(raw_val, params)
|
|
1745
|
+
values[alias] = self._serializer.serialize(resolved)
|
|
1746
|
+
request["ConditionExpression"] = condition["expression"]
|
|
1671
1747
|
|
|
1672
1748
|
def _add_key_attributes(
|
|
1673
1749
|
self, entity_name: str, item: Dict[str, Any], params: Mapping[str, Any]
|
|
@@ -18,6 +18,8 @@ from __future__ import annotations
|
|
|
18
18
|
import re
|
|
19
19
|
from typing import Any, Dict, List, Mapping, Optional
|
|
20
20
|
|
|
21
|
+
from .filters import compile_filter
|
|
22
|
+
|
|
21
23
|
_PLACEHOLDER_RE = re.compile(r"\{[^{}]+\}")
|
|
22
24
|
_WHOLE_PLACEHOLDER_RE = re.compile(r"^\{[^{}]+\}$")
|
|
23
25
|
|
|
@@ -64,6 +66,59 @@ def _resolve_value(
|
|
|
64
66
|
return _resolve_template(template, params, element)
|
|
65
67
|
|
|
66
68
|
|
|
69
|
+
def _resolve_condition_leaf(
|
|
70
|
+
value: Any,
|
|
71
|
+
params: Mapping[str, Any],
|
|
72
|
+
element: Optional[Mapping[str, Any]],
|
|
73
|
+
) -> Any:
|
|
74
|
+
"""Resolve one declarative-condition operand leaf in a transaction (#114-A).
|
|
75
|
+
|
|
76
|
+
A ``{"$param": name}`` marker binds to the typed param value, or — when the
|
|
77
|
+
name is ``item.<field>`` — to the current ``forEach`` element's field; a
|
|
78
|
+
native literal is returned as-is.
|
|
79
|
+
"""
|
|
80
|
+
if isinstance(value, dict) and isinstance(value.get("$param"), str):
|
|
81
|
+
return _resolve_value("{" + value["$param"] + "}", params, element)
|
|
82
|
+
return value
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _resolve_condition_tree(
|
|
86
|
+
node: Any,
|
|
87
|
+
params: Mapping[str, Any],
|
|
88
|
+
element: Optional[Mapping[str, Any]],
|
|
89
|
+
) -> Any:
|
|
90
|
+
"""Resolve every ``{"$param"}`` marker in a serialized condition tree (#114-A).
|
|
91
|
+
|
|
92
|
+
Mirrors the runtime's resolver but element-aware, producing a tree shaped
|
|
93
|
+
like a declarative ``filter`` for :func:`compile_filter`.
|
|
94
|
+
"""
|
|
95
|
+
out: Dict[str, Any] = {}
|
|
96
|
+
for key, value in node.items():
|
|
97
|
+
if key in ("and", "or"):
|
|
98
|
+
out[key] = [_resolve_condition_tree(s, params, element) for s in value]
|
|
99
|
+
elif key == "not":
|
|
100
|
+
out[key] = _resolve_condition_tree(value, params, element)
|
|
101
|
+
else:
|
|
102
|
+
ops: Dict[str, Any] = {}
|
|
103
|
+
for op, op_val in value.items():
|
|
104
|
+
if op == "between":
|
|
105
|
+
lo, hi = op_val
|
|
106
|
+
ops[op] = [
|
|
107
|
+
_resolve_condition_leaf(lo, params, element),
|
|
108
|
+
_resolve_condition_leaf(hi, params, element),
|
|
109
|
+
]
|
|
110
|
+
elif op == "in":
|
|
111
|
+
ops[op] = [
|
|
112
|
+
_resolve_condition_leaf(v, params, element) for v in op_val
|
|
113
|
+
]
|
|
114
|
+
elif op == "attributeExists":
|
|
115
|
+
ops[op] = op_val
|
|
116
|
+
else:
|
|
117
|
+
ops[op] = _resolve_condition_leaf(op_val, params, element)
|
|
118
|
+
out[key] = ops
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
67
122
|
def _resolve_record(
|
|
68
123
|
record: Optional[Mapping[str, str]],
|
|
69
124
|
params: Mapping[str, Any],
|
|
@@ -318,6 +373,35 @@ class TransactionExpander:
|
|
|
318
373
|
target["ConditionExpression"] = f"{fn}(#ce)"
|
|
319
374
|
return
|
|
320
375
|
values = target.setdefault("ExpressionAttributeValues", {})
|
|
376
|
+
if kind == "expr":
|
|
377
|
+
# Declarative operator tree (issue #114-A): resolve each
|
|
378
|
+
# ``{"$param": name}`` marker against the tx params / forEach element
|
|
379
|
+
# (a ``{item.field}`` name binds from the element), then compile with
|
|
380
|
+
# the SAME ``compile_filter`` mechanics as the read-side filter. The
|
|
381
|
+
# compiled ``#f`` / ``:vf`` aliases never collide with the update
|
|
382
|
+
# ``#u`` namespace, so they merge cleanly.
|
|
383
|
+
concrete = _resolve_condition_tree(
|
|
384
|
+
condition["declarative"], params, element
|
|
385
|
+
)
|
|
386
|
+
compiled = compile_filter(concrete, self._serializer)
|
|
387
|
+
if compiled is None:
|
|
388
|
+
return
|
|
389
|
+
names.update(compiled["ExpressionAttributeNames"])
|
|
390
|
+
values.update(compiled["ExpressionAttributeValues"])
|
|
391
|
+
target["ConditionExpression"] = compiled["FilterExpression"]
|
|
392
|
+
return
|
|
393
|
+
if kind == "raw":
|
|
394
|
+
# A raw ``cond`` write condition (issue #114-B): the expression and
|
|
395
|
+
# ``#cr_*`` aliases are finished; bind each ``:crN`` value to its
|
|
396
|
+
# literal or — when a ``{"$param": name}`` marker — against the tx
|
|
397
|
+
# params / forEach element (a ``{item.field}`` name binds from the
|
|
398
|
+
# element). ``#cr_*`` / ``:cr*`` never collide with the update aliases.
|
|
399
|
+
names.update(condition["names"])
|
|
400
|
+
for alias, raw_val in condition["values"].items():
|
|
401
|
+
resolved = _resolve_condition_leaf(raw_val, params, element)
|
|
402
|
+
values[alias] = self._serializer.serialize(resolved)
|
|
403
|
+
target["ConditionExpression"] = condition["expression"]
|
|
404
|
+
return
|
|
321
405
|
clauses = []
|
|
322
406
|
for i, (field, tmpl) in enumerate(condition["fields"].items()):
|
|
323
407
|
n = f"#e{i}"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "graphddb-runtime"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.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"
|
|
@@ -342,6 +342,145 @@ def test_conditional_put_emits_attribute_not_exists_on_named_field():
|
|
|
342
342
|
assert req["ExpressionAttributeNames"]["#ce"] == "PK"
|
|
343
343
|
|
|
344
344
|
|
|
345
|
+
def test_conditional_expr_tree_renders_via_compile_filter():
|
|
346
|
+
"""Issue #114-A: a declarative operator tree (`{ kind: expr, declarative }`)
|
|
347
|
+
resolves its `{$param}` markers (typed) and compiles to a ConditionExpression
|
|
348
|
+
with the read-side filter mechanics (`#f`/`:vf` aliases).
|
|
349
|
+
"""
|
|
350
|
+
client = FakeClient()
|
|
351
|
+
rt = make_runtime(client)
|
|
352
|
+
rt._commands["addGroupMember"]["params"] = {
|
|
353
|
+
**rt._commands["addGroupMember"]["params"],
|
|
354
|
+
"role_0": {"type": "string", "required": True},
|
|
355
|
+
"role_1": {"type": "string", "required": True},
|
|
356
|
+
}
|
|
357
|
+
rt._commands["addGroupMember"]["condition"] = {
|
|
358
|
+
"kind": "expr",
|
|
359
|
+
"declarative": {
|
|
360
|
+
"and": [
|
|
361
|
+
{"role": {"in": [{"$param": "role_0"}, {"$param": "role_1"}]}},
|
|
362
|
+
{"role": {"beginsWith": {"$param": "role"}}},
|
|
363
|
+
]
|
|
364
|
+
},
|
|
365
|
+
}
|
|
366
|
+
rt.execute_command(
|
|
367
|
+
"addGroupMember",
|
|
368
|
+
{
|
|
369
|
+
"groupId": "eng",
|
|
370
|
+
"userId": "alice",
|
|
371
|
+
"role": "admin",
|
|
372
|
+
"role_0": "admin",
|
|
373
|
+
"role_1": "owner",
|
|
374
|
+
},
|
|
375
|
+
)
|
|
376
|
+
_method, req = client.calls[0]
|
|
377
|
+
assert req["ConditionExpression"] == (
|
|
378
|
+
"(#f0 IN (:vf0, :vf1) AND begins_with(#f0, :vf2))"
|
|
379
|
+
)
|
|
380
|
+
assert req["ExpressionAttributeNames"]["#f0"] == "role"
|
|
381
|
+
values = plain(req["ExpressionAttributeValues"])
|
|
382
|
+
assert values[":vf0"] == "admin"
|
|
383
|
+
assert values[":vf1"] == "owner"
|
|
384
|
+
assert values[":vf2"] == "admin"
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def test_conditional_expr_numeric_comparison_keeps_type():
|
|
388
|
+
"""A numeric `{$param}` operand stays numeric (serialized as N), so `gt`
|
|
389
|
+
compares numerically rather than against a stringified value.
|
|
390
|
+
"""
|
|
391
|
+
client = FakeClient()
|
|
392
|
+
rt = make_runtime(client)
|
|
393
|
+
rt._commands["disableUser"]["params"] = {
|
|
394
|
+
**rt._commands["disableUser"]["params"],
|
|
395
|
+
"minVersion": {"type": "number", "required": True},
|
|
396
|
+
}
|
|
397
|
+
rt._commands["disableUser"]["condition"] = {
|
|
398
|
+
"kind": "expr",
|
|
399
|
+
"declarative": {"version": {"gt": {"$param": "minVersion"}}},
|
|
400
|
+
}
|
|
401
|
+
rt.execute_command(
|
|
402
|
+
"disableUser", {"userId": "alice", "status": "disabled", "minVersion": 3}
|
|
403
|
+
)
|
|
404
|
+
_method, req = client.calls[0]
|
|
405
|
+
assert req["ConditionExpression"] == "#f0 > :vf0"
|
|
406
|
+
assert req["ExpressionAttributeValues"][":vf0"] == {"N": "3"}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def test_conditional_expr_size_renders_size_function():
|
|
410
|
+
"""Audit fix (issue #114-A): the `size` operator must compile to
|
|
411
|
+
`size(#f) = :v`, not be silently dropped. The numeric length operand stays
|
|
412
|
+
N-typed.
|
|
413
|
+
"""
|
|
414
|
+
client = FakeClient()
|
|
415
|
+
rt = make_runtime(client)
|
|
416
|
+
rt._commands["addGroupMember"]["params"] = {
|
|
417
|
+
**rt._commands["addGroupMember"]["params"],
|
|
418
|
+
"role_size": {"type": "number", "required": True},
|
|
419
|
+
}
|
|
420
|
+
rt._commands["addGroupMember"]["condition"] = {
|
|
421
|
+
"kind": "expr",
|
|
422
|
+
"declarative": {"role": {"size": {"$param": "role_size"}}},
|
|
423
|
+
}
|
|
424
|
+
rt.execute_command(
|
|
425
|
+
"addGroupMember",
|
|
426
|
+
{"groupId": "eng", "userId": "alice", "role": "admin", "role_size": 5},
|
|
427
|
+
)
|
|
428
|
+
_method, req = client.calls[0]
|
|
429
|
+
assert req["ConditionExpression"] == "size(#f0) = :vf0"
|
|
430
|
+
assert req["ExpressionAttributeNames"]["#f0"] == "role"
|
|
431
|
+
assert req["ExpressionAttributeValues"][":vf0"] == {"N": "5"}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def test_conditional_raw_cond_literal_renders_verbatim():
|
|
435
|
+
"""Issue #114-B: a raw `cond` write condition (`{ kind: raw }`) attaches its
|
|
436
|
+
pre-compiled expression + `#cr_*` names verbatim, binding each `:crN` literal
|
|
437
|
+
value (here 'banned') exactly as the TS `serializeRawCondition` emitted it.
|
|
438
|
+
"""
|
|
439
|
+
client = FakeClient()
|
|
440
|
+
rt = make_runtime(client)
|
|
441
|
+
rt._commands["addGroupMember"]["condition"] = {
|
|
442
|
+
"kind": "raw",
|
|
443
|
+
"expression": "#cr_role <> :cr0 AND attribute_exists(#cr_role)",
|
|
444
|
+
"names": {"#cr_role": "role"},
|
|
445
|
+
"values": {":cr0": "banned"},
|
|
446
|
+
}
|
|
447
|
+
rt.execute_command(
|
|
448
|
+
"addGroupMember", {"groupId": "eng", "userId": "alice", "role": "admin"}
|
|
449
|
+
)
|
|
450
|
+
_method, req = client.calls[0]
|
|
451
|
+
assert req["ConditionExpression"] == (
|
|
452
|
+
"#cr_role <> :cr0 AND attribute_exists(#cr_role)"
|
|
453
|
+
)
|
|
454
|
+
assert req["ExpressionAttributeNames"]["#cr_role"] == "role"
|
|
455
|
+
assert plain(req["ExpressionAttributeValues"])[":cr0"] == "banned"
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def test_conditional_raw_cond_binds_param_marker():
|
|
459
|
+
"""Issue #114-B: a `{ $param }` value marker in a raw `cond` condition binds
|
|
460
|
+
from the typed caller param (here `cond_p0`), so TS and Python compare against
|
|
461
|
+
the same bound value.
|
|
462
|
+
"""
|
|
463
|
+
client = FakeClient()
|
|
464
|
+
rt = make_runtime(client)
|
|
465
|
+
rt._commands["addGroupMember"]["params"] = {
|
|
466
|
+
**rt._commands["addGroupMember"]["params"],
|
|
467
|
+
"cond_p0": {"type": "string", "required": True},
|
|
468
|
+
}
|
|
469
|
+
rt._commands["addGroupMember"]["condition"] = {
|
|
470
|
+
"kind": "raw",
|
|
471
|
+
"expression": "#cr_role = :cr0",
|
|
472
|
+
"names": {"#cr_role": "role"},
|
|
473
|
+
"values": {":cr0": {"$param": "cond_p0"}},
|
|
474
|
+
}
|
|
475
|
+
rt.execute_command(
|
|
476
|
+
"addGroupMember",
|
|
477
|
+
{"groupId": "eng", "userId": "alice", "role": "admin", "cond_p0": "admin"},
|
|
478
|
+
)
|
|
479
|
+
_method, req = client.calls[0]
|
|
480
|
+
assert req["ConditionExpression"] == "#cr_role = :cr0"
|
|
481
|
+
assert plain(req["ExpressionAttributeValues"])[":cr0"] == "admin"
|
|
482
|
+
|
|
483
|
+
|
|
345
484
|
# ── declarative transactions (issue #46) ────────────────────────────────────
|
|
346
485
|
|
|
347
486
|
|
|
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-0.2.0 → graphddb_runtime-0.2.1}/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
|
|
File without changes
|