execsql2 2.22.4__py3-none-any.whl → 2.22.6__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 +291 -13
- {execsql2-2.22.4.dist-info → execsql2-2.22.6.dist-info}/METADATA +2 -2
- {execsql2-2.22.4.dist-info → execsql2-2.22.6.dist-info}/RECORD +21 -21
- {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.22.4.dist-info → execsql2-2.22.6.dist-info}/WHEEL +0 -0
- {execsql2-2.22.4.dist-info → execsql2-2.22.6.dist-info}/entry_points.txt +0 -0
- {execsql2-2.22.4.dist-info → execsql2-2.22.6.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.22.4.dist-info → execsql2-2.22.6.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,228 @@ 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
|
+
def _literal_line_starts(lines: list[str]) -> list[int]:
|
|
282
|
+
"""Return the character offset at which each of *lines* starts once joined."""
|
|
283
|
+
starts: list[int] = []
|
|
284
|
+
offset = 0
|
|
285
|
+
for line in lines:
|
|
286
|
+
starts.append(offset)
|
|
287
|
+
offset += len(line) + 1 # +1 for the newline joined between lines
|
|
288
|
+
return starts
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _literal_interior_lines(lines: list[str]) -> set[int]:
|
|
292
|
+
"""Return indices of *lines* that begin inside a multi-line string literal.
|
|
293
|
+
|
|
294
|
+
The line that *opens* a literal is ordinary SQL up to the delimiter, so it
|
|
295
|
+
is indented with the statement as usual. Every later line of that literal
|
|
296
|
+
— through and including the one holding the closing delimiter — begins
|
|
297
|
+
inside the string, which makes its leading whitespace part of the stored
|
|
298
|
+
value rather than layout. Re-indenting those lines changes what the
|
|
299
|
+
database receives, so callers must emit them verbatim.
|
|
300
|
+
|
|
301
|
+
This covers every literal kind the scanner reports, not just dollar-quoted
|
|
302
|
+
bodies: an ordinary ``'…'`` string spanning lines carries its newlines and
|
|
303
|
+
indentation exactly the same way.
|
|
304
|
+
"""
|
|
305
|
+
if len(lines) < 2:
|
|
306
|
+
return set()
|
|
307
|
+
text = "\n".join(lines)
|
|
308
|
+
starts = _literal_line_starts(lines)
|
|
309
|
+
|
|
310
|
+
interior: set[int] = set()
|
|
311
|
+
for start, end, _kind in _iter_sql_literals(text):
|
|
312
|
+
interior.update(i for i, line_start in enumerate(starts) if start < line_start < end)
|
|
313
|
+
return interior
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _literal_spanning_lines(lines: list[str]) -> set[int]:
|
|
317
|
+
"""Return indices of every line that any multi-line literal touches.
|
|
318
|
+
|
|
319
|
+
Unlike :func:`_literal_interior_lines` this also includes the line that
|
|
320
|
+
*opens* the literal. Passes that move text between lines — rather than
|
|
321
|
+
only adjusting leading whitespace — must leave the whole span alone, since
|
|
322
|
+
the opening line holds literal content after its delimiter.
|
|
323
|
+
"""
|
|
324
|
+
if len(lines) < 2:
|
|
325
|
+
return set()
|
|
326
|
+
text = "\n".join(lines)
|
|
327
|
+
starts = _literal_line_starts(lines)
|
|
328
|
+
|
|
329
|
+
spanning: set[int] = set()
|
|
330
|
+
for start, end, _kind in _iter_sql_literals(text):
|
|
331
|
+
touched = [i for i, line_start in enumerate(starts) if start < line_start < end]
|
|
332
|
+
if not touched:
|
|
333
|
+
continue # single-line literal — nothing spans
|
|
334
|
+
spanning.update(touched)
|
|
335
|
+
spanning.add(min(touched) - 1) # the line that opens the literal
|
|
336
|
+
return spanning
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# Placeholder standing in for a masked E'...' literal. It is a plain literal
|
|
340
|
+
# so that sqlglot sees a string where a string was, and lays the statement out
|
|
341
|
+
# the same way it would have for the real one.
|
|
342
|
+
_ESTR_PLACEHOLDER = "execsqlestr"
|
|
343
|
+
_ESTR_RESTORE_RE = re.compile(rf"'{_ESTR_PLACEHOLDER}(\d+)'")
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _protect_estrings(sql: str) -> tuple[str, list[str]]:
|
|
347
|
+
"""Replace ``E'...'`` literals with placeholders, return (protected, originals).
|
|
348
|
+
|
|
349
|
+
sqlglot's Postgres tokenizer consumes backslash escapes inside an escape
|
|
350
|
+
string but its generator never re-emits them, so ``E'\\\\s+'`` round-trips
|
|
351
|
+
to ``e'\\s+'`` — a different string (tobymao/sqlglot#8191). Masking these
|
|
352
|
+
literals keeps them out of the round trip entirely and restores them
|
|
353
|
+
byte-for-byte, independently of the installed sqlglot version.
|
|
354
|
+
"""
|
|
355
|
+
originals: list[str] = []
|
|
356
|
+
out: list[str] = []
|
|
357
|
+
last = 0
|
|
358
|
+
for start, end, kind in _iter_sql_literals(sql):
|
|
359
|
+
if kind != "estring":
|
|
360
|
+
continue
|
|
361
|
+
out.append(sql[last:start])
|
|
362
|
+
out.append(f"'{_ESTR_PLACEHOLDER}{len(originals)}'")
|
|
363
|
+
originals.append(sql[start:end])
|
|
364
|
+
last = end
|
|
365
|
+
if not originals:
|
|
366
|
+
return sql, []
|
|
367
|
+
out.append(sql[last:])
|
|
368
|
+
return "".join(out), originals
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _restore_estrings(sql: str, originals: list[str]) -> str | None:
|
|
372
|
+
"""Put masked ``E'...'`` literals back; return None if any went missing.
|
|
373
|
+
|
|
374
|
+
A missing placeholder means sqlglot dropped or mangled the literal, so the
|
|
375
|
+
caller must fall back to the unformatted text rather than emit SQL with a
|
|
376
|
+
placeholder left in it.
|
|
377
|
+
"""
|
|
378
|
+
if not originals:
|
|
379
|
+
return sql
|
|
380
|
+
seen: set[int] = set()
|
|
381
|
+
|
|
382
|
+
def replace(m: re.Match[str]) -> str:
|
|
383
|
+
idx = int(m.group(1))
|
|
384
|
+
if idx >= len(originals):
|
|
385
|
+
return m.group(0)
|
|
386
|
+
seen.add(idx)
|
|
387
|
+
return originals[idx]
|
|
388
|
+
|
|
389
|
+
restored = _ESTR_RESTORE_RE.sub(replace, sql)
|
|
390
|
+
if len(seen) != len(originals) or _ESTR_PLACEHOLDER in restored:
|
|
391
|
+
return None
|
|
392
|
+
return restored
|
|
393
|
+
|
|
394
|
+
|
|
172
395
|
def _protect_variables(sql: str) -> tuple[str, list[tuple[str, str]]]:
|
|
173
396
|
"""Replace execsql substitutions with valid SQL identifiers, return (protected, replacements)."""
|
|
174
397
|
replacements: list[tuple[str, str]] = []
|
|
@@ -224,6 +447,9 @@ def _sqlglot_format(
|
|
|
224
447
|
|
|
225
448
|
text = "\n".join(sql_lines)
|
|
226
449
|
protected, replacements = _protect_variables(text)
|
|
450
|
+
# Keep E'...' literals out of the round trip; sqlglot loses their
|
|
451
|
+
# backslash escapes (see _protect_estrings).
|
|
452
|
+
protected, estrings = _protect_estrings(protected)
|
|
227
453
|
|
|
228
454
|
# Count semicolons in input as a rough statement count.
|
|
229
455
|
input_semis = protected.count(";")
|
|
@@ -275,8 +501,29 @@ def _sqlglot_format(
|
|
|
275
501
|
output_alnum_len = len(_alnum.sub("", joined))
|
|
276
502
|
if input_alnum_len and output_alnum_len < input_alnum_len * 0.7:
|
|
277
503
|
return sql_lines
|
|
504
|
+
|
|
505
|
+
# Literal round-trip check. The alphanumeric check above is a
|
|
506
|
+
# gross-token-loss detector: it deletes every non-alphanumeric
|
|
507
|
+
# character before counting, so a transformation that changes only
|
|
508
|
+
# punctuation — a dropped backslash, a swapped quote — is invisible to
|
|
509
|
+
# it at any threshold. Requiring every literal to survive verbatim
|
|
510
|
+
# closes that blind spot for the whole class, not just for the E'...'
|
|
511
|
+
# case masked above.
|
|
512
|
+
#
|
|
513
|
+
# Containment, not equality: sqlglot rewrites dialect-specific
|
|
514
|
+
# constructs into Postgres equivalents that can legitimately repeat a
|
|
515
|
+
# literal (``SUBSTRING_INDEX(d, ',', 1)`` expands into a form using
|
|
516
|
+
# ``','`` more than once), so the output may hold more literals than
|
|
517
|
+
# the input. What must never happen is an input literal coming back
|
|
518
|
+
# altered or not at all — that is content loss, and it falls back.
|
|
519
|
+
if set(_sql_literal_texts(protected)) - set(_sql_literal_texts(joined)):
|
|
520
|
+
return sql_lines
|
|
521
|
+
|
|
278
522
|
joined = re.sub(r"\bINTO TEMPORARY\b(?!\s+TABLE)", "INTO TEMPORARY TABLE", joined)
|
|
279
|
-
|
|
523
|
+
restored = _restore_estrings(joined, estrings)
|
|
524
|
+
if restored is None:
|
|
525
|
+
return sql_lines
|
|
526
|
+
return _restore_variables(restored, replacements).split("\n")
|
|
280
527
|
except Exception:
|
|
281
528
|
return sql_lines
|
|
282
529
|
|
|
@@ -314,6 +561,14 @@ def _has_mid_statement_comments(lines: list[str]) -> bool:
|
|
|
314
561
|
|
|
315
562
|
_CMT_MARKER = "EXECSQL_CMTMARKER_"
|
|
316
563
|
_CMT_MARKER_RE = re.compile(rf"/\*\s*({re.escape(_CMT_MARKER)}\d+)\s*\*/")
|
|
564
|
+
# A whole run of adjacent markers plus the whitespace injected around them.
|
|
565
|
+
# Markers are written as ``/* marker */ line`` and several consecutive source
|
|
566
|
+
# comments attach several markers to one line, so removing them one at a time
|
|
567
|
+
# leaves a separator space behind for each. When sqlglot moves the run into
|
|
568
|
+
# the middle of an expression that residue shows up as ``AND RTRIM(`` and is
|
|
569
|
+
# only normalised on the following pass, which costs an extra pass to
|
|
570
|
+
# converge. Matching the run as a unit removes it cleanly. See issue #35.
|
|
571
|
+
_CMT_MARKER_RUN_RE = re.compile(rf"\s*(?:/\*\s*{re.escape(_CMT_MARKER)}\d+\s*\*/\s*)+")
|
|
317
572
|
|
|
318
573
|
|
|
319
574
|
def _format_preserving_comments(
|
|
@@ -381,12 +636,16 @@ def _format_preserving_comments(
|
|
|
381
636
|
markers_here = _CMT_MARKER_RE.findall(fline)
|
|
382
637
|
if markers_here:
|
|
383
638
|
# Strip markers to get the underlying SQL line and its indent
|
|
384
|
-
cleaned =
|
|
385
|
-
#
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
639
|
+
cleaned = _CMT_MARKER_RUN_RE.sub(" ", fline).strip()
|
|
640
|
+
# Indent comes from the line's own leading whitespace, never from
|
|
641
|
+
# what is left after removing the marker. Markers are injected as
|
|
642
|
+
# ``/* marker */ line`` — with a separating space — so measuring
|
|
643
|
+
# after removal counts that space as indentation. Whenever
|
|
644
|
+
# sqlglot leaves the line untouched (an unparsable statement falls
|
|
645
|
+
# back verbatim) the extra space is re-measured and re-added on the
|
|
646
|
+
# next run, growing the indent by one on every pass and never
|
|
647
|
+
# converging. See issue #35.
|
|
648
|
+
line_indent = fline[: len(fline) - len(fline.lstrip())]
|
|
390
649
|
for m in markers_here:
|
|
391
650
|
if m in comment_store:
|
|
392
651
|
orig = comment_store[m]
|
|
@@ -482,12 +741,24 @@ def format_sql_block(
|
|
|
482
741
|
if not non_empty:
|
|
483
742
|
return [""] * len(lines)
|
|
484
743
|
|
|
485
|
-
|
|
744
|
+
# Lines that begin inside a multi-line literal carry content, not layout —
|
|
745
|
+
# dedenting or indenting them changes the value the database stores.
|
|
746
|
+
# Exclude them from the indent arithmetic and emit them verbatim.
|
|
747
|
+
interior = _literal_interior_lines(lines)
|
|
748
|
+
if interior:
|
|
749
|
+
# format_file already withholds dollar-quoted blocks from sqlglot; keep
|
|
750
|
+
# that true here so the line indices below stay aligned with the output.
|
|
751
|
+
use_sql = False
|
|
752
|
+
|
|
753
|
+
layout_lines = [line for i, line in enumerate(lines) if line.strip() and i not in interior]
|
|
754
|
+
base = min((len(line) - len(line.lstrip()) for line in layout_lines), default=0)
|
|
486
755
|
target_prefix = " " * (depth * indent)
|
|
487
|
-
rebased = [line[base:] if line.strip() else "" for line in lines]
|
|
756
|
+
rebased = [line if i in interior else (line[base:] if line.strip() else "") for i, line in enumerate(lines)]
|
|
488
757
|
|
|
489
758
|
if not use_sql:
|
|
490
|
-
return [
|
|
759
|
+
return [
|
|
760
|
+
line if i in interior else (target_prefix + line if line.strip() else "") for i, line in enumerate(rebased)
|
|
761
|
+
]
|
|
491
762
|
|
|
492
763
|
# When comments appear mid-statement, use the marker-based approach
|
|
493
764
|
# which preserves both comments AND sqlglot formatting. When all
|
|
@@ -558,6 +829,9 @@ def _normalize_to_trailing_comma(text: str) -> str:
|
|
|
558
829
|
every invocation. Comments between the two SQL lines stay in place.
|
|
559
830
|
"""
|
|
560
831
|
lines = text.split("\n")
|
|
832
|
+
# Symmetric with _apply_leading_comma: a leading comma inside a multi-line
|
|
833
|
+
# literal is data, not layout, so the whole span is left alone.
|
|
834
|
+
protected = _literal_spanning_lines(lines)
|
|
561
835
|
|
|
562
836
|
def find_prev_sql_line(idx: int) -> int:
|
|
563
837
|
k = idx - 1
|
|
@@ -567,7 +841,7 @@ def _normalize_to_trailing_comma(text: str) -> str:
|
|
|
567
841
|
|
|
568
842
|
for i, line in enumerate(lines):
|
|
569
843
|
stripped = line.lstrip()
|
|
570
|
-
if not stripped.startswith(",") or _is_comment_only(line):
|
|
844
|
+
if not stripped.startswith(",") or _is_comment_only(line) or i in protected:
|
|
571
845
|
continue
|
|
572
846
|
# Don't try to rewrite leading-comma inside a comment-only line.
|
|
573
847
|
# Move the comma onto the previous SQL line as a trailing `,`.
|
|
@@ -603,15 +877,19 @@ def _apply_leading_comma(text: str) -> str:
|
|
|
603
877
|
TestIdempotency).
|
|
604
878
|
"""
|
|
605
879
|
lines = text.split("\n")
|
|
880
|
+
# A comma inside a multi-line string literal is data. Moving it to the next
|
|
881
|
+
# line would rewrite the stored value, so leave every line such a literal
|
|
882
|
+
# touches — including the one that opens it — exactly as it is.
|
|
883
|
+
protected = _literal_spanning_lines(lines)
|
|
606
884
|
i = 0
|
|
607
885
|
n = len(lines)
|
|
608
886
|
while i < n:
|
|
609
887
|
rstripped = lines[i].rstrip()
|
|
610
|
-
if rstripped.endswith(",") and not _is_comment_only(lines[i]):
|
|
888
|
+
if rstripped.endswith(",") and not _is_comment_only(lines[i]) and i not in protected:
|
|
611
889
|
j = i + 1
|
|
612
890
|
while j < n and (not lines[j].strip() or _is_comment_only(lines[j])):
|
|
613
891
|
j += 1
|
|
614
|
-
if j < n:
|
|
892
|
+
if j < n and j not in protected:
|
|
615
893
|
stripped = lines[i].rstrip()
|
|
616
894
|
comma_idx = stripped.rfind(",")
|
|
617
895
|
lines[i] = stripped[:comma_idx] + stripped[comma_idx + 1 :]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.22.
|
|
3
|
+
Version: 2.22.6
|
|
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.6
|
|
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=YJH_JA1_IqBfeWhgrusA3KKs_xrmbm3ktuVf_xJryws,46017
|
|
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.6.data/data/execsql2_extras/README.md,sha256=vX4NTL095dUoA_hesyRMGYBorEZ_Y_tJ9qrd-MVV09I,5032
|
|
105
|
+
execsql2-2.22.6.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
|
|
106
|
+
execsql2-2.22.6.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
|
|
107
|
+
execsql2-2.22.6.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
|
|
108
|
+
execsql2-2.22.6.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
|
|
109
|
+
execsql2-2.22.6.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
|
|
110
|
+
execsql2-2.22.6.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
|
|
111
|
+
execsql2-2.22.6.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
|
|
112
|
+
execsql2-2.22.6.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
|
|
113
|
+
execsql2-2.22.6.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
|
|
114
|
+
execsql2-2.22.6.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
|
|
115
|
+
execsql2-2.22.6.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
|
|
116
|
+
execsql2-2.22.6.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
|
|
117
|
+
execsql2-2.22.6.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
|
|
118
|
+
execsql2-2.22.6.dist-info/METADATA,sha256=zX8fCinRuVHLFNIAKypooRUh_qPFfn-J6oBwu30_J_U,20382
|
|
119
|
+
execsql2-2.22.6.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
120
|
+
execsql2-2.22.6.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
|
|
121
|
+
execsql2-2.22.6.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
|
|
122
|
+
execsql2-2.22.6.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
|
|
123
|
+
execsql2-2.22.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{execsql2-2.22.4.data → execsql2-2.22.6.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
|