execsql2 2.11.1__py3-none-any.whl → 2.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- execsql/cli/__init__.py +6 -0
- execsql/cli/run.py +15 -10
- 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 +2 -2
- {execsql2-2.11.1.dist-info → execsql2-2.12.0.dist-info}/METADATA +1 -1
- {execsql2-2.11.1.dist-info → execsql2-2.12.0.dist-info}/RECORD +28 -27
- execsql/metacommands/debug_repl.py +0 -289
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.11.1.data → execsql2-2.12.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.11.1.dist-info → execsql2-2.12.0.dist-info}/WHEEL +0 -0
- {execsql2-2.11.1.dist-info → execsql2-2.12.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.11.1.dist-info → execsql2-2.12.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.11.1.dist-info → execsql2-2.12.0.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/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
|
@@ -505,9 +505,9 @@ class CommandList:
|
|
|
505
505
|
_state.subvars.add_substitution("$SCRIPT_LINE", str(cmditem.line_no))
|
|
506
506
|
if _state.step_mode:
|
|
507
507
|
_state.step_mode = False
|
|
508
|
-
from execsql.
|
|
508
|
+
from execsql.debug.repl import _debug_repl
|
|
509
509
|
|
|
510
|
-
_debug_repl()
|
|
510
|
+
_debug_repl(step=True)
|
|
511
511
|
_profiling = _state.profile_data is not None
|
|
512
512
|
if _profiling:
|
|
513
513
|
import time as _time
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.12.0
|
|
4
4
|
Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
|
|
5
5
|
Project-URL: Repository, https://github.com/geocoug/execsql
|
|
6
6
|
Project-URL: Issues, https://github.com/geocoug/execsql/issues
|
|
@@ -9,11 +9,11 @@ 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
19
|
execsql/db/base.py,sha256=hfMFj8fXY0T1aXLvWJHqb0aU4EQUDFOc-YrS29HH8U4,30405
|
|
@@ -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,7 +78,7 @@ 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/engine.py,sha256=NJk4Is7Lp2Ov6zO8NNOOU8ACfmIFs2dD7PdhvJn6nNQ,41083
|
|
81
82
|
execsql/script/variables.py,sha256=MOT9XEHucpuuuHQZM5bklxGMBQcwHzwTBxd0q3aO0XY,11641
|
|
82
83
|
execsql/utils/__init__.py,sha256=0uR6JwVJQRX3vceByNBduCAf5dd5assKjeqJUWvpZoA,278
|
|
83
84
|
execsql/utils/auth.py,sha256=onXzNkNZQZxGC5w7eey06sjvAIAX_Lf9g7nUJtcsel0,7009
|
|
@@ -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.0.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
|
|
96
|
+
execsql2-2.12.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
|
|
97
|
+
execsql2-2.12.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
|
|
98
|
+
execsql2-2.12.0.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
|
|
99
|
+
execsql2-2.12.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
|
|
100
|
+
execsql2-2.12.0.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
|
|
101
|
+
execsql2-2.12.0.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
|
|
102
|
+
execsql2-2.12.0.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
|
|
103
|
+
execsql2-2.12.0.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
|
|
104
|
+
execsql2-2.12.0.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
|
|
105
|
+
execsql2-2.12.0.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
|
|
106
|
+
execsql2-2.12.0.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
|
|
107
|
+
execsql2-2.12.0.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
|
|
108
|
+
execsql2-2.12.0.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
|
|
109
|
+
execsql2-2.12.0.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
|
|
110
|
+
execsql2-2.12.0.dist-info/METADATA,sha256=l6mKQU4tPA9vkX6ZlUWNFRbou5rw5KPLML8V3urrV10,17381
|
|
111
|
+
execsql2-2.12.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
112
|
+
execsql2-2.12.0.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
|
|
113
|
+
execsql2-2.12.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
|
|
114
|
+
execsql2-2.12.0.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
|
|
115
|
+
execsql2-2.12.0.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.0.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
|