dotted-notation 0.43.4__tar.gz → 0.43.5__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.
- {dotted_notation-0.43.4/dotted_notation.egg-info → dotted_notation-0.43.5}/PKG-INFO +25 -7
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/README.md +24 -6
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/__init__.py +6 -2
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/access.py +8 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/api.py +88 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/base.py +6 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/filters.py +45 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/groups.py +10 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/sqlize.py +52 -59
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/wrappers.py +25 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5/dotted_notation.egg-info}/PKG-INFO +25 -7
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/setup.py +1 -1
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_api.py +109 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_sqlize.py +33 -31
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/LICENSE +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/__main__.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/cli/main.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/containers.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/engine.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/grammar.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/matchers.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/predicates.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/recursive.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/results.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/transforms.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/utils.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted/utypes.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/setup.cfg +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/__init__.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_appender.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_assemble.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_attrs.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_bindings.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_cli.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_concat.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_container_filter.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_cut.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_empty.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_filter_keyvalue.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_get.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_guard_transforms.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_invert.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_json_sentinels.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_keys_values.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_match.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_matchable.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_named_subst.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_negation.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_nop.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_numeric.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_opgroup.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_pluck.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_predicates.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_quote_idempotent.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_recursive.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_reference.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_replace.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_slice.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_softcut.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_strict.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_string_glob.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_subst_escape.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_subst_transforms.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_threading.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_transforms.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_translate.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_type_restriction.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_unpack.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_update.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_update_if.py +0 -0
- {dotted_notation-0.43.4 → dotted_notation-0.43.5}/tests/test_value_guard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.43.
|
|
3
|
+
Version: 0.43.5
|
|
4
4
|
Summary: Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms
|
|
5
5
|
Home-page: https://github.com/freywaid/dotted
|
|
6
6
|
Author: Frey Waid
|
|
@@ -2871,18 +2871,36 @@ time:
|
|
|
2871
2871
|
|
|
2872
2872
|
>>> r = dotted.sqlize("age >= $(min_age)")
|
|
2873
2873
|
>>> r
|
|
2874
|
-
{'select': 'age', 'where': 'age >= :min_age', 'params': {},
|
|
2874
|
+
{'select': 'age', 'where': 'age >= :min_age', 'params': {},
|
|
2875
|
+
'unbound': {'min_age': 'min_age'}}
|
|
2875
2876
|
|
|
2876
|
-
`
|
|
2877
|
-
`
|
|
2877
|
+
`unbound` is a dict keyed by the **SQL bind parameter name** — the name
|
|
2878
|
+
that appears in the SQL `:placeholder` and as a key in `params`. Each
|
|
2879
|
+
value is the **original substitution name** (what was inside `$(...)`),
|
|
2880
|
+
kept as provenance so callers can trace a bind back to its source.
|
|
2881
|
+
|
|
2882
|
+
`unbound.keys()` is the list of bind slots the caller needs to fill.
|
|
2883
|
+
Most often you just fill `params` by bind name directly:
|
|
2878
2884
|
|
|
2879
2885
|
>>> r['params']['min_age'] = 30
|
|
2880
2886
|
>>> # hand (r['where'], r['params']) to cursor.execute / session.execute / etc.
|
|
2881
2887
|
|
|
2882
|
-
|
|
2888
|
+
Non-identifier names (dotted paths, quoted keys, spaces, etc.) hash to a
|
|
2889
|
+
deterministic `_s_<hex>` bind name — the bind-side name is different
|
|
2890
|
+
from what appeared in source, but the original is preserved in the
|
|
2891
|
+
`unbound` value:
|
|
2892
|
+
|
|
2893
|
+
>>> r = dotted.sqlize('age >= $(user.min_age)')
|
|
2894
|
+
>>> r['where'] # doctest: +ELLIPSIS
|
|
2895
|
+
'age >= :_s_...'
|
|
2896
|
+
>>> list(r['unbound'].values())
|
|
2897
|
+
['user.min_age']
|
|
2898
|
+
|
|
2899
|
+
Repeated uses of the same name share one entry automatically:
|
|
2883
2900
|
|
|
2884
2901
|
>>> dotted.sqlize('(age >= $(x) & weight = $(x))')
|
|
2885
|
-
{'where': '(age >= :x) AND (weight = :x)', 'params': {},
|
|
2902
|
+
{'where': '(age >= :x) AND (weight = :x)', 'params': {},
|
|
2903
|
+
'unbound': {'x': 'x'}}
|
|
2886
2904
|
|
|
2887
2905
|
If you pass `bindings=`, substitutions are resolved at sqlize time and hoisted
|
|
2888
2906
|
like any other literal:
|
|
@@ -2910,7 +2928,7 @@ Returned dict may contain any subset of:
|
|
|
2910
2928
|
| `select` | `str` | Expression yielding the value dotted would return |
|
|
2911
2929
|
| `where` | `str` | Predicate from guards / filters / boolean groups |
|
|
2912
2930
|
| `params` | `dict` | Hoisted values keyed by name |
|
|
2913
|
-
| `
|
|
2931
|
+
| `unbound` | `dict` | Deferred substitutions: `{bind_param_name: original_name}` |
|
|
2914
2932
|
|
|
2915
2933
|
`select` is omitted when combining predicates across different columns
|
|
2916
2934
|
(e.g. top-level groups), since there's no single meaningful expression.
|
|
@@ -2834,18 +2834,36 @@ time:
|
|
|
2834
2834
|
|
|
2835
2835
|
>>> r = dotted.sqlize("age >= $(min_age)")
|
|
2836
2836
|
>>> r
|
|
2837
|
-
{'select': 'age', 'where': 'age >= :min_age', 'params': {},
|
|
2837
|
+
{'select': 'age', 'where': 'age >= :min_age', 'params': {},
|
|
2838
|
+
'unbound': {'min_age': 'min_age'}}
|
|
2838
2839
|
|
|
2839
|
-
`
|
|
2840
|
-
`
|
|
2840
|
+
`unbound` is a dict keyed by the **SQL bind parameter name** — the name
|
|
2841
|
+
that appears in the SQL `:placeholder` and as a key in `params`. Each
|
|
2842
|
+
value is the **original substitution name** (what was inside `$(...)`),
|
|
2843
|
+
kept as provenance so callers can trace a bind back to its source.
|
|
2844
|
+
|
|
2845
|
+
`unbound.keys()` is the list of bind slots the caller needs to fill.
|
|
2846
|
+
Most often you just fill `params` by bind name directly:
|
|
2841
2847
|
|
|
2842
2848
|
>>> r['params']['min_age'] = 30
|
|
2843
2849
|
>>> # hand (r['where'], r['params']) to cursor.execute / session.execute / etc.
|
|
2844
2850
|
|
|
2845
|
-
|
|
2851
|
+
Non-identifier names (dotted paths, quoted keys, spaces, etc.) hash to a
|
|
2852
|
+
deterministic `_s_<hex>` bind name — the bind-side name is different
|
|
2853
|
+
from what appeared in source, but the original is preserved in the
|
|
2854
|
+
`unbound` value:
|
|
2855
|
+
|
|
2856
|
+
>>> r = dotted.sqlize('age >= $(user.min_age)')
|
|
2857
|
+
>>> r['where'] # doctest: +ELLIPSIS
|
|
2858
|
+
'age >= :_s_...'
|
|
2859
|
+
>>> list(r['unbound'].values())
|
|
2860
|
+
['user.min_age']
|
|
2861
|
+
|
|
2862
|
+
Repeated uses of the same name share one entry automatically:
|
|
2846
2863
|
|
|
2847
2864
|
>>> dotted.sqlize('(age >= $(x) & weight = $(x))')
|
|
2848
|
-
{'where': '(age >= :x) AND (weight = :x)', 'params': {},
|
|
2865
|
+
{'where': '(age >= :x) AND (weight = :x)', 'params': {},
|
|
2866
|
+
'unbound': {'x': 'x'}}
|
|
2849
2867
|
|
|
2850
2868
|
If you pass `bindings=`, substitutions are resolved at sqlize time and hoisted
|
|
2851
2869
|
like any other literal:
|
|
@@ -2873,7 +2891,7 @@ Returned dict may contain any subset of:
|
|
|
2873
2891
|
| `select` | `str` | Expression yielding the value dotted would return |
|
|
2874
2892
|
| `where` | `str` | Predicate from guards / filters / boolean groups |
|
|
2875
2893
|
| `params` | `dict` | Hoisted values keyed by name |
|
|
2876
|
-
| `
|
|
2894
|
+
| `unbound` | `dict` | Deferred substitutions: `{bind_param_name: original_name}` |
|
|
2877
2895
|
|
|
2878
2896
|
`select` is omitted when combining predicates across different columns
|
|
2879
2897
|
(e.g. top-level groups), since there's no single meaningful expression.
|
|
@@ -56,7 +56,8 @@ For full documentation including all options and flags:
|
|
|
56
56
|
or see README.md
|
|
57
57
|
"""
|
|
58
58
|
from .api import \
|
|
59
|
-
parse, is_pattern, is_template,
|
|
59
|
+
parse, is_pattern, is_template, is_reference, is_indeterminate, is_simple, \
|
|
60
|
+
is_inverted, mutable, quote, ANY, AUTO, Attrs, GroupMode, \
|
|
60
61
|
register, transform, \
|
|
61
62
|
assemble, assemble_multi, \
|
|
62
63
|
build, build_multi, \
|
|
@@ -82,7 +83,10 @@ __all__ = [
|
|
|
82
83
|
# Transform
|
|
83
84
|
'apply', 'apply_multi', 'register', 'transform',
|
|
84
85
|
# Utility
|
|
85
|
-
'parse', 'assemble', 'assemble_multi', 'quote',
|
|
86
|
+
'parse', 'assemble', 'assemble_multi', 'quote',
|
|
87
|
+
'is_pattern', 'is_template', 'is_reference',
|
|
88
|
+
'is_indeterminate', 'is_simple',
|
|
89
|
+
'is_inverted', 'mutable',
|
|
86
90
|
# SQL
|
|
87
91
|
'sqlize', 'TranslationError',
|
|
88
92
|
# Constants
|
|
@@ -870,6 +870,14 @@ class SliceFilter(BaseOp):
|
|
|
870
870
|
hasattr(f, 'is_template') and f.is_template()
|
|
871
871
|
for f in self.filters)
|
|
872
872
|
|
|
873
|
+
def is_reference(self):
|
|
874
|
+
"""
|
|
875
|
+
True if any attached filter contains an internal reference.
|
|
876
|
+
"""
|
|
877
|
+
return any(
|
|
878
|
+
hasattr(f, 'is_reference') and f.is_reference()
|
|
879
|
+
for f in self.filters)
|
|
880
|
+
|
|
873
881
|
def is_empty(self, node):
|
|
874
882
|
return not node
|
|
875
883
|
|
|
@@ -188,6 +188,94 @@ def is_template(key):
|
|
|
188
188
|
return _is_template(parse(key))
|
|
189
189
|
|
|
190
190
|
|
|
191
|
+
@functools.lru_cache(CACHE_SIZE)
|
|
192
|
+
def _is_reference(ops):
|
|
193
|
+
if not ops:
|
|
194
|
+
return False
|
|
195
|
+
if ops[0].is_reference():
|
|
196
|
+
return True
|
|
197
|
+
return _is_reference(ops[1:])
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def is_reference(key):
|
|
201
|
+
"""
|
|
202
|
+
True if dotted path contains internal references ($$(...)).
|
|
203
|
+
Walks into wraps, filters, and group branches.
|
|
204
|
+
|
|
205
|
+
>>> is_reference('a.$$(b)')
|
|
206
|
+
True
|
|
207
|
+
>>> is_reference('a.b')
|
|
208
|
+
False
|
|
209
|
+
>>> is_reference('a.$(x)')
|
|
210
|
+
False
|
|
211
|
+
"""
|
|
212
|
+
if isinstance(key, results.Dotted):
|
|
213
|
+
return _is_reference(key)
|
|
214
|
+
return _is_reference(parse(key))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def is_indeterminate(key):
|
|
218
|
+
"""
|
|
219
|
+
True if the path contains substitutions or references — anything
|
|
220
|
+
whose access target isn't pinned down by the source alone and
|
|
221
|
+
requires bindings or data to resolve. Patterns are not indeterminate:
|
|
222
|
+
they describe a set inherent to the query.
|
|
223
|
+
|
|
224
|
+
>>> is_indeterminate('a.$(x)')
|
|
225
|
+
True
|
|
226
|
+
>>> is_indeterminate('a.$$(b)')
|
|
227
|
+
True
|
|
228
|
+
>>> is_indeterminate('a.b')
|
|
229
|
+
False
|
|
230
|
+
>>> is_indeterminate('a.*.b')
|
|
231
|
+
False
|
|
232
|
+
"""
|
|
233
|
+
parsed = key if isinstance(key, results.Dotted) else parse(key)
|
|
234
|
+
return _is_template(parsed) or _is_reference(parsed)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@functools.lru_cache(CACHE_SIZE)
|
|
238
|
+
def _is_simple(ops):
|
|
239
|
+
for op in ops:
|
|
240
|
+
if not isinstance(op, (access.Key, access.Attr, access.Slot)):
|
|
241
|
+
return False
|
|
242
|
+
if not isinstance(op.op, matchers.Const):
|
|
243
|
+
return False
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def is_simple(key):
|
|
248
|
+
"""
|
|
249
|
+
True if the path is a plain chain of access ops (Key, Attr, Slot)
|
|
250
|
+
with literal matchers — no patterns, substitutions, references,
|
|
251
|
+
guards, transforms, filters, groups, or other decorations.
|
|
252
|
+
|
|
253
|
+
This is the strictest classification: paths that spell out one
|
|
254
|
+
specific access target character-for-character.
|
|
255
|
+
|
|
256
|
+
>>> is_simple('a.b.c')
|
|
257
|
+
True
|
|
258
|
+
>>> is_simple('a[0].b')
|
|
259
|
+
True
|
|
260
|
+
>>> is_simple('a@attr')
|
|
261
|
+
True
|
|
262
|
+
>>> is_simple('a.*.b')
|
|
263
|
+
False
|
|
264
|
+
>>> is_simple('a=30')
|
|
265
|
+
False
|
|
266
|
+
>>> is_simple('a|int')
|
|
267
|
+
False
|
|
268
|
+
>>> is_simple('a.$(x)')
|
|
269
|
+
False
|
|
270
|
+
>>> is_simple('a.$$(b)')
|
|
271
|
+
False
|
|
272
|
+
"""
|
|
273
|
+
parsed = key if isinstance(key, results.Dotted) else parse(key)
|
|
274
|
+
if parsed.transforms:
|
|
275
|
+
return False
|
|
276
|
+
return _is_simple(parsed)
|
|
277
|
+
|
|
278
|
+
|
|
191
279
|
def is_inverted(key):
|
|
192
280
|
"""
|
|
193
281
|
True if an inverted style pattern
|
|
@@ -13,6 +13,13 @@ def _any_template(items):
|
|
|
13
13
|
return any(hasattr(x, 'is_template') and x.is_template() for x in items)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def _any_reference(items):
|
|
17
|
+
"""
|
|
18
|
+
True if any item in an iterable has is_reference() returning True.
|
|
19
|
+
"""
|
|
20
|
+
return any(hasattr(x, 'is_reference') and x.is_reference() for x in items)
|
|
21
|
+
|
|
22
|
+
|
|
16
23
|
class FilterOp(base.MatchOp):
|
|
17
24
|
def is_pattern(self):
|
|
18
25
|
return False
|
|
@@ -63,6 +70,12 @@ class FilterKey(base.MatchOp):
|
|
|
63
70
|
"""
|
|
64
71
|
return _any_template(self.parts)
|
|
65
72
|
|
|
73
|
+
def is_reference(self):
|
|
74
|
+
"""
|
|
75
|
+
True if any part of the filter key contains an internal reference.
|
|
76
|
+
"""
|
|
77
|
+
return _any_reference(self.parts)
|
|
78
|
+
|
|
66
79
|
def get_values(self, node):
|
|
67
80
|
"""
|
|
68
81
|
Get all values from node matching this key, traversing dotted path if needed.
|
|
@@ -211,6 +224,17 @@ class FilterKeyValue(FilterOp):
|
|
|
211
224
|
return True
|
|
212
225
|
return _any_template(self.transforms)
|
|
213
226
|
|
|
227
|
+
def is_reference(self):
|
|
228
|
+
"""
|
|
229
|
+
True if the key, value, or any transform contains an internal
|
|
230
|
+
reference.
|
|
231
|
+
"""
|
|
232
|
+
if hasattr(self.key, 'is_reference') and self.key.is_reference():
|
|
233
|
+
return True
|
|
234
|
+
if hasattr(self.val, 'is_reference') and self.val.is_reference():
|
|
235
|
+
return True
|
|
236
|
+
return _any_reference(self.transforms)
|
|
237
|
+
|
|
214
238
|
def _eq_match(self, node):
|
|
215
239
|
"""
|
|
216
240
|
True if any value from self.key in node matches self.val (after transforms).
|
|
@@ -379,6 +403,11 @@ class FilterGroup(FilterOp):
|
|
|
379
403
|
and hasattr(self.inner, 'is_template')
|
|
380
404
|
and self.inner.is_template())
|
|
381
405
|
|
|
406
|
+
def is_reference(self):
|
|
407
|
+
return (self.inner is not None
|
|
408
|
+
and hasattr(self.inner, 'is_reference')
|
|
409
|
+
and self.inner.is_reference())
|
|
410
|
+
|
|
382
411
|
def resolve(self, bindings, partial=False):
|
|
383
412
|
"""
|
|
384
413
|
Resolve $N in inner filter.
|
|
@@ -434,6 +463,9 @@ class FilterAnd(FilterOp):
|
|
|
434
463
|
def is_template(self):
|
|
435
464
|
return _any_template(self.filters)
|
|
436
465
|
|
|
466
|
+
def is_reference(self):
|
|
467
|
+
return _any_reference(self.filters)
|
|
468
|
+
|
|
437
469
|
def resolve(self, bindings, partial=False):
|
|
438
470
|
"""
|
|
439
471
|
Resolve $N in each filter.
|
|
@@ -477,6 +509,9 @@ class FilterOr(FilterOp):
|
|
|
477
509
|
def is_template(self):
|
|
478
510
|
return _any_template(self.filters)
|
|
479
511
|
|
|
512
|
+
def is_reference(self):
|
|
513
|
+
return _any_reference(self.filters)
|
|
514
|
+
|
|
480
515
|
def resolve(self, bindings, partial=False):
|
|
481
516
|
"""
|
|
482
517
|
Resolve $N in each filter.
|
|
@@ -527,6 +562,11 @@ class FilterKeyValueFirst(FilterOp):
|
|
|
527
562
|
and hasattr(self.inner, 'is_template')
|
|
528
563
|
and self.inner.is_template())
|
|
529
564
|
|
|
565
|
+
def is_reference(self):
|
|
566
|
+
return (self.inner is not None
|
|
567
|
+
and hasattr(self.inner, 'is_reference')
|
|
568
|
+
and self.inner.is_reference())
|
|
569
|
+
|
|
530
570
|
def resolve(self, bindings, partial=False):
|
|
531
571
|
"""
|
|
532
572
|
Resolve $N in inner filter.
|
|
@@ -585,6 +625,11 @@ class FilterNot(FilterOp):
|
|
|
585
625
|
and hasattr(self.inner, 'is_template')
|
|
586
626
|
and self.inner.is_template())
|
|
587
627
|
|
|
628
|
+
def is_reference(self):
|
|
629
|
+
return (self.inner is not None
|
|
630
|
+
and hasattr(self.inner, 'is_reference')
|
|
631
|
+
and self.inner.is_reference())
|
|
632
|
+
|
|
588
633
|
def resolve(self, bindings, partial=False):
|
|
589
634
|
"""
|
|
590
635
|
Resolve $N in inner filter.
|
|
@@ -42,6 +42,16 @@ class OpGroup(base.TraversalOp):
|
|
|
42
42
|
return True
|
|
43
43
|
return False
|
|
44
44
|
|
|
45
|
+
def is_reference(self):
|
|
46
|
+
"""
|
|
47
|
+
True if any branch contains an internal reference.
|
|
48
|
+
"""
|
|
49
|
+
for branch in base.branches_only(self.branches):
|
|
50
|
+
for op in branch:
|
|
51
|
+
if hasattr(op, 'is_reference') and op.is_reference():
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
45
55
|
def default(self):
|
|
46
56
|
"""
|
|
47
57
|
Derive default from the first branch's first op, so auto-creation works
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Translate a dotted path into SQL clause components.
|
|
3
3
|
|
|
4
|
-
Returns a dict with keys select, where, from, params,
|
|
4
|
+
Returns a dict with keys select, where, from, params, unbound. Only keys that
|
|
5
5
|
apply to the given path are included. Callers stitch the fragments into a
|
|
6
6
|
full statement, e.g.:
|
|
7
7
|
|
|
@@ -13,10 +13,12 @@ navigation inside it.
|
|
|
13
13
|
|
|
14
14
|
Placeholders are emitted in SQLAlchemy named style (`:name`). Literals on the
|
|
15
15
|
RHS of guards are hoisted into `params` under generated names (`_p1`, `_p2`,
|
|
16
|
-
…). Unresolved substitutions
|
|
17
|
-
`
|
|
16
|
+
…). Unresolved substitutions appear in `unbound` as a dict mapping
|
|
17
|
+
`bind_param_name → original_name`; callers fill the params dict by the bind
|
|
18
|
+
name before executing.
|
|
18
19
|
"""
|
|
19
20
|
import decimal
|
|
21
|
+
import hashlib
|
|
20
22
|
import re
|
|
21
23
|
|
|
22
24
|
from . import access
|
|
@@ -46,44 +48,29 @@ def _quote_ident(name):
|
|
|
46
48
|
return '"' + name.replace('"', '""') + '"'
|
|
47
49
|
|
|
48
50
|
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'@': '_at_',
|
|
56
|
-
'[': '_br_',
|
|
57
|
-
']': '',
|
|
58
|
-
}
|
|
51
|
+
# Substitution names that aren't already valid SQL identifiers are mapped
|
|
52
|
+
# to a hash-based name for use as a SQL bind parameter. Deterministic —
|
|
53
|
+
# the same subst name always produces the same hashed param.
|
|
54
|
+
_HASH_PREFIX = '_s_'
|
|
55
|
+
_GEN_PREFIX = '_p'
|
|
56
|
+
_RESERVED_PREFIXES = (_HASH_PREFIX, _GEN_PREFIX)
|
|
59
57
|
|
|
60
58
|
|
|
61
|
-
def
|
|
59
|
+
def _param_name_for_subst(name):
|
|
62
60
|
"""
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
Return a SQL identifier-safe bind parameter name for a substitution.
|
|
62
|
+
|
|
63
|
+
Plain identifiers pass through unchanged. Anything else — dotted
|
|
64
|
+
paths, quoted keys, special characters — hashes to a deterministic
|
|
65
|
+
`_s_<hex>` name. Names that collide with our reserved generated-name
|
|
66
|
+
prefixes (`_p<N>`, `_s_`) are also hashed so they can never clobber
|
|
67
|
+
hoisted literals.
|
|
66
68
|
"""
|
|
67
|
-
if _IDENT_RE.fullmatch(name)
|
|
69
|
+
if _IDENT_RE.fullmatch(name) and not any(
|
|
70
|
+
name.startswith(p) for p in _RESERVED_PREFIXES):
|
|
68
71
|
return name
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if c in _OP_ENCODE:
|
|
72
|
-
out.append(_OP_ENCODE[c])
|
|
73
|
-
elif c.isalnum() or c == '_':
|
|
74
|
-
out.append(c)
|
|
75
|
-
else:
|
|
76
|
-
raise TranslationError(
|
|
77
|
-
f'cannot encode character {c!r} in substitution name {name!r}; '
|
|
78
|
-
'resolve via bindings= at sqlize time'
|
|
79
|
-
)
|
|
80
|
-
encoded = ''.join(out)
|
|
81
|
-
if not _IDENT_RE.fullmatch(encoded):
|
|
82
|
-
raise TranslationError(
|
|
83
|
-
f'substitution name {name!r} does not encode to a valid '
|
|
84
|
-
'SQL identifier'
|
|
85
|
-
)
|
|
86
|
-
return encoded
|
|
72
|
+
h = hashlib.sha256(name.encode('utf-8')).hexdigest()[:12]
|
|
73
|
+
return f'{_HASH_PREFIX}{h}'
|
|
87
74
|
|
|
88
75
|
|
|
89
76
|
def _pg_path_segment(seg):
|
|
@@ -112,38 +99,41 @@ def _pg_string_literal(s):
|
|
|
112
99
|
class _ParamState:
|
|
113
100
|
"""
|
|
114
101
|
Shared state for hoisted params across a translate call and its
|
|
115
|
-
sub-translators. Generated
|
|
116
|
-
|
|
102
|
+
sub-translators. Generated literal names use `_p{N}`; deferred
|
|
103
|
+
substitutions use their name when it's already a valid identifier,
|
|
104
|
+
or a `_s_<hex>` hash otherwise.
|
|
117
105
|
"""
|
|
118
106
|
|
|
119
107
|
def __init__(self):
|
|
120
|
-
self.params = {}
|
|
121
|
-
self.
|
|
108
|
+
self.params = {} # bind name → resolved value
|
|
109
|
+
self.unbound = {} # bind name → original subst name
|
|
122
110
|
self._gen_counter = 0
|
|
111
|
+
self._by_orig = {} # original subst name → bind name (dedup)
|
|
123
112
|
|
|
124
113
|
def hoist_literal(self, value):
|
|
125
114
|
"""
|
|
126
115
|
Hoist a concrete literal into a generated-name slot.
|
|
127
116
|
"""
|
|
128
117
|
self._gen_counter += 1
|
|
129
|
-
name = f'
|
|
118
|
+
name = f'{_GEN_PREFIX}{self._gen_counter}'
|
|
130
119
|
self.params[name] = value
|
|
131
120
|
return name
|
|
132
121
|
|
|
133
122
|
def hoist_named(self, name):
|
|
134
123
|
"""
|
|
135
|
-
Hoist a named substitution. If
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
encoded into plain identifiers using mnemonic tokens for access
|
|
141
|
-
ops so they can serve as SQL bind-parameter names.
|
|
124
|
+
Hoist a named substitution. If the name is already a valid SQL
|
|
125
|
+
identifier it's used directly as the bind parameter; otherwise a
|
|
126
|
+
deterministic `_s_<hash>` name is generated. The bind parameter
|
|
127
|
+
name keys the mapping in `unbound`, with the original source
|
|
128
|
+
name as the value. Repeated uses share one entry.
|
|
142
129
|
"""
|
|
143
|
-
|
|
144
|
-
if
|
|
145
|
-
self.
|
|
146
|
-
|
|
130
|
+
name = str(name)
|
|
131
|
+
if name in self._by_orig:
|
|
132
|
+
return self._by_orig[name]
|
|
133
|
+
param = _param_name_for_subst(name)
|
|
134
|
+
self._by_orig[name] = param
|
|
135
|
+
self.unbound[param] = name
|
|
136
|
+
return param
|
|
147
137
|
|
|
148
138
|
|
|
149
139
|
class _Translator:
|
|
@@ -196,8 +186,8 @@ class _Translator:
|
|
|
196
186
|
def _result(self, **kw):
|
|
197
187
|
d = {k: v for k, v in kw.items() if v is not None}
|
|
198
188
|
d['params'] = dict(self.state.params)
|
|
199
|
-
if self.state.
|
|
200
|
-
d['
|
|
189
|
+
if self.state.unbound:
|
|
190
|
+
d['unbound'] = dict(self.state.unbound)
|
|
201
191
|
return d
|
|
202
192
|
|
|
203
193
|
def _column_name(self, op):
|
|
@@ -490,20 +480,23 @@ def sqlize(path, *, bindings=None, flavor='postgres', format='sqlalchemy'):
|
|
|
490
480
|
Translate a dotted path into SQL clause components.
|
|
491
481
|
|
|
492
482
|
Returns a dict with some subset of keys select, where, from, params,
|
|
493
|
-
|
|
483
|
+
unbound.
|
|
494
484
|
|
|
495
485
|
path — dotted path string or pre-parsed Dotted result.
|
|
496
486
|
bindings — optional mapping/list used to resolve substitutions before
|
|
497
487
|
translation. Path-position substitutions must be resolved or a
|
|
498
488
|
TranslationError is raised. Unresolved value-position substitutions
|
|
499
|
-
|
|
489
|
+
appear in 'unbound'.
|
|
500
490
|
flavor — SQL flavor for JSONB operators; only 'postgres' is implemented.
|
|
501
491
|
format — placeholder style. Only 'sqlalchemy' (`:name`) is supported in v1.
|
|
502
492
|
|
|
503
493
|
Literals on the RHS of guards are hoisted into params under generated
|
|
504
|
-
names (`_p1`, `_p2`, …).
|
|
505
|
-
|
|
506
|
-
|
|
494
|
+
names (`_p1`, `_p2`, …). Substitutions are hoisted under their own
|
|
495
|
+
name when it's a valid SQL identifier; otherwise a deterministic
|
|
496
|
+
`_s_<hash>` bind name is generated. Unresolved substitutions appear
|
|
497
|
+
in 'unbound' as a dict mapping bind_param_name → original_name so
|
|
498
|
+
callers can fill params by bind name and recover the source name
|
|
499
|
+
when needed.
|
|
507
500
|
|
|
508
501
|
>>> sqlize("status = 'active'")
|
|
509
502
|
{'select': 'status', 'where': 'status = :_p1', 'params': {'_p1': 'active'}}
|
|
@@ -216,6 +216,20 @@ class ValueGuard(Wrap):
|
|
|
216
216
|
return True
|
|
217
217
|
return False
|
|
218
218
|
|
|
219
|
+
def is_reference(self):
|
|
220
|
+
"""
|
|
221
|
+
True if the inner op, the guard value, or any transform contains
|
|
222
|
+
an internal reference.
|
|
223
|
+
"""
|
|
224
|
+
if self.inner.is_reference():
|
|
225
|
+
return True
|
|
226
|
+
if hasattr(self.guard, 'is_reference') and self.guard.is_reference():
|
|
227
|
+
return True
|
|
228
|
+
for t in self.transforms:
|
|
229
|
+
if hasattr(t, 'is_reference') and t.is_reference():
|
|
230
|
+
return True
|
|
231
|
+
return False
|
|
232
|
+
|
|
219
233
|
def is_recursive(self):
|
|
220
234
|
return self.inner.is_recursive()
|
|
221
235
|
|
|
@@ -444,6 +458,17 @@ class FilterWrap(Wrap):
|
|
|
444
458
|
hasattr(f, 'is_template') and f.is_template()
|
|
445
459
|
for f in self.filters)
|
|
446
460
|
|
|
461
|
+
def is_reference(self):
|
|
462
|
+
"""
|
|
463
|
+
True if the inner op or any attached filter contains an internal
|
|
464
|
+
reference.
|
|
465
|
+
"""
|
|
466
|
+
if self.inner.is_reference():
|
|
467
|
+
return True
|
|
468
|
+
return any(
|
|
469
|
+
hasattr(f, 'is_reference') and f.is_reference()
|
|
470
|
+
for f in self.filters)
|
|
471
|
+
|
|
447
472
|
def default(self):
|
|
448
473
|
"""
|
|
449
474
|
Filters imply the value has structure to check against,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.43.
|
|
3
|
+
Version: 0.43.5
|
|
4
4
|
Summary: Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms
|
|
5
5
|
Home-page: https://github.com/freywaid/dotted
|
|
6
6
|
Author: Frey Waid
|
|
@@ -2871,18 +2871,36 @@ time:
|
|
|
2871
2871
|
|
|
2872
2872
|
>>> r = dotted.sqlize("age >= $(min_age)")
|
|
2873
2873
|
>>> r
|
|
2874
|
-
{'select': 'age', 'where': 'age >= :min_age', 'params': {},
|
|
2874
|
+
{'select': 'age', 'where': 'age >= :min_age', 'params': {},
|
|
2875
|
+
'unbound': {'min_age': 'min_age'}}
|
|
2875
2876
|
|
|
2876
|
-
`
|
|
2877
|
-
`
|
|
2877
|
+
`unbound` is a dict keyed by the **SQL bind parameter name** — the name
|
|
2878
|
+
that appears in the SQL `:placeholder` and as a key in `params`. Each
|
|
2879
|
+
value is the **original substitution name** (what was inside `$(...)`),
|
|
2880
|
+
kept as provenance so callers can trace a bind back to its source.
|
|
2881
|
+
|
|
2882
|
+
`unbound.keys()` is the list of bind slots the caller needs to fill.
|
|
2883
|
+
Most often you just fill `params` by bind name directly:
|
|
2878
2884
|
|
|
2879
2885
|
>>> r['params']['min_age'] = 30
|
|
2880
2886
|
>>> # hand (r['where'], r['params']) to cursor.execute / session.execute / etc.
|
|
2881
2887
|
|
|
2882
|
-
|
|
2888
|
+
Non-identifier names (dotted paths, quoted keys, spaces, etc.) hash to a
|
|
2889
|
+
deterministic `_s_<hex>` bind name — the bind-side name is different
|
|
2890
|
+
from what appeared in source, but the original is preserved in the
|
|
2891
|
+
`unbound` value:
|
|
2892
|
+
|
|
2893
|
+
>>> r = dotted.sqlize('age >= $(user.min_age)')
|
|
2894
|
+
>>> r['where'] # doctest: +ELLIPSIS
|
|
2895
|
+
'age >= :_s_...'
|
|
2896
|
+
>>> list(r['unbound'].values())
|
|
2897
|
+
['user.min_age']
|
|
2898
|
+
|
|
2899
|
+
Repeated uses of the same name share one entry automatically:
|
|
2883
2900
|
|
|
2884
2901
|
>>> dotted.sqlize('(age >= $(x) & weight = $(x))')
|
|
2885
|
-
{'where': '(age >= :x) AND (weight = :x)', 'params': {},
|
|
2902
|
+
{'where': '(age >= :x) AND (weight = :x)', 'params': {},
|
|
2903
|
+
'unbound': {'x': 'x'}}
|
|
2886
2904
|
|
|
2887
2905
|
If you pass `bindings=`, substitutions are resolved at sqlize time and hoisted
|
|
2888
2906
|
like any other literal:
|
|
@@ -2910,7 +2928,7 @@ Returned dict may contain any subset of:
|
|
|
2910
2928
|
| `select` | `str` | Expression yielding the value dotted would return |
|
|
2911
2929
|
| `where` | `str` | Predicate from guards / filters / boolean groups |
|
|
2912
2930
|
| `params` | `dict` | Hoisted values keyed by name |
|
|
2913
|
-
| `
|
|
2931
|
+
| `unbound` | `dict` | Deferred substitutions: `{bind_param_name: original_name}` |
|
|
2914
2932
|
|
|
2915
2933
|
`select` is omitted when combining predicates across different columns
|
|
2916
2934
|
(e.g. top-level groups), since there's no single meaningful expression.
|
|
@@ -5,7 +5,7 @@ with open("README.md", "rt") as f:
|
|
|
5
5
|
|
|
6
6
|
setuptools.setup(
|
|
7
7
|
name="dotted_notation",
|
|
8
|
-
version="0.43.
|
|
8
|
+
version="0.43.5",
|
|
9
9
|
author="Frey Waid",
|
|
10
10
|
author_email="logophage1@gmail.com",
|
|
11
11
|
description="Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms",
|
|
@@ -65,6 +65,115 @@ def test_is_inverted_false():
|
|
|
65
65
|
assert dotted.is_inverted('*') is False
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
# ---- is_reference ----
|
|
69
|
+
|
|
70
|
+
def test_is_reference_top_level():
|
|
71
|
+
assert dotted.is_reference('a.$$(b)') is True
|
|
72
|
+
assert dotted.is_reference('$$(x.y)') is True
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_is_reference_in_guard():
|
|
76
|
+
# Ref in guard position (previously missed)
|
|
77
|
+
assert dotted.is_reference('a=$$(b)') is True
|
|
78
|
+
assert dotted.is_reference('a>=$$(b)') is True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_is_reference_in_filter():
|
|
82
|
+
assert dotted.is_reference('a[*&x=$$(b)]') is True
|
|
83
|
+
assert dotted.is_reference('a[$$(k)=1]') is True
|
|
84
|
+
assert dotted.is_reference('a[!x=$$(b)]') is True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_is_reference_in_group():
|
|
88
|
+
assert dotted.is_reference('(a=$$(b) & c=1)') is True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_is_reference_false_for_plain():
|
|
92
|
+
assert dotted.is_reference('a.b') is False
|
|
93
|
+
assert dotted.is_reference('a.*.b') is False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_is_reference_false_for_template():
|
|
97
|
+
# Substitutions are not references
|
|
98
|
+
assert dotted.is_reference('a.$(x)') is False
|
|
99
|
+
assert dotted.is_reference('a=$(x)') is False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ---- is_indeterminate ----
|
|
103
|
+
|
|
104
|
+
def test_is_indeterminate_templates():
|
|
105
|
+
assert dotted.is_indeterminate('a.$(x)') is True
|
|
106
|
+
assert dotted.is_indeterminate('a=$(x)') is True
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_is_indeterminate_references():
|
|
110
|
+
assert dotted.is_indeterminate('a.$$(b)') is True
|
|
111
|
+
assert dotted.is_indeterminate('a=$$(b)') is True
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_is_indeterminate_false_for_plain():
|
|
115
|
+
assert dotted.is_indeterminate('a.b.c') is False
|
|
116
|
+
assert dotted.is_indeterminate('a[0]') is False
|
|
117
|
+
assert dotted.is_indeterminate('a=30') is False
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_is_indeterminate_false_for_patterns():
|
|
121
|
+
# Patterns are not indeterminate — they describe a set inherent to
|
|
122
|
+
# the query, not a placeholder awaiting info.
|
|
123
|
+
assert dotted.is_indeterminate('a.*.b') is False
|
|
124
|
+
assert dotted.is_indeterminate('a.**') is False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---- is_simple ----
|
|
128
|
+
|
|
129
|
+
def test_is_simple_true_for_plain_paths():
|
|
130
|
+
assert dotted.is_simple('a') is True
|
|
131
|
+
assert dotted.is_simple('a.b.c') is True
|
|
132
|
+
assert dotted.is_simple('a[0].b') is True
|
|
133
|
+
assert dotted.is_simple('a@attr') is True
|
|
134
|
+
assert dotted.is_simple('a.b[0].c') is True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_is_simple_false_for_patterns():
|
|
138
|
+
assert dotted.is_simple('a.*.b') is False
|
|
139
|
+
assert dotted.is_simple('a[*]') is False
|
|
140
|
+
assert dotted.is_simple('a.**') is False
|
|
141
|
+
assert dotted.is_simple('a=/re/') is False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_is_simple_false_for_templates():
|
|
145
|
+
assert dotted.is_simple('a.$(x)') is False
|
|
146
|
+
assert dotted.is_simple('a.$0') is False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_is_simple_false_for_references():
|
|
150
|
+
assert dotted.is_simple('a.$$(b)') is False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_is_simple_false_for_guards():
|
|
154
|
+
assert dotted.is_simple('a=30') is False
|
|
155
|
+
assert dotted.is_simple('a.b=1') is False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_is_simple_false_for_transforms():
|
|
159
|
+
assert dotted.is_simple('a|int') is False
|
|
160
|
+
assert dotted.is_simple('a.b|str') is False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_is_simple_false_for_filters():
|
|
164
|
+
assert dotted.is_simple('a[*&x=1]') is False
|
|
165
|
+
assert dotted.is_simple('a[x=1]') is False
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_is_simple_false_for_groups():
|
|
169
|
+
assert dotted.is_simple('(a, b)') is False
|
|
170
|
+
assert dotted.is_simple('a.(b, c)') is False
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_is_simple_false_for_inverted():
|
|
174
|
+
assert dotted.is_simple('!a') is False
|
|
175
|
+
|
|
176
|
+
|
|
68
177
|
# build / build_multi
|
|
69
178
|
|
|
70
179
|
def test_build_simple():
|
|
@@ -297,23 +297,24 @@ def test_value_sub_with_binding():
|
|
|
297
297
|
|
|
298
298
|
|
|
299
299
|
def test_value_sub_without_binding():
|
|
300
|
-
# Unbound substitution
|
|
300
|
+
# Unbound substitution: name is already a valid identifier, so it's
|
|
301
|
+
# used directly as the bind name. unbound maps bind → orig.
|
|
301
302
|
result = sqlize('age>=$(min_age)')
|
|
302
303
|
assert result == {
|
|
303
304
|
'select': 'age',
|
|
304
305
|
'where': 'age >= :min_age',
|
|
305
306
|
'params': {},
|
|
306
|
-
'
|
|
307
|
+
'unbound': {'min_age': 'min_age'},
|
|
307
308
|
}
|
|
308
309
|
|
|
309
310
|
|
|
310
311
|
def test_value_sub_repeated_dedupes():
|
|
311
|
-
# Same name used twice → single
|
|
312
|
+
# Same name used twice → single placeholder, one unbound entry.
|
|
312
313
|
result = sqlize('(age>=$(x) & weight=$(x))')
|
|
313
314
|
assert result == {
|
|
314
315
|
'where': '(age >= :x) AND (weight = :x)',
|
|
315
316
|
'params': {},
|
|
316
|
-
'
|
|
317
|
+
'unbound': {'x': 'x'},
|
|
317
318
|
}
|
|
318
319
|
|
|
319
320
|
|
|
@@ -322,7 +323,7 @@ def test_mixed_subst_and_literal():
|
|
|
322
323
|
assert result == {
|
|
323
324
|
'where': '(age >= :min_age) AND (weight > :_p1)',
|
|
324
325
|
'params': {'_p1': 200},
|
|
325
|
-
'
|
|
326
|
+
'unbound': {'min_age': 'min_age'},
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
|
|
@@ -337,39 +338,40 @@ def test_value_sub_dotted_name_with_bindings():
|
|
|
337
338
|
}
|
|
338
339
|
|
|
339
340
|
|
|
340
|
-
def
|
|
341
|
-
# Dotted subst name
|
|
341
|
+
def test_value_sub_dotted_name_hashed():
|
|
342
|
+
# Dotted subst name hashes to an _s_<hex> bind name; unbound maps
|
|
343
|
+
# bind → orig so callers can recover the source name.
|
|
342
344
|
result = sqlize('age>=$(user.min_age)')
|
|
343
|
-
assert result ==
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
def test_value_sub_attr_name_encoded():
|
|
352
|
-
result = sqlize('age>=$(obj@attr)')
|
|
353
|
-
assert result['where'] == 'age >= :obj_at_attr'
|
|
354
|
-
assert result['missing'] == ['obj_at_attr']
|
|
345
|
+
assert result['select'] == 'age'
|
|
346
|
+
assert set(result['unbound'].values()) == {'user.min_age'}
|
|
347
|
+
[(bind, orig)] = result['unbound'].items()
|
|
348
|
+
assert bind.startswith('_s_')
|
|
349
|
+
assert orig == 'user.min_age'
|
|
350
|
+
assert result['where'] == f'age >= :{bind}'
|
|
351
|
+
assert result['params'] == {}
|
|
355
352
|
|
|
356
353
|
|
|
357
|
-
def
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
354
|
+
def test_value_sub_hash_is_deterministic():
|
|
355
|
+
# Same subst name → same hash across calls (still keyed by bind)
|
|
356
|
+
a = list(sqlize('age>=$(user.min_age)')['unbound'])[0]
|
|
357
|
+
b = list(sqlize('weight>=$(user.min_age)')['unbound'])[0]
|
|
358
|
+
assert a == b
|
|
361
359
|
|
|
362
360
|
|
|
363
|
-
def
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
361
|
+
def test_value_sub_hash_names_are_distinct():
|
|
362
|
+
# Different subst names produce different hashes
|
|
363
|
+
a = list(sqlize('age>=$(user.min_age)')['unbound'])[0]
|
|
364
|
+
b = list(sqlize('age>=$(user.max_age)')['unbound'])[0]
|
|
365
|
+
assert a != b
|
|
367
366
|
|
|
368
367
|
|
|
369
|
-
def
|
|
370
|
-
#
|
|
371
|
-
|
|
372
|
-
|
|
368
|
+
def test_value_sub_special_char_hashed():
|
|
369
|
+
# Names with spaces / quotes / punctuation hash without erroring
|
|
370
|
+
result = sqlize("age>=$('dr pepper')")
|
|
371
|
+
assert set(result['unbound'].values()) == {"'dr pepper'"}
|
|
372
|
+
[(bind, orig)] = result['unbound'].items()
|
|
373
|
+
assert bind.startswith('_s_')
|
|
374
|
+
assert orig == "'dr pepper'"
|
|
373
375
|
|
|
374
376
|
|
|
375
377
|
def test_path_sub_with_binding():
|
|
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
|
{dotted_notation-0.43.4 → dotted_notation-0.43.5}/dotted_notation.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
|
|
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
|
|
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
|