dotted-notation 0.43.6__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.
Files changed (80) hide show
  1. {dotted_notation-0.43.6/dotted_notation.egg-info → dotted_notation-0.43.7}/PKG-INFO +172 -123
  2. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/README.md +173 -124
  3. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/__init__.py +2 -2
  4. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/sqlize.py +372 -80
  5. {dotted_notation-0.43.6 → dotted_notation-0.43.7/dotted_notation.egg-info}/PKG-INFO +172 -123
  6. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/setup.py +1 -1
  7. dotted_notation-0.43.7/tests/test_sqlize.py +756 -0
  8. dotted_notation-0.43.6/tests/test_sqlize.py +0 -523
  9. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/LICENSE +0 -0
  10. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/__main__.py +0 -0
  11. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/access.py +0 -0
  12. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/api.py +0 -0
  13. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/base.py +0 -0
  14. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/cli/__init__.py +0 -0
  15. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/cli/_compat.py +0 -0
  16. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/cli/formats.py +0 -0
  17. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/cli/main.py +0 -0
  18. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/containers.py +0 -0
  19. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/engine.py +0 -0
  20. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/filters.py +0 -0
  21. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/grammar.py +0 -0
  22. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/groups.py +0 -0
  23. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/matchers.py +0 -0
  24. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/predicates.py +0 -0
  25. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/recursive.py +0 -0
  26. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/results.py +0 -0
  27. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/transforms.py +0 -0
  28. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/utils.py +0 -0
  29. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/utypes.py +0 -0
  30. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted/wrappers.py +0 -0
  31. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted_notation.egg-info/SOURCES.txt +0 -0
  32. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted_notation.egg-info/dependency_links.txt +0 -0
  33. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted_notation.egg-info/entry_points.txt +0 -0
  34. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted_notation.egg-info/requires.txt +0 -0
  35. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/dotted_notation.egg-info/top_level.txt +0 -0
  36. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/setup.cfg +0 -0
  37. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/__init__.py +0 -0
  38. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_api.py +0 -0
  39. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_appender.py +0 -0
  40. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_assemble.py +0 -0
  41. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_attrs.py +0 -0
  42. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_bindings.py +0 -0
  43. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_cli.py +0 -0
  44. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_concat.py +0 -0
  45. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_container_filter.py +0 -0
  46. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_cut.py +0 -0
  47. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_empty.py +0 -0
  48. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_filter_keyvalue.py +0 -0
  49. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_get.py +0 -0
  50. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_guard_transforms.py +0 -0
  51. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_invert.py +0 -0
  52. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_json_sentinels.py +0 -0
  53. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_keys_values.py +0 -0
  54. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_match.py +0 -0
  55. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_matchable.py +0 -0
  56. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_named_subst.py +0 -0
  57. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_negation.py +0 -0
  58. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_nop.py +0 -0
  59. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_numeric.py +0 -0
  60. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_opgroup.py +0 -0
  61. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_pluck.py +0 -0
  62. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_predicates.py +0 -0
  63. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_quote_idempotent.py +0 -0
  64. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_recursive.py +0 -0
  65. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_reference.py +0 -0
  66. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_replace.py +0 -0
  67. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_slice.py +0 -0
  68. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_softcut.py +0 -0
  69. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_strict.py +0 -0
  70. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_string_glob.py +0 -0
  71. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_subst_escape.py +0 -0
  72. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_subst_transforms.py +0 -0
  73. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_threading.py +0 -0
  74. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_transforms.py +0 -0
  75. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_translate.py +0 -0
  76. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_type_restriction.py +0 -0
  77. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_unpack.py +0 -0
  78. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_update.py +0 -0
  79. {dotted_notation-0.43.6 → dotted_notation-0.43.7}/tests/test_update_if.py +0 -0
  80. {dotted_notation-0.43.6 → 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.6
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,190 +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, output format, and supported subset
2804
- > of dotted features may still change. Postgres-only, SQLAlchemy-style
2805
- > named placeholders. Pattern paths (`*`, `[*]`, `**`, filter brackets)
2806
- > are supported only as WHERE-side existence checks via
2807
- > `jsonb_path_exists`; pattern paths that yield sets as SELECT (needing
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 SQL clause components — a
2811
- `select` expression, a `where` predicate, and a `params` dict with hoisted
2812
- values. Literals are hoisted for injection safety; substitutions are
2813
- preserved as named placeholders so callers can late-bind them.
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. Any further segments
2816
- are JSON navigation inside that column (JSONB). This means scalar columns
2817
- and JSONB columns share one surface:
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
- {'select': 'status', 'where': 'status = :_p1', 'params': {'_p1': 'active'}}
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
- {'select': "data #>> '{user,age}'", 'where': "(data #>> '{user,age}')::numeric >= :_p1", 'params': {'_p1': 30}}
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
- {'select': "data #>> '{user,name}'", 'params': {}}
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
- {'select': 'age', 'where': 'age >= :_p1', 'params': {'_p1': 30}}
2836
+ >>> dotted.Resolver.build(dotted.sqlize("age >= 30").where)
2837
+ ('age >= :_p1', {'_p1': 30})
2833
2838
 
2834
- >>> dotted.sqlize("deleted_at = None")
2835
- {'select': 'deleted_at', 'where': 'deleted_at IS NULL', 'params': {}}
2839
+ >>> dotted.Resolver.build(dotted.sqlize("deleted_at = None").where)
2840
+ ('deleted_at IS NULL', {})
2836
2841
 
2837
- >>> dotted.sqlize("name = /^alice/")
2838
- {'select': 'name', 'where': 'name ~ :_p1', 'params': {'_p1': '^alice'}}
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
- {'where': '(age >= :_p1) AND (status = :_p2)', 'params': {'_p1': 18, '_p2': 'active'}}
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")['where']
2851
- "(data #>> '{user,age}')::int >= :_p1"
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
- ### Hoisted params
2857
+ ### SQLFragment and composition
2854
2858
 
2855
- Every RHS value becomes an entry in the `params` dict, keyed by name. This
2856
- is the mechanism that makes the output safe — user input can't end up as
2857
- SQL text, only as a parameter value.
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
- >>> r = dotted.sqlize(f"name = {repr(user_input)}") # user input literal
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
- Literal values get generated names (`_p1`, `_p2`, …). Substitutions keep
2866
- their declared names see below.
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
- ### Substitutions (late binding)
2872
+ ### Hoisted params
2869
2873
 
2870
- A substitution is preserved in the SQL output as a named placeholder,
2871
- letting the caller supply the value at execute time rather than sqlize
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
- >>> r = dotted.sqlize("age >= $(min_age)")
2875
- >>> r
2876
- {'select': 'age', 'where': 'age >= :min_age', 'params': {},
2877
- 'unbound': {'min_age': 'min_age'}}
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;--"})
2883
+
2884
+ The user input stays a value — never parsed as SQL.
2878
2885
 
2879
- `unbound` is a dict keyed by the **SQL bind parameter name** — the name
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.
2886
+ ### Substitutions (late binding)
2883
2887
 
2884
- `unbound.keys()` is the list of bind slots the caller needs to fill.
2885
- Most often you just fill `params` by bind name directly:
2888
+ A substitution is preserved as a deferred bind; supply the value at
2889
+ build time:
2886
2890
 
2887
- >>> r['params']['min_age'] = 30
2888
- >>> # hand (r['where'], r['params']) to cursor.execute / session.execute / etc.
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})
2889
2896
 
2890
- Non-identifier names (dotted paths, quoted keys, spaces, etc.) hash to a
2891
- deterministic `_s_<hex>` bind name the bind-side name is different
2892
- from what appeared in source, but the original is preserved in the
2893
- `unbound` value:
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['where'] # doctest: +ELLIPSIS
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 automatically:
2907
+ Repeated uses of the same name share one entry:
2902
2908
 
2903
- >>> dotted.sqlize('(age >= $(x) & weight = $(x))')
2904
- {'where': '(age >= :x) AND (weight = :x)', 'params': {},
2905
- 'unbound': {'x': 'x'}}
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=`, substitutions are resolved at sqlize time and hoisted
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
- {'select': 'age', 'where': 'age >= :_p1', 'params': {'_p1': 30}}
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
- {'select': "data ->> (data #>> '{config,field}')", 'where': "(data ->> (data #>> '{config,field}')) = :_p1", 'params': {'_p1': 'Alice'}}
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 supported
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 (`**`), or
2927
- bracket filters (`[pred]`, `[*&pred]`) translate to a Postgres
2928
- `jsonb_path_exists(…)` WHERE predicate. The dotted path becomes a JSONPath
2929
- expression; guard values embed inline when safely literal (numbers,
2930
- booleans, null) or flow through a `jsonb_build_object` vars argument
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)"
2932
2968
 
2933
- >>> dotted.sqlize("data.users[*].age >= 30")['where']
2934
- "jsonb_path_exists(data, '$.users[*].age ? (@ >= 30)')"
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.
2935
2972
 
2936
- >>> dotted.sqlize("data.**.active = True")['where']
2937
- "jsonb_path_exists(data, '$.**.active ? (@ == true)')"
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
2938
2977
 
2939
- >>> dotted.sqlize("data.users[age>=30]")['where']
2940
- "jsonb_path_exists(data, '$.users[*] ? (@.age >= 30)')"
2978
+ ### Paramstyles
2941
2979
 
2942
- >>> dotted.sqlize('data.users[*&age>=30].name = "alice"')['where']
2943
- "jsonb_path_exists(data, '$.users[*] ? (@.age >= 30).name ? (@ == :_p1)', jsonb_build_object('_p1', :_p1))"
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:
2944
2984
 
2945
- Pattern predicates compose with scalar ones via dotted's `&` / `,` / `!`:
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'])
2946
2988
 
2947
- >>> dotted.sqlize('(data.users[*].age >= 30 & status = "active")')['where']
2948
- "(jsonb_path_exists(data, '$.users[*].age ? (@ >= 30)')) AND (status = :_p1)"
2989
+ >>> r = dotted.sqlize("age >= $(min_age)")
2990
+ >>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric', min_age=30)
2991
+ ('age >= $1', [30])
2949
2992
 
2950
- `select` is set to the column itself pattern paths don't have a single
2951
- extractable value. Use the WHERE predicate to filter rows; write your
2952
- own SELECT for the columns you want.
2993
+ Under `'dollar-numeric'`, the second element is a list of positional
2994
+ args ready for `await conn.execute(sql, *args)` asyncpg style.
2953
2995
 
2954
- Not yet supported:
2955
- - Pattern paths as SELECT (needing `LATERAL jsonb_path_query`)
2956
- - Regex-in-access like `data./prefix_\d+/.field`
2957
- - Pattern refs
2996
+ A `ParamStyle` `StrEnum` is also available for autocomplete and type
2997
+ checking:
2958
2998
 
2959
- ### Output keys
2999
+ >>> dotted.ParamStyle.named, dotted.ParamStyle.dollar_numeric
3000
+ (<ParamStyle.named: 'named'>, <ParamStyle.dollar_numeric: 'dollar-numeric'>)
2960
3001
 
2961
- Returned dict may contain any subset of:
3002
+ ### Resolver attributes
2962
3003
 
2963
- | Key | Type | Meaning |
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}` |
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}` |
2969
3010
 
2970
- `select` is omitted when combining predicates across different columns
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:
3011
+ A `SQLFragment` carries:
2973
3012
 
2974
- >>> r = dotted.sqlize("data.user.age >= 30")
2975
- >>> sql = f"SELECT {r['select']} FROM users WHERE {r['where']}"
2976
- >>> # sql, r['params'] go to your driver
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}` |
2977
3018
 
2978
3019
  ### Keyword arguments
2979
3020
 
2980
- `dotted.sqlize(path, *, bindings=None, flavor='postgres', format='sqlalchemy')`
3021
+ `dotted.sqlize(path, *, bindings=None, flavor='postgres')`
2981
3022
 
2982
- - `bindings` — resolve substitutions at sqlize time.
3023
+ - `bindings` — resolve substitutions at sqlize time. Path-position
3024
+ substitutions must be resolved here.
2983
3025
  - `flavor` — SQL flavor for JSONB operators; only `'postgres'` is
2984
3026
  implemented.
2985
- - `format` — placeholder style; only `'sqlalchemy'` (`:name`) is supported
2986
- in v1.
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.
2987
3036
 
2988
3037
  Unsupported features and pattern paths raise `dotted.TranslationError`.
2989
3038