execsql2 2.15.7__py3-none-any.whl → 2.15.11__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 (31) hide show
  1. execsql/__init__.py +4 -3
  2. execsql/cli/__init__.py +17 -0
  3. execsql/cli/run.py +4 -1
  4. execsql/config.py +20 -4
  5. execsql/db/base.py +4 -1
  6. execsql/debug/repl.py +27 -10
  7. execsql/gui/tui.py +59 -2
  8. execsql/metacommands/conditions.py +20 -2
  9. execsql/metacommands/upsert.py +20 -10
  10. execsql/utils/gui.py +139 -17
  11. {execsql2-2.15.7.dist-info → execsql2-2.15.11.dist-info}/METADATA +4 -3
  12. {execsql2-2.15.7.dist-info → execsql2-2.15.11.dist-info}/RECORD +31 -31
  13. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/README.md +0 -0
  14. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  15. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  16. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/execsql.conf +0 -0
  17. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/make_config_db.sql +0 -0
  18. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/md_compare.sql +0 -0
  19. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/md_glossary.sql +0 -0
  20. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/md_upsert.sql +0 -0
  21. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/pg_compare.sql +0 -0
  22. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  23. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  24. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/script_template.sql +0 -0
  25. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/ss_compare.sql +0 -0
  26. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  27. {execsql2-2.15.7.data → execsql2-2.15.11.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  28. {execsql2-2.15.7.dist-info → execsql2-2.15.11.dist-info}/WHEEL +0 -0
  29. {execsql2-2.15.7.dist-info → execsql2-2.15.11.dist-info}/entry_points.txt +0 -0
  30. {execsql2-2.15.7.dist-info → execsql2-2.15.11.dist-info}/licenses/LICENSE.txt +0 -0
  31. {execsql2-2.15.7.dist-info → execsql2-2.15.11.dist-info}/licenses/NOTICE +0 -0
execsql/__init__.py CHANGED
@@ -1,9 +1,10 @@
1
1
  """
2
2
  execsql — a maintained fork of the execsql SQL scripting tool.
3
3
 
4
- This package provides the ``execsql2`` CLI (entry point ``execsql2``) and the
5
- ``execsql`` importable module. The top-level package exposes only the package
6
- version; all public functionality lives in sub-modules.
4
+ This package provides the ``execsql`` CLI command (distributed as the
5
+ ``execsql2`` package on PyPI) and the ``execsql`` importable module.
6
+ The top-level package exposes only the package version; all public
7
+ functionality lives in sub-modules.
7
8
  """
8
9
 
9
10
  from __future__ import annotations
execsql/cli/__init__.py CHANGED
@@ -283,6 +283,16 @@ def main(
283
283
  "--profile-limit",
284
284
  help="Number of top statements to show in the --profile timing summary (default: 20).",
285
285
  ),
286
+ config_file: str | None = typer.Option(
287
+ None,
288
+ "--config",
289
+ metavar="FILE",
290
+ help=(
291
+ "Path to an execsql configuration file. "
292
+ "Loaded after the implicit search paths so its values take precedence. "
293
+ "The file may chain additional configs via its [cyan][config][/cyan] section."
294
+ ),
295
+ ),
286
296
  debug: bool = typer.Option(
287
297
  False,
288
298
  "--debug",
@@ -376,6 +386,12 @@ def main(
376
386
  webbrowser.open("https://execsql2.readthedocs.io/en/latest/", new=2, autoraise=True)
377
387
  raise typer.Exit()
378
388
 
389
+ if config_file and not Path(config_file).is_file():
390
+ _err_console.print(
391
+ f"[bold red]Error:[/bold red] Config file [cyan]{config_file!r}[/cyan] does not exist.",
392
+ )
393
+ raise typer.Exit(code=2)
394
+
379
395
  positional = args or []
380
396
  if command is not None:
381
397
  script_name = None # inline mode — no script file
@@ -458,6 +474,7 @@ def main(
458
474
  ping=ping,
459
475
  lint=lint,
460
476
  debug=debug,
477
+ config_file=config_file,
461
478
  )
462
479
 
463
480
 
execsql/cli/run.py CHANGED
@@ -12,6 +12,7 @@ from typing import Any
12
12
  import datetime
13
13
  import getpass
14
14
  import os
15
+ import platform
15
16
  import sys
16
17
  import traceback
17
18
  from pathlib import Path
@@ -219,6 +220,7 @@ def _run(
219
220
  ping: bool = False,
220
221
  lint: bool = False,
221
222
  debug: bool = False,
223
+ config_file: str | None = None,
222
224
  ) -> None:
223
225
  """Initialise state, connect to the database, load the script, and run it.
224
226
 
@@ -276,13 +278,14 @@ def _run(
276
278
  elif osys.startswith("win"):
277
279
  osys = "windows"
278
280
  _state.subvars.add_substitution("$OS", osys)
281
+ _state.subvars.add_substitution("$HOSTNAME", platform.node())
279
282
  _state.subvars.add_substitution("$PYTHON_EXECUTABLE", sys.executable)
280
283
 
281
284
  # ------------------------------------------------------------------
282
285
  # Read configuration file
283
286
  # ------------------------------------------------------------------
284
287
  script_path = str(Path(script_name).resolve().parent) if script_name else os.getcwd()
285
- _state.conf = ConfigData(script_path, _state.subvars)
288
+ _state.conf = ConfigData(script_path, _state.subvars, config_file=config_file)
286
289
  conf = _state.conf
287
290
 
288
291
  # ------------------------------------------------------------------
execsql/config.py CHANGED
@@ -198,7 +198,13 @@ class ConfigData:
198
198
  raise ConfigError(f"Invalid {key}: {val}; must be >= {min_val}.")
199
199
  setattr(self, attr, val)
200
200
 
201
- def __init__(self, script_path: str, variable_pool: object) -> None:
201
+ def __init__(
202
+ self,
203
+ script_path: str,
204
+ variable_pool: object,
205
+ *,
206
+ config_file: str | None = None,
207
+ ) -> None:
202
208
  """Load and merge all discoverable execsql.conf files for the given script path.
203
209
 
204
210
  Args:
@@ -207,6 +213,10 @@ class ConfigData:
207
213
  variable_pool: Substitution-variable registry used to expand
208
214
  ``config_file`` path values and to populate ``[variables]``
209
215
  sections.
216
+ config_file: Optional explicit config file path (from ``--config``).
217
+ Loaded after the implicit search paths so its values take
218
+ precedence over system, user, script, and working-directory
219
+ config files.
210
220
  """
211
221
  self.db_type = "a"
212
222
  self.server = None
@@ -290,9 +300,15 @@ class ConfigData:
290
300
  config_files = [sys_config_file, user_config_file, script_config_file, startdir_config_file]
291
301
  else:
292
302
  config_files = [sys_config_file, user_config_file, startdir_config_file]
303
+ if config_file:
304
+ config_files.append(str(Path(config_file).resolve()))
305
+ from collections import deque
306
+
293
307
  _MAX_CONFIG_CHAIN = 20 # Guard against circular config_file references.
308
+ config_queue: deque[str] = deque(config_files)
294
309
  self.files_read: list = []
295
- for ix, configfile in enumerate(config_files):
310
+ while config_queue:
311
+ configfile = config_queue.popleft()
296
312
  if len(self.files_read) >= _MAX_CONFIG_CHAIN:
297
313
  break
298
314
  if configfile not in self.files_read and Path(configfile).is_file():
@@ -425,7 +441,7 @@ class ConfigData:
425
441
  conffile = str(Path(conffile) / self.config_file_name)
426
442
  if Path(conffile).is_file():
427
443
  # Silently ignore a non-existent file, for cross-OS compatibility.
428
- config_files.insert(ix + 1, conffile)
444
+ config_queue.appendleft(conffile)
429
445
  # OS-specific additional config files.
430
446
  _os_config_key: str | None = None
431
447
  if sys.platform == "linux" and cp.has_option(self._CONFIG_SECTION, "linux_config_file"):
@@ -445,7 +461,7 @@ class ConfigData:
445
461
  if not Path(conffile).is_file():
446
462
  conffile = str(Path(conffile) / self.config_file_name)
447
463
  if Path(conffile).is_file():
448
- config_files.insert(ix + 1, conffile)
464
+ config_queue.appendleft(conffile)
449
465
  self._get_bool(cp, self._CONFIG_SECTION, "user_logfile", "user_logfile")
450
466
  # dao_flush_delay_secs has a specific error message — keep inline
451
467
  if cp.has_option(self._CONFIG_SECTION, "dao_flush_delay_secs"):
execsql/db/base.py CHANGED
@@ -747,4 +747,7 @@ class DatabasePool:
747
747
  _state.exec_log.log_status_error(
748
748
  f"Can't close database {nm} aliased as {alias}",
749
749
  )
750
- self.__init__()
750
+ self.pool = {}
751
+ self.initial_db = None
752
+ self.current_db = None
753
+ self.do_rollback = True
execsql/debug/repl.py CHANGED
@@ -45,23 +45,39 @@ _YELLOW = "\033[33m"
45
45
  _CYAN = "\033[36m"
46
46
 
47
47
 
48
+ _color_cache: bool | None = None
49
+
50
+
48
51
  def _use_color() -> bool:
49
52
  """Return True if the output stream supports ANSI color.
50
53
 
51
54
  Checks ``NO_COLOR`` and ``EXECSQL_NO_COLOR`` environment variables first
52
55
  (either set → color off). Then tests whether the active output stream
53
56
  reports itself as a TTY.
57
+
58
+ The result is cached after the first call; call ``_reset_color_cache()``
59
+ to force re-evaluation (e.g. when entering the REPL).
54
60
  """
55
- if os.environ.get("NO_COLOR") is not None:
56
- return False
57
- if os.environ.get("EXECSQL_NO_COLOR") is not None:
58
- return False
59
- output = _state.output
60
- if output is not None and hasattr(output, "isatty"):
61
- return output.isatty()
62
- # WriteHooks (the default _state.output) has no isatty — fall through
63
- # to check the underlying stream it would write to.
64
- return sys.stdout.isatty()
61
+ global _color_cache # noqa: PLW0603
62
+ if _color_cache is not None:
63
+ return _color_cache
64
+ if os.environ.get("NO_COLOR") is not None or os.environ.get("EXECSQL_NO_COLOR") is not None:
65
+ _color_cache = False
66
+ else:
67
+ output = _state.output
68
+ if output is not None and hasattr(output, "isatty"):
69
+ _color_cache = output.isatty()
70
+ else:
71
+ # WriteHooks (the default _state.output) has no isatty — fall through
72
+ # to check the underlying stream it would write to.
73
+ _color_cache = sys.stdout.isatty()
74
+ return _color_cache
75
+
76
+
77
+ def _reset_color_cache() -> None:
78
+ """Clear the cached color decision so it is re-evaluated on next use."""
79
+ global _color_cache # noqa: PLW0603
80
+ _color_cache = None
65
81
 
66
82
 
67
83
  def _c(code: str, text: str) -> str:
@@ -169,6 +185,7 @@ def _debug_repl(*, step: bool = False) -> None:
169
185
  step: When ``True``, the entry banner says "Step" instead of
170
186
  "Breakpoint" to indicate the REPL was re-entered via step mode.
171
187
  """
188
+ _reset_color_cache()
172
189
  try:
173
190
  import readline as _readline # noqa: F401 — side-effect: enables history/arrow keys
174
191
  except ImportError:
execsql/gui/tui.py CHANGED
@@ -244,7 +244,31 @@ class MsgScreen(_BaseDialog):
244
244
 
245
245
 
246
246
  class PauseScreen(_BaseDialog):
247
- """Pause dialog with optional countdown and Continue/Cancel buttons."""
247
+ """Pause dialog with optional countdown progress bar and Continue/Cancel buttons."""
248
+
249
+ DEFAULT_CSS = (
250
+ _BaseDialog.DEFAULT_CSS
251
+ + """
252
+ #countdown-container {
253
+ height: auto;
254
+ margin: 1 0;
255
+ }
256
+ #countdown-container ProgressBar {
257
+ width: 1fr;
258
+ }
259
+ #countdown-container Bar {
260
+ width: 1fr;
261
+ &> .bar--bar {
262
+ background: $primary 30%;
263
+ }
264
+ }
265
+ #countdown-container .countdown-label {
266
+ text-align: center;
267
+ color: $text-muted;
268
+ width: 1fr;
269
+ }
270
+ """
271
+ )
248
272
 
249
273
  BINDINGS = [
250
274
  *_BaseDialog.BINDINGS,
@@ -254,17 +278,50 @@ class PauseScreen(_BaseDialog):
254
278
  def compose(self) -> ComposeResult:
255
279
  title = self.args.get("title", "Pause")
256
280
  message = self.args.get("message", "")
281
+ countdown = self.args.get("countdown")
257
282
  with Container(id="dialog"):
258
283
  yield Label(title, id="title")
259
284
  yield Static(message, id="message")
285
+ if countdown is not None:
286
+ with Vertical(id="countdown-container"):
287
+ yield ProgressBar(
288
+ total=float(countdown),
289
+ show_percentage=False,
290
+ show_eta=False,
291
+ id="countdown-bar",
292
+ )
293
+ yield Static("", classes="countdown-label")
260
294
  with Horizontal(id="buttons"):
261
295
  yield Button("Cancel", id="btn_cancel_exit", variant="warning")
262
296
  yield Button("Continue", id="btn_continue", variant="primary")
263
297
 
264
298
  def on_mount(self) -> None:
299
+ import time
300
+
265
301
  countdown = self.args.get("countdown")
266
302
  if countdown is not None:
267
- self.set_timer(float(countdown), self._auto_continue)
303
+ self._countdown_total = float(countdown)
304
+ self._countdown_start = time.time()
305
+ self._tick_interval = self.set_interval(0.2, self._tick)
306
+
307
+ def _tick(self) -> None:
308
+ import time
309
+
310
+ elapsed = time.time() - self._countdown_start
311
+ remaining = max(0.0, self._countdown_total - elapsed)
312
+ progress = min(self._countdown_total, elapsed)
313
+ bar = self.query_one("#countdown-bar", ProgressBar)
314
+ bar.update(progress=progress)
315
+ label = self.query_one(".countdown-label", Static)
316
+ if remaining >= 60:
317
+ mins = int(remaining) // 60
318
+ secs = int(remaining) % 60
319
+ label.update(f"{mins}m {secs:02d}s remaining")
320
+ else:
321
+ label.update(f"{remaining:.0f}s remaining")
322
+ if remaining <= 0:
323
+ self._tick_interval.stop()
324
+ self._auto_continue()
268
325
 
269
326
  def _auto_continue(self) -> None:
270
327
  self._result = {"quit": False}
@@ -30,6 +30,24 @@ from execsql.utils.gui import gui_console_isrunning
30
30
  from execsql.utils.strings import unquoted
31
31
 
32
32
 
33
+ def _quote_table_name(name: str) -> str:
34
+ """Quote a potentially schema-qualified table name for safe SQL interpolation.
35
+
36
+ Splits on ``.`` and quotes each component with standard SQL double-quoting
37
+ (embedded double-quotes are escaped to ``""``).
38
+
39
+ Examples::
40
+
41
+ >>> _quote_table_name("books")
42
+ '"books"'
43
+ >>> _quote_table_name("staging.books")
44
+ '"staging"."books"'
45
+ >>> _quote_table_name('my"table')
46
+ '"my""table"'
47
+ """
48
+ return ".".join('"' + part.replace('"', '""') + '"' for part in name.split("."))
49
+
50
+
33
51
  def xf_contains(**kwargs: Any) -> bool:
34
52
  s1 = kwargs["string1"]
35
53
  s2 = kwargs["string2"]
@@ -59,7 +77,7 @@ def xf_endswith(**kwargs: Any) -> bool:
59
77
 
60
78
  def xf_hasrows(**kwargs: Any) -> bool:
61
79
  queryname = kwargs["queryname"]
62
- sql = f"select count(*) from {queryname};"
80
+ sql = f"select count(*) from {_quote_table_name(queryname)};"
63
81
  try:
64
82
  hdrs, rec = _state.dbs.current().select_data(sql)
65
83
  except ErrInfo:
@@ -84,7 +102,7 @@ def _row_count(queryname: str, sql_context: str, metacommandline: str) -> int:
84
102
  Raises:
85
103
  ErrInfo: If the query fails or the result is not numeric.
86
104
  """
87
- sql = f"select count(*) from {queryname};"
105
+ sql = f"select count(*) from {_quote_table_name(queryname)};"
88
106
  try:
89
107
  _hdrs, rec = _state.dbs.current().select_data(sql)
90
108
  except ErrInfo:
@@ -26,11 +26,11 @@ from execsql.utils.errors import exception_desc
26
26
 
27
27
  _KW_METHOD = re.compile(r"\bMETHOD\s+(upsert|update|insert)\b", re.IGNORECASE)
28
28
  _KW_EXCLUDE = re.compile(
29
- r"\bEXCLUDE\s+([\w\s,]+?)(?=\s+(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE_NULL|LOGFILE|CLEANUP|EXPORT_FAILURES|EXPORT_FORMAT|EXPORT_MAX_ROWS)\b|\s*$)",
29
+ r"\bEXCLUDE\s+([\w\s,]+?)(?=\s+(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE_NULL|LOGFILE|CLEANUP|EXPORT_FAILURES|EXPORT_FORMAT|EXPORT_MAX_ROWS|STRICT_COLUMNS)\b|\s*$)",
30
30
  re.IGNORECASE,
31
31
  )
32
32
  _KW_EXCLUDE_NULL = re.compile(
33
- r"\bEXCLUDE_NULL\s+([\w\s,]+?)(?=\s+(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE|LOGFILE|CLEANUP|EXPORT_FAILURES|EXPORT_FORMAT|EXPORT_MAX_ROWS)\b|\s*$)",
33
+ r"\bEXCLUDE_NULL\s+([\w\s,]+?)(?=\s+(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE|LOGFILE|CLEANUP|EXPORT_FAILURES|EXPORT_FORMAT|EXPORT_MAX_ROWS|STRICT_COLUMNS)\b|\s*$)",
34
34
  re.IGNORECASE,
35
35
  )
36
36
  _KW_COMMIT = re.compile(r"\bCOMMIT\b", re.IGNORECASE)
@@ -44,10 +44,11 @@ _KW_EXPORT_FAILURES = re.compile(
44
44
  )
45
45
  _KW_EXPORT_FORMAT = re.compile(r"\bEXPORT_FORMAT\s+(\S+)", re.IGNORECASE)
46
46
  _KW_EXPORT_MAX_ROWS = re.compile(r"\bEXPORT_MAX_ROWS\s+(\S+)", re.IGNORECASE)
47
+ _KW_STRICT_COLUMNS = re.compile(r"\bSTRICT_COLUMNS\b", re.IGNORECASE)
47
48
 
48
49
  # All recognized keywords — used to split table names from options.
49
50
  _ALL_KEYWORDS = re.compile(
50
- r"\b(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE_NULL|EXCLUDE|LOGFILE|CLEANUP|EXPORT_FAILURES|EXPORT_FORMAT|EXPORT_MAX_ROWS)\b",
51
+ r"\b(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE_NULL|EXCLUDE|LOGFILE|CLEANUP|EXPORT_FAILURES|EXPORT_FORMAT|EXPORT_MAX_ROWS|STRICT_COLUMNS)\b",
51
52
  re.IGNORECASE,
52
53
  )
53
54
 
@@ -145,6 +146,7 @@ def _parse_tables_and_options(tail: str) -> dict[str, Any]:
145
146
  "export_failures": export_failures,
146
147
  "export_format": export_format,
147
148
  "export_max_rows": export_max_rows,
149
+ "strict_columns": bool(_KW_STRICT_COLUMNS.search(opts_part)),
148
150
  }
149
151
 
150
152
 
@@ -191,6 +193,9 @@ def _set_subvars(result: Any) -> None:
191
193
  sv("$PG_UPSERT_STARTED_AT", result.started_at)
192
194
  sv("$PG_UPSERT_FINISHED_AT", result.finished_at)
193
195
  sv("$PG_UPSERT_RESULT_JSON", json.dumps(result.to_dict(), separators=(",", ":")))
196
+ # Warnings: tables with WARNING-level findings (still qa_passed=True).
197
+ warned_tables = [t.table_name for t in result.tables if t.qa_warnings]
198
+ sv("$PG_UPSERT_QA_WARNINGS", ", ".join(warned_tables) if warned_tables else "")
194
199
  # Default export path subvar to empty; _export_failures_if_requested
195
200
  # will overwrite it with the actual path if an export was produced.
196
201
  sv("$PG_UPSERT_EXPORT_PATH", "")
@@ -230,16 +235,20 @@ def _require_postgres(db: Any, metacommandline: str | None) -> None:
230
235
  )
231
236
 
232
237
 
233
- def _build_result_from_qa_errors(ups: Any) -> Any:
234
- """Build an UpsertResult from ``ups.qa_errors`` after a QA/CHECK run."""
238
+ def _build_result_from_qa_findings(ups: Any) -> Any:
239
+ """Build an UpsertResult from ``ups.qa_findings`` after a QA/CHECK run.
240
+
241
+ Uses ``qa_findings`` (all findings: errors + warnings) so that
242
+ ``$PG_UPSERT_RESULT_JSON`` includes both severity levels.
243
+ """
235
244
  from pg_upsert.models import TableResult, UpsertResult
236
245
 
237
246
  table_results: dict[str, Any] = {}
238
247
  for table_name in ups.tables:
239
248
  table_results[table_name] = TableResult(table_name=table_name)
240
- for err in ups.qa_errors:
241
- if err.table in table_results:
242
- table_results[err.table].qa_errors.append(err)
249
+ for finding in ups.qa_findings:
250
+ if finding.table in table_results:
251
+ table_results[finding.table]._qa_findings.append(finding)
243
252
  return UpsertResult(
244
253
  tables=list(table_results.values()),
245
254
  committed=False,
@@ -289,6 +298,7 @@ def _create_pgupsert(
289
298
  "upsert_method": opts["method"],
290
299
  "exclude_cols": opts["exclude_cols"],
291
300
  "exclude_null_check_cols": opts["exclude_null_check_cols"],
301
+ "strict_columns": opts.get("strict_columns", False),
292
302
  "ui_mode": ui_mode,
293
303
  "callback": _make_callback(),
294
304
  }
@@ -508,7 +518,7 @@ def x_pg_upsert_qa(**kwargs: Any) -> None:
508
518
  finally:
509
519
  _detach_log_handlers(loggers, handlers, prev_levels)
510
520
 
511
- result = _build_result_from_qa_errors(ups)
521
+ result = _build_result_from_qa_findings(ups)
512
522
  _set_subvars(result)
513
523
  _export_failures_if_requested(result, opts, metacommandline)
514
524
  if opts.get("cleanup"):
@@ -551,7 +561,7 @@ def x_pg_upsert_check(**kwargs: Any) -> None:
551
561
  finally:
552
562
  _detach_log_handlers(loggers, handlers, prev_levels)
553
563
 
554
- result = _build_result_from_qa_errors(ups)
564
+ result = _build_result_from_qa_findings(ups)
555
565
  _set_subvars(result)
556
566
  _export_failures_if_requested(result, opts, metacommandline)
557
567
  if opts.get("cleanup"):
execsql/utils/gui.py CHANGED
@@ -544,30 +544,98 @@ def get_yn_win(prompt: str) -> bool:
544
544
  return get_yn(prompt)
545
545
 
546
546
 
547
+ def _clear_progress_line() -> None:
548
+ """Erase the SIGALRM progress bar line and move to the next line."""
549
+ # \r returns to column 0, the spaces overwrite the bar, then \n advances.
550
+ sys.stdout.write("\r" + " " * 50 + "\r\n")
551
+ sys.stdout.flush()
552
+
553
+
547
554
  def pause(
548
555
  text: str,
549
556
  action: str | None = None,
550
557
  countdown: float | None = None,
551
558
  timeunit: str | None = None,
552
559
  ) -> int:
553
- """Display a pause message and wait for the user.
560
+ """Display a pause message and wait for the user (POSIX).
554
561
 
555
- Returns 0 (user continued), 1 (user quit), or 2 (timed out).
556
- In headless mode, prints the message and continues immediately unless
557
- countdown/action is set, in which case it sleeps then returns 2.
558
- """
559
- import time
562
+ Returns 0 (user continued), 1 (user quit/ESC), or 2 (timed out).
563
+
564
+ Uses raw-mode terminal reading so that single keypresses (Enter, Esc)
565
+ are detected without requiring the user to press Enter first. When
566
+ *countdown* is set, a SIGALRM-based progress bar is displayed via
567
+ :class:`~execsql.utils.timer.TimerHandler`.
560
568
 
569
+ Note: *countdown* is expected in **seconds** — the caller (``x_pause``)
570
+ has already converted minutes to seconds, so *timeunit* is not used here.
571
+ """
561
572
  print(f"\n{text}", file=sys.stderr)
562
- if countdown is not None and action is not None:
563
- seconds = float(countdown)
564
- if timeunit and timeunit.upper() == "MINUTES":
565
- seconds *= 60
566
- time.sleep(seconds)
567
- return 2 # timed out
568
- else:
569
- input("Press Enter to continue...")
570
- return 0
573
+
574
+ # When stdin is not a real TTY (pytest, piped input, etc.) or the
575
+ # platform lacks POSIX terminal APIs (Windows), fall back to simple
576
+ # blocking behaviour.
577
+ if sys.platform == "win32" or not hasattr(sys.stdin, "fileno") or not sys.stdin.isatty():
578
+ if countdown is not None and action is not None:
579
+ import time
580
+
581
+ time.sleep(float(countdown))
582
+ return 2
583
+ try:
584
+ input("Press Enter to continue...")
585
+ except EOFError:
586
+ pass
587
+ return 0
588
+
589
+ # POSIX-only imports — guarded by the isatty() check above so this
590
+ # function can still be called safely on Windows (falls back above).
591
+ import signal
592
+ import termios
593
+ import tty
594
+
595
+ from execsql.exceptions import ExecSqlTimeoutError
596
+ from execsql.utils.timer import TimerHandler
597
+
598
+ if countdown is None or action is None:
599
+ sys.stderr.write("Press Enter to continue, Esc to quit...\n")
600
+ sys.stderr.flush()
601
+
602
+ fd = sys.stdin.fileno()
603
+ old_settings = termios.tcgetattr(fd)
604
+
605
+ timer_handler: TimerHandler | None = None
606
+ old_alarm = signal.SIG_DFL
607
+
608
+ try:
609
+ tty.setraw(fd)
610
+
611
+ if countdown is not None and action is not None:
612
+ seconds = float(countdown)
613
+ timer_handler = TimerHandler(seconds)
614
+ old_alarm = signal.signal(signal.SIGALRM, timer_handler.alarm_handler)
615
+ signal.setitimer(signal.ITIMER_REAL, 0.01, 0.01)
616
+
617
+ has_bar = countdown is not None and action is not None
618
+
619
+ while True:
620
+ try:
621
+ ch = sys.stdin.read(1)
622
+ except ExecSqlTimeoutError:
623
+ # TimerHandler raised — countdown expired.
624
+ _clear_progress_line()
625
+ return 2 # timed out
626
+ if ch in ("\r", "\n"):
627
+ if has_bar:
628
+ _clear_progress_line()
629
+ return 0 # user continued
630
+ if ch == "\x1b":
631
+ if has_bar:
632
+ _clear_progress_line()
633
+ return 1 # user quit (ESC)
634
+ finally:
635
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
636
+ if timer_handler is not None:
637
+ signal.setitimer(signal.ITIMER_REAL, 0)
638
+ signal.signal(signal.SIGALRM, old_alarm)
571
639
 
572
640
 
573
641
  def pause_win(
@@ -576,5 +644,59 @@ def pause_win(
576
644
  countdown: float | None = None,
577
645
  timeunit: str | None = None,
578
646
  ) -> int:
579
- """GUI pause dialog falls back to terminal in headless mode."""
580
- return pause(text, action=action, countdown=countdown, timeunit=timeunit)
647
+ """Display a pause message and wait for the user (Windows).
648
+
649
+ Returns 0 (user continued), 1 (user quit/ESC), or 2 (timed out).
650
+
651
+ Uses ``msvcrt.kbhit()``/``msvcrt.getwch()`` for non-blocking single-char
652
+ reads with a polling loop that renders a text-mode progress bar when
653
+ *countdown* is set. Falls back to :func:`pause` on non-Windows platforms.
654
+
655
+ Note: *countdown* is expected in **seconds** — the caller (``x_pause``)
656
+ has already converted minutes to seconds, so *timeunit* is not used here.
657
+ """
658
+ try:
659
+ import msvcrt
660
+ except ImportError:
661
+ # Not on Windows — delegate to the POSIX implementation.
662
+ return pause(text, action=action, countdown=countdown, timeunit=timeunit)
663
+
664
+ import time
665
+
666
+ print(f"\n{text}", file=sys.stderr)
667
+
668
+ if countdown is not None and action is not None:
669
+ seconds = float(countdown)
670
+ start = time.time()
671
+ bar_len = 30
672
+ while True:
673
+ elapsed = time.time() - start
674
+ remaining = max(0.0, seconds - elapsed)
675
+ if remaining <= 0:
676
+ _clear_progress_line()
677
+ return 2 # timed out
678
+ bar_left = int(round(bar_len * remaining / seconds, 0))
679
+ sys.stdout.write(
680
+ "{:8.1f} |{}{}|\r".format(remaining, "+" * bar_left, "-" * (bar_len - bar_left)),
681
+ )
682
+ sys.stdout.flush()
683
+ if msvcrt.kbhit():
684
+ ch = msvcrt.getwch()
685
+ if ch in ("\r", "\n"):
686
+ _clear_progress_line()
687
+ return 0
688
+ if ch == "\x1b":
689
+ _clear_progress_line()
690
+ return 1
691
+ time.sleep(0.01)
692
+ else:
693
+ sys.stderr.write("Press Enter to continue, Esc to quit...\n")
694
+ sys.stderr.flush()
695
+ while True:
696
+ if msvcrt.kbhit():
697
+ ch = msvcrt.getwch()
698
+ if ch in ("\r", "\n"):
699
+ return 0
700
+ if ch == "\x1b":
701
+ return 1
702
+ time.sleep(0.01)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.15.7
3
+ Version: 2.15.11
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
@@ -52,7 +52,7 @@ Requires-Dist: keyring; extra == 'all'
52
52
  Requires-Dist: odfpy; extra == 'all'
53
53
  Requires-Dist: openpyxl; extra == 'all'
54
54
  Requires-Dist: oracledb; extra == 'all'
55
- Requires-Dist: pg-upsert>=1.21.0; extra == 'all'
55
+ Requires-Dist: pg-upsert>=1.22.0; extra == 'all'
56
56
  Requires-Dist: polars; extra == 'all'
57
57
  Requires-Dist: psycopg2-binary; extra == 'all'
58
58
  Requires-Dist: pymysql; extra == 'all'
@@ -117,7 +117,7 @@ Requires-Dist: oracledb; extra == 'oracle'
117
117
  Provides-Extra: postgres
118
118
  Requires-Dist: psycopg2-binary; extra == 'postgres'
119
119
  Provides-Extra: upsert
120
- Requires-Dist: pg-upsert>=1.21.0; extra == 'upsert'
120
+ Requires-Dist: pg-upsert>=1.22.0; extra == 'upsert'
121
121
  Description-Content-Type: text/markdown
122
122
 
123
123
  > [!NOTE]
@@ -240,6 +240,7 @@ execsql script.sql # read connection from config file
240
240
  | `--ping` | Test database connectivity and exit |
241
241
  | `--profile` | Show per-statement timing summary after execution |
242
242
  | `--progress` | Show a progress bar for long-running IMPORT operations |
243
+ | `--config FILE` | Load an explicit config file (highest priority after CLI args) |
243
244
  | `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
244
245
  | `--dump-keywords` | Print metacommand keywords as JSON and exit |
245
246
  | `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
@@ -1,6 +1,6 @@
1
- execsql/__init__.py,sha256=BIny4bL8uHuSl3gXPqEkIB2FtcexlARjR7IYPwtD9bM,486
1
+ execsql/__init__.py,sha256=zinIcvuXFile66yk4JKvD1NO3QqxR5MoSHXBHeRMRC0,515
2
2
  execsql/__main__.py,sha256=HdbK-SAhyUmfB6xINY5AzxdMSxGzWSGEG_2dv42Jn64,315
3
- execsql/config.py,sha256=DUW5cET6Vr7fsZURRSt2JpswrZKW96jyouFjUGLjOT0,28543
3
+ execsql/config.py,sha256=YkLEtyLfjfihEMmAXM9Bkz2h_AkCh7CroWwlAqtmR2M,29083
4
4
  execsql/exceptions.py,sha256=j8hykBiof9H3Za9hwLIbDcVB2Xn65ODXXplp1jkvdgM,8453
5
5
  execsql/format.py,sha256=-6iknDddqbkapMo4NKmT5LAynDLqMW5kHgDWRg0KSws,11990
6
6
  execsql/models.py,sha256=xXhMfycetIBGLhrEWwz2Witn3k7rdgMok73HSdTLSCw,13445
@@ -8,14 +8,14 @@ execsql/parser.py,sha256=P3ea8k7T_XLMrbhpFNZXwytdShrY302MKnhosqza1lo,15493
8
8
  execsql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  execsql/state.py,sha256=gX7By2R9Ytxbgy3QFG69DJa8ebO-0NSpYNpCYCrnO1w,15258
10
10
  execsql/types.py,sha256=5K3aTuWQZHftz5slFODwqxlcvvt6lROISUnvUtRUazs,31799
11
- execsql/cli/__init__.py,sha256=YXxOVF2lNkCkifXyjoC7yWrhHJFT9PzI7cnCzsLJwT8,16488
11
+ execsql/cli/__init__.py,sha256=jYw5J88e_vOthmEyzsNxxzkRf7xc7CphLixqBrUjr_c,17115
12
12
  execsql/cli/dsn.py,sha256=svaZtrUXFRL2W5G6FRRiKtR6kehOp7urrVhIx_642Z8,2820
13
13
  execsql/cli/help.py,sha256=Sn_TgSJiQeBx-xZH0fuP5OvR_wasSTumjWF9UHfIX5k,5414
14
14
  execsql/cli/lint.py,sha256=mVZu3Knp13-yQZkofiaeDmxLTWP7DkbaZoDeHPsfAEQ,24094
15
- execsql/cli/run.py,sha256=JGfndnBnJMkEqbz26pflhEdXDScZNIdGu6b6jTRLYl8,30681
15
+ execsql/cli/run.py,sha256=HXOkqCI5Qyve8_c3OeHuB2RylmgS0iF5gUSPl-Zn5do,30824
16
16
  execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
17
17
  execsql/db/access.py,sha256=v0GKN4yE30r-A1gcv224jZhj0JmmRTmGSuz_KH11iY4,18119
18
- execsql/db/base.py,sha256=vbPvthhj_6phNN4QeOUnYzkShqOjJTeg5QEgftj11qY,31229
18
+ execsql/db/base.py,sha256=-el1s7YrxHkdy0lFx86BKZ8mBXWD-1EvE44e5ZhD5KI,31322
19
19
  execsql/db/dsn.py,sha256=sMXHOCVPZMMq52ReF6AywmWfyZjkQPmOIDXiIe_PGD4,5400
20
20
  execsql/db/duckdb.py,sha256=pkmjD89gkbl0sHfcGpowx4X7PGTEpg-CoTpnzHR0Cu0,3174
21
21
  execsql/db/factory.py,sha256=YR1m_c2Hhj_GXVGGkWoSEPZBpgNu_c4FxRnbp-xV3rs,5230
@@ -26,7 +26,7 @@ execsql/db/postgres.py,sha256=Oq881tnKgOzO0nbRmzyM_Ym8twu7QlPsRjXpHz20wpk,21012
26
26
  execsql/db/sqlite.py,sha256=0aPomjNsyXww6lY039GuZufTp2T4Eyw8wdJjmXebNsE,10378
27
27
  execsql/db/sqlserver.py,sha256=XbhprA1URZYPmSIMHEAGNdCbxZBs3yy5Kb6IvDxbACU,7597
28
28
  execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
29
- execsql/debug/repl.py,sha256=HeQ9emFKUjo7UTouxuTcmpGCTJIR1nOLxKkRJ5mvd2c,16669
29
+ execsql/debug/repl.py,sha256=ngUVuQjey7TilSMumMqOouUhzx3Zx7W5rKCqZ5XoLnY,17227
30
30
  execsql/exporters/__init__.py,sha256=-Cnji-OgodJV8ftcDcOyTof0kQMy9J5kKVC8GVFpc3o,670
31
31
  execsql/exporters/base.py,sha256=W9USFyk_2eztjJ51X6CJh7-chE1i3cSx-STOtbHXCNI,6373
32
32
  execsql/exporters/delimited.py,sha256=URvEQo1IRF_tfdVHL3uBwEonihC-XfDm0f1argQPf4M,32088
@@ -53,7 +53,7 @@ execsql/gui/__init__.py,sha256=oCb-cyhLZzVpWJ4WU5HbqEDBrV-lm0ytEwxemrOZyqs,2048
53
53
  execsql/gui/base.py,sha256=15wCaSLvSb21hToCczru46ghmd56ryMpEOOZ0R_dE3U,12740
54
54
  execsql/gui/console.py,sha256=pCBUcFGjlKXMkMjztbmt9glP3me9jAKAgQxnmUE38-0,19396
55
55
  execsql/gui/desktop.py,sha256=Cz4Epr2H4tXMbjGVda0VcVYjEmtW6S394GBSCdsfMJw,54708
56
- execsql/gui/tui.py,sha256=dsy-YHWUoFgIPz-r5KEG6srWEyu8rIviSkrkwuMM9no,63425
56
+ execsql/gui/tui.py,sha256=INBCuW6iEEV15P_JrHeviI1JlNCXfoiyPBU-IC5MaLY,65200
57
57
  execsql/importers/__init__.py,sha256=dDsxSVeQYXBvm9yGqN3QswyGbLWTwt08pvUuRJgZhl4,289
58
58
  execsql/importers/base.py,sha256=FGVz3ntN6xHL99rQixlQj3tAf570K_oU83UtbYE1lJg,4124
59
59
  execsql/importers/csv.py,sha256=GUVRP294vHlOlF8XNecPEzatUBOOFIqnrEV9cBQkiv0,4849
@@ -62,7 +62,7 @@ execsql/importers/json.py,sha256=Z7QJZQ9fyqaxFxjfqcfZoaoW2GSZt-DMqW5LZiEfyNs,468
62
62
  execsql/importers/ods.py,sha256=MJsdsjropzCvxAA3DDZfAL_AnmZ4yij7DnrjGyDJqHQ,2843
63
63
  execsql/importers/xls.py,sha256=e0Zfe47ZiCpA1Ae3XDJ1ko3sCiH3-8U6XLKi6NvD0jQ,3683
64
64
  execsql/metacommands/__init__.py,sha256=3Kz-VasFS9B-C-UdHOjr3RMXjheMeYHe6qYBwp5e7wE,11434
65
- execsql/metacommands/conditions.py,sha256=V7t1Xtmve5UOaPWHFjAz93uW3rYMFejvA2gNh8YABvs,28602
65
+ execsql/metacommands/conditions.py,sha256=B6fBumkqoPO4wcQbw_ypYITaSnzPemAA1g5GrNLNfD0,29204
66
66
  execsql/metacommands/connect.py,sha256=U17ByjVmwrLN5DB7Ea9lTLyFJr8On2c9LfPO1Wc4l4U,14890
67
67
  execsql/metacommands/control.py,sha256=PAZFK1ck5SDSm5QdFV1ctif3KpEiyYWIXdDceRWgQ6k,8513
68
68
  execsql/metacommands/data.py,sha256=tRQBGTAuW-eJ2tBNWaoZI9OjTyNNyHJISo7gOdL-sm8,11370
@@ -76,7 +76,7 @@ execsql/metacommands/io_write.py,sha256=NpL2aYGfBpbqmPpYsqniYltYfd_SCA1EQz3_4qSd
76
76
  execsql/metacommands/prompt.py,sha256=E2e7q4pxbl_wEBrhco0B2gm5hO_HG3rNIF75PLdTgGg,36767
77
77
  execsql/metacommands/script_ext.py,sha256=TUgAldB2LSJAwZrCvDDi804hQ1d9BDQD2GDqHNPVOcM,2280
78
78
  execsql/metacommands/system.py,sha256=azRbv_P8l0t8BkDM9bmAUkhpnLSLHSCcmByqs-a3FxQ,7352
79
- execsql/metacommands/upsert.py,sha256=imXKk2A9TuG4rS_mr40_g2A1IY9iOMKhFYkAm4n3dFk,19214
79
+ execsql/metacommands/upsert.py,sha256=DxtAhGpC6qD--sk932u40cbfQfvFlV-SXnV7WUTwk_8,19858
80
80
  execsql/script/__init__.py,sha256=HbVQmQEVn4gBtzwy5_nlbDGuRnbWd4dI4nG-q1KyBxs,3498
81
81
  execsql/script/control.py,sha256=s-1eZdGARM6H1FwZ6VDdO_f50j7bvvRtTHesfUm9tbc,6144
82
82
  execsql/script/engine.py,sha256=HDlwm3_EqU6A4Z4O27dqQRvkpIACUuzYh3dZZAc8C98,43130
@@ -87,30 +87,30 @@ execsql/utils/crypto.py,sha256=2OnBWwn9bCBGc1ZkyRv16TvhottoCNYtXqgbE3mG3Sg,2960
87
87
  execsql/utils/datetime.py,sha256=rMCXAbvj6bxKCYzC97vrludO6PU5DYQ39buZ0smDC5A,3573
88
88
  execsql/utils/errors.py,sha256=cIYU7mKgjrRy5D2D4aDFHsH__lToz4Nv3fLQ62R1V2M,7786
89
89
  execsql/utils/fileio.py,sha256=3HDm6QFGeXwFMmCsbea4H5Ub6u2Jrx-oNsWA3Q5aytQ,23860
90
- execsql/utils/gui.py,sha256=eZeFJm8EaWnzeHIw_O-tn9hO8sxGjZRX_aUFDtGQp4w,18396
90
+ execsql/utils/gui.py,sha256=h_7V2zYfp0l6C3Ft8QtUJAPgxCUrFkgIqwimz5s1bbQ,22821
91
91
  execsql/utils/mail.py,sha256=Sd7vWj-dz3w0XDSFU9PM8gmy41pojk-Vsgbfven2DMk,5786
92
92
  execsql/utils/numeric.py,sha256=xh02ANSRk3nUpQ-rtm66ILoMqoi7HtzCoRMIOT9U8QI,1570
93
93
  execsql/utils/regex.py,sha256=diEzTZqU_HHwVMadPAvN1Vgzhl7I03eVaEFGCXyGGL8,3770
94
94
  execsql/utils/strings.py,sha256=5Dvzrk-9SIw2lpxXZQkiJbNyo1sy7iXXAtSULlZ0KG8,8488
95
95
  execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
96
- execsql2-2.15.7.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
97
- execsql2-2.15.7.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
98
- execsql2-2.15.7.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
99
- execsql2-2.15.7.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
100
- execsql2-2.15.7.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
101
- execsql2-2.15.7.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
102
- execsql2-2.15.7.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
103
- execsql2-2.15.7.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
104
- execsql2-2.15.7.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
105
- execsql2-2.15.7.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
106
- execsql2-2.15.7.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
107
- execsql2-2.15.7.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
108
- execsql2-2.15.7.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
109
- execsql2-2.15.7.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
110
- execsql2-2.15.7.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
111
- execsql2-2.15.7.dist-info/METADATA,sha256=EJ8BkbT-M-KMojwtJD1l7vTvSWj7tFBJ-JQ4XTZWC-s,18114
112
- execsql2-2.15.7.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
113
- execsql2-2.15.7.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
114
- execsql2-2.15.7.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
115
- execsql2-2.15.7.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
116
- execsql2-2.15.7.dist-info/RECORD,,
96
+ execsql2-2.15.11.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
97
+ execsql2-2.15.11.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
98
+ execsql2-2.15.11.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
99
+ execsql2-2.15.11.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
100
+ execsql2-2.15.11.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
101
+ execsql2-2.15.11.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
102
+ execsql2-2.15.11.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
103
+ execsql2-2.15.11.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
104
+ execsql2-2.15.11.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
105
+ execsql2-2.15.11.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
106
+ execsql2-2.15.11.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
107
+ execsql2-2.15.11.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
108
+ execsql2-2.15.11.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
109
+ execsql2-2.15.11.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
110
+ execsql2-2.15.11.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
111
+ execsql2-2.15.11.dist-info/METADATA,sha256=ClzgnoVBb7i4U2HC5k3bAycug19JGF6loGfMHDrzXJA,18221
112
+ execsql2-2.15.11.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
113
+ execsql2-2.15.11.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
114
+ execsql2-2.15.11.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
115
+ execsql2-2.15.11.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
116
+ execsql2-2.15.11.dist-info/RECORD,,