dotted-notation 0.43.5__tar.gz → 0.43.7__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.5/dotted_notation.egg-info → dotted_notation-0.43.7}/PKG-INFO +183 -97
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/README.md +184 -98
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/__init__.py +2 -2
- dotted_notation-0.43.7/dotted/sqlize.py +1164 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7/dotted_notation.egg-info}/PKG-INFO +183 -97
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/setup.py +1 -1
- dotted_notation-0.43.7/tests/test_sqlize.py +756 -0
- dotted_notation-0.43.5/dotted/sqlize.py +0 -510
- dotted_notation-0.43.5/tests/test_sqlize.py +0 -437
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/LICENSE +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/__main__.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/access.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/api.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/base.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/cli/main.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/containers.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/engine.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/filters.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/grammar.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/groups.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/matchers.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/predicates.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/recursive.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/results.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/transforms.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/utils.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/utypes.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/wrappers.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/setup.cfg +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/__init__.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_api.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_appender.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_assemble.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_attrs.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_bindings.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_cli.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_concat.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_container_filter.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_cut.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_empty.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_filter_keyvalue.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_get.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_guard_transforms.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_invert.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_json_sentinels.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_keys_values.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_match.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_matchable.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_named_subst.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_negation.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_nop.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_numeric.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_opgroup.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_pluck.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_predicates.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_quote_idempotent.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_recursive.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_reference.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_replace.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_slice.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_softcut.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_strict.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_string_glob.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_subst_escape.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_subst_transforms.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_threading.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_transforms.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_translate.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_type_restriction.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_unpack.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_update.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_update_if.py +0 -0
- {dotted_notation-0.43.5 → dotted_notation-0.43.7}/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.7
|
|
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,153 +2800,239 @@ 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
|
-
>
|
|
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.
|
|
2807
2808
|
|
|
2808
|
-
`dotted.sqlize` translates a dotted path into
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
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.
|
|
2812
2814
|
|
|
2813
|
-
The first segment of the dotted path is the SQL column
|
|
2814
|
-
are JSON navigation inside
|
|
2815
|
-
|
|
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:
|
|
2816
2818
|
|
|
2817
2819
|
>>> import dotted
|
|
2818
|
-
>>> dotted.sqlize("status = 'active'")
|
|
2819
|
-
|
|
2820
|
+
>>> r = dotted.sqlize("status = 'active'")
|
|
2821
|
+
>>> r.where
|
|
2822
|
+
SQLFragment('status = {_p1}')
|
|
2823
|
+
>>> dotted.Resolver.build(r.where)
|
|
2824
|
+
('status = :_p1', {'_p1': 'active'})
|
|
2820
2825
|
|
|
2821
|
-
>>> dotted.sqlize("data.user.age >= 30")
|
|
2822
|
-
|
|
2826
|
+
>>> r = dotted.sqlize("data.user.age >= 30")
|
|
2827
|
+
>>> dotted.Resolver.build(r.where)
|
|
2828
|
+
("(data #>> '{user,age}')::numeric >= :_p1", {'_p1': 30})
|
|
2823
2829
|
|
|
2824
|
-
>>> dotted.sqlize("data.user.name") # pure traversal, no predicate
|
|
2825
|
-
|
|
2830
|
+
>>> r = dotted.sqlize("data.user.name") # pure traversal, no predicate
|
|
2831
|
+
>>> dotted.Resolver.build(r.select)
|
|
2832
|
+
("data #>> '{user,name}'", {})
|
|
2826
2833
|
|
|
2827
2834
|
Guards encode the predicate:
|
|
2828
2835
|
|
|
2829
|
-
>>> dotted.sqlize("age >= 30")
|
|
2830
|
-
|
|
2836
|
+
>>> dotted.Resolver.build(dotted.sqlize("age >= 30").where)
|
|
2837
|
+
('age >= :_p1', {'_p1': 30})
|
|
2831
2838
|
|
|
2832
|
-
>>> dotted.sqlize("deleted_at = None")
|
|
2833
|
-
|
|
2839
|
+
>>> dotted.Resolver.build(dotted.sqlize("deleted_at = None").where)
|
|
2840
|
+
('deleted_at IS NULL', {})
|
|
2834
2841
|
|
|
2835
|
-
>>> dotted.sqlize("name = /^alice/")
|
|
2836
|
-
|
|
2842
|
+
>>> dotted.Resolver.build(dotted.sqlize("name = /^alice/").where)
|
|
2843
|
+
('name ~ :_p1', {'_p1': '^alice'})
|
|
2837
2844
|
|
|
2838
2845
|
Boolean grouping maps directly onto `AND` / `OR` / `NOT`:
|
|
2839
2846
|
|
|
2840
|
-
>>> dotted.sqlize('(age >= 18 & status = "active")')
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
>>> dotted.sqlize('data.(user.age >= 30 & status = "active")')['where']
|
|
2844
|
-
"((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'})
|
|
2845
2850
|
|
|
2846
2851
|
Guard transforms become SQL casts (`int`, `float`, `str`, `bool`):
|
|
2847
2852
|
|
|
2848
|
-
>>> dotted.sqlize("data.user.age|int >= 30")
|
|
2849
|
-
|
|
2853
|
+
>>> r = dotted.sqlize("data.user.age|int >= 30")
|
|
2854
|
+
>>> dotted.Resolver.build(r.where)
|
|
2855
|
+
("(data #>> '{user,age}')::int >= :_p1", {'_p1': 30})
|
|
2850
2856
|
|
|
2851
|
-
###
|
|
2857
|
+
### SQLFragment and composition
|
|
2852
2858
|
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
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 `{{`…`}}`.
|
|
2856
2864
|
|
|
2857
|
-
|
|
2858
|
-
>>> r['where']
|
|
2859
|
-
'name = :_p1'
|
|
2860
|
-
>>> r['params']
|
|
2861
|
-
{'_p1': "'; DROP TABLE users;--"} # stays a value, never parsed as SQL
|
|
2865
|
+
Fragments concatenate naturally via `+` / `__radd__`; metadata merges:
|
|
2862
2866
|
|
|
2863
|
-
|
|
2864
|
-
|
|
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})
|
|
2865
2871
|
|
|
2866
|
-
###
|
|
2872
|
+
### Hoisted params
|
|
2867
2873
|
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
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:
|
|
2871
2876
|
|
|
2872
|
-
>>>
|
|
2873
|
-
>>> r
|
|
2874
|
-
|
|
2875
|
-
|
|
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;--"})
|
|
2876
2883
|
|
|
2877
|
-
|
|
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.
|
|
2884
|
+
The user input stays a value — never parsed as SQL.
|
|
2881
2885
|
|
|
2882
|
-
|
|
2883
|
-
Most often you just fill `params` by bind name directly:
|
|
2886
|
+
### Substitutions (late binding)
|
|
2884
2887
|
|
|
2885
|
-
|
|
2886
|
-
|
|
2888
|
+
A substitution is preserved as a deferred bind; supply the value at
|
|
2889
|
+
build time:
|
|
2887
2890
|
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
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:
|
|
2892
2902
|
|
|
2893
2903
|
>>> r = dotted.sqlize('age >= $(user.min_age)')
|
|
2894
|
-
>>> r
|
|
2895
|
-
'age >= :_s_...'
|
|
2896
|
-
>>> list(r['unbound'].values())
|
|
2904
|
+
>>> list(r.where.unbound.values())
|
|
2897
2905
|
['user.min_age']
|
|
2898
2906
|
|
|
2899
|
-
Repeated uses of the same name share one entry
|
|
2907
|
+
Repeated uses of the same name share one entry:
|
|
2900
2908
|
|
|
2901
|
-
>>> dotted.sqlize('(age >= $(x) & weight = $(x))')
|
|
2902
|
-
|
|
2903
|
-
|
|
2909
|
+
>>> r = dotted.sqlize('(age >= $(x) & weight = $(x))')
|
|
2910
|
+
>>> dotted.Resolver.build(r.where, x=42)
|
|
2911
|
+
('(age >= :x) AND (weight = :x)', {'x': 42})
|
|
2904
2912
|
|
|
2905
|
-
If you pass `bindings
|
|
2906
|
-
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`:
|
|
2907
2916
|
|
|
2908
|
-
>>> dotted.sqlize("age >= $(min_age)", bindings={'min_age': 30})
|
|
2909
|
-
|
|
2917
|
+
>>> r = dotted.sqlize("age >= $(min_age)", bindings={'min_age': 30})
|
|
2918
|
+
>>> dotted.Resolver.build(r.where)
|
|
2919
|
+
('age >= :_p1', {'_p1': 30})
|
|
2910
2920
|
|
|
2911
2921
|
### References
|
|
2912
2922
|
|
|
2913
|
-
Absolute-root references (`$$(path)`) map to dynamic JSON key lookups
|
|
2914
|
-
Postgres's `->` / `#>` accept runtime-computed keys:
|
|
2923
|
+
Absolute-root references (`$$(path)`) map to dynamic JSON key lookups
|
|
2924
|
+
— Postgres's `->` / `#>` accept runtime-computed keys:
|
|
2915
2925
|
|
|
2916
|
-
>>> dotted.sqlize('data.$$(data.config.field) = "Alice"')
|
|
2917
|
-
|
|
2926
|
+
>>> r = dotted.sqlize('data.$$(data.config.field) = "Alice"')
|
|
2927
|
+
>>> dotted.Resolver.build(r.where)
|
|
2928
|
+
("(data ->> (data #>> '{config,field}')) = :_p1", {'_p1': 'Alice'})
|
|
2918
2929
|
|
|
2919
|
-
Relative references (`^`) and parent references (`^^+`) are not
|
|
2920
|
-
yet.
|
|
2930
|
+
Relative references (`^`) and parent references (`^^+`) are not
|
|
2931
|
+
supported yet.
|
|
2921
2932
|
|
|
2922
|
-
###
|
|
2933
|
+
### Pattern paths
|
|
2923
2934
|
|
|
2924
|
-
|
|
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).
|
|
2925
2941
|
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
| `where` | `str` | Predicate from guards / filters / boolean groups |
|
|
2930
|
-
| `params` | `dict` | Hoisted values keyed by name |
|
|
2931
|
-
| `unbound` | `dict` | Deferred substitutions: `{bind_param_name: original_name}` |
|
|
2942
|
+
>>> r = dotted.sqlize("data.users[*].age >= 30")
|
|
2943
|
+
>>> dotted.Resolver.build(r.where)
|
|
2944
|
+
("jsonb_path_exists(data, '$.users[*].age ? (@ >= 30)')", {})
|
|
2932
2945
|
|
|
2933
|
-
|
|
2934
|
-
(
|
|
2935
|
-
|
|
2946
|
+
>>> r = dotted.sqlize("data.**.active = True")
|
|
2947
|
+
>>> dotted.Resolver.build(r.where)
|
|
2948
|
+
("jsonb_path_exists(data, '$.**.active ? (@ == true)')", {})
|
|
2936
2949
|
|
|
2937
|
-
>>> r = dotted.sqlize("data.
|
|
2938
|
-
>>>
|
|
2939
|
-
|
|
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
|
|
2979
|
+
|
|
2980
|
+
`Resolver.build(sql, paramstyle='named', **bindings)` defaults to
|
|
2981
|
+
PEP 249 named style (`:name`). For Postgres-native `$N` (what asyncpg
|
|
2982
|
+
and other raw PG drivers accept), pass `paramstyle='dollar-numeric'`
|
|
2983
|
+
— the output changes to positional:
|
|
2984
|
+
|
|
2985
|
+
>>> r = dotted.sqlize('(age >= 18 & status = "active")')
|
|
2986
|
+
>>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric')
|
|
2987
|
+
('(age >= $1) AND (status = $2)', [18, 'active'])
|
|
2988
|
+
|
|
2989
|
+
>>> r = dotted.sqlize("age >= $(min_age)")
|
|
2990
|
+
>>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric', min_age=30)
|
|
2991
|
+
('age >= $1', [30])
|
|
2992
|
+
|
|
2993
|
+
Under `'dollar-numeric'`, the second element is a list of positional
|
|
2994
|
+
args ready for `await conn.execute(sql, *args)` asyncpg style.
|
|
2995
|
+
|
|
2996
|
+
A `ParamStyle` `StrEnum` is also available for autocomplete and type
|
|
2997
|
+
checking:
|
|
2998
|
+
|
|
2999
|
+
>>> dotted.ParamStyle.named, dotted.ParamStyle.dollar_numeric
|
|
3000
|
+
(<ParamStyle.named: 'named'>, <ParamStyle.dollar_numeric: 'dollar-numeric'>)
|
|
3001
|
+
|
|
3002
|
+
### Resolver attributes
|
|
3003
|
+
|
|
3004
|
+
| Attribute | Type | Meaning |
|
|
3005
|
+
|-----------------|------------------------------|--------------------------------------------------------------------|
|
|
3006
|
+
| `.select` | `SQLFragment` \| `None` | Extraction expression (None when path is a predicate-only group) |
|
|
3007
|
+
| `.where` | `SQLFragment` \| `None` | Predicate (None for pure traversals) |
|
|
3008
|
+
| `.from_` | `SQLFragment` \| `None` | LATERAL / join (future — None in v1) |
|
|
3009
|
+
| `.unbound` | `dict` | Union of fragments' unbound mappings: `{bind_name: original_name}` |
|
|
3010
|
+
|
|
3011
|
+
A `SQLFragment` carries:
|
|
3012
|
+
|
|
3013
|
+
| Attribute | Type | Meaning |
|
|
3014
|
+
|-------------|--------|--------------------------------------------------------------------|
|
|
3015
|
+
| `.text` | `str` | Format-string SQL with `{name}` markers |
|
|
3016
|
+
| `.params` | `dict` | Pre-hoisted values: `{marker_name: value}` |
|
|
3017
|
+
| `.unbound` | `dict` | Deferred bindings: `{marker_name: original_substitution_name}` |
|
|
2940
3018
|
|
|
2941
3019
|
### Keyword arguments
|
|
2942
3020
|
|
|
2943
|
-
`dotted.sqlize(path, *, bindings=None, flavor='postgres'
|
|
3021
|
+
`dotted.sqlize(path, *, bindings=None, flavor='postgres')`
|
|
2944
3022
|
|
|
2945
|
-
- `bindings` — resolve substitutions at sqlize time.
|
|
3023
|
+
- `bindings` — resolve substitutions at sqlize time. Path-position
|
|
3024
|
+
substitutions must be resolved here.
|
|
2946
3025
|
- `flavor` — SQL flavor for JSONB operators; only `'postgres'` is
|
|
2947
3026
|
implemented.
|
|
2948
|
-
|
|
2949
|
-
|
|
3027
|
+
|
|
3028
|
+
`Resolver.build(sql, paramstyle='named', **bindings)`
|
|
3029
|
+
|
|
3030
|
+
- `sql` — a `SQLFragment` (usually `r.where`, `r.select`, or a
|
|
3031
|
+
composition like `r.select + " FROM t WHERE " + r.where`).
|
|
3032
|
+
- `paramstyle` — `'named'` or `'dollar-numeric'`, or a `ParamStyle`
|
|
3033
|
+
enum member.
|
|
3034
|
+
- `**bindings` — values for substitutions, keyed by original
|
|
3035
|
+
substitution name.
|
|
2950
3036
|
|
|
2951
3037
|
Unsupported features and pattern paths raise `dotted.TranslationError`.
|
|
2952
3038
|
|