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.
- execsql/cli/__init__.py +6 -0
- execsql/cli/run.py +15 -10
- execsql/db/base.py +37 -23
- execsql/debug/__init__.py +6 -0
- execsql/debug/repl.py +472 -0
- execsql/metacommands/__init__.py +1 -1
- execsql/metacommands/dispatch.py +1 -1
- execsql/script/engine.py +15 -6
- execsql/script/variables.py +2 -25
- {execsql2-2.11.1.dist-info → execsql2-2.12.1.dist-info}/METADATA +1 -1
- {execsql2-2.11.1.dist-info → execsql2-2.12.1.dist-info}/RECORD +30 -29
- execsql/metacommands/debug_repl.py +0 -289
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.11.1.dist-info → execsql2-2.12.1.dist-info}/WHEEL +0 -0
- {execsql2-2.11.1.dist-info → execsql2-2.12.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.11.1.dist-info → execsql2-2.12.1.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
|
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[:
|
|
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) >
|
|
119
|
-
omitted = len(sorted_data) -
|
|
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
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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,
|
|
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."""
|
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")
|
execsql/metacommands/__init__.py
CHANGED
|
@@ -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.
|
|
103
|
+
from execsql.debug.repl import x_breakpoint
|
|
104
104
|
from execsql.metacommands.io import (
|
|
105
105
|
x_export,
|
|
106
106
|
x_export_query,
|
execsql/metacommands/dispatch.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
510
|
+
cmditem.source_dir,
|
|
502
511
|
)
|
|
503
|
-
_state.subvars.add_substitution("$CURRENT_SCRIPT_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.
|
|
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
|
-
|
|
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 =
|
|
754
|
+
cmdstr = command_str
|
|
746
755
|
subs_made = True
|
|
747
756
|
iterations = 0
|
|
748
757
|
while subs_made:
|
execsql/script/variables.py
CHANGED
|
@@ -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
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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/
|
|
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=
|
|
81
|
-
execsql/script/variables.py,sha256=
|
|
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.
|
|
95
|
-
execsql2-2.
|
|
96
|
-
execsql2-2.
|
|
97
|
-
execsql2-2.
|
|
98
|
-
execsql2-2.
|
|
99
|
-
execsql2-2.
|
|
100
|
-
execsql2-2.
|
|
101
|
-
execsql2-2.
|
|
102
|
-
execsql2-2.
|
|
103
|
-
execsql2-2.
|
|
104
|
-
execsql2-2.
|
|
105
|
-
execsql2-2.
|
|
106
|
-
execsql2-2.
|
|
107
|
-
execsql2-2.
|
|
108
|
-
execsql2-2.
|
|
109
|
-
execsql2-2.
|
|
110
|
-
execsql2-2.
|
|
111
|
-
execsql2-2.
|
|
112
|
-
execsql2-2.
|
|
113
|
-
execsql2-2.
|
|
114
|
-
execsql2-2.
|
|
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
|
|
File without changes
|
|
File without changes
|
{execsql2-2.11.1.data → execsql2-2.12.1.data}/data/execsql2_extras/example_config_prompt.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|