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.
Files changed (85) hide show
  1. {dotted_notation-0.43.8/dotted_notation.egg-info → dotted_notation-0.43.9}/PKG-INFO +133 -118
  2. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/README.md +132 -117
  3. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/__init__.py +1 -1
  4. dotted_notation-0.43.9/dotted/sql/__init__.py +41 -0
  5. dotted_notation-0.43.9/dotted/sql/core.py +631 -0
  6. dotted_notation-0.43.8/dotted/sqlize.py → dotted_notation-0.43.9/dotted/sql/pg.py +278 -509
  7. {dotted_notation-0.43.8 → dotted_notation-0.43.9/dotted_notation.egg-info}/PKG-INFO +133 -118
  8. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted_notation.egg-info/SOURCES.txt +8 -2
  9. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/setup.py +1 -1
  10. dotted_notation-0.43.9/tests/integration/__init__.py +0 -0
  11. dotted_notation-0.43.9/tests/integration/conftest.py +259 -0
  12. dotted_notation-0.43.9/tests/integration/test_smoke.py +14 -0
  13. dotted_notation-0.43.9/tests/integration/test_sqlize_e2e.py +191 -0
  14. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_sqlize.py +43 -11
  15. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/LICENSE +0 -0
  16. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/__main__.py +0 -0
  17. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/access.py +0 -0
  18. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/api.py +0 -0
  19. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/base.py +0 -0
  20. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/cli/__init__.py +0 -0
  21. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/cli/_compat.py +0 -0
  22. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/cli/formats.py +0 -0
  23. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/cli/main.py +0 -0
  24. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/containers.py +0 -0
  25. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/engine.py +0 -0
  26. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/filters.py +0 -0
  27. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/grammar.py +0 -0
  28. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/groups.py +0 -0
  29. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/matchers.py +0 -0
  30. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/predicates.py +0 -0
  31. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/recursive.py +0 -0
  32. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/results.py +0 -0
  33. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/transforms.py +0 -0
  34. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/utils.py +0 -0
  35. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/utypes.py +0 -0
  36. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted/wrappers.py +0 -0
  37. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted_notation.egg-info/dependency_links.txt +0 -0
  38. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted_notation.egg-info/entry_points.txt +0 -0
  39. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted_notation.egg-info/requires.txt +0 -0
  40. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/dotted_notation.egg-info/top_level.txt +0 -0
  41. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/setup.cfg +0 -0
  42. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/__init__.py +0 -0
  43. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_api.py +0 -0
  44. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_appender.py +0 -0
  45. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_assemble.py +0 -0
  46. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_attrs.py +0 -0
  47. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_bindings.py +0 -0
  48. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_cli.py +0 -0
  49. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_concat.py +0 -0
  50. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_container_filter.py +0 -0
  51. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_cut.py +0 -0
  52. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_empty.py +0 -0
  53. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_filter_keyvalue.py +0 -0
  54. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_get.py +0 -0
  55. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_guard_transforms.py +0 -0
  56. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_invert.py +0 -0
  57. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_json_sentinels.py +0 -0
  58. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_keys_values.py +0 -0
  59. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_match.py +0 -0
  60. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_matchable.py +0 -0
  61. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_named_subst.py +0 -0
  62. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_negation.py +0 -0
  63. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_nop.py +0 -0
  64. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_numeric.py +0 -0
  65. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_opgroup.py +0 -0
  66. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_pluck.py +0 -0
  67. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_predicates.py +0 -0
  68. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_quote_idempotent.py +0 -0
  69. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_recursive.py +0 -0
  70. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_reference.py +0 -0
  71. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_replace.py +0 -0
  72. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_slice.py +0 -0
  73. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_softcut.py +0 -0
  74. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_strict.py +0 -0
  75. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_string_glob.py +0 -0
  76. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_subst_escape.py +0 -0
  77. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_subst_transforms.py +0 -0
  78. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_threading.py +0 -0
  79. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_transforms.py +0 -0
  80. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_translate.py +0 -0
  81. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_type_restriction.py +0 -0
  82. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_unpack.py +0 -0
  83. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_update.py +0 -0
  84. {dotted_notation-0.43.8 → dotted_notation-0.43.9}/tests/test_update_if.py +0 -0
  85. {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.8
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 `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
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
- >>> dotted.Resolver.build(r.where)
2824
- ('status = :_p1', {'_p1': 'active'})
2834
+ >>> r.build(r.where)
2835
+ ('status = $1', ['active'])
2825
2836
 
2826
- >>> r = dotted.sqlize("data.user.age >= 30")
2827
- >>> dotted.Resolver.build(r.where)
2828
- ("(data #>> '{user,age}')::numeric >= :_p1", {'_p1': 30})
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
- >>> dotted.Resolver.build(r.select)
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.Resolver.build(dotted.sqlize("age >= 30").where)
2837
- ('age >= :_p1', {'_p1': 30})
2847
+ >>> r = dotted.sqlize("age >= 30", driver='asyncpg')
2848
+ >>> r.build(r.where)
2849
+ ('age >= $1', [30])
2838
2850
 
2839
- >>> dotted.Resolver.build(dotted.sqlize("deleted_at = None").where)
2840
- ('deleted_at IS NULL', {})
2851
+ >>> r = dotted.sqlize("deleted_at = None", driver='asyncpg')
2852
+ >>> r.build(r.where)
2853
+ ('deleted_at IS NULL', [])
2841
2854
 
2842
- >>> dotted.Resolver.build(dotted.sqlize("name = /^alice/").where)
2843
- ('name ~ :_p1', {'_p1': '^alice'})
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
- >>> dotted.Resolver.build(r.where)
2849
- ('(age >= :_p1) AND (status = :_p2)', {'_p1': 18, '_p2': 'active'})
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
- >>> dotted.Resolver.build(r.where)
2855
- ("(data #>> '{user,age}')::int >= :_p1", {'_p1': 30})
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
- `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 `{{`…`}}`.
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
- >>> dotted.Resolver.build(combined, min_age=30)
2870
- ('WHERE age >= :min_age', {'min_age': 30})
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
- >>> dotted.Resolver.build(r.where)
2882
- ("name = :_p1", {'_p1': "'; DROP TABLE users;--"})
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
- >>> dotted.Resolver.build(r.where, min_age=30)
2895
- ('age >= :min_age', {'min_age': 30})
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
- `:name` / `$N`); each value is the **original substitution name**
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
- >>> dotted.Resolver.build(r.where, x=42)
2911
- ('(age >= :x) AND (weight = :x)', {'x': 42})
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)", bindings={'min_age': 30})
2918
- >>> dotted.Resolver.build(r.where)
2919
- ('age >= :_p1', {'_p1': 30})
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
- >>> dotted.Resolver.build(r.where)
2928
- ("(data ->> (data #>> '{config,field}')) = :_p1", {'_p1': 'Alice'})
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
- >>> dotted.Resolver.build(r.where)
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
- >>> dotted.Resolver.build(r.where)
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
- >>> dotted.Resolver.build(r.where)
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
- >>> sql, params = dotted.Resolver.build(r.where)
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', :_p1))"
2978
+ "jsonb_path_exists(data, '$.users[*] ? (@.age >= 30).name ? (@ == $_p1)', jsonb_build_object('_p1', $1::text))"
2958
2979
  >>> params
2959
- {'_p1': 'alice'}
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
- >>> sql, params = dotted.Resolver.build(r.where)
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 = :_p1)"
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
- ### Paramstyles
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
- The dict styles (`named`, `pyformat`) and the numbered positional
2993
- styles (`numeric`, `dollar-numeric`) have **back-reference**: a
2994
- substitution used in multiple places in the SQL shares a single bind
2995
- entry. `qmark` and `format` have no back-reference — each occurrence
2996
- consumes its own arg, so a repeated substitution emits its value
2997
- multiple times.
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
- Examples:
3012
+ import dotted
3013
+ from dotted.sql.pg import PostgresMixin
3000
3014
 
3001
- >>> r = dotted.sqlize('(age >= 18 & status = "active")')
3002
- >>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric')
3003
- ('(age >= $1) AND (status = $2)', [18, 'active'])
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
- >>> r = dotted.sqlize("age >= $(min_age)")
3006
- >>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric', min_age=30)
3007
- ('age >= $1', [30])
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
- ### Keyword arguments
3047
+ ### API reference
3044
3048
 
3045
- `dotted.sqlize(path, *, bindings=None, flavor='postgres')`
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
- `Resolver.build(sql, paramstyle='named', **bindings)`
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>