dotted-notation 0.43.6__tar.gz → 0.43.8__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.6/dotted_notation.egg-info → dotted_notation-0.43.8}/PKG-INFO +196 -123
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/README.md +197 -124
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/__init__.py +2 -2
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/sqlize.py +407 -80
- {dotted_notation-0.43.6 → dotted_notation-0.43.8/dotted_notation.egg-info}/PKG-INFO +196 -123
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/setup.py +1 -1
- dotted_notation-0.43.8/tests/test_sqlize.py +786 -0
- dotted_notation-0.43.6/tests/test_sqlize.py +0 -523
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/LICENSE +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/__main__.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/access.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/api.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/base.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/cli/main.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/containers.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/engine.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/filters.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/grammar.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/groups.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/matchers.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/predicates.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/recursive.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/results.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/transforms.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/utils.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/utypes.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted/wrappers.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/setup.cfg +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/__init__.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_api.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_appender.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_assemble.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_attrs.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_bindings.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_cli.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_concat.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_container_filter.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_cut.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_empty.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_filter_keyvalue.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_get.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_guard_transforms.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_invert.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_json_sentinels.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_keys_values.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_match.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_matchable.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_named_subst.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_negation.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_nop.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_numeric.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_opgroup.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_pluck.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_predicates.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_quote_idempotent.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_recursive.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_reference.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_replace.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_slice.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_softcut.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_strict.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_string_glob.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_subst_escape.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_subst_transforms.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_threading.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_transforms.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_translate.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_type_restriction.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_unpack.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_update.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/tests/test_update_if.py +0 -0
- {dotted_notation-0.43.6 → dotted_notation-0.43.8}/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.8
|
|
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
|
|
@@ -2800,190 +2800,263 @@ View all registered transforms with `dotted.registry()`.
|
|
|
2800
2800
|
<a id="sqlize"></a>
|
|
2801
2801
|
## SQL Translation (`sqlize`)
|
|
2802
2802
|
|
|
2803
|
-
> **Under development.** The API shape
|
|
2804
|
-
>
|
|
2805
|
-
>
|
|
2806
|
-
>
|
|
2807
|
-
>
|
|
2808
|
-
> `LATERAL jsonb_path_query`) are not supported yet.
|
|
2803
|
+
> **Under development.** The API shape and supported subset of dotted
|
|
2804
|
+
> features may still change. Postgres-only. Pattern paths (`*`, `[*]`,
|
|
2805
|
+
> `**`, filter brackets) are supported only as WHERE-side existence
|
|
2806
|
+
> checks via `jsonb_path_exists`; pattern paths that yield sets as
|
|
2807
|
+
> SELECT (needing `LATERAL jsonb_path_query`) are not supported yet.
|
|
2809
2808
|
|
|
2810
|
-
`dotted.sqlize` translates a dotted path into
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2809
|
+
`dotted.sqlize(path)` translates a dotted path into a `Resolver`
|
|
2810
|
+
carrying paramstyle-neutral SQL fragments. `Resolver.build(sql,
|
|
2811
|
+
paramstyle=..., **bindings)` renders a fragment (or any composition of
|
|
2812
|
+
fragments) into final SQL + a params dict / args list ready for a
|
|
2813
|
+
driver.
|
|
2814
2814
|
|
|
2815
|
-
The first segment of the dotted path is the SQL column
|
|
2816
|
-
are JSON navigation inside
|
|
2817
|
-
|
|
2815
|
+
The first segment of the dotted path is the SQL column; further
|
|
2816
|
+
segments are JSON navigation inside it (JSONB). Scalar columns and
|
|
2817
|
+
JSONB columns share one surface:
|
|
2818
2818
|
|
|
2819
2819
|
>>> import dotted
|
|
2820
|
-
>>> dotted.sqlize("status = 'active'")
|
|
2821
|
-
|
|
2820
|
+
>>> r = dotted.sqlize("status = 'active'")
|
|
2821
|
+
>>> r.where
|
|
2822
|
+
SQLFragment('status = {_p1}')
|
|
2823
|
+
>>> dotted.Resolver.build(r.where)
|
|
2824
|
+
('status = :_p1', {'_p1': 'active'})
|
|
2822
2825
|
|
|
2823
|
-
>>> dotted.sqlize("data.user.age >= 30")
|
|
2824
|
-
|
|
2826
|
+
>>> r = dotted.sqlize("data.user.age >= 30")
|
|
2827
|
+
>>> dotted.Resolver.build(r.where)
|
|
2828
|
+
("(data #>> '{user,age}')::numeric >= :_p1", {'_p1': 30})
|
|
2825
2829
|
|
|
2826
|
-
>>> dotted.sqlize("data.user.name") # pure traversal, no predicate
|
|
2827
|
-
|
|
2830
|
+
>>> r = dotted.sqlize("data.user.name") # pure traversal, no predicate
|
|
2831
|
+
>>> dotted.Resolver.build(r.select)
|
|
2832
|
+
("data #>> '{user,name}'", {})
|
|
2828
2833
|
|
|
2829
2834
|
Guards encode the predicate:
|
|
2830
2835
|
|
|
2831
|
-
>>> dotted.sqlize("age >= 30")
|
|
2832
|
-
|
|
2836
|
+
>>> dotted.Resolver.build(dotted.sqlize("age >= 30").where)
|
|
2837
|
+
('age >= :_p1', {'_p1': 30})
|
|
2833
2838
|
|
|
2834
|
-
>>> dotted.sqlize("deleted_at = None")
|
|
2835
|
-
|
|
2839
|
+
>>> dotted.Resolver.build(dotted.sqlize("deleted_at = None").where)
|
|
2840
|
+
('deleted_at IS NULL', {})
|
|
2836
2841
|
|
|
2837
|
-
>>> dotted.sqlize("name = /^alice/")
|
|
2838
|
-
|
|
2842
|
+
>>> dotted.Resolver.build(dotted.sqlize("name = /^alice/").where)
|
|
2843
|
+
('name ~ :_p1', {'_p1': '^alice'})
|
|
2839
2844
|
|
|
2840
2845
|
Boolean grouping maps directly onto `AND` / `OR` / `NOT`:
|
|
2841
2846
|
|
|
2842
|
-
>>> dotted.sqlize('(age >= 18 & status = "active")')
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
>>> dotted.sqlize('data.(user.age >= 30 & status = "active")')['where']
|
|
2846
|
-
"((data #>> '{user,age}')::numeric >= :_p1) AND ((data #>> '{status}') = :_p2)"
|
|
2847
|
+
>>> r = dotted.sqlize('(age >= 18 & status = "active")')
|
|
2848
|
+
>>> dotted.Resolver.build(r.where)
|
|
2849
|
+
('(age >= :_p1) AND (status = :_p2)', {'_p1': 18, '_p2': 'active'})
|
|
2847
2850
|
|
|
2848
2851
|
Guard transforms become SQL casts (`int`, `float`, `str`, `bool`):
|
|
2849
2852
|
|
|
2850
|
-
>>> dotted.sqlize("data.user.age|int >= 30")
|
|
2851
|
-
|
|
2853
|
+
>>> r = dotted.sqlize("data.user.age|int >= 30")
|
|
2854
|
+
>>> dotted.Resolver.build(r.where)
|
|
2855
|
+
("(data #>> '{user,age}')::int >= :_p1", {'_p1': 30})
|
|
2852
2856
|
|
|
2853
|
-
###
|
|
2857
|
+
### SQLFragment and composition
|
|
2854
2858
|
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2859
|
+
`Resolver.select`, `Resolver.where`, `Resolver.from_` are `SQLFragment`
|
|
2860
|
+
objects. The underlying text uses Python format-string syntax with
|
|
2861
|
+
`{name}` markers — each marker is resolved to a driver placeholder
|
|
2862
|
+
(`:name` or `$N`) at build time. Literal braces in the SQL (e.g.
|
|
2863
|
+
JSONB path arrays) are doubled as `{{`…`}}`.
|
|
2858
2864
|
|
|
2859
|
-
|
|
2860
|
-
>>> r['where']
|
|
2861
|
-
'name = :_p1'
|
|
2862
|
-
>>> r['params']
|
|
2863
|
-
{'_p1': "'; DROP TABLE users;--"} # stays a value, never parsed as SQL
|
|
2865
|
+
Fragments concatenate naturally via `+` / `__radd__`; metadata merges:
|
|
2864
2866
|
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
+
>>> r = dotted.sqlize("age >= $(min_age)")
|
|
2868
|
+
>>> combined = "WHERE " + r.where
|
|
2869
|
+
>>> dotted.Resolver.build(combined, min_age=30)
|
|
2870
|
+
('WHERE age >= :min_age', {'min_age': 30})
|
|
2867
2871
|
|
|
2868
|
-
###
|
|
2872
|
+
### Hoisted params
|
|
2869
2873
|
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
time:
|
|
2874
|
+
Every RHS value is hoisted into a params entry keyed by a generated
|
|
2875
|
+
name (`_p1`, `_p2`, …). This keeps values out of the SQL string:
|
|
2873
2876
|
|
|
2874
|
-
>>>
|
|
2875
|
-
>>> r
|
|
2876
|
-
|
|
2877
|
-
|
|
2877
|
+
>>> user_input = "'; DROP TABLE users;--"
|
|
2878
|
+
>>> r = dotted.sqlize(f"name = {user_input!r}")
|
|
2879
|
+
>>> str(r.where)
|
|
2880
|
+
'name = {_p1}'
|
|
2881
|
+
>>> dotted.Resolver.build(r.where)
|
|
2882
|
+
("name = :_p1", {'_p1': "'; DROP TABLE users;--"})
|
|
2878
2883
|
|
|
2879
|
-
|
|
2880
|
-
that appears in the SQL `:placeholder` and as a key in `params`. Each
|
|
2881
|
-
value is the **original substitution name** (what was inside `$(...)`),
|
|
2882
|
-
kept as provenance so callers can trace a bind back to its source.
|
|
2884
|
+
The user input stays a value — never parsed as SQL.
|
|
2883
2885
|
|
|
2884
|
-
|
|
2885
|
-
Most often you just fill `params` by bind name directly:
|
|
2886
|
+
### Substitutions (late binding)
|
|
2886
2887
|
|
|
2887
|
-
|
|
2888
|
-
|
|
2888
|
+
A substitution is preserved as a deferred bind; supply the value at
|
|
2889
|
+
build time:
|
|
2889
2890
|
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2891
|
+
>>> r = dotted.sqlize("age >= $(min_age)")
|
|
2892
|
+
>>> r.where.unbound # bind_name → original_name
|
|
2893
|
+
{'min_age': 'min_age'}
|
|
2894
|
+
>>> dotted.Resolver.build(r.where, min_age=30)
|
|
2895
|
+
('age >= :min_age', {'min_age': 30})
|
|
2896
|
+
|
|
2897
|
+
`unbound` is keyed by the **bind marker name** (what ends up as
|
|
2898
|
+
`:name` / `$N`); each value is the **original substitution name**
|
|
2899
|
+
(what was inside `$(...)`), kept as provenance. Plain identifiers bind
|
|
2900
|
+
by that same name; non-identifier names (dotted paths, quoted keys,
|
|
2901
|
+
spaces) hash to a deterministic `_s_<hex>` marker:
|
|
2894
2902
|
|
|
2895
2903
|
>>> r = dotted.sqlize('age >= $(user.min_age)')
|
|
2896
|
-
>>> r
|
|
2897
|
-
'age >= :_s_...'
|
|
2898
|
-
>>> list(r['unbound'].values())
|
|
2904
|
+
>>> list(r.where.unbound.values())
|
|
2899
2905
|
['user.min_age']
|
|
2900
2906
|
|
|
2901
|
-
Repeated uses of the same name share one entry
|
|
2907
|
+
Repeated uses of the same name share one entry:
|
|
2902
2908
|
|
|
2903
|
-
>>> dotted.sqlize('(age >= $(x) & weight = $(x))')
|
|
2904
|
-
|
|
2905
|
-
|
|
2909
|
+
>>> r = dotted.sqlize('(age >= $(x) & weight = $(x))')
|
|
2910
|
+
>>> dotted.Resolver.build(r.where, x=42)
|
|
2911
|
+
('(age >= :x) AND (weight = :x)', {'x': 42})
|
|
2906
2912
|
|
|
2907
|
-
If you pass `bindings
|
|
2908
|
-
like any other literal
|
|
2913
|
+
If you pass `bindings=` at sqlize time, substitutions are resolved
|
|
2914
|
+
immediately and hoisted like any other literal — no longer appearing
|
|
2915
|
+
in `unbound`:
|
|
2909
2916
|
|
|
2910
|
-
>>> dotted.sqlize("age >= $(min_age)", bindings={'min_age': 30})
|
|
2911
|
-
|
|
2917
|
+
>>> r = dotted.sqlize("age >= $(min_age)", bindings={'min_age': 30})
|
|
2918
|
+
>>> dotted.Resolver.build(r.where)
|
|
2919
|
+
('age >= :_p1', {'_p1': 30})
|
|
2912
2920
|
|
|
2913
2921
|
### References
|
|
2914
2922
|
|
|
2915
|
-
Absolute-root references (`$$(path)`) map to dynamic JSON key lookups
|
|
2916
|
-
Postgres's `->` / `#>` accept runtime-computed keys:
|
|
2923
|
+
Absolute-root references (`$$(path)`) map to dynamic JSON key lookups
|
|
2924
|
+
— Postgres's `->` / `#>` accept runtime-computed keys:
|
|
2917
2925
|
|
|
2918
|
-
>>> dotted.sqlize('data.$$(data.config.field) = "Alice"')
|
|
2919
|
-
|
|
2926
|
+
>>> r = dotted.sqlize('data.$$(data.config.field) = "Alice"')
|
|
2927
|
+
>>> dotted.Resolver.build(r.where)
|
|
2928
|
+
("(data ->> (data #>> '{config,field}')) = :_p1", {'_p1': 'Alice'})
|
|
2920
2929
|
|
|
2921
|
-
Relative references (`^`) and parent references (`^^+`) are not
|
|
2922
|
-
yet.
|
|
2930
|
+
Relative references (`^`) and parent references (`^^+`) are not
|
|
2931
|
+
supported yet.
|
|
2923
2932
|
|
|
2924
2933
|
### Pattern paths
|
|
2925
2934
|
|
|
2926
|
-
Paths that contain wildcards (`*`, `[*]`), recursive descent (`**`),
|
|
2927
|
-
bracket filters (`[pred]`, `[*&pred]`) translate to a Postgres
|
|
2928
|
-
`jsonb_path_exists(…)` WHERE predicate. The dotted path becomes a
|
|
2929
|
-
expression; guard values embed inline when safely literal
|
|
2930
|
-
booleans, null) or flow through a `jsonb_build_object` vars
|
|
2931
|
-
(strings, substitutions).
|
|
2935
|
+
Paths that contain wildcards (`*`, `[*]`), recursive descent (`**`),
|
|
2936
|
+
or bracket filters (`[pred]`, `[*&pred]`) translate to a Postgres
|
|
2937
|
+
`jsonb_path_exists(…)` WHERE predicate. The dotted path becomes a
|
|
2938
|
+
JSONPath expression; guard values embed inline when safely literal
|
|
2939
|
+
(numbers, booleans, null) or flow through a `jsonb_build_object` vars
|
|
2940
|
+
argument (strings, substitutions).
|
|
2941
|
+
|
|
2942
|
+
>>> r = dotted.sqlize("data.users[*].age >= 30")
|
|
2943
|
+
>>> dotted.Resolver.build(r.where)
|
|
2944
|
+
("jsonb_path_exists(data, '$.users[*].age ? (@ >= 30)')", {})
|
|
2945
|
+
|
|
2946
|
+
>>> r = dotted.sqlize("data.**.active = True")
|
|
2947
|
+
>>> dotted.Resolver.build(r.where)
|
|
2948
|
+
("jsonb_path_exists(data, '$.**.active ? (@ == true)')", {})
|
|
2949
|
+
|
|
2950
|
+
>>> r = dotted.sqlize("data.users[age>=30]")
|
|
2951
|
+
>>> dotted.Resolver.build(r.where)
|
|
2952
|
+
("jsonb_path_exists(data, '$.users[*] ? (@.age >= 30)')", {})
|
|
2953
|
+
|
|
2954
|
+
>>> r = dotted.sqlize('data.users[*&age>=30].name = "alice"')
|
|
2955
|
+
>>> sql, params = dotted.Resolver.build(r.where)
|
|
2956
|
+
>>> sql
|
|
2957
|
+
"jsonb_path_exists(data, '$.users[*] ? (@.age >= 30).name ? (@ == $_p1)', jsonb_build_object('_p1', :_p1))"
|
|
2958
|
+
>>> params
|
|
2959
|
+
{'_p1': 'alice'}
|
|
2960
|
+
|
|
2961
|
+
Pattern predicates compose with scalar ones via dotted's `&` / `,` /
|
|
2962
|
+
`!`:
|
|
2963
|
+
|
|
2964
|
+
>>> r = dotted.sqlize('(data.users[*].age >= 30 & status = "active")')
|
|
2965
|
+
>>> sql, params = dotted.Resolver.build(r.where)
|
|
2966
|
+
>>> sql
|
|
2967
|
+
"(jsonb_path_exists(data, '$.users[*].age ? (@ >= 30)')) AND (status = :_p1)"
|
|
2968
|
+
|
|
2969
|
+
`select` for a pattern path is set to the column itself — pattern
|
|
2970
|
+
paths don't have a single extractable value. Use the WHERE predicate
|
|
2971
|
+
to filter rows; write your own SELECT for the columns you want.
|
|
2972
|
+
|
|
2973
|
+
Not yet supported:
|
|
2974
|
+
- Pattern paths as SELECT (needing `LATERAL jsonb_path_query`)
|
|
2975
|
+
- Regex-in-access like `data./prefix_\d+/.field`
|
|
2976
|
+
- Pattern refs
|
|
2977
|
+
|
|
2978
|
+
### Paramstyles
|
|
2932
2979
|
|
|
2933
|
-
|
|
2934
|
-
|
|
2980
|
+
`Resolver.build(sql, paramstyle='named', **bindings)` renders the
|
|
2981
|
+
fragment in the chosen PEP 249 paramstyle. Supported:
|
|
2935
2982
|
|
|
2936
|
-
|
|
2937
|
-
|
|
2983
|
+
| paramstyle | placeholder | output shape | typical driver |
|
|
2984
|
+
|-------------------|----------------|--------------|---------------------------|
|
|
2985
|
+
| `named` | `:name` | dict | SQLAlchemy `text`, sqlite3 |
|
|
2986
|
+
| `pyformat` | `%(name)s` | dict | psycopg (v2/v3) named |
|
|
2987
|
+
| `qmark` | `?` | list | sqlite3, ODBC |
|
|
2988
|
+
| `format` | `%s` | list | psycopg positional |
|
|
2989
|
+
| `numeric` | `:1, :2` | list | Oracle-style |
|
|
2990
|
+
| `dollar-numeric` | `$1, $2` | list | asyncpg, native Postgres |
|
|
2938
2991
|
|
|
2939
|
-
|
|
2940
|
-
|
|
2992
|
+
The dict styles (`named`, `pyformat`) and the numbered positional
|
|
2993
|
+
styles (`numeric`, `dollar-numeric`) have **back-reference**: a
|
|
2994
|
+
substitution used in multiple places in the SQL shares a single bind
|
|
2995
|
+
entry. `qmark` and `format` have no back-reference — each occurrence
|
|
2996
|
+
consumes its own arg, so a repeated substitution emits its value
|
|
2997
|
+
multiple times.
|
|
2941
2998
|
|
|
2942
|
-
|
|
2943
|
-
"jsonb_path_exists(data, '$.users[*] ? (@.age >= 30).name ? (@ == :_p1)', jsonb_build_object('_p1', :_p1))"
|
|
2999
|
+
Examples:
|
|
2944
3000
|
|
|
2945
|
-
|
|
3001
|
+
>>> r = dotted.sqlize('(age >= 18 & status = "active")')
|
|
3002
|
+
>>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric')
|
|
3003
|
+
('(age >= $1) AND (status = $2)', [18, 'active'])
|
|
2946
3004
|
|
|
2947
|
-
>>> dotted.sqlize(
|
|
2948
|
-
|
|
3005
|
+
>>> r = dotted.sqlize("age >= $(min_age)")
|
|
3006
|
+
>>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric', min_age=30)
|
|
3007
|
+
('age >= $1', [30])
|
|
2949
3008
|
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
own SELECT for the columns you want.
|
|
3009
|
+
>>> dotted.Resolver.build(r.where, paramstyle='pyformat', min_age=30)
|
|
3010
|
+
('age >= %(min_age)s', {'min_age': 30})
|
|
2953
3011
|
|
|
2954
|
-
|
|
2955
|
-
- Pattern paths as SELECT (needing `LATERAL jsonb_path_query`)
|
|
2956
|
-
- Regex-in-access like `data./prefix_\d+/.field`
|
|
2957
|
-
- Pattern refs
|
|
3012
|
+
Back-reference vs no-backref on repeated substitutions:
|
|
2958
3013
|
|
|
2959
|
-
|
|
3014
|
+
>>> r = dotted.sqlize('(age >= $(x) & weight = $(x))')
|
|
3015
|
+
>>> dotted.Resolver.build(r.where, paramstyle='numeric', x=42)
|
|
3016
|
+
('(age >= :1) AND (weight = :1)', [42])
|
|
3017
|
+
>>> dotted.Resolver.build(r.where, paramstyle='qmark', x=42)
|
|
3018
|
+
('(age >= ?) AND (weight = ?)', [42, 42])
|
|
2960
3019
|
|
|
2961
|
-
|
|
3020
|
+
A `ParamStyle` `StrEnum` is also available for autocomplete and type
|
|
3021
|
+
checking:
|
|
2962
3022
|
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
| `select` | `str` | Expression yielding the value dotted would return |
|
|
2966
|
-
| `where` | `str` | Predicate from guards / filters / boolean groups |
|
|
2967
|
-
| `params` | `dict` | Hoisted values keyed by name |
|
|
2968
|
-
| `unbound` | `dict` | Deferred substitutions: `{bind_param_name: original_name}` |
|
|
3023
|
+
>>> dotted.ParamStyle.named, dotted.ParamStyle.dollar_numeric
|
|
3024
|
+
(<ParamStyle.named: 'named'>, <ParamStyle.dollar_numeric: 'dollar-numeric'>)
|
|
2969
3025
|
|
|
2970
|
-
|
|
2971
|
-
(e.g. top-level groups), since there's no single meaningful expression.
|
|
2972
|
-
`where` is omitted when the path is pure traversal. Usage pattern:
|
|
3026
|
+
### Resolver attributes
|
|
2973
3027
|
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
3028
|
+
| Attribute | Type | Meaning |
|
|
3029
|
+
|-----------------|------------------------------|--------------------------------------------------------------------|
|
|
3030
|
+
| `.select` | `SQLFragment` \| `None` | Extraction expression (None when path is a predicate-only group) |
|
|
3031
|
+
| `.where` | `SQLFragment` \| `None` | Predicate (None for pure traversals) |
|
|
3032
|
+
| `.from_` | `SQLFragment` \| `None` | LATERAL / join (future — None in v1) |
|
|
3033
|
+
| `.unbound` | `dict` | Union of fragments' unbound mappings: `{bind_name: original_name}` |
|
|
3034
|
+
|
|
3035
|
+
A `SQLFragment` carries:
|
|
3036
|
+
|
|
3037
|
+
| Attribute | Type | Meaning |
|
|
3038
|
+
|-------------|--------|--------------------------------------------------------------------|
|
|
3039
|
+
| `.text` | `str` | Format-string SQL with `{name}` markers |
|
|
3040
|
+
| `.params` | `dict` | Pre-hoisted values: `{marker_name: value}` |
|
|
3041
|
+
| `.unbound` | `dict` | Deferred bindings: `{marker_name: original_substitution_name}` |
|
|
2977
3042
|
|
|
2978
3043
|
### Keyword arguments
|
|
2979
3044
|
|
|
2980
|
-
`dotted.sqlize(path, *, bindings=None, flavor='postgres'
|
|
3045
|
+
`dotted.sqlize(path, *, bindings=None, flavor='postgres')`
|
|
2981
3046
|
|
|
2982
|
-
- `bindings` — resolve substitutions at sqlize time.
|
|
3047
|
+
- `bindings` — resolve substitutions at sqlize time. Path-position
|
|
3048
|
+
substitutions must be resolved here.
|
|
2983
3049
|
- `flavor` — SQL flavor for JSONB operators; only `'postgres'` is
|
|
2984
3050
|
implemented.
|
|
2985
|
-
|
|
2986
|
-
|
|
3051
|
+
|
|
3052
|
+
`Resolver.build(sql, paramstyle='named', **bindings)`
|
|
3053
|
+
|
|
3054
|
+
- `sql` — a `SQLFragment` (usually `r.where`, `r.select`, or a
|
|
3055
|
+
composition like `r.select + " FROM t WHERE " + r.where`).
|
|
3056
|
+
- `paramstyle` — `'named'` or `'dollar-numeric'`, or a `ParamStyle`
|
|
3057
|
+
enum member.
|
|
3058
|
+
- `**bindings` — values for substitutions, keyed by original
|
|
3059
|
+
substitution name.
|
|
2987
3060
|
|
|
2988
3061
|
Unsupported features and pattern paths raise `dotted.TranslationError`.
|
|
2989
3062
|
|