sqlakit 0.22.6__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.6 → sqlakit-0.22.8}/PKG-INFO +1 -1
- {sqlakit-0.22.6 → sqlakit-0.22.8}/pyproject.toml +1 -1
- {sqlakit-0.22.6 → sqlakit-0.22.8}/pyproject.toml.orig +1 -1
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_sql.py +78 -8
- {sqlakit-0.22.6 → sqlakit-0.22.8}/LICENSE +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/README.md +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/__init__.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_base.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_cli.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_db.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_linters.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_model.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_project.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_pycharm.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_query.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_recording.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_registry.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_render.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_routing.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_sql_formatter.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_static.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/asyncio/__init__.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/asyncio/_db.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/asyncio/_registry.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/asyncio/orm.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/asyncio/sql.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/exceptions.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/orm.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/py.typed +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/pytest_plugin.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/sql.py +0 -0
- {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/testing.py +0 -0
- {sqlakit-0.22.6 → 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
|
|
@@ -328,12 +328,15 @@ class Context:
|
|
|
328
328
|
self.values: dict[str, Any] = dict(values)
|
|
329
329
|
self._bound = 0
|
|
330
330
|
|
|
331
|
-
|
|
332
|
-
|
|
331
|
+
@property
|
|
332
|
+
def no_order(self) -> str:
|
|
333
|
+
"""An `ORDER BY` term that orders by nothing, for a macro with nothing to sort by.
|
|
333
334
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
PostgreSQL and SQL Server refuse a bare `NULL` there, and take
|
|
336
|
+
`(SELECT NULL)`. Snowflake refuses a subquery there, and takes `NULL`,
|
|
337
|
+
as MySQL, SQLite and Oracle do. `0` would be a column's position.
|
|
338
|
+
"""
|
|
339
|
+
return "(SELECT NULL)" if self.dialect in ("postgresql", "mssql") else "NULL"
|
|
337
340
|
|
|
338
341
|
def __repr__(self) -> str:
|
|
339
342
|
return f"{type(self).__name__}({self.dialect!r})"
|
|
@@ -2074,7 +2077,8 @@ def order_by(ctx: Context, sort: Param, column: Sql, *columns: Sql) -> str:
|
|
|
2074
2077
|
the nulls of every term whose sort string does not say, and MySQL, which
|
|
2075
2078
|
has no `NULLS LAST`, sorts by `IS NULL` first. Any other string is the sort
|
|
2076
2079
|
when the call passes none, `'name.asc'` above. With neither, the rows come
|
|
2077
|
-
in no order: `(SELECT NULL)
|
|
2080
|
+
in no order: `NULL`, or `(SELECT NULL)` on PostgreSQL and SQL Server, which
|
|
2081
|
+
refuse a bare `NULL`.
|
|
2078
2082
|
"""
|
|
2079
2083
|
written = [one.strip() for one in (column, *columns)]
|
|
2080
2084
|
options = [one.strip("'") for one in written if one.startswith("'")]
|
|
@@ -2289,6 +2293,69 @@ def array_contains_all(ctx: Context, array: Sql, other: Sql) -> str:
|
|
|
2289
2293
|
return f"({array} @> {other})"
|
|
2290
2294
|
|
|
2291
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
|
+
|
|
2292
2359
|
@sql_macro(name="values")
|
|
2293
2360
|
def values_table(ctx: Context, rows: Param) -> str:
|
|
2294
2361
|
"""Write a small table out in the query, one parameter per value.
|
|
@@ -2306,7 +2373,9 @@ def values_table(ctx: Context, rows: Param) -> str:
|
|
|
2306
2373
|
and PostgreSQL wants one type down a column: cast where the rows mix them.
|
|
2307
2374
|
|
|
2308
2375
|
For a few dozen rows. Hundreds are better sent as one JSON value and
|
|
2309
|
-
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.
|
|
2310
2379
|
|
|
2311
2380
|
Raises:
|
|
2312
2381
|
MacroArgumentError: if there are no rows, or they differ in length.
|
|
@@ -2591,6 +2660,7 @@ BUILTIN_MACROS: Mapping[str, Macro] = {
|
|
|
2591
2660
|
arrays_overlap,
|
|
2592
2661
|
array_contains_all,
|
|
2593
2662
|
values_table,
|
|
2663
|
+
any_of,
|
|
2594
2664
|
)
|
|
2595
2665
|
}
|
|
2596
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
|