execsql2 2.22.4__py3-none-any.whl → 2.22.5__py3-none-any.whl
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.
- execsql/format.py +190 -1
- {execsql2-2.22.4.dist-info → execsql2-2.22.5.dist-info}/METADATA +2 -2
- {execsql2-2.22.4.dist-info → execsql2-2.22.5.dist-info}/RECORD +21 -21
- {execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.22.4.dist-info → execsql2-2.22.5.dist-info}/WHEEL +0 -0
- {execsql2-2.22.4.dist-info → execsql2-2.22.5.dist-info}/entry_points.txt +0 -0
- {execsql2-2.22.4.dist-info → execsql2-2.22.5.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.22.4.dist-info → execsql2-2.22.5.dist-info}/licenses/NOTICE +0 -0
execsql/format.py
CHANGED
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import contextlib
|
|
15
15
|
import io
|
|
16
16
|
import re
|
|
17
|
+
from collections.abc import Iterator
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
|
|
19
20
|
__all__ = ["collect_paths", "format_file", "main", "parse_keyword"]
|
|
@@ -169,6 +170,170 @@ def parse_keyword(payload: str) -> str:
|
|
|
169
170
|
_EXECSQL_VAR_RE = re.compile(r"""!(['"]?)!([^!\s][^!]*)!\1!|!\{[^}]+\}!""")
|
|
170
171
|
|
|
171
172
|
|
|
173
|
+
# Characters that may appear in an identifier, used to tell an ``E'...'``
|
|
174
|
+
# escape-string prefix from a trailing ``e`` on an identifier (``abce'x'`` is
|
|
175
|
+
# the identifier ``abce`` followed by a separate literal).
|
|
176
|
+
_IDENT_CHAR_RE = re.compile(r"[A-Za-z0-9_$]")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _iter_sql_literals(sql: str) -> Iterator[tuple[int, int, str]]:
|
|
180
|
+
"""Yield ``(start, end, kind)`` spans for every string literal in *sql*.
|
|
181
|
+
|
|
182
|
+
This is a lexical walk rather than a regex sweep, because an apostrophe
|
|
183
|
+
only starts a literal in some contexts. Line comments, block comments
|
|
184
|
+
(which nest in PostgreSQL), and quoted identifiers are skipped entirely,
|
|
185
|
+
so quotes inside them never open a literal.
|
|
186
|
+
|
|
187
|
+
*kind* is one of:
|
|
188
|
+
|
|
189
|
+
``"estring"``
|
|
190
|
+
A PostgreSQL escape string, ``E'...'``. Backslash escapes are
|
|
191
|
+
significant inside it, which is what makes it unsafe to hand to
|
|
192
|
+
sqlglot — see :func:`_protect_estrings`.
|
|
193
|
+
``"plain"``
|
|
194
|
+
An ordinary ``'...'`` literal, where only a doubled ``''`` escapes.
|
|
195
|
+
``"dollar"``
|
|
196
|
+
A dollar-quoted body, ``$$...$$`` or ``$tag$...$tag$``, in which no
|
|
197
|
+
character is special.
|
|
198
|
+
"""
|
|
199
|
+
i, n = 0, len(sql)
|
|
200
|
+
while i < n:
|
|
201
|
+
c = sql[i]
|
|
202
|
+
|
|
203
|
+
if c == "-" and sql.startswith("--", i):
|
|
204
|
+
nl = sql.find("\n", i)
|
|
205
|
+
i = n if nl < 0 else nl + 1
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
if c == "/" and sql.startswith("/*", i):
|
|
209
|
+
depth, i = 1, i + 2
|
|
210
|
+
while i < n and depth:
|
|
211
|
+
if sql.startswith("/*", i):
|
|
212
|
+
depth, i = depth + 1, i + 2
|
|
213
|
+
elif sql.startswith("*/", i):
|
|
214
|
+
depth, i = depth - 1, i + 2
|
|
215
|
+
else:
|
|
216
|
+
i += 1
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
if c == '"':
|
|
220
|
+
i += 1
|
|
221
|
+
while i < n:
|
|
222
|
+
if sql[i] == '"':
|
|
223
|
+
if sql.startswith('""', i):
|
|
224
|
+
i += 2
|
|
225
|
+
continue
|
|
226
|
+
i += 1
|
|
227
|
+
break
|
|
228
|
+
i += 1
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
if c == "$":
|
|
232
|
+
m = _DOLLAR_QUOTE_RE.match(sql, i)
|
|
233
|
+
if m:
|
|
234
|
+
tag = m.group(0)
|
|
235
|
+
close = sql.find(tag, m.end())
|
|
236
|
+
stop = n if close < 0 else close + len(tag)
|
|
237
|
+
yield i, stop, "dollar"
|
|
238
|
+
i = stop
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
# E'...' — only when the `e` is not the tail of an identifier.
|
|
242
|
+
if c in "eE" and sql.startswith("'", i + 1) and not (i and _IDENT_CHAR_RE.match(sql[i - 1])):
|
|
243
|
+
j = i + 2
|
|
244
|
+
while j < n:
|
|
245
|
+
if sql[j] == "\\" and j + 1 < n:
|
|
246
|
+
j += 2
|
|
247
|
+
continue
|
|
248
|
+
if sql[j] == "'":
|
|
249
|
+
if sql.startswith("''", j):
|
|
250
|
+
j += 2
|
|
251
|
+
continue
|
|
252
|
+
j += 1
|
|
253
|
+
break
|
|
254
|
+
j += 1
|
|
255
|
+
yield i, j, "estring"
|
|
256
|
+
i = j
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
if c == "'":
|
|
260
|
+
j = i + 1
|
|
261
|
+
while j < n:
|
|
262
|
+
if sql[j] == "'":
|
|
263
|
+
if sql.startswith("''", j):
|
|
264
|
+
j += 2
|
|
265
|
+
continue
|
|
266
|
+
j += 1
|
|
267
|
+
break
|
|
268
|
+
j += 1
|
|
269
|
+
yield i, j, "plain"
|
|
270
|
+
i = j
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
i += 1
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _sql_literal_texts(sql: str) -> list[str]:
|
|
277
|
+
"""Return the source text of every string literal in *sql*, in order."""
|
|
278
|
+
return [sql[start:end] for start, end, _ in _iter_sql_literals(sql)]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Placeholder standing in for a masked E'...' literal. It is a plain literal
|
|
282
|
+
# so that sqlglot sees a string where a string was, and lays the statement out
|
|
283
|
+
# the same way it would have for the real one.
|
|
284
|
+
_ESTR_PLACEHOLDER = "execsqlestr"
|
|
285
|
+
_ESTR_RESTORE_RE = re.compile(rf"'{_ESTR_PLACEHOLDER}(\d+)'")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _protect_estrings(sql: str) -> tuple[str, list[str]]:
|
|
289
|
+
"""Replace ``E'...'`` literals with placeholders, return (protected, originals).
|
|
290
|
+
|
|
291
|
+
sqlglot's Postgres tokenizer consumes backslash escapes inside an escape
|
|
292
|
+
string but its generator never re-emits them, so ``E'\\\\s+'`` round-trips
|
|
293
|
+
to ``e'\\s+'`` — a different string (tobymao/sqlglot#8191). Masking these
|
|
294
|
+
literals keeps them out of the round trip entirely and restores them
|
|
295
|
+
byte-for-byte, independently of the installed sqlglot version.
|
|
296
|
+
"""
|
|
297
|
+
originals: list[str] = []
|
|
298
|
+
out: list[str] = []
|
|
299
|
+
last = 0
|
|
300
|
+
for start, end, kind in _iter_sql_literals(sql):
|
|
301
|
+
if kind != "estring":
|
|
302
|
+
continue
|
|
303
|
+
out.append(sql[last:start])
|
|
304
|
+
out.append(f"'{_ESTR_PLACEHOLDER}{len(originals)}'")
|
|
305
|
+
originals.append(sql[start:end])
|
|
306
|
+
last = end
|
|
307
|
+
if not originals:
|
|
308
|
+
return sql, []
|
|
309
|
+
out.append(sql[last:])
|
|
310
|
+
return "".join(out), originals
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _restore_estrings(sql: str, originals: list[str]) -> str | None:
|
|
314
|
+
"""Put masked ``E'...'`` literals back; return None if any went missing.
|
|
315
|
+
|
|
316
|
+
A missing placeholder means sqlglot dropped or mangled the literal, so the
|
|
317
|
+
caller must fall back to the unformatted text rather than emit SQL with a
|
|
318
|
+
placeholder left in it.
|
|
319
|
+
"""
|
|
320
|
+
if not originals:
|
|
321
|
+
return sql
|
|
322
|
+
seen: set[int] = set()
|
|
323
|
+
|
|
324
|
+
def replace(m: re.Match[str]) -> str:
|
|
325
|
+
idx = int(m.group(1))
|
|
326
|
+
if idx >= len(originals):
|
|
327
|
+
return m.group(0)
|
|
328
|
+
seen.add(idx)
|
|
329
|
+
return originals[idx]
|
|
330
|
+
|
|
331
|
+
restored = _ESTR_RESTORE_RE.sub(replace, sql)
|
|
332
|
+
if len(seen) != len(originals) or _ESTR_PLACEHOLDER in restored:
|
|
333
|
+
return None
|
|
334
|
+
return restored
|
|
335
|
+
|
|
336
|
+
|
|
172
337
|
def _protect_variables(sql: str) -> tuple[str, list[tuple[str, str]]]:
|
|
173
338
|
"""Replace execsql substitutions with valid SQL identifiers, return (protected, replacements)."""
|
|
174
339
|
replacements: list[tuple[str, str]] = []
|
|
@@ -224,6 +389,9 @@ def _sqlglot_format(
|
|
|
224
389
|
|
|
225
390
|
text = "\n".join(sql_lines)
|
|
226
391
|
protected, replacements = _protect_variables(text)
|
|
392
|
+
# Keep E'...' literals out of the round trip; sqlglot loses their
|
|
393
|
+
# backslash escapes (see _protect_estrings).
|
|
394
|
+
protected, estrings = _protect_estrings(protected)
|
|
227
395
|
|
|
228
396
|
# Count semicolons in input as a rough statement count.
|
|
229
397
|
input_semis = protected.count(";")
|
|
@@ -275,8 +443,29 @@ def _sqlglot_format(
|
|
|
275
443
|
output_alnum_len = len(_alnum.sub("", joined))
|
|
276
444
|
if input_alnum_len and output_alnum_len < input_alnum_len * 0.7:
|
|
277
445
|
return sql_lines
|
|
446
|
+
|
|
447
|
+
# Literal round-trip check. The alphanumeric check above is a
|
|
448
|
+
# gross-token-loss detector: it deletes every non-alphanumeric
|
|
449
|
+
# character before counting, so a transformation that changes only
|
|
450
|
+
# punctuation — a dropped backslash, a swapped quote — is invisible to
|
|
451
|
+
# it at any threshold. Requiring every literal to survive verbatim
|
|
452
|
+
# closes that blind spot for the whole class, not just for the E'...'
|
|
453
|
+
# case masked above.
|
|
454
|
+
#
|
|
455
|
+
# Containment, not equality: sqlglot rewrites dialect-specific
|
|
456
|
+
# constructs into Postgres equivalents that can legitimately repeat a
|
|
457
|
+
# literal (``SUBSTRING_INDEX(d, ',', 1)`` expands into a form using
|
|
458
|
+
# ``','`` more than once), so the output may hold more literals than
|
|
459
|
+
# the input. What must never happen is an input literal coming back
|
|
460
|
+
# altered or not at all — that is content loss, and it falls back.
|
|
461
|
+
if set(_sql_literal_texts(protected)) - set(_sql_literal_texts(joined)):
|
|
462
|
+
return sql_lines
|
|
463
|
+
|
|
278
464
|
joined = re.sub(r"\bINTO TEMPORARY\b(?!\s+TABLE)", "INTO TEMPORARY TABLE", joined)
|
|
279
|
-
|
|
465
|
+
restored = _restore_estrings(joined, estrings)
|
|
466
|
+
if restored is None:
|
|
467
|
+
return sql_lines
|
|
468
|
+
return _restore_variables(restored, replacements).split("\n")
|
|
280
469
|
except Exception:
|
|
281
470
|
return sql_lines
|
|
282
471
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.22.
|
|
3
|
+
Version: 2.22.5
|
|
4
4
|
Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
|
|
5
5
|
Project-URL: Homepage, https://execsql2.readthedocs.io
|
|
6
6
|
Project-URL: Repository, https://github.com/geocoug/execsql
|
|
@@ -384,7 +384,7 @@ execsql-format --no-sql --in-place scripts/
|
|
|
384
384
|
```yaml
|
|
385
385
|
repos:
|
|
386
386
|
- repo: https://github.com/geocoug/execsql
|
|
387
|
-
rev: v2.22.
|
|
387
|
+
rev: v2.22.5
|
|
388
388
|
hooks:
|
|
389
389
|
- id: execsql-format
|
|
390
390
|
```
|
|
@@ -3,7 +3,7 @@ execsql/__main__.py,sha256=HdbK-SAhyUmfB6xINY5AzxdMSxGzWSGEG_2dv42Jn64,315
|
|
|
3
3
|
execsql/api.py,sha256=EOtBwMnm-8fmB_KmvxyjglR6C7CjdYhCPBisJ9CFfJg,22227
|
|
4
4
|
execsql/config.py,sha256=txKziUJLV7UPonj2sGFgkmlinDdSV50aHmR_cuCbEIo,34369
|
|
5
5
|
execsql/exceptions.py,sha256=GlphAiR8iZu4UR4U50WgSqEUMd9_8AeQJ_hhPcJOeQU,9381
|
|
6
|
-
execsql/format.py,sha256=
|
|
6
|
+
execsql/format.py,sha256=tfg0m3EbDVRlAR3ZhwV8BjgMFckL3u3IdldyCONrutU,41400
|
|
7
7
|
execsql/models.py,sha256=-DWTVrPNwM-n1h3e3rXMoBv9-wpWVdZahyy4RB7fRRE,15502
|
|
8
8
|
execsql/parser.py,sha256=aOxZ7Mzgvk7G4-nvy9Bgz7xlaZm6oo81cffDeBk_LCk,15597
|
|
9
9
|
execsql/plugins.py,sha256=l82VWa5NpvOQWD6vcO-WKP-vtn0wp9__wWDFAbwxZg0,12838
|
|
@@ -101,23 +101,23 @@ execsql/utils/numeric.py,sha256=_7bi5R6pHjMCkOPne_Sh5nUj-B8e9g6WFmjptFHaunA,1573
|
|
|
101
101
|
execsql/utils/regex.py,sha256=B3mSBhRRx_DpUnX_t9IoYd05KvWAebiLMCZqQAmEwcw,3885
|
|
102
102
|
execsql/utils/strings.py,sha256=UQNjpRCEFa1UO6feU-M-9e24wWAvizs_iu_4fFusLxo,8516
|
|
103
103
|
execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
|
|
104
|
-
execsql2-2.22.
|
|
105
|
-
execsql2-2.22.
|
|
106
|
-
execsql2-2.22.
|
|
107
|
-
execsql2-2.22.
|
|
108
|
-
execsql2-2.22.
|
|
109
|
-
execsql2-2.22.
|
|
110
|
-
execsql2-2.22.
|
|
111
|
-
execsql2-2.22.
|
|
112
|
-
execsql2-2.22.
|
|
113
|
-
execsql2-2.22.
|
|
114
|
-
execsql2-2.22.
|
|
115
|
-
execsql2-2.22.
|
|
116
|
-
execsql2-2.22.
|
|
117
|
-
execsql2-2.22.
|
|
118
|
-
execsql2-2.22.
|
|
119
|
-
execsql2-2.22.
|
|
120
|
-
execsql2-2.22.
|
|
121
|
-
execsql2-2.22.
|
|
122
|
-
execsql2-2.22.
|
|
123
|
-
execsql2-2.22.
|
|
104
|
+
execsql2-2.22.5.data/data/execsql2_extras/README.md,sha256=vX4NTL095dUoA_hesyRMGYBorEZ_Y_tJ9qrd-MVV09I,5032
|
|
105
|
+
execsql2-2.22.5.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
|
|
106
|
+
execsql2-2.22.5.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
|
|
107
|
+
execsql2-2.22.5.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
|
|
108
|
+
execsql2-2.22.5.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
|
|
109
|
+
execsql2-2.22.5.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
|
|
110
|
+
execsql2-2.22.5.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
|
|
111
|
+
execsql2-2.22.5.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
|
|
112
|
+
execsql2-2.22.5.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
|
|
113
|
+
execsql2-2.22.5.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
|
|
114
|
+
execsql2-2.22.5.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
|
|
115
|
+
execsql2-2.22.5.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
|
|
116
|
+
execsql2-2.22.5.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
|
|
117
|
+
execsql2-2.22.5.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
|
|
118
|
+
execsql2-2.22.5.dist-info/METADATA,sha256=OHGAN6pqfqOTBPDSst372rskkKNgeXUofH9K_mfl3sc,20382
|
|
119
|
+
execsql2-2.22.5.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
120
|
+
execsql2-2.22.5.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
|
|
121
|
+
execsql2-2.22.5.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
|
|
122
|
+
execsql2-2.22.5.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
|
|
123
|
+
execsql2-2.22.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{execsql2-2.22.4.data → execsql2-2.22.5.data}/data/execsql2_extras/example_config_prompt.sql
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
|