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.
Files changed (34) hide show
  1. {sqlakit-0.22.6 → sqlakit-0.22.8}/PKG-INFO +1 -1
  2. {sqlakit-0.22.6 → sqlakit-0.22.8}/pyproject.toml +1 -1
  3. {sqlakit-0.22.6 → sqlakit-0.22.8}/pyproject.toml.orig +1 -1
  4. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_sql.py +78 -8
  5. {sqlakit-0.22.6 → sqlakit-0.22.8}/LICENSE +0 -0
  6. {sqlakit-0.22.6 → sqlakit-0.22.8}/README.md +0 -0
  7. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/__init__.py +0 -0
  8. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_base.py +0 -0
  9. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_cli.py +0 -0
  10. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_db.py +0 -0
  11. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_discovery.py +0 -0
  12. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_linters.py +0 -0
  13. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_model.py +0 -0
  14. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_project.py +0 -0
  15. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_pycharm.py +0 -0
  16. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_query.py +0 -0
  17. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_recording.py +0 -0
  18. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_registry.py +0 -0
  19. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_render.py +0 -0
  20. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_routing.py +0 -0
  21. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_sql_formatter.py +0 -0
  22. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/_static.py +0 -0
  23. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/asyncio/__init__.py +0 -0
  24. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/asyncio/_db.py +0 -0
  25. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/asyncio/_registry.py +0 -0
  26. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/asyncio/orm.py +0 -0
  27. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/asyncio/sql.py +0 -0
  28. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/exceptions.py +0 -0
  29. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/orm.py +0 -0
  30. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/py.typed +0 -0
  31. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/pytest_plugin.py +0 -0
  32. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/sql.py +0 -0
  33. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/testing.py +0 -0
  34. {sqlakit-0.22.6 → sqlakit-0.22.8}/sqlakit/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlakit
3
- Version: 0.22.6
3
+ Version: 0.22.8
4
4
  Summary: A toolkit for SQLAlchemy applications.
5
5
  Keywords: sqlalchemy,database,orm,sql,asyncio
6
6
  Author: Anton Ruhlov
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.22.6"
3
+ version = "0.22.8"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit"
3
- version = "0.22.6"
3
+ version = "0.22.8"
4
4
  description = "A toolkit for SQLAlchemy applications."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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
- no_order = "(SELECT NULL)"
332
- """An `ORDER BY` term that orders by nothing, for a macro with nothing to sort by.
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
- Every database takes it after `ORDER BY`, and a direction after it: `NULL`
335
- and `NULL DESC` are refused by PostgreSQL, and `0` is a column's position.
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)`, since PostgreSQL refuses a bare `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