sqlakit 0.21.0__tar.gz → 0.22.0__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.21.0 → sqlakit-0.22.0}/PKG-INFO +1 -1
- {sqlakit-0.21.0 → sqlakit-0.22.0}/pyproject.toml +1 -1
- {sqlakit-0.21.0 → sqlakit-0.22.0}/pyproject.toml.orig +1 -1
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/_project.py +28 -8
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/_sql.py +0 -17
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/_sqruff.py +69 -6
- {sqlakit-0.21.0 → sqlakit-0.22.0}/LICENSE +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/README.md +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/__init__.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/_base.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/_cli.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/_db.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/_discovery.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/_model.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/_pycharm.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/_query.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/_recording.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/_registry.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/_routing.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/_static.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/asyncio/__init__.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/asyncio/_db.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/asyncio/_registry.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/asyncio/orm.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/asyncio/sql.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/exceptions.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/orm.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/py.typed +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/pytest_plugin.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/sql.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/testing.py +0 -0
- {sqlakit-0.21.0 → sqlakit-0.22.0}/sqlakit/types.py +0 -0
|
@@ -48,7 +48,7 @@ from .exceptions import (
|
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
if TYPE_CHECKING:
|
|
51
|
-
from collections.abc import Iterator
|
|
51
|
+
from collections.abc import Collection, Iterable, Iterator
|
|
52
52
|
|
|
53
53
|
__all__ = ["Problem", "Project", "load_project"]
|
|
54
54
|
|
|
@@ -212,7 +212,23 @@ class Project:
|
|
|
212
212
|
if found.group("macro")
|
|
213
213
|
]
|
|
214
214
|
|
|
215
|
-
def
|
|
215
|
+
def parameter_names(self) -> set[str]:
|
|
216
|
+
"""Return the name of every `:parameter` the templates and macros read."""
|
|
217
|
+
names: set[str] = set()
|
|
218
|
+
for path in self._linted_files():
|
|
219
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
220
|
+
names.update(
|
|
221
|
+
found.group("param")
|
|
222
|
+
for found in _PARAMETER_IN_TEXT.finditer(text)
|
|
223
|
+
if found.group("param")
|
|
224
|
+
)
|
|
225
|
+
return names
|
|
226
|
+
|
|
227
|
+
def _linted_files(self) -> set[Path]:
|
|
228
|
+
paths = [self.path_of(name) for name in self.templates.names()]
|
|
229
|
+
return {*(one for one in paths if one is not None), *self.macro_files()}
|
|
230
|
+
|
|
231
|
+
def placeholder_values(self, reserved: Iterable[str] = ()) -> dict[str, str]:
|
|
216
232
|
"""Return a value for each parameter a linter cannot read by its name.
|
|
217
233
|
|
|
218
234
|
With no value, the `placeholder` templater writes a parameter's name in
|
|
@@ -221,12 +237,14 @@ class Project:
|
|
|
221
237
|
writes to, a linter reads only `@stage/path`,
|
|
222
238
|
and inside `SAMPLE (...)` only a number. A name SQL reserves reads as
|
|
223
239
|
neither, `LIMIT :limit` reading `LIMIT limit`, so it gets `1`, or its
|
|
224
|
-
name with `_` after it when a path follows, `:order.id`.
|
|
240
|
+
name with `_` after it when a path follows, `:order.id`. ``reserved``
|
|
241
|
+
adds the words a linter's dialect keeps beside those.
|
|
225
242
|
"""
|
|
226
243
|
values: dict[str, str] = {}
|
|
227
|
-
|
|
228
|
-
for path in
|
|
229
|
-
|
|
244
|
+
words = RESERVED | {word.lower() for word in reserved}
|
|
245
|
+
for path in self._linted_files():
|
|
246
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
247
|
+
placeholders_of(text, values, words)
|
|
230
248
|
return dict(sorted(values.items()))
|
|
231
249
|
|
|
232
250
|
def macro_files(self) -> list[Path]:
|
|
@@ -258,7 +276,9 @@ class Project:
|
|
|
258
276
|
return found
|
|
259
277
|
|
|
260
278
|
|
|
261
|
-
def placeholders_of(
|
|
279
|
+
def placeholders_of(
|
|
280
|
+
text: str, values: dict[str, str], reserved: Collection[str] = RESERVED
|
|
281
|
+
) -> dict[str, str]:
|
|
262
282
|
"""Add to ``values`` what a linter needs for each parameter of the text.
|
|
263
283
|
|
|
264
284
|
See `Project.placeholder_values`, which reads every template with it.
|
|
@@ -276,7 +296,7 @@ def placeholders_of(text: str, values: dict[str, str]) -> dict[str, str]:
|
|
|
276
296
|
values[param] = "@stage/path"
|
|
277
297
|
elif SAMPLE_AFTER.search(before):
|
|
278
298
|
values[param] = "10"
|
|
279
|
-
elif param.lower() in
|
|
299
|
+
elif param.lower() in reserved:
|
|
280
300
|
# `COPY INTO :table` needs a name, and other positions a value.
|
|
281
301
|
named = dotted or inline_position(before)
|
|
282
302
|
values.setdefault(param, f"{param}_" if named else "1")
|
|
@@ -1954,22 +1954,6 @@ def unless_set(value: Param, expr: Sql, otherwise: Sql = Sql("TRUE")) -> str: #
|
|
|
1954
1954
|
return _grouped(str(otherwise if _is_set(value.value) else expr))
|
|
1955
1955
|
|
|
1956
1956
|
|
|
1957
|
-
@sql_macro(optional=True, lazy=True)
|
|
1958
|
-
def when(value: Param, sql: Sql, *more: Sql) -> str:
|
|
1959
|
-
"""Write `sql` when the parameter holds a value, and nothing when it does not.
|
|
1960
|
-
|
|
1961
|
-
For a clause that is there or not, rather than a condition that is true or
|
|
1962
|
-
not: `tpl.when(:team, JOIN teams AS t ON t.id = u.team_id)`. `if_set`
|
|
1963
|
-
writes `TRUE` in its place, which only a condition can stand.
|
|
1964
|
-
|
|
1965
|
-
Everything after the parameter is the clause, commas and all:
|
|
1966
|
-
`tpl.when(:limit, ORDER BY score DESC, created_at DESC LIMIT :limit)`.
|
|
1967
|
-
"""
|
|
1968
|
-
if not _is_set(value.value):
|
|
1969
|
-
return ""
|
|
1970
|
-
return ", ".join(str(part) for part in (sql, *more))
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
1957
|
@sql_macro(optional=True)
|
|
1974
1958
|
def array(ctx: Context, values: Param, type_name: Sql = Sql("")) -> str: # noqa: B008
|
|
1975
1959
|
"""Write a list as an array, one parameter per value.
|
|
@@ -2580,7 +2564,6 @@ BUILTIN_MACROS: Mapping[str, Macro] = {
|
|
|
2580
2564
|
for macro in (
|
|
2581
2565
|
if_set,
|
|
2582
2566
|
unless_set,
|
|
2583
|
-
when,
|
|
2584
2567
|
order_by,
|
|
2585
2568
|
icontains,
|
|
2586
2569
|
icollate,
|
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
The `placeholder` templater writes each `:name` as `name`. Where that reads as
|
|
4
4
|
something else, `settings` gives the parameter a value; see
|
|
5
|
-
`Project.placeholder_values`.
|
|
5
|
+
`Project.placeholder_values`. Which names a dialect keeps for itself is asked
|
|
6
|
+
of `sqruff`, when it is installed, since its dialects keep words SQLAlchemy's
|
|
7
|
+
do not: `exclude` and `row` on SQLite.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
from __future__ import annotations
|
|
9
11
|
|
|
10
12
|
import json
|
|
11
13
|
import re
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import tempfile
|
|
12
17
|
import tomllib
|
|
13
|
-
from typing import TYPE_CHECKING
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
14
19
|
|
|
15
20
|
from ._project import LINT_EXCLUDED
|
|
16
21
|
from .exceptions import ProjectConfigError
|
|
@@ -33,7 +38,7 @@ def stale(project: Project, pyproject: str) -> list[str]:
|
|
|
33
38
|
"""Return the tables of `pyproject.toml` that no longer read the templates."""
|
|
34
39
|
sqruff = tomllib.loads(pyproject).get("tool", {}).get("sqruff", {})
|
|
35
40
|
written = sqruff.get("templater", {}).get("placeholder", {})
|
|
36
|
-
values = project.placeholder_values()
|
|
41
|
+
values = project.placeholder_values(_kept(project, _dialect(sqruff, None, project)))
|
|
37
42
|
checked = (
|
|
38
43
|
("[tool.sqruff.core]", "core" in sqruff),
|
|
39
44
|
(
|
|
@@ -59,9 +64,8 @@ def settings(project: Project, pyproject: str, dialect: str | None) -> str:
|
|
|
59
64
|
written = dict(sqruff.get("templater", {}).get("placeholder", {}))
|
|
60
65
|
written.pop("param_style", None)
|
|
61
66
|
text = pyproject
|
|
67
|
+
chosen = _dialect(sqruff, dialect, project)
|
|
62
68
|
if "core" not in sqruff:
|
|
63
|
-
chosen = dialect or project.dialect
|
|
64
|
-
chosen = _DIALECTS.get(chosen or "", chosen)
|
|
65
69
|
core = [
|
|
66
70
|
"[tool.sqruff.core]",
|
|
67
71
|
*([f'dialect = "{chosen}"'] if chosen else []),
|
|
@@ -69,7 +73,8 @@ def settings(project: Project, pyproject: str, dialect: str | None) -> str:
|
|
|
69
73
|
f'exclude_rules = "{",".join(LINT_EXCLUDED)}"',
|
|
70
74
|
]
|
|
71
75
|
text = text.rstrip("\n") + "\n\n" + "\n".join(core) + "\n"
|
|
72
|
-
|
|
76
|
+
values = project.placeholder_values(_kept(project, chosen))
|
|
77
|
+
merged = dict(sorted({**written, **values}.items()))
|
|
73
78
|
table = "\n".join(
|
|
74
79
|
[
|
|
75
80
|
"[tool.sqruff.templater.placeholder]",
|
|
@@ -97,6 +102,64 @@ def settings(project: Project, pyproject: str, dialect: str | None) -> str:
|
|
|
97
102
|
return written_out
|
|
98
103
|
|
|
99
104
|
|
|
105
|
+
def _dialect(sqruff: dict, given: str | None, project: Project) -> str | None:
|
|
106
|
+
"""Return the dialect `sqruff` reads the templates in, by its name."""
|
|
107
|
+
if "core" in sqruff:
|
|
108
|
+
# sqruff reads a table without a dialect as ANSI, and so does this.
|
|
109
|
+
written = sqruff["core"].get("dialect")
|
|
110
|
+
return written if isinstance(written, str) else None
|
|
111
|
+
chosen = given or project.dialect
|
|
112
|
+
return _DIALECTS.get(chosen or "", chosen)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _kept(project: Project, dialect: str | None) -> set[str]:
|
|
116
|
+
"""Return the parameter names `sqruff` cannot read as a column in the dialect.
|
|
117
|
+
|
|
118
|
+
Each name is linted as `SELECT <name>;`, one to a line, in one run. Without
|
|
119
|
+
`sqruff` installed, the dialects of SQLAlchemy decide alone.
|
|
120
|
+
"""
|
|
121
|
+
binary = shutil.which("sqruff")
|
|
122
|
+
names = sorted(project.parameter_names())
|
|
123
|
+
if binary is None or not names:
|
|
124
|
+
return set()
|
|
125
|
+
# JSON, as the report of its own is another shape on GitHub Actions.
|
|
126
|
+
command = [binary, "lint", "--parsing-errors", "--format", "json", "-"]
|
|
127
|
+
if dialect:
|
|
128
|
+
command[2:2] = ["--dialect", dialect]
|
|
129
|
+
source = "".join(f"SELECT {name};\n" for name in names)
|
|
130
|
+
# Away from the project, so no setting of its own changes the reading.
|
|
131
|
+
with tempfile.TemporaryDirectory() as empty:
|
|
132
|
+
found = subprocess.run( # noqa: S603 - sqruff, found on the PATH
|
|
133
|
+
command,
|
|
134
|
+
input=source,
|
|
135
|
+
capture_output=True,
|
|
136
|
+
text=True,
|
|
137
|
+
cwd=empty,
|
|
138
|
+
check=False,
|
|
139
|
+
)
|
|
140
|
+
lines = {
|
|
141
|
+
problem["range"]["start"]["line"]
|
|
142
|
+
for problems in _report(found.stdout, found.stderr).values()
|
|
143
|
+
for problem in problems
|
|
144
|
+
# The name starts after `SELECT `, at the eighth character.
|
|
145
|
+
if problem.get("message") == "Unparsable section"
|
|
146
|
+
and problem["range"]["start"]["character"] == len("SELECT ") + 1
|
|
147
|
+
}
|
|
148
|
+
return {name for number, name in enumerate(names, 1) if number in lines}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _report(*outputs: str) -> dict[str, list[dict[str, Any]]]:
|
|
152
|
+
"""Return the JSON report, from whichever stream `sqruff` wrote it to."""
|
|
153
|
+
for output in outputs:
|
|
154
|
+
try:
|
|
155
|
+
report = json.loads(output)
|
|
156
|
+
except json.JSONDecodeError:
|
|
157
|
+
continue
|
|
158
|
+
if isinstance(report, dict):
|
|
159
|
+
return report
|
|
160
|
+
return {}
|
|
161
|
+
|
|
162
|
+
|
|
100
163
|
def _toml_value(value: object) -> str:
|
|
101
164
|
"""Return a value as TOML writes it: a string quoted, a flag in lower case."""
|
|
102
165
|
if isinstance(value, bool):
|
|
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
|