dotted-notation 0.43.8__tar.gz → 0.43.9__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.8/dotted_notation.egg-info → dotted_notation-0.43.9}/PKG-INFO +133 -118
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/README.md +132 -117
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/__init__.py +1 -1
- dotted_notation-0.43.9/dotted/sql/__init__.py +41 -0
- dotted_notation-0.43.9/dotted/sql/core.py +631 -0
- dotted_notation-0.43.8/dotted/sqlize.py → dotted_notation-0.43.9/dotted/sql/pg.py +278 -509
- {dotted_notation-0.43.8 → dotted_notation-0.43.9/dotted_notation.egg-info}/PKG-INFO +133 -118
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted_notation.egg-info/SOURCES.txt +8 -2
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/setup.py +1 -1
- dotted_notation-0.43.9/tests/integration/__init__.py +0 -0
- dotted_notation-0.43.9/tests/integration/conftest.py +259 -0
- dotted_notation-0.43.9/tests/integration/test_smoke.py +14 -0
- dotted_notation-0.43.9/tests/integration/test_sqlize_e2e.py +191 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_sqlize.py +43 -11
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/LICENSE +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/__main__.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/access.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/api.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/base.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/cli/main.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/containers.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/engine.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/filters.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/grammar.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/groups.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/matchers.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/predicates.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/recursive.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/results.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/transforms.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/utils.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/utypes.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/wrappers.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/setup.cfg +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/__init__.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_api.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_appender.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_assemble.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_attrs.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_bindings.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_cli.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_concat.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_container_filter.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_cut.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_empty.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_filter_keyvalue.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_get.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_guard_transforms.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_invert.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_json_sentinels.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_keys_values.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_match.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_matchable.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_named_subst.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_negation.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_nop.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_numeric.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_opgroup.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_pluck.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_predicates.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_quote_idempotent.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_recursive.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_reference.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_replace.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_slice.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_softcut.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_strict.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_string_glob.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_subst_escape.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_subst_transforms.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_threading.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_transforms.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_translate.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_type_restriction.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_unpack.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_update.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_update_if.py +0 -0
- {dotted_notation-0.43.8 → dotted_notation-0.43.9}/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.9
|
|
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
|
|
@@ -2806,68 +2806,82 @@ View all registered transforms with `dotted.registry()`.
|
|
|
2806
2806
|
> checks via `jsonb_path_exists`; pattern paths that yield sets as
|
|
2807
2807
|
> SELECT (needing `LATERAL jsonb_path_query`) are not supported yet.
|
|
2808
2808
|
|
|
2809
|
-
`dotted.sqlize(path)` translates a dotted path into a
|
|
2810
|
-
carrying
|
|
2811
|
-
|
|
2812
|
-
fragments) into final SQL + a params dict / args list ready for
|
|
2809
|
+
`dotted.sqlize(path, driver='<name>')` translates a dotted path into a
|
|
2810
|
+
driver-specific `Resolver` instance carrying SQL fragments.
|
|
2811
|
+
`r.build(sql, **bindings)` renders a fragment (or any composition of
|
|
2812
|
+
fragments) into final SQL + a params dict / args list ready for the
|
|
2813
2813
|
driver.
|
|
2814
2814
|
|
|
2815
|
+
Registered drivers:
|
|
2816
|
+
|
|
2817
|
+
| driver | placeholders | output shape | notes |
|
|
2818
|
+
|-------------|-----------------|--------------|-------------------------------------------|
|
|
2819
|
+
| `asyncpg` | `$1, $2, ...` | list | Native Postgres; always server-side PREPARE — emits `::cast` for polymorphic contexts. |
|
|
2820
|
+
| `psycopg2` | `%(name)s` / `%s` | dict | Client-side parameter substitution. |
|
|
2821
|
+
| `psycopg` | `%(name)s` / `%s` | dict | psycopg v3 — server-side binding in binary mode, so casts on. |
|
|
2822
|
+
|
|
2823
|
+
Pick the driver you'll actually hand the SQL to; it determines the
|
|
2824
|
+
placeholder syntax and whether explicit SQL casts are emitted.
|
|
2825
|
+
|
|
2815
2826
|
The first segment of the dotted path is the SQL column; further
|
|
2816
2827
|
segments are JSON navigation inside it (JSONB). Scalar columns and
|
|
2817
2828
|
JSONB columns share one surface:
|
|
2818
2829
|
|
|
2819
2830
|
>>> import dotted
|
|
2820
|
-
>>> r = dotted.sqlize("status = 'active'")
|
|
2831
|
+
>>> r = dotted.sqlize("status = 'active'", driver='asyncpg')
|
|
2821
2832
|
>>> r.where
|
|
2822
2833
|
SQLFragment('status = {_p1}')
|
|
2823
|
-
>>>
|
|
2824
|
-
('status =
|
|
2834
|
+
>>> r.build(r.where)
|
|
2835
|
+
('status = $1', ['active'])
|
|
2825
2836
|
|
|
2826
|
-
>>> r = dotted.sqlize("data.user.age >= 30")
|
|
2827
|
-
>>>
|
|
2828
|
-
("(data #>> '{user,age}')::numeric >=
|
|
2837
|
+
>>> r = dotted.sqlize("data.user.age >= 30", driver='asyncpg')
|
|
2838
|
+
>>> r.build(r.where)
|
|
2839
|
+
("(data #>> '{user,age}')::numeric >= $1", [30])
|
|
2829
2840
|
|
|
2830
|
-
>>> r = dotted.sqlize("data.user.name") # pure traversal, no predicate
|
|
2831
|
-
>>>
|
|
2832
|
-
("data #>> '{user,name}'",
|
|
2841
|
+
>>> r = dotted.sqlize("data.user.name", driver='asyncpg') # pure traversal, no predicate
|
|
2842
|
+
>>> r.build(r.select)
|
|
2843
|
+
("data #>> '{user,name}'", [])
|
|
2833
2844
|
|
|
2834
2845
|
Guards encode the predicate:
|
|
2835
2846
|
|
|
2836
|
-
>>> dotted.
|
|
2837
|
-
(
|
|
2847
|
+
>>> r = dotted.sqlize("age >= 30", driver='asyncpg')
|
|
2848
|
+
>>> r.build(r.where)
|
|
2849
|
+
('age >= $1', [30])
|
|
2838
2850
|
|
|
2839
|
-
>>> dotted.
|
|
2840
|
-
(
|
|
2851
|
+
>>> r = dotted.sqlize("deleted_at = None", driver='asyncpg')
|
|
2852
|
+
>>> r.build(r.where)
|
|
2853
|
+
('deleted_at IS NULL', [])
|
|
2841
2854
|
|
|
2842
|
-
>>> dotted.
|
|
2843
|
-
(
|
|
2855
|
+
>>> r = dotted.sqlize("name = /^alice/", driver='asyncpg')
|
|
2856
|
+
>>> r.build(r.where)
|
|
2857
|
+
('name ~ $1', ['^alice'])
|
|
2844
2858
|
|
|
2845
2859
|
Boolean grouping maps directly onto `AND` / `OR` / `NOT`:
|
|
2846
2860
|
|
|
2847
|
-
>>> r = dotted.sqlize('(age >= 18 & status = "active")')
|
|
2848
|
-
>>>
|
|
2849
|
-
('(age >=
|
|
2861
|
+
>>> r = dotted.sqlize('(age >= 18 & status = "active")', driver='asyncpg')
|
|
2862
|
+
>>> r.build(r.where)
|
|
2863
|
+
('(age >= $1) AND (status = $2)', [18, 'active'])
|
|
2850
2864
|
|
|
2851
2865
|
Guard transforms become SQL casts (`int`, `float`, `str`, `bool`):
|
|
2852
2866
|
|
|
2853
|
-
>>> r = dotted.sqlize("data.user.age|int >= 30")
|
|
2854
|
-
>>>
|
|
2855
|
-
("(data #>> '{user,age}')::int >=
|
|
2867
|
+
>>> r = dotted.sqlize("data.user.age|int >= 30", driver='asyncpg')
|
|
2868
|
+
>>> r.build(r.where)
|
|
2869
|
+
("(data #>> '{user,age}')::int >= $1", [30])
|
|
2856
2870
|
|
|
2857
2871
|
### SQLFragment and composition
|
|
2858
2872
|
|
|
2859
|
-
`
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2873
|
+
`r.select`, `r.where`, `r.from_` are `SQLFragment` objects. The
|
|
2874
|
+
underlying text uses Python format-string syntax with `{name}` markers
|
|
2875
|
+
— each marker is resolved to a driver placeholder (`$N` / `%s` / etc.)
|
|
2876
|
+
at build time. Literal braces in the SQL (e.g. JSONB path arrays) are
|
|
2877
|
+
doubled as `{{`…`}}`.
|
|
2864
2878
|
|
|
2865
2879
|
Fragments concatenate naturally via `+` / `__radd__`; metadata merges:
|
|
2866
2880
|
|
|
2867
|
-
>>> r = dotted.sqlize("age >= $(min_age)")
|
|
2881
|
+
>>> r = dotted.sqlize("age >= $(min_age)", driver='asyncpg')
|
|
2868
2882
|
>>> combined = "WHERE " + r.where
|
|
2869
|
-
>>>
|
|
2870
|
-
('WHERE age >=
|
|
2883
|
+
>>> r.build(combined, min_age=30)
|
|
2884
|
+
('WHERE age >= $1::bigint', [30])
|
|
2871
2885
|
|
|
2872
2886
|
### Hoisted params
|
|
2873
2887
|
|
|
@@ -2875,11 +2889,11 @@ Every RHS value is hoisted into a params entry keyed by a generated
|
|
|
2875
2889
|
name (`_p1`, `_p2`, …). This keeps values out of the SQL string:
|
|
2876
2890
|
|
|
2877
2891
|
>>> user_input = "'; DROP TABLE users;--"
|
|
2878
|
-
>>> r = dotted.sqlize(f"name = {user_input!r}")
|
|
2892
|
+
>>> r = dotted.sqlize(f"name = {user_input!r}", driver='asyncpg')
|
|
2879
2893
|
>>> str(r.where)
|
|
2880
2894
|
'name = {_p1}'
|
|
2881
|
-
>>>
|
|
2882
|
-
(
|
|
2895
|
+
>>> r.build(r.where)
|
|
2896
|
+
('name = $1', ["'; DROP TABLE users;--"])
|
|
2883
2897
|
|
|
2884
2898
|
The user input stays a value — never parsed as SQL.
|
|
2885
2899
|
|
|
@@ -2888,44 +2902,50 @@ The user input stays a value — never parsed as SQL.
|
|
|
2888
2902
|
A substitution is preserved as a deferred bind; supply the value at
|
|
2889
2903
|
build time:
|
|
2890
2904
|
|
|
2891
|
-
>>> r = dotted.sqlize("age >= $(min_age)")
|
|
2905
|
+
>>> r = dotted.sqlize("age >= $(min_age)", driver='asyncpg')
|
|
2892
2906
|
>>> r.where.unbound # bind_name → original_name
|
|
2893
2907
|
{'min_age': 'min_age'}
|
|
2894
|
-
>>>
|
|
2895
|
-
('age >=
|
|
2908
|
+
>>> r.build(r.where, min_age=30)
|
|
2909
|
+
('age >= $1', [30])
|
|
2896
2910
|
|
|
2897
2911
|
`unbound` is keyed by the **bind marker name** (what ends up as
|
|
2898
|
-
|
|
2912
|
+
`$N` / `%(name)s`); each value is the **original substitution name**
|
|
2899
2913
|
(what was inside `$(...)`), kept as provenance. Plain identifiers bind
|
|
2900
2914
|
by that same name; non-identifier names (dotted paths, quoted keys,
|
|
2901
2915
|
spaces) hash to a deterministic `_s_<hex>` marker:
|
|
2902
2916
|
|
|
2903
|
-
>>> r = dotted.sqlize('age >= $(user.min_age)')
|
|
2917
|
+
>>> r = dotted.sqlize('age >= $(user.min_age)', driver='asyncpg')
|
|
2904
2918
|
>>> list(r.where.unbound.values())
|
|
2905
2919
|
['user.min_age']
|
|
2906
2920
|
|
|
2907
|
-
Repeated uses of the same name share one entry
|
|
2921
|
+
Repeated uses of the same name share one entry — back-referenced by
|
|
2922
|
+
the placeholder position (or by name under `pyformat`):
|
|
2908
2923
|
|
|
2909
|
-
>>> r = dotted.sqlize('(age >= $(x) & weight = $(x))')
|
|
2910
|
-
>>>
|
|
2911
|
-
('(age >=
|
|
2924
|
+
>>> r = dotted.sqlize('(age >= $(x) & weight = $(x))', driver='asyncpg')
|
|
2925
|
+
>>> r.build(r.where, x=42)
|
|
2926
|
+
('(age >= $1) AND (weight = $1)', [42])
|
|
2927
|
+
>>> r2 = dotted.sqlize('(age >= $(x) & weight = $(x))', driver='psycopg2')
|
|
2928
|
+
>>> r2.build(r2.where, x=42)
|
|
2929
|
+
('(age >= %(x)s) AND (weight = %(x)s)', {'x': 42})
|
|
2912
2930
|
|
|
2913
2931
|
If you pass `bindings=` at sqlize time, substitutions are resolved
|
|
2914
2932
|
immediately and hoisted like any other literal — no longer appearing
|
|
2915
2933
|
in `unbound`:
|
|
2916
2934
|
|
|
2917
|
-
>>> r = dotted.sqlize("age >= $(min_age)",
|
|
2918
|
-
|
|
2919
|
-
(
|
|
2935
|
+
>>> r = dotted.sqlize("age >= $(min_age)",
|
|
2936
|
+
... driver='asyncpg', bindings={'min_age': 30})
|
|
2937
|
+
>>> r.build(r.where)
|
|
2938
|
+
('age >= $1', [30])
|
|
2920
2939
|
|
|
2921
2940
|
### References
|
|
2922
2941
|
|
|
2923
2942
|
Absolute-root references (`$$(path)`) map to dynamic JSON key lookups
|
|
2924
2943
|
— Postgres's `->` / `#>` accept runtime-computed keys:
|
|
2925
2944
|
|
|
2926
|
-
>>> r = dotted.sqlize('data.$$(data.config.field) = "Alice"'
|
|
2927
|
-
|
|
2928
|
-
|
|
2945
|
+
>>> r = dotted.sqlize('data.$$(data.config.field) = "Alice"',
|
|
2946
|
+
... driver='asyncpg')
|
|
2947
|
+
>>> r.build(r.where)
|
|
2948
|
+
("(data ->> (data #>> '{config,field}')) = $1", ['Alice'])
|
|
2929
2949
|
|
|
2930
2950
|
Relative references (`^`) and parent references (`^^+`) are not
|
|
2931
2951
|
supported yet.
|
|
@@ -2939,32 +2959,39 @@ JSONPath expression; guard values embed inline when safely literal
|
|
|
2939
2959
|
(numbers, booleans, null) or flow through a `jsonb_build_object` vars
|
|
2940
2960
|
argument (strings, substitutions).
|
|
2941
2961
|
|
|
2942
|
-
>>> r = dotted.sqlize("data.users[*].age >= 30")
|
|
2943
|
-
>>>
|
|
2944
|
-
("jsonb_path_exists(data, '$.users[*].age ? (@ >= 30)')",
|
|
2962
|
+
>>> r = dotted.sqlize("data.users[*].age >= 30", driver='asyncpg')
|
|
2963
|
+
>>> r.build(r.where)
|
|
2964
|
+
("jsonb_path_exists(data, '$.users[*].age ? (@ >= 30)')", [])
|
|
2945
2965
|
|
|
2946
|
-
>>> r = dotted.sqlize("data.**.active = True")
|
|
2947
|
-
>>>
|
|
2948
|
-
("jsonb_path_exists(data, '$.**.active ? (@ == true)')",
|
|
2966
|
+
>>> r = dotted.sqlize("data.**.active = True", driver='asyncpg')
|
|
2967
|
+
>>> r.build(r.where)
|
|
2968
|
+
("jsonb_path_exists(data, '$.**.active ? (@ == true)')", [])
|
|
2949
2969
|
|
|
2950
|
-
>>> r = dotted.sqlize("data.users[age>=30]")
|
|
2951
|
-
>>>
|
|
2952
|
-
("jsonb_path_exists(data, '$.users[*] ? (@.age >= 30)')",
|
|
2970
|
+
>>> r = dotted.sqlize("data.users[age>=30]", driver='asyncpg')
|
|
2971
|
+
>>> r.build(r.where)
|
|
2972
|
+
("jsonb_path_exists(data, '$.users[*] ? (@.age >= 30)')", [])
|
|
2953
2973
|
|
|
2954
|
-
>>> r = dotted.sqlize('data.users[*&age>=30].name = "alice"'
|
|
2955
|
-
|
|
2974
|
+
>>> r = dotted.sqlize('data.users[*&age>=30].name = "alice"',
|
|
2975
|
+
... driver='asyncpg')
|
|
2976
|
+
>>> sql, params = r.build(r.where)
|
|
2956
2977
|
>>> sql
|
|
2957
|
-
"jsonb_path_exists(data, '$.users[*] ? (@.age >= 30).name ? (@ == $_p1)', jsonb_build_object('_p1',
|
|
2978
|
+
"jsonb_path_exists(data, '$.users[*] ? (@.age >= 30).name ? (@ == $_p1)', jsonb_build_object('_p1', $1::text))"
|
|
2958
2979
|
>>> params
|
|
2959
|
-
|
|
2980
|
+
['alice']
|
|
2981
|
+
|
|
2982
|
+
The `::text` on the placeholder above is driver-dependent. `asyncpg`
|
|
2983
|
+
and `psycopg` (v3) emit explicit SQL casts inside `jsonb_build_object`
|
|
2984
|
+
because their placeholders get typed server-side at PREPARE time;
|
|
2985
|
+
`psycopg2` substitutes client-side and has no such cast.
|
|
2960
2986
|
|
|
2961
2987
|
Pattern predicates compose with scalar ones via dotted's `&` / `,` /
|
|
2962
2988
|
`!`:
|
|
2963
2989
|
|
|
2964
|
-
>>> r = dotted.sqlize('(data.users[*].age >= 30 & status = "active")'
|
|
2965
|
-
|
|
2990
|
+
>>> r = dotted.sqlize('(data.users[*].age >= 30 & status = "active")',
|
|
2991
|
+
... driver='asyncpg')
|
|
2992
|
+
>>> sql, params = r.build(r.where)
|
|
2966
2993
|
>>> sql
|
|
2967
|
-
"(jsonb_path_exists(data, '$.users[*].age ? (@ >= 30)')) AND (status =
|
|
2994
|
+
"(jsonb_path_exists(data, '$.users[*].age ? (@ >= 30)')) AND (status = $1)"
|
|
2968
2995
|
|
|
2969
2996
|
`select` for a pattern path is set to the column itself — pattern
|
|
2970
2997
|
paths don't have a single extractable value. Use the WHERE predicate
|
|
@@ -2975,58 +3002,35 @@ Not yet supported:
|
|
|
2975
3002
|
- Regex-in-access like `data./prefix_\d+/.field`
|
|
2976
3003
|
- Pattern refs
|
|
2977
3004
|
|
|
2978
|
-
###
|
|
2979
|
-
|
|
2980
|
-
`Resolver.build(sql, paramstyle='named', **bindings)` renders the
|
|
2981
|
-
fragment in the chosen PEP 249 paramstyle. Supported:
|
|
2982
|
-
|
|
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 |
|
|
3005
|
+
### Adding a driver
|
|
2991
3006
|
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
consumes its own arg, so a repeated substitution emits its value
|
|
2997
|
-
multiple times.
|
|
3007
|
+
Drivers are registered via the `@dotted.sql.driver('<name>')`
|
|
3008
|
+
decorator on a `Resolver` subclass that mixes in a flavor (today just
|
|
3009
|
+
`PostgresMixin` in `dotted.sql.pg`). Each subclass declares its
|
|
3010
|
+
paramstyle and whether it needs explicit casts:
|
|
2998
3011
|
|
|
2999
|
-
|
|
3012
|
+
import dotted
|
|
3013
|
+
from dotted.sql.pg import PostgresMixin
|
|
3000
3014
|
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3015
|
+
@dotted.sql.driver('my-driver')
|
|
3016
|
+
class MyDriverResolver(PostgresMixin, dotted.Resolver):
|
|
3017
|
+
paramstyle = 'pyformat' # one of 'named' | 'pyformat' |
|
|
3018
|
+
# 'qmark' | 'format' | 'numeric' |
|
|
3019
|
+
# 'dollar-numeric'
|
|
3020
|
+
cast = False # True if driver uses server-side PREPARE
|
|
3021
|
+
# in polymorphic jsonb_build_object contexts
|
|
3004
3022
|
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
>>> dotted.Resolver.build(r.where, paramstyle='pyformat', min_age=30)
|
|
3010
|
-
('age >= %(min_age)s', {'min_age': 30})
|
|
3011
|
-
|
|
3012
|
-
Back-reference vs no-backref on repeated substitutions:
|
|
3013
|
-
|
|
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])
|
|
3019
|
-
|
|
3020
|
-
A `ParamStyle` `StrEnum` is also available for autocomplete and type
|
|
3021
|
-
checking:
|
|
3022
|
-
|
|
3023
|
-
>>> dotted.ParamStyle.named, dotted.ParamStyle.dollar_numeric
|
|
3024
|
-
(<ParamStyle.named: 'named'>, <ParamStyle.dollar_numeric: 'dollar-numeric'>)
|
|
3023
|
+
Once registered, `dotted.sqlize(path, driver='my-driver')` dispatches
|
|
3024
|
+
to it. `dotted.sql.drivers()` returns the names of all currently
|
|
3025
|
+
registered drivers (built-ins + anything third-party code has added).
|
|
3025
3026
|
|
|
3026
3027
|
### Resolver attributes
|
|
3027
3028
|
|
|
3028
3029
|
| Attribute | Type | Meaning |
|
|
3029
3030
|
|-----------------|------------------------------|--------------------------------------------------------------------|
|
|
3031
|
+
| `.driver` | `str` | Name the resolver was registered under |
|
|
3032
|
+
| `.paramstyle` | `str` | PEP 249 paramstyle emitted by `.build()` |
|
|
3033
|
+
| `.cast` | `bool` | Whether `.build()` emits explicit SQL casts in polymorphic contexts |
|
|
3030
3034
|
| `.select` | `SQLFragment` \| `None` | Extraction expression (None when path is a predicate-only group) |
|
|
3031
3035
|
| `.where` | `SQLFragment` \| `None` | Predicate (None for pure traversals) |
|
|
3032
3036
|
| `.from_` | `SQLFragment` \| `None` | LATERAL / join (future — None in v1) |
|
|
@@ -3040,24 +3044,35 @@ A `SQLFragment` carries:
|
|
|
3040
3044
|
| `.params` | `dict` | Pre-hoisted values: `{marker_name: value}` |
|
|
3041
3045
|
| `.unbound` | `dict` | Deferred bindings: `{marker_name: original_substitution_name}` |
|
|
3042
3046
|
|
|
3043
|
-
###
|
|
3047
|
+
### API reference
|
|
3044
3048
|
|
|
3045
|
-
`dotted.sqlize(path, *, bindings=None
|
|
3049
|
+
`dotted.sqlize(path, *, driver, bindings=None)`
|
|
3046
3050
|
|
|
3051
|
+
- `path` — dotted path string or pre-parsed result.
|
|
3052
|
+
- `driver` — **required**. Name of a registered driver
|
|
3053
|
+
(`'asyncpg'`, `'psycopg2'`, `'psycopg'`, or anything registered via
|
|
3054
|
+
`@register`). An unknown driver raises `TranslationError`.
|
|
3047
3055
|
- `bindings` — resolve substitutions at sqlize time. Path-position
|
|
3048
3056
|
substitutions must be resolved here.
|
|
3049
|
-
- `flavor` — SQL flavor for JSONB operators; only `'postgres'` is
|
|
3050
|
-
implemented.
|
|
3051
3057
|
|
|
3052
|
-
`
|
|
3058
|
+
`r.build(sql, **bindings)`
|
|
3053
3059
|
|
|
3054
3060
|
- `sql` — a `SQLFragment` (usually `r.where`, `r.select`, or a
|
|
3055
3061
|
composition like `r.select + " FROM t WHERE " + r.where`).
|
|
3056
|
-
- `paramstyle` — `'named'` or `'dollar-numeric'`, or a `ParamStyle`
|
|
3057
|
-
enum member.
|
|
3058
3062
|
- `**bindings` — values for substitutions, keyed by original
|
|
3059
3063
|
substitution name.
|
|
3060
3064
|
|
|
3065
|
+
The `Resolver.build(sql, paramstyle='...', **bindings)` classmethod is
|
|
3066
|
+
retained as a low-level escape hatch that takes an explicit paramstyle
|
|
3067
|
+
and bypasses the driver's cast setting. Normal application code uses
|
|
3068
|
+
the instance form above.
|
|
3069
|
+
|
|
3070
|
+
A `ParamStyle` `StrEnum` is available for the low-level classmethod
|
|
3071
|
+
and for introspection:
|
|
3072
|
+
|
|
3073
|
+
>>> dotted.ParamStyle.named, dotted.ParamStyle.dollar_numeric
|
|
3074
|
+
(<ParamStyle.named: 'named'>, <ParamStyle.dollar_numeric: 'dollar-numeric'>)
|
|
3075
|
+
|
|
3061
3076
|
Unsupported features and pattern paths raise `dotted.TranslationError`.
|
|
3062
3077
|
|
|
3063
3078
|
<a id="constants-and-exceptions"></a>
|