execsql2 2.11.0__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/lint.py +1 -1
- execsql/cli/run.py +18 -12
- execsql/debug/__init__.py +6 -0
- execsql/debug/repl.py +472 -0
- execsql/exporters/xlsx.py +5 -0
- execsql/exporters/yaml.py +2 -0
- execsql/metacommands/__init__.py +1 -1
- execsql/metacommands/conditions.py +1 -1
- execsql/metacommands/control.py +4 -10
- execsql/metacommands/debug.py +1 -1
- execsql/metacommands/dispatch.py +1 -1
- execsql/script/engine.py +15 -5
- execsql/state.py +2 -2
- execsql/utils/gui.py +26 -4
- {execsql2-2.11.0.dist-info → execsql2-2.12.0.dist-info}/METADATA +5 -2
- {execsql2-2.11.0.dist-info → execsql2-2.12.0.dist-info}/RECORD +36 -35
- execsql/metacommands/debug_repl.py +0 -288
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.11.0.data → execsql2-2.12.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.11.0.dist-info → execsql2-2.12.0.dist-info}/WHEEL +0 -0
- {execsql2-2.11.0.dist-info → execsql2-2.12.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.11.0.dist-info → execsql2-2.12.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.11.0.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/lint.py
CHANGED
|
@@ -194,7 +194,7 @@ def _lint_cmdlist(
|
|
|
194
194
|
for cmd in cmdlist.cmdlist:
|
|
195
195
|
src = cmd.source
|
|
196
196
|
lno = cmd.line_no
|
|
197
|
-
stmt = cmd.command.statement
|
|
197
|
+
stmt = cmd.command.statement
|
|
198
198
|
|
|
199
199
|
if cmd.command_type == "sql":
|
|
200
200
|
# SQL statements: check for variable references only
|
execsql/cli/run.py
CHANGED
|
@@ -8,6 +8,7 @@ and drives the main execution loop. Separated from argument parsing
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import atexit
|
|
11
|
+
from typing import Any
|
|
11
12
|
import datetime
|
|
12
13
|
import getpass
|
|
13
14
|
import os
|
|
@@ -70,12 +71,14 @@ def _print_dry_run(cmdlist: object) -> None:
|
|
|
70
71
|
# ---------------------------------------------------------------------------
|
|
71
72
|
|
|
72
73
|
|
|
73
|
-
def _print_profile(profile_data: list[tuple]) -> None:
|
|
74
|
+
def _print_profile(profile_data: list[tuple], limit: int = 20) -> None:
|
|
74
75
|
"""Print a per-statement timing summary to stdout.
|
|
75
76
|
|
|
76
77
|
Args:
|
|
77
78
|
profile_data: List of ``(source, line_no, command_type, elapsed_secs,
|
|
78
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.
|
|
79
82
|
"""
|
|
80
83
|
if not profile_data:
|
|
81
84
|
_console.print("[dim]Profile: no statements recorded.[/dim]")
|
|
@@ -84,9 +87,9 @@ def _print_profile(profile_data: list[tuple]) -> None:
|
|
|
84
87
|
total_secs = sum(row[3] for row in profile_data)
|
|
85
88
|
n = len(profile_data)
|
|
86
89
|
|
|
87
|
-
# Sort descending by elapsed time; show top
|
|
90
|
+
# Sort descending by elapsed time; show top `limit` (or all if <= limit).
|
|
88
91
|
sorted_data = sorted(profile_data, key=lambda r: r[3], reverse=True)
|
|
89
|
-
display = sorted_data[:
|
|
92
|
+
display = sorted_data[:limit]
|
|
90
93
|
|
|
91
94
|
_console.print()
|
|
92
95
|
_console.print(f"[bold cyan]Profile:[/bold cyan] {n} statement{'s' if n != 1 else ''} in {total_secs:.3f}s")
|
|
@@ -114,10 +117,10 @@ def _print_profile(profile_data: list[tuple]) -> None:
|
|
|
114
117
|
f"{preview_short}",
|
|
115
118
|
)
|
|
116
119
|
|
|
117
|
-
if len(sorted_data) >
|
|
118
|
-
omitted = len(sorted_data) -
|
|
120
|
+
if len(sorted_data) > limit:
|
|
121
|
+
omitted = len(sorted_data) - limit
|
|
119
122
|
_console.print(
|
|
120
|
-
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]",
|
|
121
124
|
)
|
|
122
125
|
|
|
123
126
|
_console.print()
|
|
@@ -128,7 +131,7 @@ def _print_profile(profile_data: list[tuple]) -> None:
|
|
|
128
131
|
# ---------------------------------------------------------------------------
|
|
129
132
|
|
|
130
133
|
|
|
131
|
-
def _ping_db(db) -> None:
|
|
134
|
+
def _ping_db(db: Any) -> None:
|
|
132
135
|
"""Test connectivity for *db*, print connection details, and exit.
|
|
133
136
|
|
|
134
137
|
Attempts to execute ``SELECT version()`` (or ``SELECT sqlite_version()``
|
|
@@ -156,7 +159,7 @@ def _ping_db(db) -> None:
|
|
|
156
159
|
curs.close()
|
|
157
160
|
if row and row[0]:
|
|
158
161
|
version_str = str(row[0]).split("\n")[0].strip()
|
|
159
|
-
|
|
162
|
+
break
|
|
160
163
|
except Exception:
|
|
161
164
|
continue
|
|
162
165
|
|
|
@@ -212,6 +215,7 @@ def _run(
|
|
|
212
215
|
output_dir: str | None = None,
|
|
213
216
|
progress: bool = False,
|
|
214
217
|
profile: bool = False,
|
|
218
|
+
profile_limit: int = 20,
|
|
215
219
|
ping: bool = False,
|
|
216
220
|
lint: bool = False,
|
|
217
221
|
debug: bool = False,
|
|
@@ -551,7 +555,7 @@ def _run(
|
|
|
551
555
|
if debug:
|
|
552
556
|
_state.step_mode = True
|
|
553
557
|
|
|
554
|
-
_execute_script_direct(conf, profile=profile)
|
|
558
|
+
_execute_script_direct(conf, profile=profile, profile_limit=profile_limit)
|
|
555
559
|
|
|
556
560
|
|
|
557
561
|
# ---------------------------------------------------------------------------
|
|
@@ -610,7 +614,7 @@ def _execute_script_textual_console(conf: ConfigData) -> None:
|
|
|
610
614
|
_state.exec_log.log_exit_end()
|
|
611
615
|
|
|
612
616
|
|
|
613
|
-
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:
|
|
614
618
|
"""Run runscripts() in the current (main) thread — used when Textual is not active.
|
|
615
619
|
|
|
616
620
|
Args:
|
|
@@ -618,6 +622,8 @@ def _execute_script_direct(conf: ConfigData, *, profile: bool = False) -> None:
|
|
|
618
622
|
profile: When ``True``, print a per-statement timing summary after the
|
|
619
623
|
script completes. Timing data must already have been activated on
|
|
620
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).
|
|
621
627
|
"""
|
|
622
628
|
import execsql.state as _state
|
|
623
629
|
import execsql.utils.gui as _gui
|
|
@@ -644,7 +650,7 @@ def _execute_script_direct(conf: ConfigData, *, profile: bool = False) -> None:
|
|
|
644
650
|
gui_console_off()
|
|
645
651
|
_state.exec_log.log_status_info(f"{_state.cmds_run} commands run")
|
|
646
652
|
if profile and _state.profile_data is not None:
|
|
647
|
-
_print_profile(_state.profile_data)
|
|
653
|
+
_print_profile(_state.profile_data, limit=profile_limit)
|
|
648
654
|
sys.exit(exc.code)
|
|
649
655
|
except ConfigError:
|
|
650
656
|
raise
|
|
@@ -672,7 +678,7 @@ def _execute_script_direct(conf: ConfigData, *, profile: bool = False) -> None:
|
|
|
672
678
|
gui_console_off()
|
|
673
679
|
_state.exec_log.log_status_info(f"{_state.cmds_run} commands run")
|
|
674
680
|
if profile and _state.profile_data is not None:
|
|
675
|
-
_print_profile(_state.profile_data)
|
|
681
|
+
_print_profile(_state.profile_data, limit=profile_limit)
|
|
676
682
|
_state.exec_log.log_exit_end()
|
|
677
683
|
|
|
678
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/exporters/xlsx.py
CHANGED
|
@@ -178,6 +178,11 @@ def write_query_to_xlsx(
|
|
|
178
178
|
wb.save(outfile)
|
|
179
179
|
wb.close()
|
|
180
180
|
|
|
181
|
+
if _state.export_metadata is not None:
|
|
182
|
+
_state.export_metadata.add(
|
|
183
|
+
ExportRecord(queryname=select_stmt, outfile=outfile, zipfile=None, description=desc),
|
|
184
|
+
)
|
|
185
|
+
|
|
181
186
|
|
|
182
187
|
def write_queries_to_xlsx(
|
|
183
188
|
table_list: str,
|
execsql/exporters/yaml.py
CHANGED
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,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from execsql.exceptions import ErrInfo
|
|
3
2
|
|
|
4
3
|
"""
|
|
5
4
|
Conditional test handler functions for execsql.
|
|
@@ -15,6 +14,7 @@ at registration time.
|
|
|
15
14
|
"""
|
|
16
15
|
|
|
17
16
|
import os
|
|
17
|
+
from execsql.exceptions import ErrInfo
|
|
18
18
|
import time
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
from typing import Any
|