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.
Files changed (81) hide show
  1. {dotted_notation-0.43.5/dotted_notation.egg-info → dotted_notation-0.43.7}/PKG-INFO +183 -97
  2. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/README.md +184 -98
  3. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/__init__.py +2 -2
  4. dotted_notation-0.43.7/dotted/sqlize.py +1164 -0
  5. {dotted_notation-0.43.5 → dotted_notation-0.43.7/dotted_notation.egg-info}/PKG-INFO +183 -97
  6. {dotted_notation-0.43.5 → 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.5/dotted/sqlize.py +0 -510
  9. dotted_notation-0.43.5/tests/test_sqlize.py +0 -437
  10. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/LICENSE +0 -0
  11. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/__main__.py +0 -0
  12. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/access.py +0 -0
  13. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/api.py +0 -0
  14. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/base.py +0 -0
  15. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/cli/__init__.py +0 -0
  16. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/cli/_compat.py +0 -0
  17. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/cli/formats.py +0 -0
  18. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/cli/main.py +0 -0
  19. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/containers.py +0 -0
  20. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/engine.py +0 -0
  21. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/filters.py +0 -0
  22. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/grammar.py +0 -0
  23. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/groups.py +0 -0
  24. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/matchers.py +0 -0
  25. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/predicates.py +0 -0
  26. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/recursive.py +0 -0
  27. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/results.py +0 -0
  28. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/transforms.py +0 -0
  29. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/utils.py +0 -0
  30. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/utypes.py +0 -0
  31. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted/wrappers.py +0 -0
  32. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted_notation.egg-info/SOURCES.txt +0 -0
  33. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted_notation.egg-info/dependency_links.txt +0 -0
  34. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted_notation.egg-info/entry_points.txt +0 -0
  35. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted_notation.egg-info/requires.txt +0 -0
  36. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/dotted_notation.egg-info/top_level.txt +0 -0
  37. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/setup.cfg +0 -0
  38. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/__init__.py +0 -0
  39. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_api.py +0 -0
  40. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_appender.py +0 -0
  41. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_assemble.py +0 -0
  42. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_attrs.py +0 -0
  43. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_bindings.py +0 -0
  44. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_cli.py +0 -0
  45. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_concat.py +0 -0
  46. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_container_filter.py +0 -0
  47. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_cut.py +0 -0
  48. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_empty.py +0 -0
  49. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_filter_keyvalue.py +0 -0
  50. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_get.py +0 -0
  51. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_guard_transforms.py +0 -0
  52. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_invert.py +0 -0
  53. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_json_sentinels.py +0 -0
  54. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_keys_values.py +0 -0
  55. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_match.py +0 -0
  56. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_matchable.py +0 -0
  57. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_named_subst.py +0 -0
  58. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_negation.py +0 -0
  59. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_nop.py +0 -0
  60. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_numeric.py +0 -0
  61. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_opgroup.py +0 -0
  62. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_pluck.py +0 -0
  63. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_predicates.py +0 -0
  64. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_quote_idempotent.py +0 -0
  65. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_recursive.py +0 -0
  66. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_reference.py +0 -0
  67. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_replace.py +0 -0
  68. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_slice.py +0 -0
  69. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_softcut.py +0 -0
  70. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_strict.py +0 -0
  71. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_string_glob.py +0 -0
  72. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_subst_escape.py +0 -0
  73. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_subst_transforms.py +0 -0
  74. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_threading.py +0 -0
  75. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_transforms.py +0 -0
  76. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_translate.py +0 -0
  77. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_type_restriction.py +0 -0
  78. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_unpack.py +0 -0
  79. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_update.py +0 -0
  80. {dotted_notation-0.43.5 → dotted_notation-0.43.7}/tests/test_update_if.py +0 -0
  81. {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.5
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, output format, and supported subset
2804
- > of dotted features may still change. Postgres-only, SQLAlchemy-style named
2805
- > placeholders, scalar paths plus absolute-root references. Patterns (`*`,
2806
- > `**`, `[*]`, filter-on-array) 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.
2807
2808
 
2808
- `dotted.sqlize` translates a dotted path into SQL clause components — a
2809
- `select` expression, a `where` predicate, and a `params` dict with hoisted
2810
- values. Literals are hoisted for injection safety; substitutions are
2811
- 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.
2812
2814
 
2813
- The first segment of the dotted path is the SQL column. Any further segments
2814
- are JSON navigation inside that column (JSONB). This means scalar columns
2815
- 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:
2816
2818
 
2817
2819
  >>> import dotted
2818
- >>> dotted.sqlize("status = 'active'")
2819
- {'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'})
2820
2825
 
2821
- >>> dotted.sqlize("data.user.age >= 30")
2822
- {'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})
2823
2829
 
2824
- >>> dotted.sqlize("data.user.name") # pure traversal, no predicate
2825
- {'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}'", {})
2826
2833
 
2827
2834
  Guards encode the predicate:
2828
2835
 
2829
- >>> dotted.sqlize("age >= 30")
2830
- {'select': 'age', 'where': 'age >= :_p1', 'params': {'_p1': 30}}
2836
+ >>> dotted.Resolver.build(dotted.sqlize("age >= 30").where)
2837
+ ('age >= :_p1', {'_p1': 30})
2831
2838
 
2832
- >>> dotted.sqlize("deleted_at = None")
2833
- {'select': 'deleted_at', 'where': 'deleted_at IS NULL', 'params': {}}
2839
+ >>> dotted.Resolver.build(dotted.sqlize("deleted_at = None").where)
2840
+ ('deleted_at IS NULL', {})
2834
2841
 
2835
- >>> dotted.sqlize("name = /^alice/")
2836
- {'select': 'name', 'where': 'name ~ :_p1', 'params': {'_p1': '^alice'}}
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
- {'where': '(age >= :_p1) AND (status = :_p2)', 'params': {'_p1': 18, '_p2': 'active'}}
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")['where']
2849
- "(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})
2850
2856
 
2851
- ### Hoisted params
2857
+ ### SQLFragment and composition
2852
2858
 
2853
- Every RHS value becomes an entry in the `params` dict, keyed by name. This
2854
- is the mechanism that makes the output safe — user input can't end up as
2855
- 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 `{{`…`}}`.
2856
2864
 
2857
- >>> r = dotted.sqlize(f"name = {repr(user_input)}") # user input literal
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
- Literal values get generated names (`_p1`, `_p2`, …). Substitutions keep
2864
- 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})
2865
2871
 
2866
- ### Substitutions (late binding)
2872
+ ### Hoisted params
2867
2873
 
2868
- A substitution is preserved in the SQL output as a named placeholder,
2869
- letting the caller supply the value at execute time rather than sqlize
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
- >>> r = dotted.sqlize("age >= $(min_age)")
2873
- >>> r
2874
- {'select': 'age', 'where': 'age >= :min_age', 'params': {},
2875
- '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;--"})
2876
2883
 
2877
- `unbound` is a dict keyed by the **SQL bind parameter name** — the name
2878
- that appears in the SQL `:placeholder` and as a key in `params`. Each
2879
- value is the **original substitution name** (what was inside `$(...)`),
2880
- kept as provenance so callers can trace a bind back to its source.
2884
+ The user input stays a value never parsed as SQL.
2881
2885
 
2882
- `unbound.keys()` is the list of bind slots the caller needs to fill.
2883
- Most often you just fill `params` by bind name directly:
2886
+ ### Substitutions (late binding)
2884
2887
 
2885
- >>> r['params']['min_age'] = 30
2886
- >>> # hand (r['where'], r['params']) to cursor.execute / session.execute / etc.
2888
+ A substitution is preserved as a deferred bind; supply the value at
2889
+ build time:
2887
2890
 
2888
- Non-identifier names (dotted paths, quoted keys, spaces, etc.) hash to a
2889
- deterministic `_s_<hex>` bind name — the bind-side name is different
2890
- from what appeared in source, but the original is preserved in the
2891
- `unbound` value:
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['where'] # doctest: +ELLIPSIS
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 automatically:
2907
+ Repeated uses of the same name share one entry:
2900
2908
 
2901
- >>> dotted.sqlize('(age >= $(x) & weight = $(x))')
2902
- {'where': '(age >= :x) AND (weight = :x)', 'params': {},
2903
- '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})
2904
2912
 
2905
- If you pass `bindings=`, substitutions are resolved at sqlize time and hoisted
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
- {'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})
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
- {'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'})
2918
2929
 
2919
- Relative references (`^`) and parent references (`^^+`) are not supported
2920
- yet.
2930
+ Relative references (`^`) and parent references (`^^+`) are not
2931
+ supported yet.
2921
2932
 
2922
- ### Output keys
2933
+ ### Pattern paths
2923
2934
 
2924
- Returned dict may contain any subset of:
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
- | Key | Type | Meaning |
2927
- |-----------|-------------|-----------------------------------------------------|
2928
- | `select` | `str` | Expression yielding the value dotted would return |
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
- `select` is omitted when combining predicates across different columns
2934
- (e.g. top-level groups), since there's no single meaningful expression.
2935
- `where` is omitted when the path is pure traversal. Usage pattern:
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.user.age >= 30")
2938
- >>> sql = f"SELECT {r['select']} FROM users WHERE {r['where']}"
2939
- >>> # sql, r['params'] go to your driver
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', format='sqlalchemy')`
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
- - `format` — placeholder style; only `'sqlalchemy'` (`:name`) is supported
2949
- 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.
2950
3036
 
2951
3037
  Unsupported features and pattern paths raise `dotted.TranslationError`.
2952
3038