sqlakit 0.22.7__tar.gz → 0.22.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.
- {sqlakit-0.22.7 → sqlakit-0.22.8}/PKG-INFO +1 -1
- {sqlakit-0.22.7 → sqlakit-0.22.8}/pyproject.toml +1 -1
- {sqlakit-0.22.7 → sqlakit-0.22.8}/pyproject.toml.orig +1 -1
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_sql.py +68 -2
- {sqlakit-0.22.7 → sqlakit-0.22.8}/LICENSE +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/README.md +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/__init__.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_base.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_cli.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_db.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_linters.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_model.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_project.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_pycharm.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_query.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_recording.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_registry.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_render.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_routing.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_sql_formatter.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/_static.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/asyncio/__init__.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/asyncio/_db.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/asyncio/_registry.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/asyncio/orm.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/asyncio/sql.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/exceptions.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/orm.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/py.typed +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/pytest_plugin.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/sql.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/testing.py +0 -0
- {sqlakit-0.22.7 → sqlakit-0.22.8}/sqlakit/types.py +0 -0
|
@@ -10,7 +10,7 @@ from collections.abc import Mapping, Sized
|
|
|
10
10
|
from contextlib import contextmanager
|
|
11
11
|
from contextvars import ContextVar
|
|
12
12
|
from dataclasses import dataclass, is_dataclass
|
|
13
|
-
from functools import cache, cached_property, lru_cache
|
|
13
|
+
from functools import cache, cached_property, lru_cache, partial
|
|
14
14
|
from inspect import iscoroutinefunction
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from types import SimpleNamespace
|
|
@@ -2293,6 +2293,69 @@ def array_contains_all(ctx: Context, array: Sql, other: Sql) -> str:
|
|
|
2293
2293
|
return f"({array} @> {other})"
|
|
2294
2294
|
|
|
2295
2295
|
|
|
2296
|
+
@sql_macro(optional=True)
|
|
2297
|
+
def any_of(ctx: Context, rows: Param, row: Sql, condition: Sql) -> str:
|
|
2298
|
+
"""Whether the condition holds for any of the rows, `row.field` reading a field.
|
|
2299
|
+
|
|
2300
|
+
```sql
|
|
2301
|
+
WHERE tpl.any_of(
|
|
2302
|
+
:cities, city,
|
|
2303
|
+
ST_DISTANCE(p.location, ST_POINT(city.lon, city.lat)) <= city.radius
|
|
2304
|
+
)
|
|
2305
|
+
```
|
|
2306
|
+
|
|
2307
|
+
The condition is written once for each row, its `city.field` bound to the
|
|
2308
|
+
row's field, and the conditions joined with `OR`. A row is an object or a
|
|
2309
|
+
mapping. No rows, or none passed, is `FALSE`: wrap the call in
|
|
2310
|
+
`tpl.if_set(:cities, ...)` for a filter that is off when nothing is passed.
|
|
2311
|
+
|
|
2312
|
+
The SQL grows with the rows, so this is for a few dozen. A condition made
|
|
2313
|
+
of equalities only is better as `EXISTS (SELECT 1 FROM tpl.values(:rows) AS
|
|
2314
|
+
v WHERE t.a = v.column1)`, which the database joins. Snowflake evaluates no
|
|
2315
|
+
subquery tied to the outer row by anything else, a distance, a range or a
|
|
2316
|
+
`LIKE`, and this macro covers those.
|
|
2317
|
+
|
|
2318
|
+
Raises:
|
|
2319
|
+
MacroArgumentError: if ``row`` is not a name, or a row has no field the
|
|
2320
|
+
condition reads.
|
|
2321
|
+
|
|
2322
|
+
"""
|
|
2323
|
+
name = str(row).strip()
|
|
2324
|
+
if not _IDENTIFIER.fullmatch(name):
|
|
2325
|
+
problem = f"`{name}` is not a name to read the fields of a row under"
|
|
2326
|
+
raise MacroArgumentError(any_of.name, problem)
|
|
2327
|
+
field = re.compile(
|
|
2328
|
+
rf"{_LITERAL}|(?<![\w.]){re.escape(name)}\.(?P<field>[A-Za-z_]\w*)\b",
|
|
2329
|
+
re.DOTALL,
|
|
2330
|
+
)
|
|
2331
|
+
prefix = _named(rows)
|
|
2332
|
+
|
|
2333
|
+
def written(item: Any, found: re.Match[str]) -> str: # noqa: ANN401
|
|
2334
|
+
key = found.group("field")
|
|
2335
|
+
if key is None:
|
|
2336
|
+
return found.group()
|
|
2337
|
+
try:
|
|
2338
|
+
value = item[key] if isinstance(item, Mapping) else getattr(item, key)
|
|
2339
|
+
except (KeyError, AttributeError):
|
|
2340
|
+
problem = f"a row of `{_as_written(rows)}` has no field `{key}`"
|
|
2341
|
+
raise MacroArgumentError(any_of.name, problem) from None
|
|
2342
|
+
return ctx.bind(value, f"{prefix}_{key}" if prefix else key)
|
|
2343
|
+
|
|
2344
|
+
text = str(condition)
|
|
2345
|
+
# A `--` comment at the end would take the `)` and the `OR` after it along.
|
|
2346
|
+
if any(
|
|
2347
|
+
found.group().startswith("--") and found.end() == len(text.rstrip())
|
|
2348
|
+
for found in re.finditer(_LITERAL, text, re.DOTALL)
|
|
2349
|
+
):
|
|
2350
|
+
text = text.rstrip() + "\n"
|
|
2351
|
+
conditions = [
|
|
2352
|
+
_grouped(field.sub(partial(written, item), text)) for item in rows.value or ()
|
|
2353
|
+
]
|
|
2354
|
+
if not conditions:
|
|
2355
|
+
return "FALSE"
|
|
2356
|
+
return _grouped(" OR ".join(conditions))
|
|
2357
|
+
|
|
2358
|
+
|
|
2296
2359
|
@sql_macro(name="values")
|
|
2297
2360
|
def values_table(ctx: Context, rows: Param) -> str:
|
|
2298
2361
|
"""Write a small table out in the query, one parameter per value.
|
|
@@ -2310,7 +2373,9 @@ def values_table(ctx: Context, rows: Param) -> str:
|
|
|
2310
2373
|
and PostgreSQL wants one type down a column: cast where the rows mix them.
|
|
2311
2374
|
|
|
2312
2375
|
For a few dozen rows. Hundreds are better sent as one JSON value and
|
|
2313
|
-
unpacked in the database, with `FLATTEN` or `json_array_elements`.
|
|
2376
|
+
unpacked in the database, with `FLATTEN` or `json_array_elements`. In an
|
|
2377
|
+
`EXISTS` tied to the outer row, compare by equalities only: Snowflake
|
|
2378
|
+
refuses any other tie, and `any_of` writes that condition for each row.
|
|
2314
2379
|
|
|
2315
2380
|
Raises:
|
|
2316
2381
|
MacroArgumentError: if there are no rows, or they differ in length.
|
|
@@ -2595,6 +2660,7 @@ BUILTIN_MACROS: Mapping[str, Macro] = {
|
|
|
2595
2660
|
arrays_overlap,
|
|
2596
2661
|
array_contains_all,
|
|
2597
2662
|
values_table,
|
|
2663
|
+
any_of,
|
|
2598
2664
|
)
|
|
2599
2665
|
}
|
|
2600
2666
|
|
|
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
|