execsql2 2.11.1__py3-none-any.whl → 2.12.1__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/cli/__init__.py +6 -0
  2. execsql/cli/run.py +15 -10
  3. execsql/db/base.py +37 -23
  4. execsql/debug/__init__.py +6 -0
  5. execsql/debug/repl.py +472 -0
  6. execsql/metacommands/__init__.py +1 -1
  7. execsql/metacommands/dispatch.py +1 -1
  8. execsql/script/engine.py +15 -6
  9. execsql/script/variables.py +2 -25
  10. {execsql2-2.11.1.dist-info → execsql2-2.12.1.dist-info}/METADATA +1 -1
  11. {execsql2-2.11.1.dist-info → execsql2-2.12.1.dist-info}/RECORD +30 -29
  12. execsql/metacommands/debug_repl.py +0 -289
  13. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/README.md +0 -0
  14. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  15. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  16. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/execsql.conf +0 -0
  17. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
  18. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/md_compare.sql +0 -0
  19. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
  20. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
  21. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
  22. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  23. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  24. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/script_template.sql +0 -0
  25. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
  26. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  27. {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  28. {execsql2-2.11.1.dist-info → execsql2-2.12.1.dist-info}/WHEEL +0 -0
  29. {execsql2-2.11.1.dist-info → execsql2-2.12.1.dist-info}/entry_points.txt +0 -0
  30. {execsql2-2.11.1.dist-info → execsql2-2.12.1.dist-info}/licenses/LICENSE.txt +0 -0
  31. {execsql2-2.11.1.dist-info → execsql2-2.12.1.dist-info}/licenses/NOTICE +0 -0
execsql/cli/__init__.py CHANGED
@@ -278,6 +278,11 @@ def main(
278
278
  "--profile",
279
279
  help="Record per-statement execution times and print a timing summary after the script completes.",
280
280
  ),
281
+ profile_limit: int = typer.Option(
282
+ 20,
283
+ "--profile-limit",
284
+ help="Number of top statements to show in the --profile timing summary (default: 20).",
285
+ ),
281
286
  debug: bool = typer.Option(
282
287
  False,
283
288
  "--debug",
@@ -449,6 +454,7 @@ def main(
449
454
  output_dir=output_dir,
450
455
  progress=progress,
451
456
  profile=profile,
457
+ profile_limit=profile_limit,
452
458
  ping=ping,
453
459
  lint=lint,
454
460
  debug=debug,
execsql/cli/run.py CHANGED
@@ -71,12 +71,14 @@ def _print_dry_run(cmdlist: object) -> None:
71
71
  # ---------------------------------------------------------------------------
72
72
 
73
73
 
74
- def _print_profile(profile_data: list[tuple]) -> None:
74
+ def _print_profile(profile_data: list[tuple], limit: int = 20) -> None:
75
75
  """Print a per-statement timing summary to stdout.
76
76
 
77
77
  Args:
78
78
  profile_data: List of ``(source, line_no, command_type, elapsed_secs,
79
79
  command_text_preview)`` tuples collected during execution.
80
+ limit: Maximum number of top statements to display (default: 20).
81
+ All statements are included in totals regardless of this value.
80
82
  """
81
83
  if not profile_data:
82
84
  _console.print("[dim]Profile: no statements recorded.[/dim]")
@@ -85,9 +87,9 @@ def _print_profile(profile_data: list[tuple]) -> None:
85
87
  total_secs = sum(row[3] for row in profile_data)
86
88
  n = len(profile_data)
87
89
 
88
- # Sort descending by elapsed time; show top 20 (or all if <= 20).
90
+ # Sort descending by elapsed time; show top `limit` (or all if <= limit).
89
91
  sorted_data = sorted(profile_data, key=lambda r: r[3], reverse=True)
90
- display = sorted_data[:20]
92
+ display = sorted_data[:limit]
91
93
 
92
94
  _console.print()
93
95
  _console.print(f"[bold cyan]Profile:[/bold cyan] {n} statement{'s' if n != 1 else ''} in {total_secs:.3f}s")
@@ -115,10 +117,10 @@ def _print_profile(profile_data: list[tuple]) -> None:
115
117
  f"{preview_short}",
116
118
  )
117
119
 
118
- if len(sorted_data) > 20:
119
- omitted = len(sorted_data) - 20
120
+ if len(sorted_data) > limit:
121
+ omitted = len(sorted_data) - limit
120
122
  _console.print(
121
- f"[dim] ... {omitted} more statement{'s' if omitted != 1 else ''} not shown (top 20 by time)[/dim]",
123
+ f"[dim] ... {omitted} more statement{'s' if omitted != 1 else ''} not shown (top {limit} by time)[/dim]",
122
124
  )
123
125
 
124
126
  _console.print()
@@ -213,6 +215,7 @@ def _run(
213
215
  output_dir: str | None = None,
214
216
  progress: bool = False,
215
217
  profile: bool = False,
218
+ profile_limit: int = 20,
216
219
  ping: bool = False,
217
220
  lint: bool = False,
218
221
  debug: bool = False,
@@ -552,7 +555,7 @@ def _run(
552
555
  if debug:
553
556
  _state.step_mode = True
554
557
 
555
- _execute_script_direct(conf, profile=profile)
558
+ _execute_script_direct(conf, profile=profile, profile_limit=profile_limit)
556
559
 
557
560
 
558
561
  # ---------------------------------------------------------------------------
@@ -611,7 +614,7 @@ def _execute_script_textual_console(conf: ConfigData) -> None:
611
614
  _state.exec_log.log_exit_end()
612
615
 
613
616
 
614
- def _execute_script_direct(conf: ConfigData, *, profile: bool = False) -> None:
617
+ def _execute_script_direct(conf: ConfigData, *, profile: bool = False, profile_limit: int = 20) -> None:
615
618
  """Run runscripts() in the current (main) thread — used when Textual is not active.
616
619
 
617
620
  Args:
@@ -619,6 +622,8 @@ def _execute_script_direct(conf: ConfigData, *, profile: bool = False) -> None:
619
622
  profile: When ``True``, print a per-statement timing summary after the
620
623
  script completes. Timing data must already have been activated on
621
624
  ``_state.profile_data`` before this function is called.
625
+ profile_limit: Maximum number of top statements to display in the
626
+ profile summary (default: 20).
622
627
  """
623
628
  import execsql.state as _state
624
629
  import execsql.utils.gui as _gui
@@ -645,7 +650,7 @@ def _execute_script_direct(conf: ConfigData, *, profile: bool = False) -> None:
645
650
  gui_console_off()
646
651
  _state.exec_log.log_status_info(f"{_state.cmds_run} commands run")
647
652
  if profile and _state.profile_data is not None:
648
- _print_profile(_state.profile_data)
653
+ _print_profile(_state.profile_data, limit=profile_limit)
649
654
  sys.exit(exc.code)
650
655
  except ConfigError:
651
656
  raise
@@ -673,7 +678,7 @@ def _execute_script_direct(conf: ConfigData, *, profile: bool = False) -> None:
673
678
  gui_console_off()
674
679
  _state.exec_log.log_status_info(f"{_state.cmds_run} commands run")
675
680
  if profile and _state.profile_data is not None:
676
- _print_profile(_state.profile_data)
681
+ _print_profile(_state.profile_data, limit=profile_limit)
677
682
  _state.exec_log.log_exit_end()
678
683
 
679
684
 
execsql/db/base.py CHANGED
@@ -234,18 +234,22 @@ class Database(ABC):
234
234
  pass # Non-critical: some drivers lack rowcount support.
235
235
 
236
236
  def decode_row() -> Generator:
237
- while True:
238
- rows = curs.fetchmany()
239
- if not rows:
240
- break
241
- else:
242
- for row in rows:
243
- if self.encoding:
244
- yield [
245
- c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c for c in row
246
- ]
247
- else:
248
- yield row
237
+ try:
238
+ while True:
239
+ rows = curs.fetchmany()
240
+ if not rows:
241
+ break
242
+ else:
243
+ for row in rows:
244
+ if self.encoding:
245
+ yield [
246
+ c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c
247
+ for c in row
248
+ ]
249
+ else:
250
+ yield row
251
+ finally:
252
+ curs.close()
249
253
 
250
254
  return [d[0] for d in curs.description], decode_row()
251
255
 
@@ -253,6 +257,10 @@ class Database(ABC):
253
257
  """Execute *sql* and return ``(column_names, row_iterator)`` where each row is a ``dict``."""
254
258
  # Return an iterable that yields dictionaries of row data
255
259
  curs = self.cursor()
260
+ try:
261
+ curs.arraysize = _state.conf.export_row_buffer
262
+ except Exception:
263
+ pass # Non-critical: not all drivers support arraysize.
256
264
  try:
257
265
  curs.execute(sql)
258
266
  except Exception:
@@ -264,18 +272,24 @@ class Database(ABC):
264
272
  pass # Non-critical: some drivers lack rowcount support.
265
273
  hdrs = [d[0] for d in curs.description]
266
274
 
267
- def dict_row() -> dict | None:
268
- row = curs.fetchone()
269
- if row:
270
- if self.encoding:
271
- r = [c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c for c in row]
272
- else:
273
- r = row
274
- return dict(zip(hdrs, r))
275
- else:
276
- return None
275
+ def dict_rows() -> Generator:
276
+ try:
277
+ while True:
278
+ rows = curs.fetchmany()
279
+ if not rows:
280
+ break
281
+ for row in rows:
282
+ if self.encoding:
283
+ r = [
284
+ c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c for c in row
285
+ ]
286
+ else:
287
+ r = row
288
+ yield dict(zip(hdrs, r))
289
+ finally:
290
+ curs.close()
277
291
 
278
- return hdrs, iter(dict_row, None)
292
+ return hdrs, dict_rows()
279
293
 
280
294
  def schema_exists(self, schema_name: str) -> bool:
281
295
  """Return ``True`` if *schema_name* exists in this database."""
@@ -0,0 +1,6 @@
1
+ """Debug utilities for execsql.
2
+
3
+ This package contains interactive debugging tools for execsql scripts:
4
+
5
+ - :mod:`execsql.debug.repl` — the ``BREAKPOINT`` metacommand REPL.
6
+ """
execsql/debug/repl.py ADDED
@@ -0,0 +1,472 @@
1
+ from __future__ import annotations
2
+
3
+ """Interactive debug REPL metacommand handler for execsql.
4
+
5
+ Implements ``x_breakpoint`` — the ``BREAKPOINT`` metacommand — which pauses
6
+ script execution and drops into an interactive read-eval-print loop.
7
+
8
+ The REPL allows the user to:
9
+
10
+ - Inspect and print substitution variables.
11
+ - Run ad-hoc SQL queries against the current database.
12
+ - Step through the script one statement at a time.
13
+ - Resume or abort execution.
14
+
15
+ All REPL commands are dot-prefixed (``.continue``, ``.vars``, ``.next``)
16
+ to avoid ambiguity with variable names and SQL. Anything not starting
17
+ with ``.`` is treated as either a variable lookup (if it matches a known
18
+ variable) or SQL (if it ends with ``;``).
19
+
20
+ In non-interactive environments (CI, piped input, ``sys.stdin.isatty()`` is
21
+ ``False``) the metacommand is silently skipped so automated pipelines are not
22
+ blocked.
23
+ """
24
+
25
+ import os
26
+ import sys
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ import execsql.state as _state
31
+
32
+ __all__ = ["x_breakpoint"]
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # ANSI color support — auto-detected, respects NO_COLOR / EXECSQL_NO_COLOR
36
+ # ---------------------------------------------------------------------------
37
+
38
+ _RESET = "\033[0m"
39
+ _BOLD = "\033[1m"
40
+ _DIM = "\033[2m"
41
+ _ITALIC = "\033[3m"
42
+ _RED = "\033[31m"
43
+ _GREEN = "\033[32m"
44
+ _YELLOW = "\033[33m"
45
+ _CYAN = "\033[36m"
46
+
47
+
48
+ def _use_color() -> bool:
49
+ """Return True if the output stream supports ANSI color.
50
+
51
+ Checks ``NO_COLOR`` and ``EXECSQL_NO_COLOR`` environment variables first
52
+ (either set → color off). Then tests whether the active output stream
53
+ reports itself as a TTY.
54
+ """
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()
65
+
66
+
67
+ def _c(code: str, text: str) -> str:
68
+ """Wrap *text* in an ANSI escape sequence when color is enabled.
69
+
70
+ Returns plain *text* unchanged when ``_use_color()`` is False so that
71
+ tests and non-TTY environments receive undecorated strings.
72
+
73
+ Args:
74
+ code: One or more ANSI escape codes (e.g. ``_BOLD + _CYAN``).
75
+ text: The text to colorize.
76
+
77
+ Returns:
78
+ ``f"{code}{text}{_RESET}"`` when color is on, else *text* unchanged.
79
+ """
80
+ if not _use_color():
81
+ return text
82
+ return f"{code}{text}{_RESET}"
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Public handler
87
+ # ---------------------------------------------------------------------------
88
+
89
+ _HELP_COMMANDS = [
90
+ (".continue", ".c", "Resume script execution"),
91
+ (".abort", ".q", "Halt the script (exit 1)"),
92
+ (".vars", ".v", "List user, system, local, and counter variables"),
93
+ (".vars all", ".v all", "Include environment variables (&) in the listing"),
94
+ (".next", ".n", "Execute the next statement then pause again (step mode)"),
95
+ (".where", ".w", "Show the current script location and upcoming statement"),
96
+ (".stack", "", "Show the command-list stack (script name, line, depth)"),
97
+ (".set VAR VAL", ".s", "Set or update a substitution variable"),
98
+ (".help", ".h", "Show this help text"),
99
+ ]
100
+
101
+ _HELP_OTHER = [
102
+ ("varname", "Print a variable's value (e.g. logfile, $ARG_1, &HOME)"),
103
+ ("SELECT ...;", "Run SQL ending with ';' (expects columns returned, e.g. SELECT)"),
104
+ ]
105
+
106
+ _HELP_CMD_WIDTH = 13 # width of the command column
107
+ _HELP_SHORT_WIDTH = 7 # width of the shortcut column
108
+
109
+
110
+ def _format_help() -> str:
111
+ """Build the help text with optional ANSI color."""
112
+ lines: list[str] = []
113
+ lines.append(f"{_c(_BOLD, 'execsql debug REPL')} {_c(_DIM, '— all commands start with')} {_c(_CYAN, '.')}")
114
+ lines.append("")
115
+ for cmd, short, desc in _HELP_COMMANDS:
116
+ cmd_col = _c(_CYAN, cmd.ljust(_HELP_CMD_WIDTH))
117
+ short_col = _c(_CYAN, short.ljust(_HELP_SHORT_WIDTH)) if short else " " * _HELP_SHORT_WIDTH
118
+ lines.append(f" {cmd_col} {short_col} {desc}")
119
+ lines.append("")
120
+ lines.append(f"{_c(_BOLD, 'Everything else:')}")
121
+ for cmd, desc in _HELP_OTHER:
122
+ cmd_col = _c(_DIM, cmd.ljust(_HELP_CMD_WIDTH + _HELP_SHORT_WIDTH + 2))
123
+ lines.append(f" {cmd_col} {desc}")
124
+ lines.append("")
125
+ return "\n".join(lines) + "\n"
126
+
127
+
128
+ _WHERE_TRUNCATE = 120
129
+
130
+
131
+ def x_breakpoint(**kwargs: Any) -> None:
132
+ """Pause execution and enter the interactive debug REPL.
133
+
134
+ If ``sys.stdin`` is not a TTY (CI, piped input), the metacommand is
135
+ silently skipped — scripts will not hang in automation.
136
+
137
+ Args:
138
+ **kwargs: Keyword arguments injected by the dispatch table (unused).
139
+ """
140
+ if not sys.stdin.isatty():
141
+ return
142
+ _debug_repl()
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # REPL core
147
+ # ---------------------------------------------------------------------------
148
+
149
+
150
+ def _write_rule(label: str) -> None:
151
+ """Print a horizontal rule with an embedded label.
152
+
153
+ The rule is ``──<label>`` followed by a fixed-width suffix of dashes,
154
+ giving a consistent visual separator regardless of terminal width.
155
+
156
+ Args:
157
+ label: Text (may include ANSI codes) to embed in the rule.
158
+ """
159
+ _write(f"{_c(_DIM, '──')}{label}{_c(_DIM, '─' * 40)}\n")
160
+
161
+
162
+ def _debug_repl(*, step: bool = False) -> None:
163
+ """Interactive read-eval-print loop for script debugging.
164
+
165
+ Reads commands from stdin until the user types ``.continue`` or ``.abort``,
166
+ or until EOF / KeyboardInterrupt.
167
+
168
+ Args:
169
+ step: When ``True``, the entry banner says "Step" instead of
170
+ "Breakpoint" to indicate the REPL was re-entered via step mode.
171
+ """
172
+ try:
173
+ import readline as _readline # noqa: F401 — side-effect: enables history/arrow keys
174
+ except ImportError:
175
+ pass # readline not available on Windows; continue without it
176
+
177
+ label_word = "Step" if step else "Breakpoint"
178
+ lc = _state.last_command
179
+ if lc is not None:
180
+ location = f"{Path(lc.source).name}:{lc.line_no}"
181
+ type_tag = lc.command_type
182
+ text = lc.command.commandline()
183
+ if len(text) > _WHERE_TRUNCATE:
184
+ text = text[:_WHERE_TRUNCATE] + "..."
185
+ rule_label = f" {_c(_BOLD + _YELLOW, label_word)} {_c(_DIM, '──')} {_c(_CYAN, location)} "
186
+ else:
187
+ type_tag = None
188
+ text = None
189
+ location = "(position unknown)"
190
+ rule_label = f" {_c(_BOLD + _YELLOW, label_word)} "
191
+
192
+ _write("\n")
193
+ _write_rule(rule_label)
194
+ if type_tag and text:
195
+ _write(f" {_c(_DIM + _GREEN, '(' + type_tag + ')')} {text}\n")
196
+ _hint_help = _c(_DIM, "'.help'")
197
+ _hint_c = _c(_DIM, "'.c'")
198
+ _write(f" Type {_hint_help} for commands, {_hint_c} to resume.\n\n")
199
+
200
+ while True:
201
+ try:
202
+ line = input("execsql debug> ").strip()
203
+ except EOFError:
204
+ _write("\n")
205
+ return # Ctrl-D → continue
206
+ except KeyboardInterrupt:
207
+ _write("\n")
208
+ return # Ctrl-C → continue
209
+
210
+ if not line:
211
+ continue
212
+
213
+ # Dot-prefixed → REPL command
214
+ if line.startswith("."):
215
+ cmd = line[1:].strip().lower()
216
+ _handle_dot_command(line)
217
+ if cmd in ("continue", "c"):
218
+ return
219
+ if cmd in ("abort", "q", "quit"):
220
+ # _handle_dot_command already raised SystemExit, but guard anyway
221
+ return
222
+ if cmd in ("next", "n"):
223
+ return
224
+ continue
225
+
226
+ # SQL (ends with semicolon)
227
+ if line.rstrip().endswith(";"):
228
+ _run_sql(line)
229
+ continue
230
+
231
+ # Everything else → variable lookup
232
+ _print_var(line)
233
+
234
+
235
+ def _handle_dot_command(line: str) -> None:
236
+ """Dispatch a dot-prefixed REPL command."""
237
+ # Strip the leading dot and normalize
238
+ cmd = line[1:].strip().lower()
239
+
240
+ if cmd in ("continue", "c"):
241
+ return # caller checks and returns from _debug_repl
242
+ elif cmd in ("abort", "q", "quit"):
243
+ raise SystemExit(1)
244
+ elif cmd in ("help", "h"):
245
+ _write(_format_help())
246
+ elif cmd in ("vars all", "v all"):
247
+ _print_all_vars(include_env=True)
248
+ elif cmd in ("vars", "v"):
249
+ _print_all_vars()
250
+ elif cmd in ("where", "w"):
251
+ _print_where()
252
+ elif cmd == "stack":
253
+ _print_stack()
254
+ elif cmd in ("next", "n"):
255
+ _enable_step_mode()
256
+ elif cmd.startswith("set ") or cmd == "set":
257
+ # .set VAR VAL — set or update a substitution variable
258
+ rest = cmd[4:].strip() if cmd.startswith("set ") else ""
259
+ if not rest:
260
+ _write(" Usage: .set VAR VALUE\n")
261
+ else:
262
+ parts = rest.split(None, 1)
263
+ varname = parts[0]
264
+ value = parts[1] if len(parts) > 1 else ""
265
+ _set_var(varname, value)
266
+ elif cmd.startswith("s ") or cmd == "s":
267
+ # .s VAR VAL — shorthand for .set
268
+ rest = cmd[2:].strip() if cmd.startswith("s ") else ""
269
+ if not rest:
270
+ _write(" Usage: .s VAR VALUE\n")
271
+ else:
272
+ parts = rest.split(None, 1)
273
+ varname = parts[0]
274
+ value = parts[1] if len(parts) > 1 else ""
275
+ _set_var(varname, value)
276
+ else:
277
+ _write(f" {_c(_RED, 'Unknown command:')} {line!r}. Type '.help' for available commands.\n")
278
+
279
+
280
+ # ---------------------------------------------------------------------------
281
+ # REPL command implementations
282
+ # ---------------------------------------------------------------------------
283
+
284
+
285
+ def _write(text: str) -> None:
286
+ """Write *text* to the execsql output stream (falls back to stdout)."""
287
+ output = _state.output
288
+ if output is not None:
289
+ output.write(text)
290
+ else:
291
+ sys.stdout.write(text)
292
+ sys.stdout.flush()
293
+
294
+
295
+ def _print_where() -> None:
296
+ """Print the current script location and the upcoming statement.
297
+
298
+ Reads ``_state.last_command`` (a :class:`ScriptCmd`) and displays the
299
+ filename, line number, command type, and (truncated) statement text.
300
+ If ``last_command`` is ``None``, reports that the position is unknown.
301
+ """
302
+ lc = _state.last_command
303
+ if lc is None:
304
+ _write(" (position unknown)\n")
305
+ return
306
+ filename = Path(lc.source).name
307
+ location = f"{filename}:{lc.line_no}"
308
+ rule_label = f" {_c(_BOLD + _YELLOW, 'Location')} {_c(_DIM, '──')} {_c(_CYAN, location)} "
309
+ _write_rule(rule_label)
310
+ text = lc.command.commandline()
311
+ if len(text) > _WHERE_TRUNCATE:
312
+ text = text[:_WHERE_TRUNCATE] + "..."
313
+ _write(f" {_c(_DIM + _GREEN, '(' + lc.command_type + ')')} {text}\n\n")
314
+
315
+
316
+ def _print_all_vars(*, include_env: bool = False) -> None:
317
+ """Print substitution variables grouped by type."""
318
+ subvars = _state.subvars
319
+ if subvars is None:
320
+ _write(" (no substitution variables defined)\n\n")
321
+ return
322
+ items = subvars.substitutions # list of (name, value) tuples
323
+ if not items:
324
+ _write(" (no substitution variables defined)\n\n")
325
+ return
326
+
327
+ # Group by prefix.
328
+ user_vars: list[tuple[str, str]] = []
329
+ system_vars: list[tuple[str, str]] = []
330
+ counter_vars: list[tuple[str, str]] = []
331
+ local_vars: list[tuple[str, str]] = []
332
+ env_vars: list[tuple[str, str]] = []
333
+
334
+ for name, value in sorted(items):
335
+ if name.startswith("&"):
336
+ env_vars.append((name, value))
337
+ elif name.startswith("~"):
338
+ local_vars.append((name, value))
339
+ elif name.startswith("@"):
340
+ counter_vars.append((name, value))
341
+ elif name.startswith("$"):
342
+ system_vars.append((name, value))
343
+ else:
344
+ user_vars.append((name, value))
345
+
346
+ _write_rule(f" {_c(_BOLD + _YELLOW, 'Variables')} ")
347
+
348
+ def _print_group(label: str, group: list[tuple[str, str]]) -> None:
349
+ if not group:
350
+ return
351
+ _write(f" {_c(_BOLD, label)}:\n")
352
+ max_name = max(len(n) for n, _ in group)
353
+ for name, value in group:
354
+ _write(f" {_c(_CYAN, name):<{max_name}} {_c(_DIM, '=')} {value}\n")
355
+
356
+ _print_group("User", user_vars)
357
+ _print_group("System ($)", system_vars)
358
+ _print_group("Local (~)", local_vars)
359
+ _print_group("Counter (@)", counter_vars)
360
+ if include_env:
361
+ _print_group("Environment (&)", env_vars)
362
+
363
+ if not any([user_vars, system_vars, local_vars, counter_vars]):
364
+ if env_vars:
365
+ _write(" (no script variables defined — use '.vars all' to see environment variables)\n")
366
+ else:
367
+ _write(" (no variables defined)\n")
368
+ _write("\n")
369
+
370
+
371
+ def _print_var(varname: str) -> None:
372
+ """Print the value of a single substitution variable.
373
+
374
+ Tries the name as typed, then with the sigil prefix stripped.
375
+ """
376
+ subvars = _state.subvars
377
+ if subvars is None:
378
+ _write(f" {varname}: (substitution variables not initialised)\n")
379
+ return
380
+ # Try the name as typed first, then without the sigil prefix ($, &, @, #, ~).
381
+ # SUB creates variables without a prefix (e.g., "logfile"), but users
382
+ # may type "$logfile" at the prompt.
383
+ value = subvars.varvalue(varname)
384
+ if value is None and len(varname) > 1 and varname[0] in "$&@#~":
385
+ value = subvars.varvalue(varname[1:])
386
+ if value is None:
387
+ _write(f" {_c(_CYAN, varname)}: {_c(_DIM, '(undefined)')}\n")
388
+ else:
389
+ _write(f" {_c(_CYAN, varname)} {_c(_DIM, '=')} {value}\n")
390
+
391
+
392
+ def _print_stack() -> None:
393
+ """Print the current command-list stack (script name, line number, depth)."""
394
+ stack = _state.commandliststack
395
+ if not stack:
396
+ _write(" (command list stack is empty)\n\n")
397
+ return
398
+ _write_rule(f" {_c(_BOLD + _YELLOW, 'Stack')} ")
399
+ _write(f" {_c(_DIM, 'depth:')} {len(stack)}\n")
400
+ for depth, cmdlist in enumerate(stack):
401
+ listname = getattr(cmdlist, "listname", "<unknown>")
402
+ cmdptr = getattr(cmdlist, "cmdptr", 0)
403
+ _write(f" [{depth}] {listname} {_c(_DIM, f'(cursor at index {cmdptr})')}\n")
404
+ _write("\n")
405
+
406
+
407
+ def _run_sql(sql: str) -> None:
408
+ """Execute ad-hoc SQL against the current database and pretty-print the results."""
409
+ dbs = _state.dbs
410
+ if dbs is None:
411
+ _write(" (no database connection is active)\n")
412
+ return
413
+ db = dbs.current()
414
+ if db is None:
415
+ _write(" (no database connection is active)\n")
416
+ return
417
+ try:
418
+ colnames, rows = db.select_data(sql)
419
+ except Exception as exc:
420
+ _write(f" {_c(_RED, 'SQL error:')} {exc}\n")
421
+ return
422
+
423
+ if not colnames:
424
+ _write(" (query returned no columns)\n")
425
+ return
426
+
427
+ # Build a simple text table.
428
+ col_widths = [len(c) for c in colnames]
429
+ str_rows: list[list[str]] = []
430
+ for row in rows:
431
+ str_row = ["NULL" if v is None else str(v) for v in row]
432
+ str_rows.append(str_row)
433
+ for i, cell in enumerate(str_row):
434
+ col_widths[i] = max(col_widths[i], len(cell))
435
+
436
+ sep = _c(_DIM, "+-" + "-+-".join("-" * w for w in col_widths) + "-+")
437
+ # Header: column names in bold
438
+ header_cells = " | ".join(_c(_BOLD, c.ljust(col_widths[i])) for i, c in enumerate(colnames))
439
+ header = _c(_DIM, "| ") + header_cells + _c(_DIM, " |")
440
+ _write(" " + sep + "\n")
441
+ _write(" " + header + "\n")
442
+ _write(" " + sep + "\n")
443
+ for str_row in str_rows:
444
+ cells = " | ".join(
445
+ _c(_DIM + _ITALIC, "NULL".ljust(col_widths[i])) if cell == "NULL" else cell.ljust(col_widths[i])
446
+ for i, cell in enumerate(str_row)
447
+ )
448
+ data_line = _c(_DIM, "| ") + cells + _c(_DIM, " |")
449
+ _write(" " + data_line + "\n")
450
+ _write(" " + sep + "\n")
451
+ row_word = "row" if len(str_rows) == 1 else "rows"
452
+ _write(f" {_c(_DIM, f'({len(str_rows)} {row_word})')}\n")
453
+
454
+
455
+ def _enable_step_mode() -> None:
456
+ """Activate step mode so the engine re-enters the REPL after the next statement."""
457
+ _state.step_mode = True
458
+
459
+
460
+ def _set_var(varname: str, value: str) -> None:
461
+ """Set or update a substitution variable in the current session.
462
+
463
+ Args:
464
+ varname: The variable name (without sigil prefix for user variables).
465
+ value: The value to assign to the variable.
466
+ """
467
+ subvars = _state.subvars
468
+ if subvars is None:
469
+ _write(" Error: substitution variables are not initialised.\n")
470
+ return
471
+ subvars.add_substitution(varname, value)
472
+ _write(f" {_c(_CYAN, varname)} {_c(_DIM, '=')} {value}\n")
@@ -100,7 +100,7 @@ from execsql.metacommands.debug import (
100
100
  x_debug_write_subvars,
101
101
  x_debug_write_config,
102
102
  )
103
- from execsql.metacommands.debug_repl import x_breakpoint
103
+ from execsql.debug.repl import x_breakpoint
104
104
  from execsql.metacommands.io import (
105
105
  x_export,
106
106
  x_export_query,
@@ -100,7 +100,7 @@ from execsql.metacommands.debug import (
100
100
  x_debug_write_odbc_drivers,
101
101
  x_debug_write_subvars,
102
102
  )
103
- from execsql.metacommands.debug_repl import x_breakpoint
103
+ from execsql.debug.repl import x_breakpoint
104
104
  from execsql.metacommands.io import (
105
105
  x_cd,
106
106
  x_copy,
execsql/script/engine.py CHANGED
@@ -397,6 +397,15 @@ class ScriptCmd:
397
397
  self.line_no = command_line_no
398
398
  self.command_type = command_type
399
399
  self.command = script_command
400
+ # MIGRATION NOTE: differs from monolith (execsql.py) — source_dir and source_name are
401
+ # resolved once at construction rather than on every statement execution. For absolute
402
+ # paths (the common case) the result is identical. For relative paths the value is
403
+ # anchored to the CWD at script-load time rather than at each statement's execution time;
404
+ # the original per-statement resolve could yield inconsistent values across statements of
405
+ # the same script if a CD metacommand ran between them.
406
+ _p = Path(command_source_name)
407
+ self.source_dir: str = str(_p.resolve().parent) + os.sep
408
+ self.source_name: str = _p.name
400
409
 
401
410
  def __repr__(self) -> str:
402
411
  return f"ScriptCmd({self.source!r}, {self.line_no!r}, {self.command_type!r}, {repr(self.command)!r})"
@@ -498,16 +507,16 @@ class CommandList:
498
507
  _state.subvars.add_substitution("$CURRENT_SCRIPT", cmditem.source)
499
508
  _state.subvars.add_substitution(
500
509
  "$CURRENT_SCRIPT_PATH",
501
- str(Path(cmditem.source).resolve().parent) + os.sep,
510
+ cmditem.source_dir,
502
511
  )
503
- _state.subvars.add_substitution("$CURRENT_SCRIPT_NAME", Path(cmditem.source).name)
512
+ _state.subvars.add_substitution("$CURRENT_SCRIPT_NAME", cmditem.source_name)
504
513
  _state.subvars.add_substitution("$CURRENT_SCRIPT_LINE", str(cmditem.line_no))
505
514
  _state.subvars.add_substitution("$SCRIPT_LINE", str(cmditem.line_no))
506
515
  if _state.step_mode:
507
516
  _state.step_mode = False
508
- from execsql.metacommands.debug_repl import _debug_repl
517
+ from execsql.debug.repl import _debug_repl
509
518
 
510
- _debug_repl()
519
+ _debug_repl(step=True)
511
520
  _profiling = _state.profile_data is not None
512
521
  if _profiling:
513
522
  import time as _time
@@ -709,7 +718,7 @@ def set_system_vars() -> None:
709
718
  "ON" if _state.conf.gui_wait_on_error_halt else "OFF",
710
719
  )
711
720
  _state.subvars.add_substitution("$CONSOLE_WAIT_WHEN_DONE_STATE", "ON" if _state.conf.gui_wait_on_exit else "OFF")
712
- _state.subvars.add_substitution("$CURRENT_TIME", datetime.datetime.now().strftime("%Y-%m-%d %H:%M"))
721
+ # $CURRENT_TIME is set per-statement in run_and_increment() for accuracy.
713
722
  _state.subvars.add_substitution("$CURRENT_DIR", str(Path(".").resolve()))
714
723
  _state.subvars.add_substitution("$CURRENT_PATH", str(Path(".").resolve()) + os.sep)
715
724
  _state.subvars.add_substitution("$CURRENT_ALIAS", _state.dbs.current_alias())
@@ -742,7 +751,7 @@ def substitute_vars(command_str: str, localvars: SubVarSet | None = None) -> str
742
751
  subs = _state.subvars.merge(localvars)
743
752
  else:
744
753
  subs = _state.subvars
745
- cmdstr = copy.copy(command_str)
754
+ cmdstr = command_str
746
755
  subs_made = True
747
756
  iterations = 0
748
757
  while subs_made:
@@ -89,7 +89,6 @@ class SubVarSet:
89
89
  # compatibility with external code.
90
90
  def __init__(self) -> None:
91
91
  self._subs_dict: dict[str, Any] = {}
92
- self._compiled_patterns: dict[str, tuple] = {}
93
92
  self.prefix_list: list[str] = ["$", "&", "@"]
94
93
  # Don't construct/compile on init because deepcopy() can't handle compiled regexes.
95
94
  self.var_rx = None
@@ -106,21 +105,6 @@ class SubVarSet:
106
105
  self._subs_dict = dict(value)
107
106
  else:
108
107
  self._subs_dict = dict(value)
109
- self._rebuild_all_patterns()
110
-
111
- def _compile_patterns_for(self, varname: str) -> tuple:
112
- """Compile and return the three regex patterns (plain, single-quoted, double-quoted) for *varname*."""
113
- match_escaped = "\\" + varname if varname[0] == "$" else varname
114
- pat = re.compile(f"!!{match_escaped}!!", re.I)
115
- patq = re.compile(f"!'!{match_escaped}!'!", re.I)
116
- patdq = re.compile(f'!"!{match_escaped}!"!', re.I)
117
- return (pat, patq, patdq)
118
-
119
- def _rebuild_all_patterns(self) -> None:
120
- """Rebuild compiled patterns for every variable currently stored."""
121
- self._compiled_patterns = {}
122
- for varname in self._subs_dict:
123
- self._compiled_patterns[varname] = self._compile_patterns_for(varname)
124
108
 
125
109
  def compile_var_rx(self) -> None:
126
110
  """Compile the variable-name validation regex from the current prefix list."""
@@ -141,14 +125,12 @@ class SubVarSet:
141
125
  self.check_var_name(template_str)
142
126
  old_sub = template_str.lower()
143
127
  self._subs_dict.pop(old_sub, None)
144
- self._compiled_patterns.pop(old_sub, None)
145
128
 
146
129
  def add_substitution(self, varname: str, repl_str: Any) -> None:
147
- """Add or overwrite a substitution variable, compiling its match patterns."""
130
+ """Add or overwrite a substitution variable."""
148
131
  self.check_var_name(varname)
149
132
  varname = varname.lower()
150
133
  self._subs_dict[varname] = repl_str
151
- self._compiled_patterns[varname] = self._compile_patterns_for(varname)
152
134
 
153
135
  def append_substitution(self, varname: str, repl_str: str) -> None:
154
136
  self.check_var_name(varname)
@@ -186,15 +168,10 @@ class SubVarSet:
186
168
  return template_str.lower() in self._subs_dict
187
169
 
188
170
  def merge(self, other_subvars: SubVarSet | None) -> SubVarSet:
189
- """Return a new SubVarSet with this object's variables merged with other_subvars.
190
-
191
- Copies dictionaries and pre-compiled patterns directly instead of
192
- re-adding variables one at a time, avoiding O(V) regex recompilation.
193
- """
171
+ """Return a new SubVarSet with this object's variables merged with other_subvars."""
194
172
  if other_subvars is not None:
195
173
  newsubs = SubVarSet()
196
174
  newsubs._subs_dict = {**self._subs_dict, **other_subvars._subs_dict}
197
- newsubs._compiled_patterns = {**self._compiled_patterns, **other_subvars._compiled_patterns}
198
175
  newsubs.prefix_list = list(set(self.prefix_list + other_subvars.prefix_list))
199
176
  newsubs.compile_var_rx()
200
177
  return newsubs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.11.1
3
+ Version: 2.12.1
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: Repository, https://github.com/geocoug/execsql
6
6
  Project-URL: Issues, https://github.com/geocoug/execsql/issues
@@ -9,14 +9,14 @@ execsql/parser.py,sha256=mbNSMiAMR1NvNvFtQAZq6nxBOupMGJZXSimLWLtZeNs,15537
9
9
  execsql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  execsql/state.py,sha256=29b3SwG4GirED2KVQc-cCC7Z_-FsGN3fEt9xQNN-Puo,15090
11
11
  execsql/types.py,sha256=HVWb4umIB9lpxCGgqk3xy1hoGYPfN39xci5mHF0Izq4,31882
12
- execsql/cli/__init__.py,sha256=dPKq7KOY7soD1GfBEztoRWcKuDLY5QyhzgC6PuyeyII,16270
12
+ execsql/cli/__init__.py,sha256=YXxOVF2lNkCkifXyjoC7yWrhHJFT9PzI7cnCzsLJwT8,16488
13
13
  execsql/cli/dsn.py,sha256=svaZtrUXFRL2W5G6FRRiKtR6kehOp7urrVhIx_642Z8,2820
14
14
  execsql/cli/help.py,sha256=Sn_TgSJiQeBx-xZH0fuP5OvR_wasSTumjWF9UHfIX5k,5414
15
15
  execsql/cli/lint.py,sha256=XWuVcEsheZ8ql48VFWqICWEkAUezB2nIePX6SUiKSg8,16109
16
- execsql/cli/run.py,sha256=eiD_5R2ZL8WzhbW78DBcoSbcibmOXcr3po_aYYSoPwg,30250
16
+ execsql/cli/run.py,sha256=JGfndnBnJMkEqbz26pflhEdXDScZNIdGu6b6jTRLYl8,30681
17
17
  execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
18
18
  execsql/db/access.py,sha256=L79gUnAnnM9EJ_f4k42jr7DI0qGcKtLOnJTlBC7uPm0,17879
19
- execsql/db/base.py,sha256=hfMFj8fXY0T1aXLvWJHqb0aU4EQUDFOc-YrS29HH8U4,30405
19
+ execsql/db/base.py,sha256=CPoWu8qrxCOcQ6nh2oLyoqaPfC0yDU8bTECW_lZV9Dc,30953
20
20
  execsql/db/dsn.py,sha256=TgQUedVCxnEYA3vae7JETyhb8kK23qkNbPxsMQrNUN8,5368
21
21
  execsql/db/duckdb.py,sha256=cKeMwiSlYPyPDn1VLaJgbUD6_IEEaNqtUToLcmq7QaE,3189
22
22
  execsql/db/factory.py,sha256=YR1m_c2Hhj_GXVGGkWoSEPZBpgNu_c4FxRnbp-xV3rs,5230
@@ -26,6 +26,8 @@ execsql/db/oracle.py,sha256=AFVHhGlCzBuU7JgrAqeUG6e8TUUkk1Y80XVJQnGOqLM,10547
26
26
  execsql/db/postgres.py,sha256=oXR7ODzQhR3yO6q-aNa9_il_rO3SpOX9yYGsfIqHwLI,20139
27
27
  execsql/db/sqlite.py,sha256=2fD3AfckIGWN1oHcOaqQlQnbig26top1IlW-ejPHttI,10204
28
28
  execsql/db/sqlserver.py,sha256=mNwmIIxTzqXU-cOjpNpeFi568HjQHsAk8Xnn-tR6F_E,7563
29
+ execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
30
+ execsql/debug/repl.py,sha256=HeQ9emFKUjo7UTouxuTcmpGCTJIR1nOLxKkRJ5mvd2c,16669
29
31
  execsql/exporters/__init__.py,sha256=-Cnji-OgodJV8ftcDcOyTof0kQMy9J5kKVC8GVFpc3o,670
30
32
  execsql/exporters/base.py,sha256=W9USFyk_2eztjJ51X6CJh7-chE1i3cSx-STOtbHXCNI,6373
31
33
  execsql/exporters/delimited.py,sha256=zMvurTRVl5W-6N8DuYtn_xILUkYLMlfflwWMfvdeaF0,30304
@@ -59,14 +61,13 @@ execsql/importers/csv.py,sha256=Mu848WNzuhVO1ade-WurPyxqGOuVNRO8UwRF3-bav_I,4845
59
61
  execsql/importers/feather.py,sha256=g2B69d2uv9vmnXcmjFyTVsMP40LYEzFYkhk3gD26mGw,1900
60
62
  execsql/importers/ods.py,sha256=MJsdsjropzCvxAA3DDZfAL_AnmZ4yij7DnrjGyDJqHQ,2843
61
63
  execsql/importers/xls.py,sha256=e0Zfe47ZiCpA1Ae3XDJ1ko3sCiH3-8U6XLKi6NvD0jQ,3683
62
- execsql/metacommands/__init__.py,sha256=TT1ARHgHltHqZ7qx4Y62o1h_GOPvUztZKCem-wAE560,11215
64
+ execsql/metacommands/__init__.py,sha256=EmYUZZq1oaubbSQ26-8F9jJI_JnOJ2R697NeossXF1Q,11202
63
65
  execsql/metacommands/conditions.py,sha256=Fzrk83-pWbFOoKahYdQW7CZjQeh3zByDUbfgpTM_bjQ,29259
64
66
  execsql/metacommands/connect.py,sha256=Nsm0D91i3RX-R2rzQQ-Br-gULaI6Uvdn9fqb7DOAVfE,14804
65
67
  execsql/metacommands/control.py,sha256=xNHyTrYUM042OgDarNq7HxslY7JuQs-KOKcb-fHUngM,8510
66
68
  execsql/metacommands/data.py,sha256=tRQBGTAuW-eJ2tBNWaoZI9OjTyNNyHJISo7gOdL-sm8,11370
67
69
  execsql/metacommands/debug.py,sha256=pnT24dfvfOx8xFu86mO5czfVCGKbcvgBLyXnqaMWO4w,8184
68
- execsql/metacommands/debug_repl.py,sha256=XQ09I7HI-drVjfIg4XqsPndvAmZSxpSmXVKjWjLqwE4,9785
69
- execsql/metacommands/dispatch.py,sha256=1Mae6yqrea6wViFLBsvVt33Zgx4xP8tnhOuB_aQC89c,84054
70
+ execsql/metacommands/dispatch.py,sha256=OQwLOo9XT3N9C96wsRt0zmu1Nn4HL-7cSBOsGCfp5V4,84041
70
71
  execsql/metacommands/io.py,sha256=Duh60caM4go9JczbGYNMKKYpcMimwPzF6EQ_tshKxdE,2971
71
72
  execsql/metacommands/io_export.py,sha256=7lkCSnPhXy9FVau9_hT1u68NOVdG2DsWmvUh9hM1QWI,18359
72
73
  execsql/metacommands/io_fileops.py,sha256=RKqbWPTYiwiqCZYG-lpih0w1JVOY4RBFdWr3BJb_pnY,9669
@@ -77,8 +78,8 @@ execsql/metacommands/script_ext.py,sha256=TUgAldB2LSJAwZrCvDDi804hQ1d9BDQD2GDqHN
77
78
  execsql/metacommands/system.py,sha256=sUR5kLL7idTVg8WXIMdd-Kv7nkERIiaeL0beWsz8NyY,7293
78
79
  execsql/script/__init__.py,sha256=pIo0EJ7-vg67rSMbOvbri_BOUgLoGoSEUfJgxUN7ZS0,3380
79
80
  execsql/script/control.py,sha256=s-1eZdGARM6H1FwZ6VDdO_f50j7bvvRtTHesfUm9tbc,6144
80
- execsql/script/engine.py,sha256=1_3qMn6a1nD4oYBBRNlwMU_YwZ4f2Om6-CjUGksJT4A,41087
81
- execsql/script/variables.py,sha256=MOT9XEHucpuuuHQZM5bklxGMBQcwHzwTBxd0q3aO0XY,11641
81
+ execsql/script/engine.py,sha256=1qcWGfXPRqDd48PQwEbHmCO1eN4YYrQrS-0QUQb270g,41694
82
+ execsql/script/variables.py,sha256=mklG20WPhfv1mmqSVoRQHrzZvGN7ne_bqvRd0PMx5ss,10388
82
83
  execsql/utils/__init__.py,sha256=0uR6JwVJQRX3vceByNBduCAf5dd5assKjeqJUWvpZoA,278
83
84
  execsql/utils/auth.py,sha256=onXzNkNZQZxGC5w7eey06sjvAIAX_Lf9g7nUJtcsel0,7009
84
85
  execsql/utils/crypto.py,sha256=2OnBWwn9bCBGc1ZkyRv16TvhottoCNYtXqgbE3mG3Sg,2960
@@ -91,24 +92,24 @@ execsql/utils/numeric.py,sha256=xh02ANSRk3nUpQ-rtm66ILoMqoi7HtzCoRMIOT9U8QI,1570
91
92
  execsql/utils/regex.py,sha256=diEzTZqU_HHwVMadPAvN1Vgzhl7I03eVaEFGCXyGGL8,3770
92
93
  execsql/utils/strings.py,sha256=5Dvzrk-9SIw2lpxXZQkiJbNyo1sy7iXXAtSULlZ0KG8,8488
93
94
  execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
94
- execsql2-2.11.1.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
95
- execsql2-2.11.1.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
96
- execsql2-2.11.1.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
97
- execsql2-2.11.1.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
98
- execsql2-2.11.1.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
99
- execsql2-2.11.1.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
100
- execsql2-2.11.1.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
101
- execsql2-2.11.1.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
102
- execsql2-2.11.1.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
103
- execsql2-2.11.1.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
104
- execsql2-2.11.1.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
105
- execsql2-2.11.1.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
106
- execsql2-2.11.1.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
107
- execsql2-2.11.1.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
108
- execsql2-2.11.1.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
109
- execsql2-2.11.1.dist-info/METADATA,sha256=cGmNeSCzZvJFmoCzDbMs_M3MjYP6YCZgQF0vNpIknVk,17381
110
- execsql2-2.11.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
111
- execsql2-2.11.1.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
112
- execsql2-2.11.1.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
113
- execsql2-2.11.1.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
114
- execsql2-2.11.1.dist-info/RECORD,,
95
+ execsql2-2.12.1.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
96
+ execsql2-2.12.1.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
97
+ execsql2-2.12.1.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
98
+ execsql2-2.12.1.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
99
+ execsql2-2.12.1.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
100
+ execsql2-2.12.1.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
101
+ execsql2-2.12.1.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
102
+ execsql2-2.12.1.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
103
+ execsql2-2.12.1.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
104
+ execsql2-2.12.1.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
105
+ execsql2-2.12.1.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
106
+ execsql2-2.12.1.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
107
+ execsql2-2.12.1.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
108
+ execsql2-2.12.1.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
109
+ execsql2-2.12.1.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
110
+ execsql2-2.12.1.dist-info/METADATA,sha256=AKqGpfB4EU_8T-tJuyMs_r4oetvStQof5j-h34cRScQ,17381
111
+ execsql2-2.12.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
112
+ execsql2-2.12.1.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
113
+ execsql2-2.12.1.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
114
+ execsql2-2.12.1.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
115
+ execsql2-2.12.1.dist-info/RECORD,,
@@ -1,289 +0,0 @@
1
- from __future__ import annotations
2
-
3
- """
4
- Interactive debug REPL metacommand handler for execsql.
5
-
6
- Implements ``x_breakpoint`` — the ``BREAKPOINT`` metacommand — which pauses
7
- script execution and drops into an interactive read-eval-print loop.
8
-
9
- The REPL allows the user to:
10
-
11
- - Inspect and print substitution variables.
12
- - Run ad-hoc SQL queries against the current database.
13
- - Step through the script one statement at a time.
14
- - Resume or abort execution.
15
-
16
- All REPL commands are dot-prefixed (``.continue``, ``.vars``, ``.next``)
17
- to avoid ambiguity with variable names and SQL. Anything not starting
18
- with ``.`` is treated as either a variable lookup (if it matches a known
19
- variable) or SQL (if it ends with ``;``).
20
-
21
- In non-interactive environments (CI, piped input, ``sys.stdin.isatty()`` is
22
- ``False``) the metacommand is silently skipped so automated pipelines are not
23
- blocked.
24
- """
25
-
26
- import sys
27
- from typing import Any
28
-
29
- import execsql.state as _state
30
-
31
- __all__ = ["x_breakpoint"]
32
-
33
- # ---------------------------------------------------------------------------
34
- # Public handler
35
- # ---------------------------------------------------------------------------
36
-
37
- _HELP_TEXT = """\
38
- execsql debug REPL — all commands start with '.'
39
-
40
- .continue .c Resume script execution
41
- .abort .q Halt the script (exit 1)
42
- .vars List user, system, local, and counter variables
43
- .vars all Include environment variables (&) in the listing
44
- .next .n Execute the next statement then pause again (step mode)
45
- .stack Show the command-list stack (script name, line, depth)
46
- .help Show this help text
47
-
48
- Everything else:
49
- varname Print a variable's value (e.g. logfile, $ARG_1, &HOME)
50
- SELECT ...; Run ad-hoc SQL against the current database
51
- """
52
-
53
-
54
- def x_breakpoint(**kwargs: Any) -> None:
55
- """Pause execution and enter the interactive debug REPL.
56
-
57
- If ``sys.stdin`` is not a TTY (CI, piped input), the metacommand is
58
- silently skipped — scripts will not hang in automation.
59
-
60
- Args:
61
- **kwargs: Keyword arguments injected by the dispatch table (unused).
62
- """
63
- if not sys.stdin.isatty():
64
- return
65
- _debug_repl()
66
-
67
-
68
- # ---------------------------------------------------------------------------
69
- # REPL core
70
- # ---------------------------------------------------------------------------
71
-
72
-
73
- def _debug_repl() -> None:
74
- """Interactive read-eval-print loop for script debugging.
75
-
76
- Reads commands from stdin until the user types ``.continue`` or ``.abort``,
77
- or until EOF / KeyboardInterrupt.
78
- """
79
- try:
80
- import readline as _readline # noqa: F401 — side-effect: enables history/arrow keys
81
- except ImportError:
82
- pass # readline not available on Windows; continue without it
83
-
84
- _write("\n[Breakpoint] Script paused. Type '.help' for commands, '.c' to resume.\n")
85
-
86
- while True:
87
- try:
88
- line = input("execsql debug> ").strip()
89
- except EOFError:
90
- _write("\n")
91
- return # Ctrl-D → continue
92
- except KeyboardInterrupt:
93
- _write("\n")
94
- return # Ctrl-C → continue
95
-
96
- if not line:
97
- continue
98
-
99
- # Dot-prefixed → REPL command
100
- if line.startswith("."):
101
- cmd = line[1:].strip().lower()
102
- _handle_dot_command(line)
103
- if cmd in ("continue", "c"):
104
- return
105
- if cmd in ("abort", "q", "quit"):
106
- # _handle_dot_command already raised SystemExit, but guard anyway
107
- return
108
- if cmd in ("next", "n"):
109
- return
110
- continue
111
-
112
- # SQL (ends with semicolon)
113
- if line.rstrip().endswith(";"):
114
- _run_sql(line)
115
- continue
116
-
117
- # Everything else → variable lookup
118
- _print_var(line)
119
-
120
-
121
- def _handle_dot_command(line: str) -> None:
122
- """Dispatch a dot-prefixed REPL command."""
123
- # Strip the leading dot and normalize
124
- cmd = line[1:].strip().lower()
125
-
126
- if cmd in ("continue", "c"):
127
- return # caller checks and returns from _debug_repl
128
- elif cmd in ("abort", "q", "quit"):
129
- raise SystemExit(1)
130
- elif cmd == "help":
131
- _write(_HELP_TEXT)
132
- elif cmd == "vars all":
133
- _print_all_vars(include_env=True)
134
- elif cmd == "vars":
135
- _print_all_vars()
136
- elif cmd == "stack":
137
- _print_stack()
138
- elif cmd in ("next", "n"):
139
- _enable_step_mode()
140
- else:
141
- _write(f" Unknown command: {line!r}. Type '.help' for available commands.\n")
142
-
143
-
144
- # ---------------------------------------------------------------------------
145
- # REPL command implementations
146
- # ---------------------------------------------------------------------------
147
-
148
-
149
- def _write(text: str) -> None:
150
- """Write *text* to the execsql output stream (falls back to stdout)."""
151
- output = _state.output
152
- if output is not None:
153
- output.write(text)
154
- else:
155
- sys.stdout.write(text)
156
- sys.stdout.flush()
157
-
158
-
159
- def _print_all_vars(*, include_env: bool = False) -> None:
160
- """Print substitution variables grouped by type."""
161
- subvars = _state.subvars
162
- if subvars is None:
163
- _write(" (no substitution variables defined)\n")
164
- return
165
- items = subvars.substitutions # list of (name, value) tuples
166
- if not items:
167
- _write(" (no substitution variables defined)\n")
168
- return
169
-
170
- # Group by prefix.
171
- user_vars: list[tuple[str, str]] = []
172
- system_vars: list[tuple[str, str]] = []
173
- counter_vars: list[tuple[str, str]] = []
174
- local_vars: list[tuple[str, str]] = []
175
- env_vars: list[tuple[str, str]] = []
176
-
177
- for name, value in sorted(items):
178
- if name.startswith("&"):
179
- env_vars.append((name, value))
180
- elif name.startswith("~"):
181
- local_vars.append((name, value))
182
- elif name.startswith("@"):
183
- counter_vars.append((name, value))
184
- elif name.startswith("$"):
185
- system_vars.append((name, value))
186
- else:
187
- user_vars.append((name, value))
188
-
189
- def _print_group(label: str, group: list[tuple[str, str]]) -> None:
190
- if not group:
191
- return
192
- _write(f" {label}:\n")
193
- max_name = max(len(n) for n, _ in group)
194
- for name, value in group:
195
- _write(f" {name:<{max_name}} = {value}\n")
196
-
197
- _print_group("User variables", user_vars)
198
- _print_group("System variables ($)", system_vars)
199
- _print_group("Local variables (~)", local_vars)
200
- _print_group("Counter variables (@)", counter_vars)
201
- if include_env:
202
- _print_group("Environment variables (&)", env_vars)
203
-
204
- if not any([user_vars, system_vars, local_vars, counter_vars]):
205
- if env_vars:
206
- _write(" (no script variables defined — use '.vars all' to see environment variables)\n")
207
- else:
208
- _write(" (no variables defined)\n")
209
-
210
-
211
- def _print_var(varname: str) -> None:
212
- """Print the value of a single substitution variable.
213
-
214
- Tries the name as typed, then with the sigil prefix stripped.
215
- """
216
- subvars = _state.subvars
217
- if subvars is None:
218
- _write(f" {varname}: (substitution variables not initialised)\n")
219
- return
220
- # Try the name as typed first, then without the sigil prefix ($, &, @, #, ~).
221
- # SUB creates variables without a prefix (e.g., "logfile"), but users
222
- # may type "$logfile" at the prompt.
223
- value = subvars.varvalue(varname)
224
- if value is None and len(varname) > 1 and varname[0] in "$&@#~":
225
- value = subvars.varvalue(varname[1:])
226
- if value is None:
227
- _write(f" {varname}: (undefined)\n")
228
- else:
229
- _write(f" {varname} = {value}\n")
230
-
231
-
232
- def _print_stack() -> None:
233
- """Print the current command-list stack (script name, line number, depth)."""
234
- stack = _state.commandliststack
235
- if not stack:
236
- _write(" (command list stack is empty)\n")
237
- return
238
- _write(f" Stack depth: {len(stack)}\n")
239
- for depth, cmdlist in enumerate(stack):
240
- listname = getattr(cmdlist, "listname", "<unknown>")
241
- cmdptr = getattr(cmdlist, "cmdptr", 0)
242
- _write(f" [{depth}] {listname} (cursor at index {cmdptr})\n")
243
-
244
-
245
- def _run_sql(sql: str) -> None:
246
- """Execute ad-hoc SQL against the current database and pretty-print the results."""
247
- dbs = _state.dbs
248
- if dbs is None:
249
- _write(" (no database connection is active)\n")
250
- return
251
- db = dbs.current()
252
- if db is None:
253
- _write(" (no database connection is active)\n")
254
- return
255
- try:
256
- colnames, rows = db.select_data(sql)
257
- except Exception as exc:
258
- _write(f" SQL error: {exc}\n")
259
- return
260
-
261
- if not colnames:
262
- _write(" (query returned no columns)\n")
263
- return
264
-
265
- # Build a simple text table.
266
- col_widths = [len(c) for c in colnames]
267
- str_rows: list[list[str]] = []
268
- for row in rows:
269
- str_row = [str(v) if v is not None else "NULL" for v in row]
270
- str_rows.append(str_row)
271
- for i, cell in enumerate(str_row):
272
- col_widths[i] = max(col_widths[i], len(cell))
273
-
274
- sep = "+-" + "-+-".join("-" * w for w in col_widths) + "-+"
275
- header = "| " + " | ".join(c.ljust(col_widths[i]) for i, c in enumerate(colnames)) + " |"
276
- _write(sep + "\n")
277
- _write(header + "\n")
278
- _write(sep + "\n")
279
- for str_row in str_rows:
280
- data_line = "| " + " | ".join(cell.ljust(col_widths[i]) for i, cell in enumerate(str_row)) + " |"
281
- _write(data_line + "\n")
282
- _write(sep + "\n")
283
- row_word = "row" if len(str_rows) == 1 else "rows"
284
- _write(f" ({len(str_rows)} {row_word})\n")
285
-
286
-
287
- def _enable_step_mode() -> None:
288
- """Activate step mode so the engine re-enters the REPL after the next statement."""
289
- _state.step_mode = True