dotted-notation 0.43.7__tar.gz → 0.43.8__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.7/dotted_notation.egg-info → dotted_notation-0.43.8}/PKG-INFO +31 -7
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/README.md +30 -6
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/sqlize.py +58 -23
- {dotted_notation-0.43.7 → dotted_notation-0.43.8/dotted_notation.egg-info}/PKG-INFO +31 -7
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/setup.py +1 -1
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_sqlize.py +39 -9
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/LICENSE +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/__init__.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/__main__.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/access.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/api.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/base.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/cli/__init__.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/cli/_compat.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/cli/formats.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/cli/main.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/containers.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/engine.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/filters.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/grammar.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/groups.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/matchers.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/predicates.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/recursive.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/results.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/transforms.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/utils.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/utypes.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted/wrappers.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted_notation.egg-info/SOURCES.txt +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted_notation.egg-info/dependency_links.txt +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted_notation.egg-info/entry_points.txt +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted_notation.egg-info/requires.txt +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted_notation.egg-info/top_level.txt +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/setup.cfg +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/__init__.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_api.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_appender.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_assemble.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_attrs.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_bindings.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_cli.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_concat.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_container_filter.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_cut.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_empty.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_filter_keyvalue.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_get.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_guard_transforms.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_invert.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_json_sentinels.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_keys_values.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_match.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_matchable.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_named_subst.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_negation.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_nop.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_numeric.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_opgroup.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_pluck.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_predicates.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_quote_idempotent.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_recursive.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_reference.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_replace.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_slice.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_softcut.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_strict.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_string_glob.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_subst_escape.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_subst_transforms.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_threading.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_transforms.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_translate.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_type_restriction.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_unpack.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_update.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/tests/test_update_if.py +0 -0
- {dotted_notation-0.43.7 → dotted_notation-0.43.8}/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.8
|
|
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
|
|
@@ -2977,10 +2977,26 @@ Not yet supported:
|
|
|
2977
2977
|
|
|
2978
2978
|
### Paramstyles
|
|
2979
2979
|
|
|
2980
|
-
`Resolver.build(sql, paramstyle='named', **bindings)`
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
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 |
|
|
2991
|
+
|
|
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.
|
|
2998
|
+
|
|
2999
|
+
Examples:
|
|
2984
3000
|
|
|
2985
3001
|
>>> r = dotted.sqlize('(age >= 18 & status = "active")')
|
|
2986
3002
|
>>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric')
|
|
@@ -2990,8 +3006,16 @@ and other raw PG drivers accept), pass `paramstyle='dollar-numeric'`
|
|
|
2990
3006
|
>>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric', min_age=30)
|
|
2991
3007
|
('age >= $1', [30])
|
|
2992
3008
|
|
|
2993
|
-
|
|
2994
|
-
|
|
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])
|
|
2995
3019
|
|
|
2996
3020
|
A `ParamStyle` `StrEnum` is also available for autocomplete and type
|
|
2997
3021
|
checking:
|
|
@@ -2940,10 +2940,26 @@ Not yet supported:
|
|
|
2940
2940
|
|
|
2941
2941
|
### Paramstyles
|
|
2942
2942
|
|
|
2943
|
-
`Resolver.build(sql, paramstyle='named', **bindings)`
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2943
|
+
`Resolver.build(sql, paramstyle='named', **bindings)` renders the
|
|
2944
|
+
fragment in the chosen PEP 249 paramstyle. Supported:
|
|
2945
|
+
|
|
2946
|
+
| paramstyle | placeholder | output shape | typical driver |
|
|
2947
|
+
|-------------------|----------------|--------------|---------------------------|
|
|
2948
|
+
| `named` | `:name` | dict | SQLAlchemy `text`, sqlite3 |
|
|
2949
|
+
| `pyformat` | `%(name)s` | dict | psycopg (v2/v3) named |
|
|
2950
|
+
| `qmark` | `?` | list | sqlite3, ODBC |
|
|
2951
|
+
| `format` | `%s` | list | psycopg positional |
|
|
2952
|
+
| `numeric` | `:1, :2` | list | Oracle-style |
|
|
2953
|
+
| `dollar-numeric` | `$1, $2` | list | asyncpg, native Postgres |
|
|
2954
|
+
|
|
2955
|
+
The dict styles (`named`, `pyformat`) and the numbered positional
|
|
2956
|
+
styles (`numeric`, `dollar-numeric`) have **back-reference**: a
|
|
2957
|
+
substitution used in multiple places in the SQL shares a single bind
|
|
2958
|
+
entry. `qmark` and `format` have no back-reference — each occurrence
|
|
2959
|
+
consumes its own arg, so a repeated substitution emits its value
|
|
2960
|
+
multiple times.
|
|
2961
|
+
|
|
2962
|
+
Examples:
|
|
2947
2963
|
|
|
2948
2964
|
>>> r = dotted.sqlize('(age >= 18 & status = "active")')
|
|
2949
2965
|
>>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric')
|
|
@@ -2953,8 +2969,16 @@ and other raw PG drivers accept), pass `paramstyle='dollar-numeric'`
|
|
|
2953
2969
|
>>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric', min_age=30)
|
|
2954
2970
|
('age >= $1', [30])
|
|
2955
2971
|
|
|
2956
|
-
|
|
2957
|
-
|
|
2972
|
+
>>> dotted.Resolver.build(r.where, paramstyle='pyformat', min_age=30)
|
|
2973
|
+
('age >= %(min_age)s', {'min_age': 30})
|
|
2974
|
+
|
|
2975
|
+
Back-reference vs no-backref on repeated substitutions:
|
|
2976
|
+
|
|
2977
|
+
>>> r = dotted.sqlize('(age >= $(x) & weight = $(x))')
|
|
2978
|
+
>>> dotted.Resolver.build(r.where, paramstyle='numeric', x=42)
|
|
2979
|
+
('(age >= :1) AND (weight = :1)', [42])
|
|
2980
|
+
>>> dotted.Resolver.build(r.where, paramstyle='qmark', x=42)
|
|
2981
|
+
('(age >= ?) AND (weight = ?)', [42, 42])
|
|
2958
2982
|
|
|
2959
2983
|
A `ParamStyle` `StrEnum` is also available for autocomplete and type
|
|
2960
2984
|
checking:
|
|
@@ -198,9 +198,22 @@ class ParamStyle(enum.StrEnum):
|
|
|
198
198
|
|
|
199
199
|
`StrEnum` — members compare equal to their string values, so callers
|
|
200
200
|
can pass either `ParamStyle.named` or `'named'`.
|
|
201
|
+
|
|
202
|
+
Output shape:
|
|
203
|
+
- `named` / `pyformat`: `(sql, params_dict)`. Repeated markers
|
|
204
|
+
share one binding entry (back-reference by name).
|
|
205
|
+
- `dollar-numeric` / `numeric`: `(sql, args_list)`. Repeated
|
|
206
|
+
markers share one position (back-reference by number).
|
|
207
|
+
- `qmark` / `format`: `(sql, args_list)`. Each marker occurrence
|
|
208
|
+
produces a separate arg — these styles have no back-reference
|
|
209
|
+
so a repeated substitution's value is emitted multiple times.
|
|
201
210
|
"""
|
|
202
|
-
named = 'named' # :name
|
|
203
|
-
|
|
211
|
+
named = 'named' # :name (SQLAlchemy, sqlite3)
|
|
212
|
+
pyformat = 'pyformat' # %(name)s (psycopg)
|
|
213
|
+
qmark = 'qmark' # ? (sqlite3 positional)
|
|
214
|
+
format = 'format' # %s (psycopg positional)
|
|
215
|
+
numeric = 'numeric' # :1, :2 (Oracle-style)
|
|
216
|
+
dollar_numeric = 'dollar-numeric' # $1, $2 (asyncpg, native PG)
|
|
204
217
|
|
|
205
218
|
|
|
206
219
|
_PARAMSTYLES = tuple(ps.value for ps in ParamStyle)
|
|
@@ -280,20 +293,23 @@ class SQLFragment:
|
|
|
280
293
|
"""
|
|
281
294
|
Return self.text with every `{marker}` replaced by the return
|
|
282
295
|
value of `replacer(marker_name)`. Text-level operation: the
|
|
283
|
-
caller decides what placeholder form to emit (`:name`, `$N`,
|
|
284
|
-
|
|
296
|
+
caller decides what placeholder form to emit (`:name`, `$N`,
|
|
297
|
+
`?`, etc.) and is responsible for tracking per-marker values
|
|
285
298
|
externally. Literal braces in the fragment (doubled as `{{`/
|
|
286
|
-
`}}`) are unescaped to single braces in the output,
|
|
287
|
-
|
|
299
|
+
`}}`) are unescaped to single braces in the output, per Python
|
|
300
|
+
format-string rules.
|
|
288
301
|
|
|
289
|
-
`replacer(name)` is called
|
|
290
|
-
|
|
302
|
+
`replacer(name)` is called **once per occurrence**, in source
|
|
303
|
+
order. Callers that want back-referencing semantics (e.g.
|
|
304
|
+
`:name` or `$N` sharing one slot across multiple occurrences)
|
|
305
|
+
must cache results themselves.
|
|
291
306
|
"""
|
|
292
|
-
|
|
293
|
-
for
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
307
|
+
parts = []
|
|
308
|
+
for literal, name, _, _ in _FORMATTER.parse(self.text):
|
|
309
|
+
parts.append(literal)
|
|
310
|
+
if name is not None:
|
|
311
|
+
parts.append(replacer(name))
|
|
312
|
+
return ''.join(parts)
|
|
297
313
|
|
|
298
314
|
|
|
299
315
|
class Resolver:
|
|
@@ -370,19 +386,26 @@ class Resolver:
|
|
|
370
386
|
f'unknown binding(s) for this fragment: {sorted(extra)}'
|
|
371
387
|
)
|
|
372
388
|
|
|
373
|
-
# Build a per-paramstyle replacer
|
|
374
|
-
#
|
|
389
|
+
# Build a per-paramstyle replacer. Replacers are called once
|
|
390
|
+
# per marker occurrence — name-keyed styles cache results so
|
|
391
|
+
# repeated markers share a single param; positional back-ref
|
|
392
|
+
# styles (numeric, dollar-numeric) cache by position; qmark /
|
|
393
|
+
# format emit a fresh arg for every occurrence.
|
|
375
394
|
missing = []
|
|
376
|
-
if paramstyle
|
|
395
|
+
if paramstyle in ('named', 'pyformat'):
|
|
377
396
|
out_params = {}
|
|
397
|
+
fmt = ':{name}' if paramstyle == 'named' else '%({name})s'
|
|
378
398
|
|
|
379
399
|
def replacer(marker):
|
|
380
400
|
value = cls._lookup_value(sql, marker, bindings, missing)
|
|
381
401
|
if value is _MISSING:
|
|
382
|
-
return ''
|
|
402
|
+
return ''
|
|
383
403
|
out_params[marker] = value
|
|
384
|
-
return
|
|
385
|
-
|
|
404
|
+
return fmt.format(name=marker)
|
|
405
|
+
|
|
406
|
+
out_values = out_params
|
|
407
|
+
elif paramstyle in ('numeric', 'dollar-numeric'):
|
|
408
|
+
prefix = '$' if paramstyle == 'dollar-numeric' else ':'
|
|
386
409
|
out_args = []
|
|
387
410
|
pos_by_marker = {}
|
|
388
411
|
|
|
@@ -393,10 +416,24 @@ class Resolver:
|
|
|
393
416
|
if value is _MISSING:
|
|
394
417
|
return ''
|
|
395
418
|
out_args.append(value)
|
|
396
|
-
ph = f'
|
|
419
|
+
ph = f'{prefix}{len(out_args)}'
|
|
397
420
|
pos_by_marker[marker] = ph
|
|
398
421
|
return ph
|
|
399
422
|
|
|
423
|
+
out_values = out_args
|
|
424
|
+
else: # qmark, format — no back-reference
|
|
425
|
+
placeholder = '?' if paramstyle == 'qmark' else '%s'
|
|
426
|
+
out_args = []
|
|
427
|
+
|
|
428
|
+
def replacer(marker):
|
|
429
|
+
value = cls._lookup_value(sql, marker, bindings, missing)
|
|
430
|
+
if value is _MISSING:
|
|
431
|
+
return ''
|
|
432
|
+
out_args.append(value)
|
|
433
|
+
return placeholder
|
|
434
|
+
|
|
435
|
+
out_values = out_args
|
|
436
|
+
|
|
400
437
|
final = sql.substitute(replacer)
|
|
401
438
|
|
|
402
439
|
if missing:
|
|
@@ -404,9 +441,7 @@ class Resolver:
|
|
|
404
441
|
f'missing binding(s) for substitution(s): '
|
|
405
442
|
f'{sorted(set(missing))}'
|
|
406
443
|
)
|
|
407
|
-
|
|
408
|
-
return final, out_params
|
|
409
|
-
return final, out_args
|
|
444
|
+
return final, out_values
|
|
410
445
|
|
|
411
446
|
@staticmethod
|
|
412
447
|
def _lookup_value(sql, marker, bindings, missing):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotted_notation
|
|
3
|
-
Version: 0.43.
|
|
3
|
+
Version: 0.43.8
|
|
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
|
|
@@ -2977,10 +2977,26 @@ Not yet supported:
|
|
|
2977
2977
|
|
|
2978
2978
|
### Paramstyles
|
|
2979
2979
|
|
|
2980
|
-
`Resolver.build(sql, paramstyle='named', **bindings)`
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
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 |
|
|
2991
|
+
|
|
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.
|
|
2998
|
+
|
|
2999
|
+
Examples:
|
|
2984
3000
|
|
|
2985
3001
|
>>> r = dotted.sqlize('(age >= 18 & status = "active")')
|
|
2986
3002
|
>>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric')
|
|
@@ -2990,8 +3006,16 @@ and other raw PG drivers accept), pass `paramstyle='dollar-numeric'`
|
|
|
2990
3006
|
>>> dotted.Resolver.build(r.where, paramstyle='dollar-numeric', min_age=30)
|
|
2991
3007
|
('age >= $1', [30])
|
|
2992
3008
|
|
|
2993
|
-
|
|
2994
|
-
|
|
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])
|
|
2995
3019
|
|
|
2996
3020
|
A `ParamStyle` `StrEnum` is also available for autocomplete and type
|
|
2997
3021
|
checking:
|
|
@@ -5,7 +5,7 @@ with open("README.md", "rt") as f:
|
|
|
5
5
|
|
|
6
6
|
setuptools.setup(
|
|
7
7
|
name="dotted_notation",
|
|
8
|
-
version="0.43.
|
|
8
|
+
version="0.43.8",
|
|
9
9
|
author="Frey Waid",
|
|
10
10
|
author_email="logophage1@gmail.com",
|
|
11
11
|
description="Dotted notation for safe nested data traversal with optional chaining, pattern matching, and transforms",
|
|
@@ -12,7 +12,14 @@ from dotted.sqlize import (
|
|
|
12
12
|
|
|
13
13
|
# All supported paramstyles as fixture values. Used to parametrize
|
|
14
14
|
# scenario tests so each runs under every paramstyle.
|
|
15
|
-
PARAMSTYLES = ['named', 'dollar-numeric']
|
|
15
|
+
PARAMSTYLES = ['named', 'pyformat', 'qmark', 'format', 'numeric', 'dollar-numeric']
|
|
16
|
+
|
|
17
|
+
# Styles that share bindings by back-reference (repeated markers collapse
|
|
18
|
+
# to one entry). qmark/format have no back-reference and repeat values.
|
|
19
|
+
BACKREF_STYLES = ['named', 'pyformat', 'numeric', 'dollar-numeric']
|
|
20
|
+
|
|
21
|
+
# Styles that return a dict vs a list.
|
|
22
|
+
DICT_STYLES = ['named', 'pyformat']
|
|
16
23
|
|
|
17
24
|
|
|
18
25
|
# ---------------- scalar columns ----------------
|
|
@@ -522,7 +529,7 @@ def test_unsupported_flavor_errors():
|
|
|
522
529
|
def test_unsupported_paramstyle_errors():
|
|
523
530
|
r = sqlize('age=30')
|
|
524
531
|
with pytest.raises(TranslationError, match='unsupported paramstyle'):
|
|
525
|
-
Resolver.build(r.where, paramstyle='
|
|
532
|
+
Resolver.build(r.where, paramstyle='bogus')
|
|
526
533
|
|
|
527
534
|
|
|
528
535
|
# ---------------- identifier quoting ----------------
|
|
@@ -567,20 +574,32 @@ def test_dollar_numeric_with_subst():
|
|
|
567
574
|
def _placeholder(paramstyle, name, position):
|
|
568
575
|
"""
|
|
569
576
|
Return the expected placeholder string for a given paramstyle.
|
|
570
|
-
name: the bind name (for
|
|
577
|
+
name: the bind name (for name-keyed styles) — ignored for positional.
|
|
571
578
|
position: 1-based position in the final SQL.
|
|
572
579
|
"""
|
|
573
580
|
if paramstyle == 'named':
|
|
574
581
|
return f':{name}'
|
|
575
|
-
|
|
582
|
+
if paramstyle == 'pyformat':
|
|
583
|
+
return f'%({name})s'
|
|
584
|
+
if paramstyle == 'numeric':
|
|
585
|
+
return f':{position}'
|
|
586
|
+
if paramstyle == 'dollar-numeric':
|
|
587
|
+
return f'${position}'
|
|
588
|
+
if paramstyle == 'qmark':
|
|
589
|
+
return '?'
|
|
590
|
+
if paramstyle == 'format':
|
|
591
|
+
return '%s'
|
|
592
|
+
raise ValueError(paramstyle)
|
|
576
593
|
|
|
577
594
|
|
|
578
595
|
def _values(paramstyle, items):
|
|
579
596
|
"""
|
|
580
597
|
Build the expected params (dict) or args (list) from an ordered
|
|
581
|
-
list of (bind_name, value) tuples.
|
|
598
|
+
list of (bind_name, value) tuples. Each test passes items in the
|
|
599
|
+
canonical named-paramstyle shape; this helper converts for
|
|
600
|
+
positional styles.
|
|
582
601
|
"""
|
|
583
|
-
if paramstyle
|
|
602
|
+
if paramstyle in DICT_STYLES:
|
|
584
603
|
return dict(items)
|
|
585
604
|
return [v for _, v in items]
|
|
586
605
|
|
|
@@ -687,8 +706,9 @@ def test_ps_subst(paramstyle):
|
|
|
687
706
|
assert values == _values(paramstyle, [('min_age', 30)])
|
|
688
707
|
|
|
689
708
|
|
|
690
|
-
@pytest.mark.parametrize('paramstyle',
|
|
691
|
-
def
|
|
709
|
+
@pytest.mark.parametrize('paramstyle', BACKREF_STYLES)
|
|
710
|
+
def test_ps_subst_repeated_backref(paramstyle):
|
|
711
|
+
"""Styles with back-reference share one slot across occurrences."""
|
|
692
712
|
r = sqlize('(age >= $(x) & weight = $(x))')
|
|
693
713
|
sql, values = Resolver.build(r.where, paramstyle=paramstyle, x=42)
|
|
694
714
|
ph = _placeholder(paramstyle, 'x', 1)
|
|
@@ -696,6 +716,16 @@ def test_ps_subst_repeated_shares(paramstyle):
|
|
|
696
716
|
assert values == _values(paramstyle, [('x', 42)])
|
|
697
717
|
|
|
698
718
|
|
|
719
|
+
@pytest.mark.parametrize('paramstyle', ['qmark', 'format'])
|
|
720
|
+
def test_ps_subst_repeated_no_backref(paramstyle):
|
|
721
|
+
"""qmark/format repeat the value (no back-reference)."""
|
|
722
|
+
r = sqlize('(age >= $(x) & weight = $(x))')
|
|
723
|
+
sql, values = Resolver.build(r.where, paramstyle=paramstyle, x=42)
|
|
724
|
+
ph = _placeholder(paramstyle, 'x', 1)
|
|
725
|
+
assert sql == f'(age >= {ph}) AND (weight = {ph})'
|
|
726
|
+
assert values == [42, 42]
|
|
727
|
+
|
|
728
|
+
|
|
699
729
|
@pytest.mark.parametrize('paramstyle', PARAMSTYLES)
|
|
700
730
|
def test_ps_pattern_with_literal_inline(paramstyle):
|
|
701
731
|
r = sqlize('data.users[*].age >= 30')
|
|
@@ -746,7 +776,7 @@ def test_ps_mixed_pattern_and_scalar(paramstyle):
|
|
|
746
776
|
|
|
747
777
|
# ---------------- ParamStyle enum accepted ----------------
|
|
748
778
|
|
|
749
|
-
@pytest.mark.parametrize('style',
|
|
779
|
+
@pytest.mark.parametrize('style', list(ParamStyle))
|
|
750
780
|
def test_paramstyle_enum_member_accepted(style):
|
|
751
781
|
"""Enum members work interchangeably with their string values."""
|
|
752
782
|
r = sqlize('age = 30')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dotted_notation-0.43.7 → dotted_notation-0.43.8}/dotted_notation.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|