execsql2 2.25.0__py3-none-any.whl → 2.26.0__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 (25) hide show
  1. execsql/cli/commands/format.py +10 -0
  2. execsql/cli/lint.py +86 -26
  3. execsql/config.py +2 -0
  4. execsql/data/execsql.conf.template +3 -0
  5. execsql/format.py +106 -21
  6. {execsql2-2.25.0.dist-info → execsql2-2.26.0.dist-info}/METADATA +5 -5
  7. {execsql2-2.25.0.dist-info → execsql2-2.26.0.dist-info}/RECORD +25 -25
  8. {execsql2-2.25.0.data → execsql2-2.26.0.data}/data/execsql2_extras/README.md +0 -0
  9. {execsql2-2.25.0.data → execsql2-2.26.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  10. {execsql2-2.25.0.data → execsql2-2.26.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  11. {execsql2-2.25.0.data → execsql2-2.26.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  12. {execsql2-2.25.0.data → execsql2-2.26.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  13. {execsql2-2.25.0.data → execsql2-2.26.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  14. {execsql2-2.25.0.data → execsql2-2.26.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  15. {execsql2-2.25.0.data → execsql2-2.26.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  16. {execsql2-2.25.0.data → execsql2-2.26.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  17. {execsql2-2.25.0.data → execsql2-2.26.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  18. {execsql2-2.25.0.data → execsql2-2.26.0.data}/data/execsql2_extras/script_template.sql +0 -0
  19. {execsql2-2.25.0.data → execsql2-2.26.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  20. {execsql2-2.25.0.data → execsql2-2.26.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  21. {execsql2-2.25.0.data → execsql2-2.26.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  22. {execsql2-2.25.0.dist-info → execsql2-2.26.0.dist-info}/WHEEL +0 -0
  23. {execsql2-2.25.0.dist-info → execsql2-2.26.0.dist-info}/entry_points.txt +0 -0
  24. {execsql2-2.25.0.dist-info → execsql2-2.26.0.dist-info}/licenses/LICENSE.txt +0 -0
  25. {execsql2-2.25.0.dist-info → execsql2-2.26.0.dist-info}/licenses/NOTICE +0 -0
@@ -51,6 +51,15 @@ def format_cmd(
51
51
  help="Commas at the start of lines instead of the end. Default: [format] leading_comma, else off.",
52
52
  show_default=False,
53
53
  ),
54
+ rewrite_sql: bool | None = typer.Option(
55
+ None,
56
+ "--rewrite-sql/--no-rewrite-sql",
57
+ help=(
58
+ "Also let sqlglot rewrite SQL text (x::int as CAST(x AS INT), BTRIM as TRIM, an alias with AS); "
59
+ "without it such a statement is kept as written. Default: [format] rewrite_sql, else off."
60
+ ),
61
+ show_default=False,
62
+ ),
54
63
  script_encoding: ScriptEncodingOpt = None,
55
64
  # The old spelling of --script-encoding, kept working but out of --help.
56
65
  encoding: str | None = typer.Option(None, "--encoding", metavar="NAME", hidden=True),
@@ -92,6 +101,7 @@ def format_cmd(
92
101
  no_sql=no_sql,
93
102
  indent=conf.format_indent if indent is None else indent,
94
103
  leading_comma=conf.format_leading_comma if leading_comma is None else leading_comma,
104
+ rewrite_sql=conf.format_rewrite_sql if rewrite_sql is None else rewrite_sql,
95
105
  encoding=script_encoding or encoding or _configured_script_encoding(conf) or "utf-8",
96
106
  ),
97
107
  )
execsql/cli/lint.py CHANGED
@@ -243,6 +243,12 @@ _RX_PROMPT_CREDENTIALS = re.compile(
243
243
  )
244
244
 
245
245
  _RX_VAR_REF = re.compile(r"!!([$@&~#+]?\w+)!!", re.I)
246
+ # Every candidate reference, including one nested in another's name
247
+ # (``!!N_!!GROUP!!_CHECKS!!``), which _RX_VAR_REF's left-to-right scan skips.
248
+ _RX_VAR_REF_ANY = re.compile(r"(?=!!([$@&~#+]?\w+)!!)")
249
+ # Stands for a substituted value in _unresolved_var_refs: a word character
250
+ # (so an enclosing name still matches) that no script name contains.
251
+ _SUBSTITUTED = "ª"
246
252
 
247
253
 
248
254
  # ---------------------------------------------------------------------------
@@ -424,54 +430,108 @@ def _extract_var_definition(
424
430
  _read_ini_vars(ini_file, ini_section, script_dir, defined)
425
431
 
426
432
 
433
+ # Larger than any real SUB_INI file; lint reads no more than this.
434
+ _MAX_INI_BYTES = 1024 * 1024
435
+
436
+
427
437
  def _read_ini_vars(
428
438
  ini_file: str,
429
439
  section: str,
430
440
  script_dir: Path | None,
431
441
  defined_vars: set[str],
432
442
  ) -> None:
433
- """Read an INI file and register its section keys as defined variables."""
443
+ """Read an INI file and register its section keys as defined variables.
444
+
445
+ Only a regular file up to :data:`_MAX_INI_BYTES` is read, so a script
446
+ naming a FIFO or a device (``/dev/zero``) cannot hang lint or the
447
+ language server; a file that is unreadable or not valid INI defines
448
+ nothing.
449
+ """
434
450
  from configparser import ConfigParser
451
+ from configparser import Error as ConfigError
435
452
 
436
453
  p = Path(ini_file)
437
454
  if not p.is_absolute() and script_dir is not None:
438
455
  p = script_dir / p
439
456
 
440
- if not p.exists():
441
- return
442
-
443
457
  cp = ConfigParser()
444
- cp.read(p)
458
+ try:
459
+ if not p.is_file() or p.stat().st_size > _MAX_INI_BYTES:
460
+ return
461
+ cp.read_string(p.read_text(encoding="utf-8"), source=str(p))
462
+ except (OSError, UnicodeDecodeError, ConfigError):
463
+ return
445
464
  if cp.has_section(section):
446
465
  for key, _value in cp.items(section):
447
466
  defined_vars.add(key.upper())
448
467
 
449
468
 
450
- def _check_var_ref(
451
- raw_name: str,
452
- source: str,
453
- line_no: int,
454
- defined_vars: set[str],
455
- issues: list[_Issue],
456
- ) -> None:
457
- """Emit a warning if *raw_name* looks like an undefined user variable."""
458
- if not raw_name:
459
- return
460
-
469
+ def _is_known_var(raw_name: str, defined_vars: set[str]) -> bool:
470
+ """Whether execsql may substitute *raw_name* (as written, with its sigil) at run time."""
461
471
  sigil = raw_name[0] if raw_name[0] in ("$", "@", "&", "~", "#", "+") else ""
462
472
  name = raw_name[len(sigil) :]
463
473
 
464
474
  # Skip non-$ sigil prefixes — resolved at runtime
465
475
  if sigil in ("@", "&", "~", "#", "+"):
466
- return
476
+ return True
467
477
 
468
478
  if sigil == "$":
469
479
  # System variables: $ARG_N is set with -a/--assign-arg, $COUNTER_N by
470
480
  # CounterVars, the rest by execsql itself. A SUB cannot define one.
471
- if re.match(r"^(?:ARG|COUNTER)_\d+$", name, re.I) or name.upper() in _get_builtin_vars():
472
- return
473
- elif name.upper() in defined_vars:
474
- # User variables, defined by a SUB-family metacommand or a prompt.
481
+ return bool(re.match(r"^(?:ARG|COUNTER)_\d+$", name, re.I)) or name.upper() in _get_builtin_vars()
482
+ # User variables, defined by a SUB-family metacommand or a prompt.
483
+ return name.upper() in defined_vars
484
+
485
+
486
+ def _unresolved_var_refs(text: str, defined_vars: set[str]) -> list[str]:
487
+ """The variable names in *text* that execsql would leave unsubstituted, as written.
488
+
489
+ execsql substitutes a known variable wherever ``!!name!!`` appears and
490
+ repeats until nothing changes, so a variable written inside another's
491
+ name (``!!N_!!GROUP!!_CHECKS!!``) is replaced first, and the outer name
492
+ is only known at run time: it is not checked. Here each known variable
493
+ is replaced by a placeholder character, a name holding one counts as
494
+ known, and that repeats until nothing is replaced.
495
+
496
+ In a run of unknown candidates that share delimiters, the inner ones
497
+ (every second, from the second) are reported: with ``GROUP`` undefined,
498
+ ``!!N_!!GROUP!!_CHECKS!!`` reports ``GROUP``, not the fragments ``N_``
499
+ and ``_CHECKS``.
500
+ """
501
+ while True:
502
+ parts: list[str] = []
503
+ end = 0
504
+ for m in _RX_VAR_REF_ANY.finditer(text):
505
+ name = m.group(1)
506
+ if m.start() >= end and (_SUBSTITUTED in name or _is_known_var(name, defined_vars)):
507
+ parts += [text[end : m.start()], _SUBSTITUTED]
508
+ end = m.start() + len(name) + 4
509
+ if not parts:
510
+ break
511
+ text = "".join([*parts, text[end:]])
512
+
513
+ unresolved: list[str] = []
514
+ run: list[str] = []
515
+ end = -1
516
+ for m in _RX_VAR_REF_ANY.finditer(text):
517
+ if run and m.start() != end - 2: # not sharing the previous candidate's closing !!
518
+ unresolved.extend(run if len(run) == 1 else run[1::2])
519
+ run = []
520
+ run.append(m.group(1))
521
+ end = m.start() + len(m.group(1)) + 4
522
+ unresolved.extend(run if len(run) == 1 else run[1::2])
523
+ return unresolved
524
+
525
+
526
+ def _check_var_ref(
527
+ raw_name: str,
528
+ source: str,
529
+ line_no: int,
530
+ defined_vars: set[str],
531
+ issues: list[_Issue],
532
+ ) -> None:
533
+ """Emit a warning if *raw_name* looks like an undefined user variable."""
534
+ if not raw_name or _is_known_var(raw_name, defined_vars):
475
535
  return
476
536
 
477
537
  issues.append(
@@ -735,7 +795,7 @@ def _collect_var_references(nodes: list[Node], seen: set[str]) -> None:
735
795
  """Record every variable read anywhere beneath *nodes*: ``!!var!!``, or ``sub_defined(var)`` / ``sub_empty(var)``."""
736
796
  for node in nodes:
737
797
  for text in _referencing_text(node):
738
- for m in _RX_VAR_REF.finditer(text):
798
+ for m in _RX_VAR_REF_ANY.finditer(text):
739
799
  seen.add(m.group(1).lstrip("$@&~#+").upper())
740
800
  for m in _RX_VAR_TEST.finditer(text):
741
801
  seen.add(m.group(1).lstrip("$@&~#+").upper())
@@ -826,14 +886,14 @@ def _lint_nodes(
826
886
 
827
887
  # -- Variable references in SQL --
828
888
  if isinstance(node, SqlStatement):
829
- for m in _RX_VAR_REF.finditer(node.text):
830
- _check_var_ref(m.group(1), src, lno, defined_vars, issues)
889
+ for name in _unresolved_var_refs(node.text, defined_vars):
890
+ _check_var_ref(name, src, lno, defined_vars, issues)
831
891
 
832
892
  # -- Metacommand checks --
833
893
  elif isinstance(node, MetaCommandStatement):
834
894
  _check_metacommand(node, issues)
835
- for m in _RX_VAR_REF.finditer(node.command):
836
- _check_var_ref(m.group(1), src, lno, defined_vars, issues)
895
+ for name in _unresolved_var_refs(node.command, defined_vars):
896
+ _check_var_ref(name, src, lno, defined_vars, issues)
837
897
 
838
898
  # -- IncludeDirective checks --
839
899
  elif isinstance(node, IncludeDirective):
execsql/config.py CHANGED
@@ -379,6 +379,7 @@ class ConfigData:
379
379
  self._get_int(cp, self._FORMAT_SECTION, "indent", "format_indent")
380
380
  self._get_bool(cp, self._FORMAT_SECTION, "leading_comma", "format_leading_comma")
381
381
  self._get_bool(cp, self._FORMAT_SECTION, "sql", "format_sql")
382
+ self._get_bool(cp, self._FORMAT_SECTION, "rewrite_sql", "format_rewrite_sql")
382
383
  self._get_str(cp, self._LINT_SECTION, "select", "lint_select")
383
384
  self._get_str(cp, self._LINT_SECTION, "ignore", "lint_ignore")
384
385
  self._get_bool(cp, self._LINT_SECTION, "strict", "lint_strict")
@@ -555,6 +556,7 @@ class ConfigData:
555
556
  self.format_indent = 4
556
557
  self.format_leading_comma = False
557
558
  self.format_sql = True
559
+ self.format_rewrite_sql = False
558
560
  self.lint_select: str | None = None
559
561
  self.lint_ignore: str | None = None
560
562
  self.lint_strict = False
@@ -369,6 +369,9 @@
369
369
  #leading_comma=No
370
370
  # Reformat SQL with sqlglot (needs the [formatter] extra). No = metacommands only.
371
371
  #sql=Yes
372
+ # Also let sqlglot rewrite SQL text: x::int as CAST(x AS INT), BTRIM as TRIM,
373
+ # an alias with AS. No = a statement it would rewrite is kept as written.
374
+ #rewrite_sql=No
372
375
 
373
376
 
374
377
  [lint]
execsql/format.py CHANGED
@@ -16,6 +16,7 @@ import io
16
16
  import re
17
17
  from collections.abc import Iterator
18
18
  from pathlib import Path
19
+ from types import ModuleType
19
20
 
20
21
  __all__ = ["collect_paths", "format_file", "open_dollar_quote", "parse_keyword", "run_formatter"]
21
22
 
@@ -509,10 +510,47 @@ def _is_comment_line(line: str, in_block: bool) -> tuple[bool, bool]:
509
510
  return False, False
510
511
 
511
512
 
513
+ def _statement_sources(sqlglot: ModuleType, sql: str) -> list[str]:
514
+ """The text of each statement in *sql*, split at the semicolons sqlglot's tokenizer sees.
515
+
516
+ Comments before a statement belong to it; a semicolon inside a string or
517
+ a quoted name is not a split. A part holding no tokens (``;;``) is not a
518
+ statement, matching the ``None`` sqlglot's parser returns for it.
519
+ """
520
+ sources: list[str] = []
521
+ start = 0
522
+ has_tokens = False
523
+ for token in sqlglot.tokenize(sql, read="postgres"):
524
+ if token.token_type.name == "SEMICOLON":
525
+ if has_tokens:
526
+ sources.append(sql[start : token.start])
527
+ start, has_tokens = token.end + 1, False
528
+ else:
529
+ has_tokens = True
530
+ if has_tokens:
531
+ sources.append(sql[start:])
532
+ return sources
533
+
534
+
535
+ def _layout_tokens(sqlglot: ModuleType, sql: str) -> list[str]:
536
+ """*sql*'s tokens with everything the formatter may change taken out.
537
+
538
+ Whitespace and comments are not tokens. Keywords and unquoted names are
539
+ compared without case; strings and quoted names exactly.
540
+ """
541
+ return [
542
+ token.text
543
+ if token.token_type.name.endswith("STRING") or token.token_type.name == "IDENTIFIER"
544
+ else token.text.upper()
545
+ for token in sqlglot.tokenize(sql, read="postgres")
546
+ ]
547
+
548
+
512
549
  def _sqlglot_format(
513
550
  sql_lines: list[str],
514
551
  sql_indent: int = 4,
515
552
  leading_comma: bool = False, # noqa: ARG001 — leading-comma layout is applied as a textual post-pass on the assembled output; sqlglot's own `leading_comma=True` is non-idempotent under inline comments.
553
+ rewrite_sql: bool = False,
516
554
  ) -> list[str]:
517
555
  """Format a list of SQL-only lines (no comment-only lines) via sqlglot.
518
556
 
@@ -521,6 +559,12 @@ def _sqlglot_format(
521
559
  ``format_file``. sqlglot's own ``leading_comma=True`` reshuffles inline
522
560
  comments and is therefore non-idempotent on SQL with mid-statement
523
561
  comments, which is the dominant real-world case.
562
+
563
+ sqlglot also rewrites SQL into its preferred form (``x::int`` ->
564
+ ``CAST(x AS INT)``, ``BTRIM`` -> ``TRIM``, an alias gains ``AS``), and
565
+ which rewrites it makes changes between its releases (#72). Unless
566
+ *rewrite_sql*, a statement whose tokens would change beyond keyword case
567
+ is kept as written.
524
568
  """
525
569
  sqlglot = _require_sqlglot()
526
570
  import sqlglot.errors as sqlglot_errors
@@ -551,24 +595,28 @@ def _sqlglot_format(
551
595
  # understood the whole statement, so ask it: input that does not
552
596
  # parse is left exactly as written.
553
597
  ast = sqlglot.parse(protected, read="postgres", error_level=sqlglot_errors.ErrorLevel.RAISE)
598
+ nodes = [node for node in ast if node is not None]
599
+ sources = None if rewrite_sql else _statement_sources(sqlglot, protected)
600
+ if sources is not None and len(sources) != len(nodes):
601
+ return sql_lines
554
602
  statements: list[str] = []
555
- for node in ast:
556
- if node is None:
557
- continue
603
+ for i, node in enumerate(nodes):
558
604
  # For Command nodes (psql backslash commands, ERROR:, etc.)
559
605
  # use unpretty output to avoid mangling.
560
606
  if type(node).__name__ == "Command":
561
- statements.append(node.sql(dialect="postgres"))
607
+ generated = node.sql(dialect="postgres")
562
608
  else:
563
- statements.append(
564
- node.sql(
565
- dialect="postgres",
566
- pretty=True,
567
- pad=sql_indent,
568
- indent=sql_indent,
569
- max_text_width=120,
570
- ),
609
+ generated = node.sql(
610
+ dialect="postgres",
611
+ pretty=True,
612
+ pad=sql_indent,
613
+ indent=sql_indent,
614
+ max_text_width=120,
571
615
  )
616
+ generated = re.sub(r"\bINTO TEMPORARY\b(?!\s+TABLE)", "INTO TEMPORARY TABLE", generated)
617
+ if sources is not None and _layout_tokens(sqlglot, generated) != _layout_tokens(sqlglot, sources[i]):
618
+ generated = sources[i].lstrip("\n").rstrip() # layout only: keep it as written
619
+ statements.append(generated)
572
620
  stmts = [s for s in statements if s]
573
621
  if not stmts:
574
622
  return sql_lines
@@ -613,7 +661,6 @@ def _sqlglot_format(
613
661
  if set(_sql_literal_texts(protected)) - set(_sql_literal_texts(joined)):
614
662
  return sql_lines
615
663
 
616
- joined = re.sub(r"\bINTO TEMPORARY\b(?!\s+TABLE)", "INTO TEMPORARY TABLE", joined)
617
664
  restored = _restore_estrings(joined, estrings)
618
665
  if restored is None:
619
666
  return sql_lines
@@ -701,6 +748,7 @@ def _format_preserving_comments(
701
748
  lines: list[str],
702
749
  sql_indent: int = 4,
703
750
  leading_comma: bool = False,
751
+ rewrite_sql: bool = False,
704
752
  ) -> list[str]:
705
753
  """Format SQL with interleaved comments via marker-based round-tripping.
706
754
 
@@ -744,8 +792,12 @@ def _format_preserving_comments(
744
792
  pending_markers.append(mid)
745
793
  else:
746
794
  # SQL line — prepend any pending markers as inline comments
795
+ # After the line's indentation, not before it: a statement that
796
+ # comes back as written (unparsable, or kept by layout-only) has
797
+ # its indentation read from this line once the marker is removed.
747
798
  prefix = " ".join(f"/* {m} */" for m in pending_markers)
748
- processed.append(f"{prefix} {line}" if prefix else line)
799
+ body = line.lstrip()
800
+ processed.append(f"{line[: len(line) - len(body)]}{prefix} {body}" if prefix else line)
749
801
  for m in pending_markers:
750
802
  anchor_sql[m] = stripped
751
803
  anchor_line[m] = len(processed) - 1
@@ -760,7 +812,7 @@ def _format_preserving_comments(
760
812
  anchor_depth: dict[str, int] = {m: in_depths[i] for m, i in anchor_line.items()}
761
813
 
762
814
  # ---- Step 2: format through sqlglot ---------------------------------
763
- formatted = _sqlglot_format(processed, sql_indent=sql_indent, leading_comma=leading_comma)
815
+ formatted = _sqlglot_format(processed, sql_indent=sql_indent, leading_comma=leading_comma, rewrite_sql=rewrite_sql)
764
816
 
765
817
  # ---- Step 3: restore surviving markers to comment lines -------------
766
818
  found_markers: set[str] = set()
@@ -908,6 +960,7 @@ def format_sql_block(
908
960
  indent: int,
909
961
  use_sql: bool,
910
962
  leading_comma: bool = False,
963
+ rewrite_sql: bool = False,
911
964
  ) -> list[str]:
912
965
  """Re-indent a SQL block to the current depth, optionally formatting via sqlglot."""
913
966
  if not lines:
@@ -940,7 +993,12 @@ def format_sql_block(
940
993
  # which preserves both comments AND sqlglot formatting. When all
941
994
  # comments are between statements, the simpler segmentation works.
942
995
  if _has_mid_statement_comments(rebased):
943
- formatted_lines = _format_preserving_comments(rebased, sql_indent=indent, leading_comma=leading_comma)
996
+ formatted_lines = _format_preserving_comments(
997
+ rebased,
998
+ sql_indent=indent,
999
+ leading_comma=leading_comma,
1000
+ rewrite_sql=rewrite_sql,
1001
+ )
944
1002
  return [target_prefix + line if line.strip() else "" for line in formatted_lines]
945
1003
 
946
1004
  result: list[str] = []
@@ -954,7 +1012,7 @@ def format_sql_block(
954
1012
  if seg_is_comment:
955
1013
  result.extend(seg)
956
1014
  else:
957
- result.extend(_sqlglot_format(seg, sql_indent=indent, leading_comma=leading_comma))
1015
+ result.extend(_sqlglot_format(seg, sql_indent=indent, leading_comma=leading_comma, rewrite_sql=rewrite_sql))
958
1016
  seg.clear()
959
1017
 
960
1018
  for line in rebased:
@@ -1076,8 +1134,19 @@ def _apply_leading_comma(text: str) -> str:
1076
1134
  return "\n".join(lines)
1077
1135
 
1078
1136
 
1079
- def format_file(source: str, indent: int = 4, use_sql: bool = True, leading_comma: bool = False) -> str:
1080
- """Format the source text of an execsql script and return the result."""
1137
+ def format_file(
1138
+ source: str,
1139
+ indent: int = 4,
1140
+ use_sql: bool = True,
1141
+ leading_comma: bool = False,
1142
+ rewrite_sql: bool = False,
1143
+ ) -> str:
1144
+ """Format the source text of an execsql script and return the result.
1145
+
1146
+ SQL is laid out by sqlglot. A statement sqlglot would also rewrite
1147
+ (``x::int`` as ``CAST(x AS INT)``, ``BTRIM`` as ``TRIM``, an alias with
1148
+ ``AS``) is kept as written unless *rewrite_sql*.
1149
+ """
1081
1150
  # Normalize any leading-comma SQL in the source to trailing commas so
1082
1151
  # that sqlglot always sees the same comma shape regardless of how
1083
1152
  # the user saved the file. The post-pass at the bottom of this
@@ -1124,7 +1193,16 @@ def format_file(source: str, indent: int = 4, use_sql: bool = True, leading_comm
1124
1193
  # `sql_acc_contains_dollar_quote` catches the case where the region
1125
1194
  # already closed (`... $$;`) so `in_dollar_quote` is False at flush.
1126
1195
  safe_for_sqlglot = use_sql and not in_dollar_quote and not sql_acc_contains_dollar_quote
1127
- output.extend(format_sql_block(sql_acc, depth, indent, safe_for_sqlglot, leading_comma=leading_comma))
1196
+ output.extend(
1197
+ format_sql_block(
1198
+ sql_acc,
1199
+ depth,
1200
+ indent,
1201
+ safe_for_sqlglot,
1202
+ leading_comma=leading_comma,
1203
+ rewrite_sql=rewrite_sql,
1204
+ ),
1205
+ )
1128
1206
  sql_acc.clear()
1129
1207
  in_sql_statement = False
1130
1208
  sql_acc_contains_dollar_quote = False
@@ -1243,6 +1321,7 @@ def run_formatter(
1243
1321
  no_sql: bool = False,
1244
1322
  indent: int = 4,
1245
1323
  leading_comma: bool = False,
1324
+ rewrite_sql: bool = False,
1246
1325
  encoding: str = "utf-8",
1247
1326
  diff: bool = False,
1248
1327
  ) -> int:
@@ -1290,7 +1369,13 @@ def run_formatter(
1290
1369
  any_errors = True
1291
1370
  continue
1292
1371
 
1293
- formatted = format_file(source, indent=indent, use_sql=use_sql, leading_comma=leading_comma)
1372
+ formatted = format_file(
1373
+ source,
1374
+ indent=indent,
1375
+ use_sql=use_sql,
1376
+ leading_comma=leading_comma,
1377
+ rewrite_sql=rewrite_sql,
1378
+ )
1294
1379
 
1295
1380
  if diff:
1296
1381
  if formatted != source:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execsql2
3
- Version: 2.25.0
3
+ Version: 2.26.0
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
@@ -58,7 +58,7 @@ Requires-Dist: psycopg[binary]<4,>=3.1; extra == 'all'
58
58
  Requires-Dist: pymysql>=1.1; extra == 'all'
59
59
  Requires-Dist: pyodbc>=5.0; extra == 'all'
60
60
  Requires-Dist: pyyaml>=6.0; extra == 'all'
61
- Requires-Dist: sqlglot>=25.0; extra == 'all'
61
+ Requires-Dist: sqlglot<30.22,>=30.21.0; extra == 'all'
62
62
  Requires-Dist: tables>=3.10; extra == 'all'
63
63
  Requires-Dist: tkintermapview>=1.29; extra == 'all'
64
64
  Requires-Dist: xlrd>=2.0; extra == 'all'
@@ -92,7 +92,7 @@ Requires-Dist: pre-commit>=3.5.0; extra == 'dev'
92
92
  Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
93
93
  Requires-Dist: pyyaml>=6.0; extra == 'dev'
94
94
  Requires-Dist: ruff>=0.4; extra == 'dev'
95
- Requires-Dist: sqlglot>=25.0; extra == 'dev'
95
+ Requires-Dist: sqlglot<30.22,>=30.21.0; extra == 'dev'
96
96
  Requires-Dist: tables>=3.10; extra == 'dev'
97
97
  Requires-Dist: tox-uv>=1.13.1; extra == 'dev'
98
98
  Requires-Dist: twine>=6.1.0; extra == 'dev'
@@ -112,7 +112,7 @@ Requires-Dist: pyyaml>=6.0; extra == 'formats'
112
112
  Requires-Dist: tables>=3.10; extra == 'formats'
113
113
  Requires-Dist: xlrd>=2.0; extra == 'formats'
114
114
  Provides-Extra: formatter
115
- Requires-Dist: sqlglot>=25.0; extra == 'formatter'
115
+ Requires-Dist: sqlglot<30.22,>=30.21.0; extra == 'formatter'
116
116
  Provides-Extra: map
117
117
  Requires-Dist: tkintermapview>=1.29; extra == 'map'
118
118
  Provides-Extra: mssql
@@ -451,7 +451,7 @@ execsql format --no-sql --in-place scripts/
451
451
  ```yaml
452
452
  repos:
453
453
  - repo: https://github.com/geocoug/execsql
454
- rev: v2.25.0
454
+ rev: v2.26.0
455
455
  hooks:
456
456
  - id: execsql-format
457
457
  - id: execsql-lint
@@ -1,10 +1,10 @@
1
1
  execsql/__init__.py,sha256=ZeVnpEWu8M2bn3qLmVM72NhWRueUGNbsX8UAPTermo0,901
2
2
  execsql/__main__.py,sha256=HdbK-SAhyUmfB6xINY5AzxdMSxGzWSGEG_2dv42Jn64,315
3
3
  execsql/api.py,sha256=UJJSlFHySuINeoExaMXqhl00hVSPH1s7G4m8T0NX_xI,26484
4
- execsql/config.py,sha256=f9UMX5LECVL6KnvvTmIIAMrqOfYe_elQxjW1TOSt4fA,38040
4
+ execsql/config.py,sha256=GUKE3sLGExMMhMkmxi8eXXBWmN74E0vqnt1eTBPRsG4,38166
5
5
  execsql/config_validation.py,sha256=a4ZszyJFGTNgs1jkvXE3yMCaYFtF1Uqih2ojMDVh1v8,10556
6
6
  execsql/exceptions.py,sha256=aHavdblmP7m5ZLxyyKKmPSwwbdmP962j4ShP1Gd9r64,9651
7
- execsql/format.py,sha256=rpe-eB7V9J2z7Itr_MFtdUeU2-sR8F6B7a3vYmHXKd0,54253
7
+ execsql/format.py,sha256=_KNFGTnEihO0Bn-E8k0QNLijAS4NDE8Ee1ldL7_MKZc,57430
8
8
  execsql/interactive.py,sha256=7vm7NAlcarBJJOIffcLOcJqH9ZCInvHB_CmC2sQsXf8,8241
9
9
  execsql/manifest.py,sha256=ibceEZtH2FqsUiujbTGy5D3W0WgO4tFCs7bjIHVdep4,8062
10
10
  execsql/models.py,sha256=yXR9FMCSCRonfyiuWlYwqLBMLzWQcvFQOjGHogM8Veg,15556
@@ -19,18 +19,18 @@ execsql/cli/application.py,sha256=qKjkhDMWB7fEjEbqOLZ3pNFw80piBnRSeW_DfZg-9MA,12
19
19
  execsql/cli/dispatch.py,sha256=ew10mZNXsFHwiNd_etvpSQYD3T8CiexXkjC3Ja94C4w,2697
20
20
  execsql/cli/dsn.py,sha256=uBspf8DXCn2pXmT1Ug1c4IG7IgR5IFGKp9138puaubo,3289
21
21
  execsql/cli/help.py,sha256=Edr3d2ahKv387a6WGgyJi5vKIFe39tGPgTv1GKp3itE,10848
22
- execsql/cli/lint.py,sha256=7t4HXMqPCxHR6PoRxna0X3myu9hlxuK1rbPn5h2tX1Y,43390
22
+ execsql/cli/lint.py,sha256=ZW-FhAjLSuqVNndfP_LX0YfZNswUCkyFAvk3aNPjHpU,46256
23
23
  execsql/cli/options.py,sha256=Srhf1gEOB5yy_7PxkTKBv07xK8Qzw8H-z7PUUnItpwE,2916
24
24
  execsql/cli/run.py,sha256=5f0SX6QyNl8Ur2MwuW2GaN1OqVpWbEI8CQV3k-hBwH8,41987
25
25
  execsql/cli/commands/__init__.py,sha256=wN5sbhKs11S8EZOTBd_GPP6TPjhYfiQkrI48Sy8n6U0,521
26
26
  execsql/cli/commands/config.py,sha256=V7CGGkhSCqeJIlAXxAYyG_xNhnsORiwqth2XWEVPzss,8213
27
- execsql/cli/commands/format.py,sha256=2pftY7nl8FexJ5tnvncxRG-7UjQ_NO0mHxmqAOKd-Nw,3948
27
+ execsql/cli/commands/format.py,sha256=4BY2EUExt32DOv871SZW3kimK4x9qaUsElhu3B49yqc,4412
28
28
  execsql/cli/commands/init.py,sha256=kmuFgGz8sv_1KfiNh190E3A23evqkgYZeBRTU6vdjbs,5465
29
29
  execsql/cli/commands/lint.py,sha256=Lba5IDsXtiUbQ6DkhC9fdLnoSpfoaewoAWF-ei3h59s,8189
30
30
  execsql/cli/commands/listing.py,sha256=gWnjkgcw7uF-DJw3cMZbttOKdplDwAMlhmaBoI5drVw,2511
31
31
  execsql/cli/commands/run.py,sha256=sGFDku66CazJrorr0mmrhUI3QDvQVSMEYlU8G1Pd5fQ,16492
32
32
  execsql/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- execsql/data/execsql.conf.template,sha256=fTxA6Y01D-7BFABPgnJbEfPD5SSQ-l1xnaptMEBXuzc,12326
33
+ execsql/data/execsql.conf.template,sha256=pqh7G21ifYO0BjZoTgUhGyQAx4eKOmbIr6nToc1V7QA,12494
34
34
  execsql/data/script_header.sql,sha256=a7WKScekNgJoxVEmMRTkWv8NsAzW1jJ4vJfGy9HVy0g,349
35
35
  execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
36
36
  execsql/db/access.py,sha256=O19alGOCjhBtVFaCnGN8zwVZ2wR57h1spizAF5dhUOg,21207
@@ -118,23 +118,23 @@ execsql/utils/numeric.py,sha256=_7bi5R6pHjMCkOPne_Sh5nUj-B8e9g6WFmjptFHaunA,1573
118
118
  execsql/utils/regex.py,sha256=B3mSBhRRx_DpUnX_t9IoYd05KvWAebiLMCZqQAmEwcw,3885
119
119
  execsql/utils/strings.py,sha256=UQNjpRCEFa1UO6feU-M-9e24wWAvizs_iu_4fFusLxo,8516
120
120
  execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
121
- execsql2-2.25.0.data/data/execsql2_extras/README.md,sha256=7SG9FbBpdqkgQTTt1aBTbCgymBZyV0rLrQMQrendzxs,5032
122
- execsql2-2.25.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
123
- execsql2-2.25.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
124
- execsql2-2.25.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
125
- execsql2-2.25.0.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
126
- execsql2-2.25.0.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
127
- execsql2-2.25.0.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
128
- execsql2-2.25.0.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
129
- execsql2-2.25.0.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
130
- execsql2-2.25.0.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
131
- execsql2-2.25.0.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
132
- execsql2-2.25.0.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
133
- execsql2-2.25.0.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
134
- execsql2-2.25.0.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
135
- execsql2-2.25.0.dist-info/METADATA,sha256=8-U51LILehuLSC5E0zYP_tYCuJEtff0DkTsMSQ4RvHg,25703
136
- execsql2-2.25.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
137
- execsql2-2.25.0.dist-info/entry_points.txt,sha256=jOf53-4IVGjEZeNYUOF1GGaiPNfVZcNKxP3vDFKEyMA,58
138
- execsql2-2.25.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
139
- execsql2-2.25.0.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
140
- execsql2-2.25.0.dist-info/RECORD,,
121
+ execsql2-2.26.0.data/data/execsql2_extras/README.md,sha256=7SG9FbBpdqkgQTTt1aBTbCgymBZyV0rLrQMQrendzxs,5032
122
+ execsql2-2.26.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
123
+ execsql2-2.26.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
124
+ execsql2-2.26.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
125
+ execsql2-2.26.0.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
126
+ execsql2-2.26.0.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
127
+ execsql2-2.26.0.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
128
+ execsql2-2.26.0.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
129
+ execsql2-2.26.0.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
130
+ execsql2-2.26.0.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
131
+ execsql2-2.26.0.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
132
+ execsql2-2.26.0.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
133
+ execsql2-2.26.0.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
134
+ execsql2-2.26.0.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
135
+ execsql2-2.26.0.dist-info/METADATA,sha256=wku-RCrtWs60aSWHySUNMZlygyEnGKgw6IL6l2pRLj0,25733
136
+ execsql2-2.26.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
137
+ execsql2-2.26.0.dist-info/entry_points.txt,sha256=jOf53-4IVGjEZeNYUOF1GGaiPNfVZcNKxP3vDFKEyMA,58
138
+ execsql2-2.26.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
139
+ execsql2-2.26.0.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
140
+ execsql2-2.26.0.dist-info/RECORD,,