execsql2 2.22.5__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.
Files changed (21) hide show
  1. execsql/format.py +101 -12
  2. {execsql2-2.22.5.dist-info → execsql2-2.22.6.dist-info}/METADATA +2 -2
  3. {execsql2-2.22.5.dist-info → execsql2-2.22.6.dist-info}/RECORD +21 -21
  4. {execsql2-2.22.5.data → execsql2-2.22.6.data}/data/execsql2_extras/README.md +0 -0
  5. {execsql2-2.22.5.data → execsql2-2.22.6.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  6. {execsql2-2.22.5.data → execsql2-2.22.6.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  7. {execsql2-2.22.5.data → execsql2-2.22.6.data}/data/execsql2_extras/make_config_db.sql +0 -0
  8. {execsql2-2.22.5.data → execsql2-2.22.6.data}/data/execsql2_extras/md_compare.sql +0 -0
  9. {execsql2-2.22.5.data → execsql2-2.22.6.data}/data/execsql2_extras/md_glossary.sql +0 -0
  10. {execsql2-2.22.5.data → execsql2-2.22.6.data}/data/execsql2_extras/md_upsert.sql +0 -0
  11. {execsql2-2.22.5.data → execsql2-2.22.6.data}/data/execsql2_extras/pg_compare.sql +0 -0
  12. {execsql2-2.22.5.data → execsql2-2.22.6.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  13. {execsql2-2.22.5.data → execsql2-2.22.6.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  14. {execsql2-2.22.5.data → execsql2-2.22.6.data}/data/execsql2_extras/script_template.sql +0 -0
  15. {execsql2-2.22.5.data → execsql2-2.22.6.data}/data/execsql2_extras/ss_compare.sql +0 -0
  16. {execsql2-2.22.5.data → execsql2-2.22.6.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  17. {execsql2-2.22.5.data → execsql2-2.22.6.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  18. {execsql2-2.22.5.dist-info → execsql2-2.22.6.dist-info}/WHEEL +0 -0
  19. {execsql2-2.22.5.dist-info → execsql2-2.22.6.dist-info}/entry_points.txt +0 -0
  20. {execsql2-2.22.5.dist-info → execsql2-2.22.6.dist-info}/licenses/LICENSE.txt +0 -0
  21. {execsql2-2.22.5.dist-info → execsql2-2.22.6.dist-info}/licenses/NOTICE +0 -0
execsql/format.py CHANGED
@@ -278,6 +278,64 @@ def _sql_literal_texts(sql: str) -> list[str]:
278
278
  return [sql[start:end] for start, end, _ in _iter_sql_literals(sql)]
279
279
 
280
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
+
281
339
  # Placeholder standing in for a masked E'...' literal. It is a plain literal
282
340
  # so that sqlglot sees a string where a string was, and lays the statement out
283
341
  # the same way it would have for the real one.
@@ -503,6 +561,14 @@ def _has_mid_statement_comments(lines: list[str]) -> bool:
503
561
 
504
562
  _CMT_MARKER = "EXECSQL_CMTMARKER_"
505
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*)+")
506
572
 
507
573
 
508
574
  def _format_preserving_comments(
@@ -570,12 +636,16 @@ def _format_preserving_comments(
570
636
  markers_here = _CMT_MARKER_RE.findall(fline)
571
637
  if markers_here:
572
638
  # Strip markers to get the underlying SQL line and its indent
573
- cleaned = _CMT_MARKER_RE.sub("", fline).strip()
574
- # Determine indent: use the SQL line's indent from sqlglot
575
- line_indent = ""
576
- if cleaned:
577
- raw_cleaned = _CMT_MARKER_RE.sub("", fline)
578
- line_indent = raw_cleaned[: len(raw_cleaned) - len(raw_cleaned.lstrip())]
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())]
579
649
  for m in markers_here:
580
650
  if m in comment_store:
581
651
  orig = comment_store[m]
@@ -671,12 +741,24 @@ def format_sql_block(
671
741
  if not non_empty:
672
742
  return [""] * len(lines)
673
743
 
674
- base = min(len(line) - len(line.lstrip()) for line in non_empty)
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)
675
755
  target_prefix = " " * (depth * indent)
676
- 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)]
677
757
 
678
758
  if not use_sql:
679
- return [target_prefix + line if line.strip() else "" for line in rebased]
759
+ return [
760
+ line if i in interior else (target_prefix + line if line.strip() else "") for i, line in enumerate(rebased)
761
+ ]
680
762
 
681
763
  # When comments appear mid-statement, use the marker-based approach
682
764
  # which preserves both comments AND sqlglot formatting. When all
@@ -747,6 +829,9 @@ def _normalize_to_trailing_comma(text: str) -> str:
747
829
  every invocation. Comments between the two SQL lines stay in place.
748
830
  """
749
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)
750
835
 
751
836
  def find_prev_sql_line(idx: int) -> int:
752
837
  k = idx - 1
@@ -756,7 +841,7 @@ def _normalize_to_trailing_comma(text: str) -> str:
756
841
 
757
842
  for i, line in enumerate(lines):
758
843
  stripped = line.lstrip()
759
- if not stripped.startswith(",") or _is_comment_only(line):
844
+ if not stripped.startswith(",") or _is_comment_only(line) or i in protected:
760
845
  continue
761
846
  # Don't try to rewrite leading-comma inside a comment-only line.
762
847
  # Move the comma onto the previous SQL line as a trailing `,`.
@@ -792,15 +877,19 @@ def _apply_leading_comma(text: str) -> str:
792
877
  TestIdempotency).
793
878
  """
794
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)
795
884
  i = 0
796
885
  n = len(lines)
797
886
  while i < n:
798
887
  rstripped = lines[i].rstrip()
799
- 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:
800
889
  j = i + 1
801
890
  while j < n and (not lines[j].strip() or _is_comment_only(lines[j])):
802
891
  j += 1
803
- if j < n:
892
+ if j < n and j not in protected:
804
893
  stripped = lines[i].rstrip()
805
894
  comma_idx = stripped.rfind(",")
806
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.5
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.5
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=tfg0m3EbDVRlAR3ZhwV8BjgMFckL3u3IdldyCONrutU,41400
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.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,,
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,,