execsql2 2.19.2__py3-none-any.whl → 2.20.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 (30) hide show
  1. execsql/cli/run.py +28 -11
  2. execsql/db/access.py +5 -1
  3. execsql/db/base.py +8 -2
  4. execsql/db/dsn.py +3 -1
  5. execsql/db/firebird.py +10 -4
  6. execsql/db/mysql.py +6 -0
  7. execsql/db/sqlserver.py +4 -1
  8. execsql/format.py +172 -16
  9. execsql/utils/mail.py +7 -4
  10. {execsql2-2.19.2.data → execsql2-2.20.0.data}/data/execsql2_extras/README.md +1 -1
  11. {execsql2-2.19.2.dist-info → execsql2-2.20.0.dist-info}/METADATA +54 -56
  12. {execsql2-2.19.2.dist-info → execsql2-2.20.0.dist-info}/RECORD +29 -30
  13. execsql2-2.19.2.data/data/execsql2_extras/execsql.conf +0 -359
  14. {execsql2-2.19.2.data → execsql2-2.20.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  15. {execsql2-2.19.2.data → execsql2-2.20.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  16. {execsql2-2.19.2.data → execsql2-2.20.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  17. {execsql2-2.19.2.data → execsql2-2.20.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  18. {execsql2-2.19.2.data → execsql2-2.20.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  19. {execsql2-2.19.2.data → execsql2-2.20.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  20. {execsql2-2.19.2.data → execsql2-2.20.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  21. {execsql2-2.19.2.data → execsql2-2.20.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  22. {execsql2-2.19.2.data → execsql2-2.20.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  23. {execsql2-2.19.2.data → execsql2-2.20.0.data}/data/execsql2_extras/script_template.sql +0 -0
  24. {execsql2-2.19.2.data → execsql2-2.20.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  25. {execsql2-2.19.2.data → execsql2-2.20.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  26. {execsql2-2.19.2.data → execsql2-2.20.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  27. {execsql2-2.19.2.dist-info → execsql2-2.20.0.dist-info}/WHEEL +0 -0
  28. {execsql2-2.19.2.dist-info → execsql2-2.20.0.dist-info}/entry_points.txt +0 -0
  29. {execsql2-2.19.2.dist-info → execsql2-2.20.0.dist-info}/licenses/LICENSE.txt +0 -0
  30. {execsql2-2.19.2.dist-info → execsql2-2.20.0.dist-info}/licenses/NOTICE +0 -0
execsql/cli/run.py CHANGED
@@ -206,9 +206,29 @@ def _ping_db(db: Any) -> None:
206
206
  # ---------------------------------------------------------------------------
207
207
 
208
208
 
209
- # B12/F014: shared sensitive-name filter used by env-var seeding, -a
210
- # value logging, and any future credential-redaction sites.
211
- _SENSITIVE_SUBSTRINGS = ("SECRET", "TOKEN", "PASSWORD", "PASSWD", "PRIVATE_KEY", "CREDENTIAL")
209
+ # B12/F014: shared sensitive-name filter for env-var seeding and any
210
+ # other name-based credential-redaction sites. Case-insensitive substring
211
+ # matches: an env var whose name (uppercased) contains any of these is
212
+ # not seeded into the substitution pool. "_KEY" uses a leading underscore
213
+ # so it catches AWS_ACCESS_KEY_ID / SECRET_KEY without also blocking
214
+ # KEYBOARD-style names. Substring matching catches the common service-
215
+ # prefixed forms (STRIPE_KEY, OPENAI_API_KEY, SENTRY_DSN, SLACK_WEBHOOK);
216
+ # GitHub PAT-suffixed names (GITHUB_PAT) and URL-encoded DSNs
217
+ # (DATABASE_URL) are documented gaps — use a TOKEN- or SECRET-prefixed
218
+ # name when storing those.
219
+ _SENSITIVE_SUBSTRINGS = (
220
+ "SECRET",
221
+ "TOKEN",
222
+ "PASSWORD",
223
+ "PASSWD",
224
+ "PRIVATE_KEY",
225
+ "CREDENTIAL",
226
+ "_KEY",
227
+ "APIKEY",
228
+ "API_KEY",
229
+ "DSN",
230
+ "WEBHOOK",
231
+ )
212
232
 
213
233
 
214
234
  def _seed_early_subvars() -> SubVarSet:
@@ -517,15 +537,12 @@ def _setup_logging(
517
537
  for n, repl in enumerate(sub_vars):
518
538
  var = f"$ARG_{n + 1}"
519
539
  subvars.add_substitution(var, repl)
520
- # B12/F014: -a values are user input that may contain
521
- # secrets. Redact the value in the log line when the
522
- # surrounding -a payload looks sensitive (matches the
523
- # existing env-var filter at _seed_early_subvars).
524
- display_repl = repl
525
- if any(s in str(repl).upper() for s in _SENSITIVE_SUBSTRINGS):
526
- display_repl = "***"
540
+ # `-a` values are positional and opaque (no name to denylist
541
+ # against). High-entropy secrets (sk-live-*, AKIA*, ghp_*,
542
+ # JWTs) pass any substring/value heuristic, so log the
543
+ # assignment confirmation without the value.
527
544
  logger.log_status_info(
528
- f"Command-line substitution variable assignment: {var} set to {{{display_repl}}}",
545
+ f"Command-line substitution variable assignment: {var} set to {{***}}",
529
546
  )
530
547
 
531
548
  return logger
execsql/db/access.py CHANGED
@@ -107,7 +107,11 @@ class AccessDatabase(Database):
107
107
  else:
108
108
  connstr = cs % db_name
109
109
  try:
110
- self.conn = pyodbc.connect(connstr)
110
+ # Access is file-based but a UNC path on an
111
+ # unreachable share can still block forever; cap
112
+ # the connect attempt at 30 s to match the other
113
+ # adapters.
114
+ self.conn = pyodbc.connect(connstr, timeout=30)
111
115
  except Exception:
112
116
  if _state.exec_log is not None:
113
117
  _state.exec_log.log_status_info(
execsql/db/base.py CHANGED
@@ -239,8 +239,14 @@ class Database(ABC):
239
239
  if self.conn:
240
240
  try:
241
241
  self.conn.rollback()
242
- except Exception:
243
- pass # Best-effort; connection may already be closed.
242
+ except Exception as e:
243
+ # Best-effort; connection may already be closed. Still
244
+ # log so a cascading rollback failure isn't invisible
245
+ # in a CI / cron log.
246
+ if _state.exec_log is not None:
247
+ _state.exec_log.log_status_info(
248
+ f"Rollback failed on {self.__class__.__name__}: {e!r}",
249
+ )
244
250
 
245
251
  def needs_explicit_commit_after_ddl(self) -> bool:
246
252
  """Return True if this adapter's driver does NOT auto-commit DDL.
execsql/db/dsn.py CHANGED
@@ -90,7 +90,9 @@ class DsnDatabase(Database):
90
90
  parts.append(f"PWD={_odbc_quote(self.password)}")
91
91
  connstr = ";".join(parts) + ";"
92
92
  kwargs = {"autocommit": autocommit} if autocommit else {}
93
- self.conn = pyodbc.connect(connstr, **kwargs)
93
+ # 30 s connect timeout matches the Postgres adapter default
94
+ # so a wedged DSN peer cannot hang a script indefinitely.
95
+ self.conn = pyodbc.connect(connstr, timeout=30, **kwargs)
94
96
 
95
97
  def _try_connect():
96
98
  try:
execsql/db/firebird.py CHANGED
@@ -18,7 +18,7 @@ __all__ = ["FirebirdDatabase"]
18
18
 
19
19
 
20
20
  class FirebirdDatabase(Database):
21
- """Firebird adapter using the firebird-driver (fdb) package."""
21
+ """Firebird adapter using the firebird-driver package."""
22
22
 
23
23
  def __init__(
24
24
  self,
@@ -31,10 +31,11 @@ class FirebirdDatabase(Database):
31
31
  password: str | None = None,
32
32
  ) -> None:
33
33
  try:
34
- import fdb as firebird_lib # noqa: F401
34
+ from firebird import driver as firebird_lib # noqa: F401
35
35
  except Exception:
36
36
  fatal_error(
37
- "The fdb module is required to connect to Firebird. See https://pypi.python.org/pypi/fdb/",
37
+ "The firebird-driver module is required to connect to Firebird. "
38
+ "See https://pypi.org/project/firebird-driver/",
38
39
  )
39
40
  from execsql.types import dbt_firebird
40
41
 
@@ -67,7 +68,10 @@ class FirebirdDatabase(Database):
67
68
 
68
69
  def open_db(self) -> None:
69
70
  """Open a connection to the Firebird database."""
70
- import fdb as firebird_lib
71
+ from firebird import driver as firebird_lib
72
+
73
+ # 30 s connect timeout matches the Postgres adapter default.
74
+ connect_timeout = 30
71
75
 
72
76
  def db_conn():
73
77
  if self.user and self.password:
@@ -78,6 +82,7 @@ class FirebirdDatabase(Database):
78
82
  user=self.user,
79
83
  password=self.password,
80
84
  charset=self.encoding,
85
+ timeout=connect_timeout,
81
86
  )
82
87
  else:
83
88
  return firebird_lib.connect(
@@ -85,6 +90,7 @@ class FirebirdDatabase(Database):
85
90
  database=self.db_name,
86
91
  port=self.port,
87
92
  charset=self.encoding,
93
+ timeout=connect_timeout,
88
94
  )
89
95
 
90
96
  if self.conn is None:
execsql/db/mysql.py CHANGED
@@ -152,6 +152,10 @@ class MySQLDatabase(Database):
152
152
  """Open a connection to the MySQL or MariaDB server."""
153
153
  import pymysql as mysql_lib
154
154
 
155
+ # 30 s default matches the Postgres adapter so a wedged peer
156
+ # can't hang a script indefinitely.
157
+ connect_timeout = 30
158
+
155
159
  def db_conn():
156
160
  if self.user and self.password:
157
161
  return mysql_lib.connect(
@@ -162,6 +166,7 @@ class MySQLDatabase(Database):
162
166
  password=self.password,
163
167
  charset=self.encoding,
164
168
  local_infile=True,
169
+ connect_timeout=connect_timeout,
165
170
  )
166
171
  else:
167
172
  return mysql_lib.connect(
@@ -170,6 +175,7 @@ class MySQLDatabase(Database):
170
175
  port=self.port,
171
176
  charset=self.encoding,
172
177
  local_infile=True,
178
+ connect_timeout=connect_timeout,
173
179
  )
174
180
 
175
181
  if self.conn is None:
execsql/db/sqlserver.py CHANGED
@@ -113,7 +113,10 @@ class SqlServerDatabase(Database):
113
113
  f"DATABASE={self.db_name};Trusted_Connection=yes"
114
114
  )
115
115
  try:
116
- self.conn = pyodbc.connect(connstr)
116
+ # 30 s connect timeout matches the Postgres adapter
117
+ # default; pyodbc treats `timeout` as the login-and-
118
+ # query timeout on the connection.
119
+ self.conn = pyodbc.connect(connstr, timeout=30)
117
120
  except Exception:
118
121
  if _state.exec_log is not None:
119
122
  _state.exec_log.log_status_info(
execsql/format.py CHANGED
@@ -45,6 +45,13 @@ METACOMMAND_RE = re.compile(r"^\s*--\s*!x!\s*(.*)", re.IGNORECASE)
45
45
 
46
46
  # Multi-word keywords — checked longest-first before single-word fallback.
47
47
  # Order matters: more-specific variants must precede their prefixes.
48
+ # Only entries that appear at the *start* of a `-- !x!` payload belong
49
+ # here — `parse_keyword()` matches against the beginning of the payload,
50
+ # not embedded sub-clauses. `IN ZIPFILE` / `WITH TEMPLATE` are EXPORT
51
+ # sub-clauses and never appear at the start, so they don't go here.
52
+ # When adding a new dispatch keyword, mirror it here (or fix the missing
53
+ # entry via the `tests/test_format.py` drift check that pulls names from
54
+ # the dispatch table).
48
55
  MULTIWORD_KEYWORDS = [
49
56
  "METACOMMAND_ERROR_HALT",
50
57
  "ON ERROR_HALT",
@@ -77,18 +84,31 @@ MULTIWORD_KEYWORDS = [
77
84
  "SUB_INI",
78
85
  "PROMPT ENTRY_FORM",
79
86
  "PROMPT SELECT_SUB",
87
+ "PROMPT SELECT_ROWS",
80
88
  "PROMPT ENTER_SUB",
81
89
  "PROMPT DIRECTORY",
90
+ "PROMPT CREDENTIALS",
82
91
  "PROMPT CONNECT",
83
92
  "PROMPT COMPARE",
84
93
  "PROMPT MESSAGE",
85
94
  "PROMPT DISPLAY",
86
95
  "PROMPT ACTION",
96
+ "PROMPT OPENFILE",
97
+ "PROMPT SAVEFILE",
87
98
  "PROMPT PAUSE",
88
- "PROMPT FILE",
99
+ # PROMPT ASK COMPARE must precede PROMPT ASK so longest-match wins;
100
+ # parse_keyword iterates in list order, not by length.
101
+ "PROMPT ASK COMPARE",
89
102
  "PROMPT ASK",
90
- "WITH TEMPLATE",
91
- "IN ZIPFILE",
103
+ "PROMPT MAP",
104
+ "APPEND SCRIPT",
105
+ "PG_UPSERT CHECK",
106
+ "PG_UPSERT QA",
107
+ "RESET COUNTER",
108
+ "RESET DIALOG_CANCELED",
109
+ "SET COUNTER",
110
+ "WRITE CREATE_TABLE",
111
+ "WRITE SCRIPT",
92
112
  "SHOW SCRIPTS",
93
113
  ]
94
114
 
@@ -98,9 +118,18 @@ BLOCK_CLOSE = frozenset({"ENDIF", "END LOOP", "ENDLOOP", "END SCRIPT", "END BATC
98
118
  PIVOT = frozenset({"ELSE", "ELSEIF"}) # decrease depth before emit, increase after
99
119
  CONTINUATION = frozenset({"ANDIF", "ORIF"}) # emit at depth-1, no depth change
100
120
 
101
- # Inline IF: "IF (cond) { command }" — self-contained, no ENDIF, no depth change.
102
- # Mirrors src/execsql/cli/lint.py:_RX_IF_INLINE so formatter and linter agree.
121
+ # Inline IF: "IF (cond) { command }" — self-contained, no ENDIF, no depth
122
+ # change. Pattern must accept the same payloads as
123
+ # src/execsql/script/parser.py:_IF_INLINE_RX. Kept as a separate compiled
124
+ # pattern (not an import) so execsql-format doesn't pull in the AST parser
125
+ # module graph at startup; tests/test_format.py has a drift check that
126
+ # asserts both regexes recognise the same inputs.
103
127
  _IF_INLINE_RE = re.compile(r"^\s*IF\s*\(\s*.+\s*\)\s*\{.+\}\s*$", re.I)
128
+
129
+ # Matches both untagged `$$` and tagged `$tag$` dollar-quote markers
130
+ # (PostgreSQL PL/pgSQL / DO-block syntax). Tags are letter-or-underscore
131
+ # followed by word characters; the empty tag (just `$$`) is also valid.
132
+ _DOLLAR_QUOTE_RE = re.compile(r"\$([A-Za-z_][A-Za-z0-9_]*)?\$")
104
133
  # BLOCK_OPEN keywords whose bodies are guaranteed-SQL (not metacommand-driven).
105
134
  # Blank lines inside these belong to the SQL accumulator, not the output stream.
106
135
  _SQL_BODY_BLOCKS = frozenset({"BEGIN SQL", "BEGIN BATCH"})
@@ -180,9 +209,16 @@ def _is_comment_line(line: str, in_block: bool) -> tuple[bool, bool]:
180
209
  def _sqlglot_format(
181
210
  sql_lines: list[str],
182
211
  sql_indent: int = 4,
183
- leading_comma: bool = False,
212
+ 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.
184
213
  ) -> list[str]:
185
- """Format a list of SQL-only lines (no comment-only lines) via sqlglot."""
214
+ """Format a list of SQL-only lines (no comment-only lines) via sqlglot.
215
+
216
+ Always emits trailing-comma style; if the caller wants leading commas
217
+ they are produced by ``_apply_leading_comma`` at the end of
218
+ ``format_file``. sqlglot's own ``leading_comma=True`` reshuffles inline
219
+ comments and is therefore non-idempotent on SQL with mid-statement
220
+ comments, which is the dominant real-world case.
221
+ """
186
222
  sqlglot = _require_sqlglot()
187
223
  import sqlglot.errors as sqlglot_errors
188
224
 
@@ -211,7 +247,6 @@ def _sqlglot_format(
211
247
  pad=sql_indent,
212
248
  indent=sql_indent,
213
249
  max_text_width=120,
214
- leading_comma=leading_comma,
215
250
  ),
216
251
  )
217
252
  stmts = [s for s in statements if s]
@@ -508,13 +543,105 @@ def format_metacommand(payload: str, depth: int, indent: int) -> str:
508
543
  return f"{prefix}-- !x! {keyword}"
509
544
 
510
545
 
546
+ def _is_comment_only(s: str) -> bool:
547
+ """Strict comment classifier for the leading-comma post/pre passes."""
548
+ st = s.strip()
549
+ return st.startswith("--") or st.startswith("/*") or st.startswith("*/")
550
+
551
+
552
+ def _normalize_to_trailing_comma(text: str) -> str:
553
+ """Rewrite leading-comma SQL (`, foo`) back to trailing-comma style.
554
+
555
+ Symmetric inverse of ``_apply_leading_comma``. Used as a pre-pass so
556
+ that sqlglot — which migrates inline ``/* marker */`` comments under
557
+ leading-comma input — sees a consistent trailing-comma shape on
558
+ every invocation. Comments between the two SQL lines stay in place.
559
+ """
560
+ lines = text.split("\n")
561
+
562
+ def find_prev_sql_line(idx: int) -> int:
563
+ k = idx - 1
564
+ while k >= 0 and (not lines[k].strip() or _is_comment_only(lines[k])):
565
+ k -= 1
566
+ return k
567
+
568
+ for i, line in enumerate(lines):
569
+ stripped = line.lstrip()
570
+ if not stripped.startswith(",") or _is_comment_only(line):
571
+ continue
572
+ # Don't try to rewrite leading-comma inside a comment-only line.
573
+ # Move the comma onto the previous SQL line as a trailing `,`.
574
+ prev = find_prev_sql_line(i)
575
+ if prev < 0:
576
+ continue
577
+ # Drop the `, ` (or `,`) at the start of this line.
578
+ indent_len = len(line) - len(stripped)
579
+ rest = stripped[1:].lstrip()
580
+ lines[i] = line[:indent_len] + rest
581
+ # Append `,` to the prev SQL line (preserving any existing right-side
582
+ # trailing whitespace — there shouldn't be any after format_file).
583
+ prev_rstripped = lines[prev].rstrip()
584
+ if not prev_rstripped.endswith(","):
585
+ lines[prev] = prev_rstripped + ","
586
+ return "\n".join(lines)
587
+
588
+
589
+ def _apply_leading_comma(text: str) -> str:
590
+ """Rewrite trailing-comma SQL to leading-comma style as a textual pass.
591
+
592
+ Walks the assembled output line-by-line. For every line that ends with
593
+ ``,`` (and is not a comment), strip the comma and prepend ``, `` to the
594
+ next non-blank, non-comment line — preserving that target line's
595
+ indent. Comments between the two SQL lines stay in place. The
596
+ transformation is idempotent: rerunning it on its own output is a
597
+ no-op (the source line no longer ends with ``,``).
598
+
599
+ This decouples our user-facing ``--leading-comma`` flag from
600
+ sqlglot's own ``leading_comma=True`` mode, which is non-idempotent
601
+ when inline ``/* marker */`` comments are present — sqlglot moves
602
+ the markers around between passes (verified in tests/test_format.py
603
+ TestIdempotency).
604
+ """
605
+ lines = text.split("\n")
606
+ i = 0
607
+ n = len(lines)
608
+ while i < n:
609
+ rstripped = lines[i].rstrip()
610
+ if rstripped.endswith(",") and not _is_comment_only(lines[i]):
611
+ j = i + 1
612
+ while j < n and (not lines[j].strip() or _is_comment_only(lines[j])):
613
+ j += 1
614
+ if j < n:
615
+ stripped = lines[i].rstrip()
616
+ comma_idx = stripped.rfind(",")
617
+ lines[i] = stripped[:comma_idx] + stripped[comma_idx + 1 :]
618
+ target = lines[j]
619
+ indent_len = len(target) - len(target.lstrip())
620
+ lines[j] = target[:indent_len] + ", " + target[indent_len:]
621
+ i += 1
622
+ return "\n".join(lines)
623
+
624
+
511
625
  def format_file(source: str, indent: int = 4, use_sql: bool = True, leading_comma: bool = False) -> str:
512
626
  """Format the source text of an execsql script and return the result."""
627
+ # Normalize any leading-comma SQL in the source to trailing commas so
628
+ # that sqlglot always sees the same comma shape regardless of how
629
+ # the user saved the file. The post-pass at the bottom of this
630
+ # function re-applies leading commas when the caller asked for them.
631
+ # This is what makes leading_comma=True idempotent under inline
632
+ # comments — sqlglot itself migrates `/* marker */` comments when
633
+ # parsing leading-comma input.
634
+ source = _normalize_to_trailing_comma(source)
635
+
513
636
  depth = 0
514
637
  sql_acc: list[str] = []
515
638
  output: list[str] = []
516
639
 
517
640
  in_dollar_quote = False
641
+ # When in_dollar_quote, the tag string we are inside ("" for `$$`,
642
+ # "body" for `$body$`, etc.). Nested markers with a different tag
643
+ # are ignored — only a matching close marker re-opens us.
644
+ current_dq_tag: str | None = None
518
645
  in_block_comment = False
519
646
  # Track whether we are inside an open SQL statement (last SQL line
520
647
  # did not end with ';'). Blank lines mid-statement should NOT flush
@@ -527,7 +654,7 @@ def format_file(source: str, indent: int = 4, use_sql: bool = True, leading_comm
527
654
  in_explicit_sql_block = False
528
655
 
529
656
  def flush_sql() -> None:
530
- nonlocal in_dollar_quote, in_sql_statement
657
+ nonlocal in_dollar_quote, current_dq_tag, in_sql_statement
531
658
  if sql_acc:
532
659
  # If any line in the accumulated block is inside a $$-delimited
533
660
  # region, skip sqlglot formatting entirely. PL/pgSQL function
@@ -596,9 +723,19 @@ def format_file(source: str, indent: int = 4, use_sql: bool = True, leading_comm
596
723
  output.append(format_metacommand(payload, depth, indent))
597
724
 
598
725
  else:
599
- # Track $$ boundaries to prevent sqlglot from mangling PL/pgSQL
600
- if "$$" in raw_line and raw_line.count("$$") % 2 == 1:
601
- in_dollar_quote = not in_dollar_quote
726
+ # Track $$ and $tag$ boundaries to prevent sqlglot from mangling
727
+ # PL/pgSQL. Walk every dollar-quote marker on the line; toggle
728
+ # state only when we hit the matching open or close.
729
+ for m in _DOLLAR_QUOTE_RE.finditer(raw_line):
730
+ tag = m.group(1) or ""
731
+ if not in_dollar_quote:
732
+ in_dollar_quote = True
733
+ current_dq_tag = tag
734
+ elif tag == current_dq_tag:
735
+ in_dollar_quote = False
736
+ current_dq_tag = None
737
+ # else: tag mismatch — a foreign-tagged marker inside our
738
+ # quoted region; ignore (PG would treat it as literal text).
602
739
  sql_acc.append(raw_line)
603
740
  # Update statement tracking: if this SQL line ends with ';'
604
741
  # (and isn't a comment), the statement is complete.
@@ -610,6 +747,8 @@ def format_file(source: str, indent: int = 4, use_sql: bool = True, leading_comm
610
747
  flush_sql()
611
748
 
612
749
  result = "\n".join(output)
750
+ if leading_comma:
751
+ result = _apply_leading_comma(result)
613
752
  if not result.endswith("\n"):
614
753
  result += "\n"
615
754
  return result
@@ -665,6 +804,12 @@ def main() -> None:
665
804
  "--leading-comma",
666
805
  help="Place commas at the start of lines instead of the end.",
667
806
  ),
807
+ encoding: str = typer.Option(
808
+ "utf-8",
809
+ "--encoding",
810
+ metavar="NAME",
811
+ help="Text encoding used to read and write SQL files (default utf-8).",
812
+ ),
668
813
  ) -> None:
669
814
  use_sql = not no_sql
670
815
  paths = collect_paths(targets)
@@ -673,12 +818,23 @@ def main() -> None:
673
818
  raise typer.Exit(code=1)
674
819
 
675
820
  any_changed = False
821
+ any_errors = False
676
822
  for path in paths:
677
823
  try:
678
- source = path.read_text(encoding="utf-8")
824
+ source = path.read_text(encoding=encoding)
679
825
  except OSError as exc:
680
826
  _err_console.print(f"[bold red]Error:[/bold red] reading {path}: {exc}")
681
- raise typer.Exit(code=1) from None
827
+ any_errors = True
828
+ # Collect read errors instead of short-circuiting so a single
829
+ # unreadable file doesn't hide the rest of the report.
830
+ continue
831
+ except UnicodeDecodeError as exc:
832
+ _err_console.print(
833
+ f"[bold red]Error:[/bold red] decoding {path} as {encoding}: {exc}. "
834
+ f"Try [bold]--encoding cp1252[/bold] or another text encoding.",
835
+ )
836
+ any_errors = True
837
+ continue
682
838
 
683
839
  formatted = format_file(source, indent=indent, use_sql=use_sql, leading_comma=leading_comma)
684
840
 
@@ -688,12 +844,12 @@ def main() -> None:
688
844
  any_changed = True
689
845
  elif in_place:
690
846
  if formatted != source:
691
- path.write_text(formatted, encoding="utf-8")
847
+ path.write_text(formatted, encoding=encoding)
692
848
  _console.print(f"reformatted {path}")
693
849
  else:
694
850
  sys.stdout.write(formatted)
695
851
 
696
- if check and any_changed:
852
+ if any_errors or (check and any_changed):
697
853
  raise typer.Exit(code=1)
698
854
 
699
855
  app()
execsql/utils/mail.py CHANGED
@@ -51,16 +51,19 @@ class Mailer:
51
51
  conf = _state.conf
52
52
  if conf.smtp_host is None:
53
53
  raise ErrInfo(type="error", other_msg="Can't send email; the email host is not configured.")
54
+ # 30 s connect/read timeout matches the DB-adapter default so a
55
+ # silently-dropped SMTP peer can't hang a script (or a CI run).
56
+ smtp_timeout = 30
54
57
  if conf.smtp_port is None:
55
58
  if conf.smtp_ssl:
56
- self.smtpconn = smtplib.SMTP_SSL(conf.smtp_host)
59
+ self.smtpconn = smtplib.SMTP_SSL(conf.smtp_host, timeout=smtp_timeout)
57
60
  else:
58
- self.smtpconn = smtplib.SMTP(conf.smtp_host)
61
+ self.smtpconn = smtplib.SMTP(conf.smtp_host, timeout=smtp_timeout)
59
62
  else:
60
63
  if conf.smtp_ssl:
61
- self.smtpconn = smtplib.SMTP_SSL(conf.smtp_host, conf.smtp_port)
64
+ self.smtpconn = smtplib.SMTP_SSL(conf.smtp_host, conf.smtp_port, timeout=smtp_timeout)
62
65
  else:
63
- self.smtpconn = smtplib.SMTP(conf.smtp_host, conf.smtp_port)
66
+ self.smtpconn = smtplib.SMTP(conf.smtp_host, conf.smtp_port, timeout=smtp_timeout)
64
67
  self.smtpconn.ehlo_or_hello_if_needed()
65
68
  if conf.smtp_tls:
66
69
  self.smtpconn.starttls()
@@ -2,7 +2,7 @@
2
2
 
3
3
  Several types of templates are provided that may be useful in conjunction with execsql. These are:
4
4
 
5
- - **execsql.conf** — An annotated version of the configuration file that includes all configuration settings and notes on their usage.
5
+ - **execsql.conf** — An annotated reference of every configuration setting with notes on its usage. Generate a fresh copy in any directory with `execsql --init-config > execsql.conf`; the canonical content ships inside the installed package and is loaded via `importlib.resources` (no need to find or copy a source file).
6
6
 
7
7
  - **script_template.sql** — A framework for SQL scripts that make use of several execsql features. It includes sections for custom configuration settings, custom logfile creation, and reporting of unexpected script exits (through user cancellation or errors).
8
8
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.19.2
3
+ Version: 2.20.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
@@ -44,88 +44,87 @@ Requires-Dist: rich>=13.0
44
44
  Requires-Dist: textual>=1.0
45
45
  Requires-Dist: typer>=0.12
46
46
  Provides-Extra: all
47
- Requires-Dist: defusedxml; extra == 'all'
48
- Requires-Dist: duckdb; extra == 'all'
49
- Requires-Dist: firebird-driver; extra == 'all'
50
- Requires-Dist: jinja2; extra == 'all'
51
- Requires-Dist: keyring; extra == 'all'
52
- Requires-Dist: odfpy; extra == 'all'
53
- Requires-Dist: openpyxl; extra == 'all'
54
- Requires-Dist: oracledb; extra == 'all'
47
+ Requires-Dist: defusedxml>=0.7; extra == 'all'
48
+ Requires-Dist: duckdb>=1.0; extra == 'all'
49
+ Requires-Dist: firebird-driver>=1.10; extra == 'all'
50
+ Requires-Dist: jinja2>=3.1; extra == 'all'
51
+ Requires-Dist: keyring>=25.0; extra == 'all'
52
+ Requires-Dist: odfpy>=1.4; extra == 'all'
53
+ Requires-Dist: openpyxl>=3.1; extra == 'all'
54
+ Requires-Dist: oracledb>=3.0; extra == 'all'
55
55
  Requires-Dist: pg-upsert>=1.22.1; extra == 'all'
56
- Requires-Dist: polars; extra == 'all'
57
- Requires-Dist: psycopg2-binary; extra == 'all'
58
- Requires-Dist: pymysql; extra == 'all'
59
- Requires-Dist: pyodbc; extra == 'all'
60
- Requires-Dist: pyyaml; extra == 'all'
56
+ Requires-Dist: polars>=1.0; extra == 'all'
57
+ Requires-Dist: psycopg2-binary>=2.9; extra == 'all'
58
+ Requires-Dist: pymysql>=1.1; extra == 'all'
59
+ Requires-Dist: pyodbc>=5.0; extra == 'all'
60
+ Requires-Dist: pyyaml>=6.0; extra == 'all'
61
61
  Requires-Dist: sqlglot>=25.0; extra == 'all'
62
- Requires-Dist: tables; extra == 'all'
62
+ Requires-Dist: tables>=3.10; extra == 'all'
63
63
  Requires-Dist: tkintermapview>=1.29; extra == 'all'
64
- Requires-Dist: xlrd; extra == 'all'
64
+ Requires-Dist: xlrd>=2.0; extra == 'all'
65
65
  Provides-Extra: all-db
66
- Requires-Dist: duckdb; extra == 'all-db'
67
- Requires-Dist: firebird-driver; extra == 'all-db'
68
- Requires-Dist: oracledb; extra == 'all-db'
69
- Requires-Dist: psycopg2-binary; extra == 'all-db'
70
- Requires-Dist: pymysql; extra == 'all-db'
71
- Requires-Dist: pyodbc; extra == 'all-db'
66
+ Requires-Dist: duckdb>=1.0; extra == 'all-db'
67
+ Requires-Dist: firebird-driver>=1.10; extra == 'all-db'
68
+ Requires-Dist: oracledb>=3.0; extra == 'all-db'
69
+ Requires-Dist: psycopg2-binary>=2.9; extra == 'all-db'
70
+ Requires-Dist: pymysql>=1.1; extra == 'all-db'
71
+ Requires-Dist: pyodbc>=5.0; extra == 'all-db'
72
72
  Provides-Extra: auth
73
- Requires-Dist: keyring; extra == 'auth'
73
+ Requires-Dist: keyring>=25.0; extra == 'auth'
74
74
  Provides-Extra: auth-encrypted
75
- Requires-Dist: keyring; extra == 'auth-encrypted'
76
- Requires-Dist: keyrings-alt; extra == 'auth-encrypted'
77
- Requires-Dist: pycryptodome; extra == 'auth-encrypted'
75
+ Requires-Dist: keyring>=25.0; extra == 'auth-encrypted'
76
+ Requires-Dist: keyrings-alt>=5.0; extra == 'auth-encrypted'
77
+ Requires-Dist: pycryptodome>=3.20; extra == 'auth-encrypted'
78
78
  Provides-Extra: auth-plaintext
79
- Requires-Dist: keyring; extra == 'auth-plaintext'
80
- Requires-Dist: keyrings-alt; extra == 'auth-plaintext'
79
+ Requires-Dist: keyring>=25.0; extra == 'auth-plaintext'
80
+ Requires-Dist: keyrings-alt>=5.0; extra == 'auth-plaintext'
81
81
  Provides-Extra: dev
82
82
  Requires-Dist: build>=1.2.2.post1; extra == 'dev'
83
83
  Requires-Dist: bump-my-version>=1.2.7; extra == 'dev'
84
- Requires-Dist: defusedxml; extra == 'dev'
85
- Requires-Dist: jinja2; extra == 'dev'
86
- Requires-Dist: markdown-include>=0.8; extra == 'dev'
84
+ Requires-Dist: defusedxml>=0.7; extra == 'dev'
85
+ Requires-Dist: jinja2>=3.1; extra == 'dev'
87
86
  Requires-Dist: mkdocstrings-python>=2.0.3; extra == 'dev'
88
87
  Requires-Dist: mypy>=1.10; extra == 'dev'
89
- Requires-Dist: odfpy; extra == 'dev'
90
- Requires-Dist: openpyxl; extra == 'dev'
91
- Requires-Dist: polars; extra == 'dev'
88
+ Requires-Dist: odfpy>=1.4; extra == 'dev'
89
+ Requires-Dist: openpyxl>=3.1; extra == 'dev'
90
+ Requires-Dist: polars>=1.0; extra == 'dev'
92
91
  Requires-Dist: pre-commit>=3.5.0; extra == 'dev'
93
92
  Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
94
- Requires-Dist: pyyaml; extra == 'dev'
93
+ Requires-Dist: pyyaml>=6.0; extra == 'dev'
95
94
  Requires-Dist: ruff>=0.4; extra == 'dev'
96
95
  Requires-Dist: sqlglot>=25.0; extra == 'dev'
97
- Requires-Dist: tables; extra == 'dev'
96
+ Requires-Dist: tables>=3.10; extra == 'dev'
98
97
  Requires-Dist: tox-uv>=1.13.1; extra == 'dev'
99
98
  Requires-Dist: twine>=6.1.0; extra == 'dev'
100
- Requires-Dist: xlrd; extra == 'dev'
101
- Requires-Dist: zensical>=0.0.28; extra == 'dev'
99
+ Requires-Dist: xlrd>=2.0; extra == 'dev'
100
+ Requires-Dist: zensical==0.0.28; extra == 'dev'
102
101
  Provides-Extra: duckdb
103
- Requires-Dist: duckdb; extra == 'duckdb'
102
+ Requires-Dist: duckdb>=1.0; extra == 'duckdb'
104
103
  Provides-Extra: firebird
105
- Requires-Dist: firebird-driver; extra == 'firebird'
104
+ Requires-Dist: firebird-driver>=1.10; extra == 'firebird'
106
105
  Provides-Extra: formats
107
- Requires-Dist: defusedxml; extra == 'formats'
108
- Requires-Dist: jinja2; extra == 'formats'
109
- Requires-Dist: odfpy; extra == 'formats'
110
- Requires-Dist: openpyxl; extra == 'formats'
111
- Requires-Dist: polars; extra == 'formats'
112
- Requires-Dist: pyyaml; extra == 'formats'
113
- Requires-Dist: tables; extra == 'formats'
114
- Requires-Dist: xlrd; extra == 'formats'
106
+ Requires-Dist: defusedxml>=0.7; extra == 'formats'
107
+ Requires-Dist: jinja2>=3.1; extra == 'formats'
108
+ Requires-Dist: odfpy>=1.4; extra == 'formats'
109
+ Requires-Dist: openpyxl>=3.1; extra == 'formats'
110
+ Requires-Dist: polars>=1.0; extra == 'formats'
111
+ Requires-Dist: pyyaml>=6.0; extra == 'formats'
112
+ Requires-Dist: tables>=3.10; extra == 'formats'
113
+ Requires-Dist: xlrd>=2.0; extra == 'formats'
115
114
  Provides-Extra: formatter
116
115
  Requires-Dist: sqlglot>=25.0; extra == 'formatter'
117
116
  Provides-Extra: map
118
117
  Requires-Dist: tkintermapview>=1.29; extra == 'map'
119
118
  Provides-Extra: mssql
120
- Requires-Dist: pyodbc; extra == 'mssql'
119
+ Requires-Dist: pyodbc>=5.0; extra == 'mssql'
121
120
  Provides-Extra: mysql
122
- Requires-Dist: pymysql; extra == 'mysql'
121
+ Requires-Dist: pymysql>=1.1; extra == 'mysql'
123
122
  Provides-Extra: odbc
124
- Requires-Dist: pyodbc; extra == 'odbc'
123
+ Requires-Dist: pyodbc>=5.0; extra == 'odbc'
125
124
  Provides-Extra: oracle
126
- Requires-Dist: oracledb; extra == 'oracle'
125
+ Requires-Dist: oracledb>=3.0; extra == 'oracle'
127
126
  Provides-Extra: postgres
128
- Requires-Dist: psycopg2-binary; extra == 'postgres'
127
+ Requires-Dist: psycopg2-binary>=2.9; extra == 'postgres'
129
128
  Provides-Extra: upsert
130
129
  Requires-Dist: pg-upsert>=1.22.1; extra == 'upsert'
131
130
  Description-Content-Type: text/markdown
@@ -404,13 +403,12 @@ execsql-format --no-sql --in-place scripts/
404
403
  ```yaml
405
404
  repos:
406
405
  - repo: https://github.com/geocoug/execsql
407
- rev: v2.18.0
406
+ rev: v2.20.0
408
407
  hooks:
409
408
  - id: execsql-format
410
- args: [--in-place]
411
409
  ```
412
410
 
413
- See the [formatter documentation](https://execsql2.readthedocs.io/en/latest/guides/formatter/) for all options.
411
+ The hook rewrites `*.sql` files in place by default. See the [formatter documentation](https://execsql2.readthedocs.io/en/latest/guides/formatter/) for `--check`, `--indent`, and other options.
414
412
 
415
413
  # VS Code Syntax Highlighting
416
414
 
@@ -3,7 +3,7 @@ execsql/__main__.py,sha256=HdbK-SAhyUmfB6xINY5AzxdMSxGzWSGEG_2dv42Jn64,315
3
3
  execsql/api.py,sha256=ZFTo_XZPhG21w2vxaeS1lS6o5XmF1FUJRIaypgTOjA8,20919
4
4
  execsql/config.py,sha256=OOrCcn9m3CNuXkxVOLp7uMhQikzUS2wh_QVhvIzRqIM,33296
5
5
  execsql/exceptions.py,sha256=EkM5cw2s0D9QCOgS4BU29FEyOnEtCxJ0esPT6l1hT9s,9205
6
- execsql/format.py,sha256=3EXAnuFYPp595jRLjoou3ATc4WSbYwPhrDmyzjTLMoE,26416
6
+ execsql/format.py,sha256=dC2cHGUldOTq1YRENm7vtIIv4vtUswRhxuqeMp99d70,33648
7
7
  execsql/models.py,sha256=kCTUQg9-vReM6WNFfB_ZrEppuOW5u1uMBQThSkfPC0o,13264
8
8
  execsql/parser.py,sha256=P3ea8k7T_XLMrbhpFNZXwytdShrY302MKnhosqza1lo,15493
9
9
  execsql/plugins.py,sha256=2voLwT6eFap6BCBoZYndNNC_bMEJO1f_aP6xQTVXwYI,12815
@@ -14,21 +14,21 @@ execsql/cli/__init__.py,sha256=aJknKKIGxYCXpny0cyHXfqJsJ95dBtlEXXhPASFG8GQ,23114
14
14
  execsql/cli/dsn.py,sha256=svaZtrUXFRL2W5G6FRRiKtR6kehOp7urrVhIx_642Z8,2820
15
15
  execsql/cli/help.py,sha256=ThwdZuMIfLPxLAPpGWwXFY_UfyWvYOCjdlBNK20Vzd8,5718
16
16
  execsql/cli/lint.py,sha256=YqKzFNUhyb_Th69hYgKk1ZZVjCsZfJMIiUGqp06JwNs,17236
17
- execsql/cli/run.py,sha256=4plvi8ZaGea5fUeOhtS2f4MrVnGTaetvJyz7qX07s0I,37706
17
+ execsql/cli/run.py,sha256=0gmn7f7RjSYatOUZ34QIzK8gP_vgQFZE6U4P6EKU2H4,38188
18
18
  execsql/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  execsql/data/execsql.conf.template,sha256=Sq1Huwb_Uf_lI7zW7m3h11fqU6zjGpGnCU4OVJ_tCe0,10696
20
20
  execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
21
- execsql/db/access.py,sha256=GcY5Pq_vXdp8Thzm6aSLg9vfp5koovGiYKXipQtRt50,19167
22
- execsql/db/base.py,sha256=UUiYSztUen8xCWVCxDkRQeD098Yv1iA3WPl_qo4XjDI,34686
23
- execsql/db/dsn.py,sha256=59OzMAuCIfHcdOZNarK9TlDzaBJjhZ1SFFMvyXlH6u8,6086
21
+ execsql/db/access.py,sha256=sRJOJJo1psSO5RV6QPsFY1YJE5wc5aNOYCFDtxIn_Wo,19413
22
+ execsql/db/base.py,sha256=0Cy_M3GHdkQDJMbMeWpNyUIPux9m1cVDy21MGMjhJCI,35002
23
+ execsql/db/dsn.py,sha256=CZd5NhUSVvK3irv2maeWsSqBsZjhFsTxIuT29Z0rP2A,6240
24
24
  execsql/db/duckdb.py,sha256=79lRzKRhw1Pjfqcrba27S4Oq8a8AbDO_d0XkaNKKPQo,3197
25
25
  execsql/db/factory.py,sha256=YHdgyqQYy16548O3fGyElLC5C7DdIgva4Z29OsDxXjs,5367
26
- execsql/db/firebird.py,sha256=p_7RFWhFI7y5ukKCMXeDPE0wjeQ6dpO4IK6uz2dYjrc,9224
27
- execsql/db/mysql.py,sha256=gOm1IRzFmxnT4ekGEXiF2yM3jB6r4p38Cmtt1Utj27Y,17712
26
+ execsql/db/firebird.py,sha256=cV5wUzQHoz7yugHdNxAn8IW7fL3v1riIsGLykCZhZwA,9480
27
+ execsql/db/mysql.py,sha256=Z0vI_nQ6vhG3sYa2fv5NOO3JmAsH6_jVjM8FuujQpqI,17961
28
28
  execsql/db/oracle.py,sha256=1_odb5xmlm8vjdJdQXz7SHm9dzIbZ5sxP_IxTklH3kA,12018
29
29
  execsql/db/postgres.py,sha256=UNzrXzMniEyT3Z7qjCA_HLEUY0PVr1cJShuhAxtl5l0,21241
30
30
  execsql/db/sqlite.py,sha256=xooU6bvD9Y3frRpnbyesE63r6E1fwEHkkcN1YD_UIUE,11519
31
- execsql/db/sqlserver.py,sha256=sxtOrcN1pGJQ0x7CctrKIL9XFIaHCDVIVvEvxzEOdzY,8744
31
+ execsql/db/sqlserver.py,sha256=j2ViLoBWzizgaL0u6V4iHfjinrlJ4rpBD3XZiKKdwSU,8968
32
32
  execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
33
33
  execsql/debug/repl.py,sha256=JObeoEXh15qyk4Q3WQCCqMcBtojlIcu_Xg-ZRDZJi5Q,25491
34
34
  execsql/exporters/__init__.py,sha256=-Cnji-OgodJV8ftcDcOyTof0kQMy9J5kKVC8GVFpc3o,670
@@ -95,29 +95,28 @@ execsql/utils/datetime.py,sha256=rMCXAbvj6bxKCYzC97vrludO6PU5DYQ39buZ0smDC5A,357
95
95
  execsql/utils/errors.py,sha256=C-9hlpJ7GM5gpDBdTsT4xFhK0OHNjonHgbq7fMl-Vvc,8577
96
96
  execsql/utils/fileio.py,sha256=38Z0WBvfaMa624aw3jo9ZOSlUCsacIeJWOhQKYe9YlU,31012
97
97
  execsql/utils/gui.py,sha256=kpJkvi8zblXFdO4PgsF8yE_gkOJrsDwWoQWmaDpjJEQ,23402
98
- execsql/utils/mail.py,sha256=MeSYjG9Qr8zdrEvkvg0C3YcLKaA5Op9xqeKbfvj-RLs,5500
98
+ execsql/utils/mail.py,sha256=ojdROW6Ti7paVpVMIxRXh7opOdE61fE2duS8xoA6d2Q,5758
99
99
  execsql/utils/numeric.py,sha256=xh02ANSRk3nUpQ-rtm66ILoMqoi7HtzCoRMIOT9U8QI,1570
100
100
  execsql/utils/regex.py,sha256=diEzTZqU_HHwVMadPAvN1Vgzhl7I03eVaEFGCXyGGL8,3770
101
101
  execsql/utils/strings.py,sha256=UQNjpRCEFa1UO6feU-M-9e24wWAvizs_iu_4fFusLxo,8516
102
102
  execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
103
- execsql2-2.19.2.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
104
- execsql2-2.19.2.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
105
- execsql2-2.19.2.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
106
- execsql2-2.19.2.data/data/execsql2_extras/execsql.conf,sha256=Sq1Huwb_Uf_lI7zW7m3h11fqU6zjGpGnCU4OVJ_tCe0,10696
107
- execsql2-2.19.2.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
108
- execsql2-2.19.2.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
109
- execsql2-2.19.2.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
110
- execsql2-2.19.2.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
111
- execsql2-2.19.2.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
112
- execsql2-2.19.2.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
113
- execsql2-2.19.2.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
114
- execsql2-2.19.2.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
115
- execsql2-2.19.2.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
116
- execsql2-2.19.2.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
117
- execsql2-2.19.2.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
118
- execsql2-2.19.2.dist-info/METADATA,sha256=MM_IlZJUBFm8AmOYmEx3sydmztTSX89EZcJ3aSJtcm4,22340
119
- execsql2-2.19.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
120
- execsql2-2.19.2.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
121
- execsql2-2.19.2.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
122
- execsql2-2.19.2.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
123
- execsql2-2.19.2.dist-info/RECORD,,
103
+ execsql2-2.20.0.data/data/execsql2_extras/README.md,sha256=vX4NTL095dUoA_hesyRMGYBorEZ_Y_tJ9qrd-MVV09I,5032
104
+ execsql2-2.20.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
105
+ execsql2-2.20.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
106
+ execsql2-2.20.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
107
+ execsql2-2.20.0.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
108
+ execsql2-2.20.0.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
109
+ execsql2-2.20.0.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
110
+ execsql2-2.20.0.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
111
+ execsql2-2.20.0.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
112
+ execsql2-2.20.0.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
113
+ execsql2-2.20.0.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
114
+ execsql2-2.20.0.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
115
+ execsql2-2.20.0.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
116
+ execsql2-2.20.0.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
117
+ execsql2-2.20.0.dist-info/METADATA,sha256=2j1tQ7oxlDAfsmIPQwgDxxn9O34FN7MbKT1nLznLi-M,22603
118
+ execsql2-2.20.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
119
+ execsql2-2.20.0.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
120
+ execsql2-2.20.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
121
+ execsql2-2.20.0.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
122
+ execsql2-2.20.0.dist-info/RECORD,,
@@ -1,359 +0,0 @@
1
- # execsql.conf — Configuration file for execsql
2
- #
3
- # Documentation: https://execsql2.readthedocs.io/en/latest/reference/configuration/
4
- #
5
- # This file uses INI format. Section names are case-sensitive (lowercase).
6
- # Property names are not case-sensitive. Lines starting with # are comments.
7
- #
8
- # execsql searches for this file in the following locations (in order):
9
- # 1. System-wide: /etc/execsql.conf (Linux/macOS) or %APPDATA%\execsql.conf (Windows)
10
- # 2. User: ~/.config/execsql.conf
11
- # 3. Script directory: same directory as the SQL script
12
- # 4. Working directory: current directory
13
- #
14
- # Later files override earlier ones. CLI arguments override everything.
15
- # Generate this template with: execsql --init-config > execsql.conf
16
-
17
-
18
- [connect]
19
- # Connection information for the initial database connection.
20
-
21
- # Database type.
22
- # p: PostgreSQL l: SQLite m: MySQL/MariaDB k: DuckDB
23
- # f: Firebird s: SQL Server a: MS Access o: Oracle d: DSN
24
- #db_type=l
25
-
26
- # Server name for client-server databases.
27
- #server=
28
-
29
- # Database name for client-server databases.
30
- #db=
31
-
32
- # Database file path for file-based databases (SQLite, DuckDB, Access).
33
- #db_file=
34
-
35
- # Port number for server-based databases. Only needed if not the default.
36
- #port=
37
-
38
- # User name for client-server databases.
39
- #username=
40
-
41
- # User name for password-protected MS Access databases if not "Admin".
42
- #access_username=
43
-
44
- # Whether to prompt for a password.
45
- # Values: Yes or No. Default: Yes.
46
- #password_prompt=Yes
47
-
48
- # Whether to use the OS credential store (keyring) for password storage.
49
- # Requires: pip install execsql2[auth]
50
- # Values: Yes or No. Default: Yes.
51
- #use_keyring=Yes
52
-
53
- # Whether to create a new database if it does not exist (SQLite, PostgreSQL, DuckDB).
54
- # Values: Yes or No. Default: No.
55
- #new_db=No
56
-
57
-
58
- [encoding]
59
- # Character encoding for text input and output.
60
-
61
- #database=
62
- #script=utf8
63
- #import=utf8
64
- #output=utf8
65
-
66
- # How to handle incompatible encodings.
67
- # Values: ignore, replace, xmlcharrefreplace, or backslashreplace.
68
- #error_response=
69
-
70
-
71
- [input]
72
- # Settings for handling imported data.
73
-
74
- # Whether to convert numeric values to double precision when using MS Access.
75
- # Values: Yes or No. Default: No.
76
- #access_use_numeric=No
77
-
78
- # Whether to treat integer values of 0 and 1 as Booleans.
79
- # Values: Yes or No. Default: Yes.
80
- #boolean_int=Yes
81
-
82
- # Whether to recognize only full words as Boolean, not "Y", "N", "T", and "F".
83
- # Values: Yes or No. Default: No.
84
- #boolean_words=No
85
-
86
- # Whether to replace non-alphanumeric characters in column names with underscores.
87
- # Values: Yes or No. Default: No.
88
- #clean_column_headers=No
89
-
90
- # Whether to create column headers when missing from an input file.
91
- # Headers will be "Col1", "Col2", etc.
92
- # Values: Yes or No. Default: No.
93
- #create_column_headers=No
94
-
95
- # Whether to deduplicate column names by appending "_N" (column number).
96
- # Values: Yes or No. Default: No.
97
- #dedup_column_headers=No
98
-
99
- # Whether to completely delete columns with missing headers.
100
- # Values: Yes or No. Default: No.
101
- #delete_empty_columns=No
102
-
103
- # Whether to import completely empty rows.
104
- # Values: Yes or No. Default: Yes.
105
- #empty_rows=Yes
106
-
107
- # Whether empty strings in input data are preserved or replaced by NULL.
108
- # Values: Yes or No. Default: Yes.
109
- #empty_strings=Yes
110
-
111
- # Whether to fold column headers to lowercase, uppercase, or leave unchanged.
112
- # Values: No, Lower, or Upper. Default: No.
113
- #fold_column_headers=No
114
-
115
- # The size of the import buffer, in KB, for the Postgres fast file reading feature.
116
- #import_buffer=
117
-
118
- # Whether to ignore extra columns in imported data that are not in the target table.
119
- # Values: Yes or No. Default: No.
120
- #import_only_common_columns=No
121
-
122
- # Interval (in rows) at which to report import progress. 0 disables.
123
- # Value: A non-negative integer. Default: 0.
124
- #import_progress_interval=0
125
-
126
- # The number of rows to buffer when importing data.
127
- # Value: A positive non-zero integer. Default: 1000.
128
- #import_row_buffer=1000
129
-
130
- # The maximum value that will be assigned an integer data type when creating tables.
131
- #max_int=2147483647
132
-
133
- # Whether IMPORT treats all columns as text instead of inferring data types.
134
- # Values: Yes or No. Default: No.
135
- #only_strings=No
136
-
137
- # Whether newlines embedded in imported text should be replaced with a space.
138
- # Values: Yes or No. Default: No.
139
- #replace_newlines=No
140
-
141
- # The number of lines to scan in an input file to identify delimiters and quotes.
142
- # Value: A positive non-zero integer. Default: 100.
143
- #scan_lines=100
144
-
145
- # Whether to show a progress bar for long-running IMPORT operations.
146
- # Values: Yes or No. Default: No.
147
- #show_progress=No
148
-
149
- # Whether to trim spaces/underscores from column headers.
150
- # Values: None, Both, Left, or Right. Default: None.
151
- #trim_column_headers=None
152
-
153
- # Whether to trim leading/trailing whitespace from imported text.
154
- # Values: Yes or No. Default: No.
155
- #trim_strings=No
156
-
157
-
158
- [output]
159
- # Settings for data export and output.
160
-
161
- # Whether WRITE output should also be sent to the log file.
162
- # Values: Yes or No. Default: No.
163
- #log_write_messages=No
164
-
165
- # Whether to create non-existent directories for WRITE or EXPORT output.
166
- # Values: Yes or No. Default: No.
167
- #make_export_dirs=No
168
-
169
- # Whether to quote all text values in delimited exports.
170
- # Values: Yes or No. Default: No.
171
- #quote_all_text=No
172
-
173
- # The number of rows to buffer from the database when exporting.
174
- # Value: A positive non-zero integer. Default: 1000.
175
- #export_row_buffer=1000
176
-
177
- # The length for text data types when exporting to HDF5.
178
- # Value: A positive non-zero integer. Default: 1000.
179
- #hdf5_text_len=1000
180
-
181
- # The URI of a CSS file to use when exporting to HTML.
182
- #css_file=
183
-
184
- # CSS style commands to embed in HTML exports.
185
- #css_styles=
186
-
187
- # Seconds to keep retrying when an output file has an access conflict.
188
- # Value: A positive non-zero number. Default: 600.
189
- #outfile_open_timeout=600
190
-
191
- # The template processor for template-based exports.
192
- # Value: jinja.
193
- #template_processor=
194
-
195
- # Internal buffer size (in MB) for ZIP exports.
196
- # Value: A positive non-zero integer. Default: 10.
197
- #zip_buffer_mb=10
198
-
199
-
200
- [interface]
201
- # Settings for GUI dialogs and console output.
202
-
203
- # GUI backend framework.
204
- # Values: tkinter or textual. Default: tkinter.
205
- #gui_framework=tkinter
206
-
207
- # GUI dialog usage level.
208
- # 0: none (default)
209
- # 1: passwords and PAUSE metacommands
210
- # 2: also database selection dialogs
211
- # 3: open a console window immediately
212
- #gui_level=0
213
-
214
- # The height (in lines) of the console window.
215
- # Value: An integer >= 5. Default: 25.
216
- #console_height=25
217
-
218
- # The width (in characters) of the console window.
219
- # Value: An integer >= 20. Default: 100.
220
- #console_width=100
221
-
222
- # Whether the console stays open at the end of the script.
223
- # Values: Yes or No. Default: No.
224
- #console_wait_when_done=No
225
-
226
- # Whether the console stays open when an error halt occurs.
227
- # Values: Yes or No. Default: No.
228
- #console_wait_when_error_halt=No
229
-
230
- # Whether to write warning messages to the console.
231
- # Values: Yes or No. Default: No.
232
- #write_warnings=No
233
-
234
- # Text to prefix to WRITE output (with a space separator). "clear" removes a previous prefix.
235
- #write_prefix=
236
-
237
- # Text to append to WRITE output (with a space separator). "clear" removes a previous suffix.
238
- #write_suffix=
239
-
240
-
241
- [email]
242
- # Settings for the EMAIL metacommand.
243
-
244
- # SMTP server connection.
245
- #host=
246
- #port=
247
- #username=
248
- #password=
249
-
250
- # Whether to use implicit TLS (port 465). Preferred over use_tls.
251
- # Values: Yes or No. Default: No.
252
- #use_ssl=No
253
-
254
- # Whether to use STARTTLS (port 587).
255
- # Values: Yes or No. Default: No.
256
- #use_tls=No
257
-
258
- # Email body format.
259
- # Values: plain or html. Default: plain.
260
- #email_format=plain
261
-
262
- # Custom CSS styles for HTML email.
263
- #message_css=
264
-
265
- # Obfuscated password (XOR, not cryptographically secure — use keyring instead).
266
- # See: https://execsql2.readthedocs.io/en/latest/reference/security/#credentials
267
- #enc_password=
268
-
269
-
270
- [config]
271
- # General program configuration and additional config file chaining.
272
-
273
- # Path to an additional configuration file to load.
274
- #config_file=
275
-
276
- # Whether to allow the SYSTEM_CMD (SHELL) metacommand.
277
- # Set to No to prevent scripts from executing OS commands.
278
- # Also controllable via --no-system-cmd CLI flag.
279
- # Values: Yes or No. Default: Yes.
280
- #allow_system_cmd=Yes
281
-
282
- # Whether to allow the RM_FILE metacommand (which deletes a file).
283
- # Set to No to prevent scripts from deleting files.
284
- # Also controllable via --no-rm-file CLI flag.
285
- # Values: Yes or No. Default: Yes.
286
- #allow_rm_file=Yes
287
-
288
- # Whether to allow the SERVE metacommand (which opens an HTTP server on
289
- # a local port to serve a single file). Set to No to disable.
290
- # Also controllable via --no-serve CLI flag.
291
- # Values: Yes or No. Default: Yes.
292
- #allow_serve=Yes
293
-
294
- # Root directory under which INCLUDE / EXECUTE SCRIPT targets must
295
- # resolve. When set, attempts to include files outside this root via
296
- # ../, absolute paths, drive letters, or UNC paths are rejected with
297
- # an error. Default: no containment (any readable path is permitted).
298
- #include_root=
299
-
300
- # Root directory under which SERVE targets must resolve. Same
301
- # containment semantics as include_root. Default: no containment.
302
- #serve_root=
303
-
304
- # Root directory under which Jinja2 / string.Template loader paths
305
- # must resolve. Same containment semantics as include_root.
306
- # Default: no containment.
307
- #template_root=
308
-
309
- # Maximum byte size of any single substitution-variable expansion,
310
- # enforced by the substitute_vars() engine to defeat exponential-
311
- # expansion bombs. Default: 10 MB (10485760).
312
- #max_substitution_bytes=10485760
313
-
314
- # Whether to log all data variable assignments.
315
- # Values: Yes or No. Default: Yes.
316
- #log_datavars=Yes
317
-
318
- # Whether to log all executed SQL statements.
319
- # Values: Yes or No. Default: No.
320
- #log_sql=No
321
-
322
- # Maximum log file size in MB before rotation. 0 disables rotation.
323
- # Value: A non-negative integer. Default: 0.
324
- #max_log_size_mb=0
325
-
326
- # Whether to place execsql.log in the user's home directory instead of the script directory.
327
- # Values: Yes or No. Default: No.
328
- #user_logfile=No
329
-
330
- # Seconds between DAO query creation and ODBC data access (MS Access only).
331
- # Must be >= 5.0.
332
- #dao_flush_delay_secs=5.0
333
-
334
- # Platform-specific additional config files.
335
- #linux_config_file=
336
- #macos_config_file=
337
- #win_config_file=
338
-
339
-
340
- [variables]
341
- # Substitution variables defined at startup. No pre-defined keys.
342
- # Example:
343
- # output_dir = /data/exports
344
- # env_name = production
345
-
346
-
347
- [include_required]
348
- # Script files to include before the main script (required — error if missing).
349
- # Keys are integers defining include order; values are file paths.
350
- # Example:
351
- # 1 = /opt/execsql/common/setup.sql
352
- # 2 = /opt/execsql/common/helpers.sql
353
-
354
-
355
- [include_optional]
356
- # Script files to include before the main script (optional — skipped if missing).
357
- # Same format as [include_required].
358
- # Example:
359
- # 1 = local_overrides.sql