execsql2 2.8.0__py3-none-any.whl → 2.10.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 +25 -0
- execsql/cli/lint.py +459 -0
- execsql/cli/run.py +101 -9
- execsql/metacommands/__init__.py +3 -0
- execsql/metacommands/conditions.py +148 -0
- execsql/metacommands/debug_repl.py +223 -0
- execsql/metacommands/dispatch.py +12 -0
- execsql/script/engine.py +5 -0
- execsql/state.py +9 -0
- {execsql2-2.8.0.dist-info → execsql2-2.10.0.dist-info}/METADATA +2 -1
- {execsql2-2.8.0.dist-info → execsql2-2.10.0.dist-info}/RECORD +30 -28
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.8.0.data → execsql2-2.10.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.8.0.dist-info → execsql2-2.10.0.dist-info}/WHEEL +0 -0
- {execsql2-2.8.0.dist-info → execsql2-2.10.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.8.0.dist-info → execsql2-2.10.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.8.0.dist-info → execsql2-2.10.0.dist-info}/licenses/NOTICE +0 -0
execsql/cli/__init__.py
CHANGED
|
@@ -223,6 +223,25 @@ def main(
|
|
|
223
223
|
"--dry-run",
|
|
224
224
|
help=("Parse the script and print the command list without connecting to a database or executing anything."),
|
|
225
225
|
),
|
|
226
|
+
lint: bool = typer.Option(
|
|
227
|
+
False,
|
|
228
|
+
"--lint",
|
|
229
|
+
help=(
|
|
230
|
+
"Parse the script and perform static analysis without connecting to a database or executing anything. "
|
|
231
|
+
"Reports unmatched IF/ENDIF/LOOP/BATCH blocks (errors), potentially undefined variables, "
|
|
232
|
+
"and missing INCLUDE files (warnings). Exits 0 if no errors, 1 if errors found."
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
ping: bool = typer.Option(
|
|
236
|
+
False,
|
|
237
|
+
"--ping",
|
|
238
|
+
help=(
|
|
239
|
+
"Test database connectivity and exit. "
|
|
240
|
+
"Prints connection details and the server version on success (exit 0), "
|
|
241
|
+
"or the error message on failure (exit 1). "
|
|
242
|
+
"No script file is required."
|
|
243
|
+
),
|
|
244
|
+
),
|
|
226
245
|
dsn: str | None = typer.Option(
|
|
227
246
|
None,
|
|
228
247
|
"--dsn",
|
|
@@ -350,6 +369,10 @@ def main(
|
|
|
350
369
|
positional = args or []
|
|
351
370
|
if command is not None:
|
|
352
371
|
script_name = None # inline mode — no script file
|
|
372
|
+
elif ping:
|
|
373
|
+
# --ping does not require a script file; positional args are still
|
|
374
|
+
# available for server/db arguments if --dsn is not used.
|
|
375
|
+
script_name = None
|
|
353
376
|
else:
|
|
354
377
|
if not positional:
|
|
355
378
|
_err_console.print(
|
|
@@ -421,6 +444,8 @@ def main(
|
|
|
421
444
|
output_dir=output_dir,
|
|
422
445
|
progress=progress,
|
|
423
446
|
profile=profile,
|
|
447
|
+
ping=ping,
|
|
448
|
+
lint=lint,
|
|
424
449
|
)
|
|
425
450
|
|
|
426
451
|
|
execsql/cli/lint.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""Static analysis (lint) for execsql scripts.
|
|
2
|
+
|
|
3
|
+
:func:`_lint_script` inspects a parsed :class:`~execsql.script.CommandList`
|
|
4
|
+
for common structural problems without connecting to a database or executing
|
|
5
|
+
any commands.
|
|
6
|
+
|
|
7
|
+
Checks performed
|
|
8
|
+
----------------
|
|
9
|
+
1. **Unmatched IF / ENDIF** — mismatched nesting depth (error).
|
|
10
|
+
2. **Unmatched LOOP / END LOOP** — mismatched nesting depth (error).
|
|
11
|
+
3. **Unmatched BEGIN BATCH / END BATCH** — mismatched nesting depth (error).
|
|
12
|
+
4. **Potentially undefined variables** — ``!!$VAR!!`` tokens not preceded by a
|
|
13
|
+
``SUB`` metacommand in the same parsed command list and not in the set of
|
|
14
|
+
built-in variables (warning).
|
|
15
|
+
5. **Missing INCLUDE files** — INCLUDE target does not exist on disk relative
|
|
16
|
+
to the script directory (warning).
|
|
17
|
+
6. **Empty script** — no commands found (warning).
|
|
18
|
+
|
|
19
|
+
The function walks ``CommandList.cmdlist`` and also descends into any
|
|
20
|
+
``CommandList`` objects stored in ``_state.savedscripts`` (i.e. named scripts
|
|
21
|
+
defined with ``BEGIN SCRIPT … END SCRIPT`` in the same file). SCRIPT blocks
|
|
22
|
+
are analysed in isolation; nesting counters reset for each block.
|
|
23
|
+
|
|
24
|
+
Exit-code contract
|
|
25
|
+
------------------
|
|
26
|
+
- Returns ``1`` when at least one **error**-severity issue is found.
|
|
27
|
+
- Returns ``0`` when only warnings (or nothing) are found.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import re
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import TYPE_CHECKING
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from execsql.script.engine import CommandList
|
|
38
|
+
|
|
39
|
+
__all__ = ["_lint_script"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Compiled patterns for metacommand recognition
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
# IF block — "IF(...)" block form (single-command, no ENDIF needed)
|
|
47
|
+
_RX_IF_INLINE = re.compile(
|
|
48
|
+
r"^\s*IF\s*\(\s*.+\s*\)\s*\{.+\}\s*$",
|
|
49
|
+
re.I,
|
|
50
|
+
)
|
|
51
|
+
# IF block form that opens a block requiring ENDIF
|
|
52
|
+
_RX_IF_BLOCK = re.compile(r"^\s*IF\s*\(\s*.+\s*\)\s*$", re.I)
|
|
53
|
+
_RX_ENDIF = re.compile(r"^\s*ENDIF\s*$", re.I)
|
|
54
|
+
_RX_ELSE = re.compile(r"^\s*ELSE\s*$", re.I)
|
|
55
|
+
_RX_ELSEIF = re.compile(r"^\s*ELSEIF\s*\(\s*.+\s*\)\s*$", re.I)
|
|
56
|
+
_RX_ANDIF = re.compile(r"^\s*ANDIF\s*\(\s*.+\s*\)\s*$", re.I)
|
|
57
|
+
_RX_ORIF = re.compile(r"^\s*ORIF\s*\(\s*.+\s*\)\s*$", re.I)
|
|
58
|
+
|
|
59
|
+
# LOOP … END LOOP
|
|
60
|
+
_RX_LOOP = re.compile(r"^\s*LOOP\s+(?:WHILE|UNTIL)\s*\(", re.I)
|
|
61
|
+
_RX_END_LOOP = re.compile(r"^\s*END\s+LOOP\s*$", re.I)
|
|
62
|
+
|
|
63
|
+
# BEGIN BATCH … END BATCH
|
|
64
|
+
_RX_BEGIN_BATCH = re.compile(r"^\s*BEGIN\s+BATCH\s*$", re.I)
|
|
65
|
+
_RX_END_BATCH = re.compile(r"^\s*END\s+BATCH\s*$", re.I)
|
|
66
|
+
|
|
67
|
+
# SUB <varname> <value> — defines a substitution variable
|
|
68
|
+
_RX_SUB = re.compile(r"^\s*SUB\s+(?P<name>[+~]?\w+)\s+", re.I)
|
|
69
|
+
|
|
70
|
+
# INCLUDE <file>
|
|
71
|
+
_RX_INCLUDE = re.compile(
|
|
72
|
+
r"^\s*INCLUDE(?:\s+IF\s+EXISTS?)?\s+(?P<path>\S+.*?)\s*$",
|
|
73
|
+
re.I,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Variable reference — !!name!! where name may start with $, @, &, ~, #, +
|
|
77
|
+
_RX_VAR_REF = re.compile(r"!!([$@&~#+]?\w+)!!", re.I)
|
|
78
|
+
|
|
79
|
+
# Built-in system variables that are always defined (populated by _run before
|
|
80
|
+
# any script commands execute). Variable names are stored without the leading
|
|
81
|
+
# ``$`` for case-insensitive set membership tests.
|
|
82
|
+
_BUILTIN_VARS: frozenset[str] = frozenset(
|
|
83
|
+
{
|
|
84
|
+
# Start-time / environment
|
|
85
|
+
"SCRIPT_START_TIME",
|
|
86
|
+
"SCRIPT_START_TIME_UTC",
|
|
87
|
+
"DATE_TAG",
|
|
88
|
+
"DATETIME_TAG",
|
|
89
|
+
"DATETIME_UTC_TAG",
|
|
90
|
+
"LAST_ROWCOUNT",
|
|
91
|
+
"LAST_SQL",
|
|
92
|
+
"LAST_ERROR",
|
|
93
|
+
"ERROR_MESSAGE",
|
|
94
|
+
"USER",
|
|
95
|
+
"STARTING_PATH",
|
|
96
|
+
"PATHSEP",
|
|
97
|
+
"OS",
|
|
98
|
+
"PYTHON_EXECUTABLE",
|
|
99
|
+
"STARTING_SCRIPT",
|
|
100
|
+
"STARTING_SCRIPT_NAME",
|
|
101
|
+
"STARTING_SCRIPT_REVTIME",
|
|
102
|
+
"RUN_ID",
|
|
103
|
+
# Execution-time (set during runscripts — not available in --dry-run
|
|
104
|
+
# but always defined before any script command can reference them)
|
|
105
|
+
"CURRENT_TIME",
|
|
106
|
+
"CURRENT_TIME_UTC",
|
|
107
|
+
"CURRENT_SCRIPT",
|
|
108
|
+
"CURRENT_SCRIPT_PATH",
|
|
109
|
+
"CURRENT_SCRIPT_NAME",
|
|
110
|
+
"CURRENT_SCRIPT_LINE",
|
|
111
|
+
"SCRIPT_LINE",
|
|
112
|
+
"CURRENT_DIR",
|
|
113
|
+
"CURRENT_PATH",
|
|
114
|
+
"CURRENT_ALIAS",
|
|
115
|
+
"AUTOCOMMIT_STATE",
|
|
116
|
+
"TIMER",
|
|
117
|
+
"DB_USER",
|
|
118
|
+
"DB_SERVER",
|
|
119
|
+
"DB_NAME",
|
|
120
|
+
"DB_NEED_PWD",
|
|
121
|
+
"RANDOM",
|
|
122
|
+
"UUID",
|
|
123
|
+
"VERSION1",
|
|
124
|
+
"VERSION2",
|
|
125
|
+
"VERSION3",
|
|
126
|
+
"CANCEL_HALT_STATE",
|
|
127
|
+
"ERROR_HALT_STATE",
|
|
128
|
+
"METACOMMAND_ERROR_HALT_STATE",
|
|
129
|
+
"CONSOLE_WAIT_WHEN_ERROR_HALT_STATE",
|
|
130
|
+
"CONSOLE_WAIT_WHEN_DONE_STATE",
|
|
131
|
+
"CURRENT_DBMS",
|
|
132
|
+
"CURRENT_DATABASE",
|
|
133
|
+
"SYSTEM_CMD_EXIT_STATUS",
|
|
134
|
+
# Connection-populated
|
|
135
|
+
"DB_FILE",
|
|
136
|
+
"DB_PORT",
|
|
137
|
+
# Counter variables (@@name) are always valid — skip validation
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Issue tuple helpers
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
_Issue = tuple[str, str, int, str] # (severity, source, line_no, message)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _error(source: str, line_no: int, message: str) -> _Issue:
|
|
150
|
+
return ("error", source, line_no, message)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _warning(source: str, line_no: int, message: str) -> _Issue:
|
|
154
|
+
return ("warning", source, line_no, message)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
# Core lint implementation
|
|
159
|
+
# ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _lint_cmdlist(
|
|
163
|
+
cmdlist: CommandList,
|
|
164
|
+
script_dir: Path | None,
|
|
165
|
+
defined_vars: set[str],
|
|
166
|
+
) -> list[_Issue]:
|
|
167
|
+
"""Lint a single :class:`CommandList` and return any issues found.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
cmdlist: The parsed command list to analyse.
|
|
171
|
+
script_dir: Directory of the top-level script file, used for resolving
|
|
172
|
+
relative INCLUDE paths. ``None`` for inline (``-c``) scripts.
|
|
173
|
+
defined_vars: Mutable set of variable names (without sigil) that have
|
|
174
|
+
been defined by preceding ``SUB`` metacommands. The caller passes
|
|
175
|
+
in the set from the outer scope so that variables defined before an
|
|
176
|
+
EXECUTE SCRIPT call are visible inside the script block when
|
|
177
|
+
analysing top-level scripts. For named-script analysis the caller
|
|
178
|
+
passes a *copy* so that local definitions don't leak.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of ``(severity, source, line_no, message)`` issue tuples.
|
|
182
|
+
"""
|
|
183
|
+
issues: list[_Issue] = []
|
|
184
|
+
|
|
185
|
+
if_depth = 0
|
|
186
|
+
if_open_locs: list[tuple[str, int]] = [] # (source, line_no) of unmatched IF
|
|
187
|
+
|
|
188
|
+
loop_depth = 0
|
|
189
|
+
loop_open_locs: list[tuple[str, int]] = []
|
|
190
|
+
|
|
191
|
+
batch_depth = 0
|
|
192
|
+
batch_open_locs: list[tuple[str, int]] = []
|
|
193
|
+
|
|
194
|
+
for cmd in cmdlist.cmdlist:
|
|
195
|
+
src = cmd.source
|
|
196
|
+
lno = cmd.line_no
|
|
197
|
+
stmt = cmd.command.statement if cmd.command_type == "sql" else cmd.command.statement
|
|
198
|
+
|
|
199
|
+
if cmd.command_type == "sql":
|
|
200
|
+
# SQL statements: check for variable references only
|
|
201
|
+
for m in _RX_VAR_REF.finditer(stmt):
|
|
202
|
+
_check_var_ref(m.group(1), src, lno, defined_vars, issues)
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
# Metacommand checks
|
|
206
|
+
for m in _RX_VAR_REF.finditer(stmt):
|
|
207
|
+
_check_var_ref(m.group(1), src, lno, defined_vars, issues)
|
|
208
|
+
|
|
209
|
+
# -- IF block (opens a block requiring ENDIF) --
|
|
210
|
+
if _RX_IF_BLOCK.match(stmt) and not _RX_IF_INLINE.match(stmt):
|
|
211
|
+
if_depth += 1
|
|
212
|
+
if_open_locs.append((src, lno))
|
|
213
|
+
|
|
214
|
+
elif _RX_ENDIF.match(stmt):
|
|
215
|
+
if if_depth == 0:
|
|
216
|
+
issues.append(_error(src, lno, "ENDIF without a matching preceding IF"))
|
|
217
|
+
else:
|
|
218
|
+
if_depth -= 1
|
|
219
|
+
if_open_locs.pop()
|
|
220
|
+
|
|
221
|
+
elif _RX_ELSEIF.match(stmt) or _RX_ELSE.match(stmt) or _RX_ANDIF.match(stmt) or _RX_ORIF.match(stmt):
|
|
222
|
+
if if_depth == 0:
|
|
223
|
+
kw = stmt.strip().split(None, 1)[0].upper()
|
|
224
|
+
issues.append(_error(src, lno, f"{kw} without a matching preceding IF"))
|
|
225
|
+
|
|
226
|
+
# -- LOOP --
|
|
227
|
+
elif _RX_LOOP.match(stmt):
|
|
228
|
+
loop_depth += 1
|
|
229
|
+
loop_open_locs.append((src, lno))
|
|
230
|
+
|
|
231
|
+
elif _RX_END_LOOP.match(stmt):
|
|
232
|
+
if loop_depth == 0:
|
|
233
|
+
issues.append(_error(src, lno, "END LOOP without a matching preceding LOOP"))
|
|
234
|
+
else:
|
|
235
|
+
loop_depth -= 1
|
|
236
|
+
loop_open_locs.pop()
|
|
237
|
+
|
|
238
|
+
# -- BATCH --
|
|
239
|
+
elif _RX_BEGIN_BATCH.match(stmt):
|
|
240
|
+
batch_depth += 1
|
|
241
|
+
batch_open_locs.append((src, lno))
|
|
242
|
+
|
|
243
|
+
elif _RX_END_BATCH.match(stmt):
|
|
244
|
+
if batch_depth == 0:
|
|
245
|
+
issues.append(_error(src, lno, "END BATCH without a matching preceding BEGIN BATCH"))
|
|
246
|
+
else:
|
|
247
|
+
batch_depth -= 1
|
|
248
|
+
batch_open_locs.pop()
|
|
249
|
+
|
|
250
|
+
# -- SUB variable definition --
|
|
251
|
+
sub_m = _RX_SUB.match(stmt)
|
|
252
|
+
if sub_m:
|
|
253
|
+
varname = sub_m.group("name").lstrip("+~")
|
|
254
|
+
defined_vars.add(varname.upper())
|
|
255
|
+
|
|
256
|
+
# -- INCLUDE file existence --
|
|
257
|
+
inc_m = _RX_INCLUDE.match(stmt)
|
|
258
|
+
if inc_m:
|
|
259
|
+
raw_path = inc_m.group("path").strip().strip("\"'")
|
|
260
|
+
# Only check if no substitution variables are in the path
|
|
261
|
+
if not _RX_VAR_REF.search(raw_path):
|
|
262
|
+
_check_include_path(raw_path, script_dir, src, lno, stmt, issues)
|
|
263
|
+
|
|
264
|
+
# Report unclosed blocks at end of command list
|
|
265
|
+
for osrc, olno in if_open_locs:
|
|
266
|
+
issues.append(_error(osrc, olno, "IF without a matching ENDIF"))
|
|
267
|
+
for osrc, olno in loop_open_locs:
|
|
268
|
+
issues.append(_error(osrc, olno, "LOOP without a matching END LOOP"))
|
|
269
|
+
for osrc, olno in batch_open_locs:
|
|
270
|
+
issues.append(_error(osrc, olno, "BEGIN BATCH without a matching END BATCH"))
|
|
271
|
+
|
|
272
|
+
return issues
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _check_var_ref(
|
|
276
|
+
raw_name: str,
|
|
277
|
+
source: str,
|
|
278
|
+
line_no: int,
|
|
279
|
+
defined_vars: set[str],
|
|
280
|
+
issues: list[_Issue],
|
|
281
|
+
) -> None:
|
|
282
|
+
"""Emit a warning if *raw_name* looks like an undefined user variable.
|
|
283
|
+
|
|
284
|
+
Built-in system variables, environment-variable references (``&``-prefix),
|
|
285
|
+
column variables (``@``-prefix), counter variables (``@@``), parameter
|
|
286
|
+
variables (``#``-prefix), and ``$ARG_N`` are excluded from the check.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
raw_name: Variable name token as captured from ``!!name!!`` (with sigil).
|
|
290
|
+
source: Source file name for the issue location.
|
|
291
|
+
line_no: Line number of the command containing the reference.
|
|
292
|
+
defined_vars: Set of variable names (upper-case, no sigil) that have
|
|
293
|
+
been defined by preceding SUB metacommands.
|
|
294
|
+
issues: Issue list to append to.
|
|
295
|
+
"""
|
|
296
|
+
if not raw_name:
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
sigil = raw_name[0] if raw_name[0] in ("$", "@", "&", "~", "#", "+") else ""
|
|
300
|
+
name = raw_name[len(sigil) :]
|
|
301
|
+
|
|
302
|
+
# Skip non-$ sigil prefixes — these are always resolved at runtime
|
|
303
|
+
if sigil in ("@", "&", "~", "#", "+"):
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
# $ARG_N is set via -a/--assign-arg at invocation time
|
|
307
|
+
if re.match(r"^ARG_\d+$", name, re.I):
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
# Built-in system variables
|
|
311
|
+
if name.upper() in _BUILTIN_VARS:
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
# User-defined via SUB
|
|
315
|
+
if name.upper() in defined_vars:
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
issues.append(
|
|
319
|
+
_warning(
|
|
320
|
+
source,
|
|
321
|
+
line_no,
|
|
322
|
+
f"Potentially undefined variable: !!{raw_name}!! "
|
|
323
|
+
"(not defined by a preceding SUB; may be set by a config file or -a arg)",
|
|
324
|
+
),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _check_include_path(
|
|
329
|
+
raw_path: str,
|
|
330
|
+
script_dir: Path | None,
|
|
331
|
+
source: str,
|
|
332
|
+
line_no: int,
|
|
333
|
+
stmt: str,
|
|
334
|
+
issues: list[_Issue],
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Warn if the INCLUDE target does not exist on disk.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
raw_path: Unquoted file path string from the INCLUDE metacommand.
|
|
340
|
+
script_dir: Directory of the top-level script file; used for relative
|
|
341
|
+
path resolution. ``None`` for inline scripts.
|
|
342
|
+
source: Source file name for issue location.
|
|
343
|
+
line_no: Line number of the INCLUDE command.
|
|
344
|
+
stmt: Full metacommand statement text (for the IF EXISTS variant).
|
|
345
|
+
issues: Issue list to append to.
|
|
346
|
+
"""
|
|
347
|
+
# IF EXISTS variant — missing file is intentional; skip
|
|
348
|
+
if re.match(r"^\s*INCLUDE\s+IF\s+EXISTS?", stmt, re.I):
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
p = Path(raw_path)
|
|
352
|
+
if not p.is_absolute() and script_dir is not None:
|
|
353
|
+
p = script_dir / p
|
|
354
|
+
|
|
355
|
+
if not p.exists():
|
|
356
|
+
issues.append(
|
|
357
|
+
_warning(
|
|
358
|
+
source,
|
|
359
|
+
line_no,
|
|
360
|
+
f"INCLUDE target does not exist: {raw_path!r}",
|
|
361
|
+
),
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _lint_script(
|
|
366
|
+
cmdlist: CommandList | None,
|
|
367
|
+
script_path: str | None = None,
|
|
368
|
+
) -> list[_Issue]:
|
|
369
|
+
"""Perform static analysis on a parsed command list.
|
|
370
|
+
|
|
371
|
+
Walks every :class:`~execsql.script.ScriptCmd` in *cmdlist* and any named
|
|
372
|
+
scripts accumulated in ``_state.savedscripts`` (those defined with
|
|
373
|
+
``BEGIN SCRIPT … END SCRIPT`` in the same source file).
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
cmdlist: The top-level :class:`~execsql.script.CommandList` returned by
|
|
377
|
+
``read_sqlfile()`` / ``read_sqlstring()``. If ``None`` or empty,
|
|
378
|
+
a single "empty script" warning is returned.
|
|
379
|
+
script_path: Absolute or relative path to the SQL script file. Used
|
|
380
|
+
to resolve relative INCLUDE paths. Pass ``None`` for inline
|
|
381
|
+
(``-c``) scripts.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
List of ``(severity, source, line_no, message)`` tuples, one per issue
|
|
385
|
+
found. An empty list means the script is clean.
|
|
386
|
+
"""
|
|
387
|
+
import execsql.state as _state
|
|
388
|
+
|
|
389
|
+
issues: list[_Issue] = []
|
|
390
|
+
|
|
391
|
+
if cmdlist is None or not cmdlist.cmdlist:
|
|
392
|
+
issues.append(_warning("<script>", 0, "Script is empty — no commands found"))
|
|
393
|
+
return issues
|
|
394
|
+
|
|
395
|
+
script_dir = Path(script_path).resolve().parent if script_path else None
|
|
396
|
+
|
|
397
|
+
# Shared set of variables defined in the top-level script via SUB.
|
|
398
|
+
# Named scripts get a fresh copy so their internal definitions don't bleed
|
|
399
|
+
# back into the top-level analysis.
|
|
400
|
+
top_defined: set[str] = set()
|
|
401
|
+
|
|
402
|
+
issues.extend(_lint_cmdlist(cmdlist, script_dir, top_defined))
|
|
403
|
+
|
|
404
|
+
# Analyse each named SCRIPT block collected during parsing
|
|
405
|
+
for script_name, saved_cl in getattr(_state, "savedscripts", {}).items():
|
|
406
|
+
saved_issues = _lint_cmdlist(saved_cl, script_dir, set(top_defined))
|
|
407
|
+
for sev, src, lno, msg in saved_issues:
|
|
408
|
+
# Annotate with the script name if the source is the same file
|
|
409
|
+
issues.append((sev, src, lno, f"[script '{script_name}'] {msg}"))
|
|
410
|
+
|
|
411
|
+
return issues
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# ---------------------------------------------------------------------------
|
|
415
|
+
# Rich output helper
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _print_lint_results(issues: list[_Issue], script_label: str) -> int:
|
|
420
|
+
"""Print lint issues to the console using Rich formatting.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
issues: List of ``(severity, source, line_no, message)`` tuples.
|
|
424
|
+
script_label: Human-readable label for the script (file path or
|
|
425
|
+
``<inline>``), shown in the summary line.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
``1`` if any errors were found, ``0`` if only warnings or nothing.
|
|
429
|
+
"""
|
|
430
|
+
from execsql.cli.help import _console
|
|
431
|
+
|
|
432
|
+
n_errors = sum(1 for sev, *_ in issues if sev == "error")
|
|
433
|
+
n_warnings = sum(1 for sev, *_ in issues if sev == "warning")
|
|
434
|
+
|
|
435
|
+
_console.print(f"\n[bold cyan]Lint:[/bold cyan] {script_label}")
|
|
436
|
+
_console.print()
|
|
437
|
+
|
|
438
|
+
if not issues:
|
|
439
|
+
_console.print("[bold green]No issues found.[/bold green]")
|
|
440
|
+
_console.print()
|
|
441
|
+
return 0
|
|
442
|
+
|
|
443
|
+
for severity, source, line_no, message in issues:
|
|
444
|
+
loc = f"{source}:{line_no}" if line_no else source
|
|
445
|
+
if severity == "error":
|
|
446
|
+
_console.print(f" [bold red]ERROR [/bold red] [dim]{loc}[/dim] {message}")
|
|
447
|
+
else:
|
|
448
|
+
_console.print(f" [bold yellow]WARNING[/bold yellow] [dim]{loc}[/dim] {message}")
|
|
449
|
+
|
|
450
|
+
_console.print()
|
|
451
|
+
parts = []
|
|
452
|
+
if n_errors:
|
|
453
|
+
parts.append(f"[bold red]{n_errors} error{'s' if n_errors != 1 else ''}[/bold red]")
|
|
454
|
+
if n_warnings:
|
|
455
|
+
parts.append(f"[bold yellow]{n_warnings} warning{'s' if n_warnings != 1 else ''}[/bold yellow]")
|
|
456
|
+
_console.print(" " + ", ".join(parts))
|
|
457
|
+
_console.print()
|
|
458
|
+
|
|
459
|
+
return 1 if n_errors > 0 else 0
|
execsql/cli/run.py
CHANGED
|
@@ -24,7 +24,11 @@ from execsql.script import SubVarSet, current_script_line, read_sqlfile, read_sq
|
|
|
24
24
|
from execsql.utils.fileio import FileWriter, Logger, filewriter_end
|
|
25
25
|
from execsql.utils.gui import gui_connect, gui_console_isrunning, gui_console_off, gui_console_on, gui_console_wait_user
|
|
26
26
|
|
|
27
|
-
__all__ = ["_connect_initial_db", "_print_dry_run", "_print_profile", "_run"]
|
|
27
|
+
__all__ = ["_connect_initial_db", "_ping_db", "_print_dry_run", "_print_profile", "_run"]
|
|
28
|
+
|
|
29
|
+
# Lint helper — imported lazily inside _run() to keep start-up cost low, but
|
|
30
|
+
# re-exported here so that tests and callers can reach it via cli.run.
|
|
31
|
+
from execsql.cli.lint import _lint_script, _print_lint_results # noqa: F401 — re-export
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
# ---------------------------------------------------------------------------
|
|
@@ -119,6 +123,64 @@ def _print_profile(profile_data: list[tuple]) -> None:
|
|
|
119
123
|
_console.print()
|
|
120
124
|
|
|
121
125
|
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
# --ping helper
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _ping_db(db) -> None:
|
|
132
|
+
"""Test connectivity for *db*, print connection details, and exit.
|
|
133
|
+
|
|
134
|
+
Attempts to execute ``SELECT version()`` (or ``SELECT sqlite_version()``
|
|
135
|
+
for SQLite) to retrieve the server version string. If the query fails the
|
|
136
|
+
connection is still reported as successful — only the version line is
|
|
137
|
+
omitted. On success the function raises :class:`SystemExit` with code 0.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
db: An open :class:`~execsql.db.base.Database` instance.
|
|
141
|
+
"""
|
|
142
|
+
dbms_id: str = db.type.dbms_id if db.type else "unknown"
|
|
143
|
+
|
|
144
|
+
# Try to fetch a human-readable server version string.
|
|
145
|
+
version_str: str | None = None
|
|
146
|
+
_version_queries = [
|
|
147
|
+
"SELECT version()",
|
|
148
|
+
"SELECT sqlite_version()",
|
|
149
|
+
"SELECT @@VERSION",
|
|
150
|
+
]
|
|
151
|
+
for sql in _version_queries:
|
|
152
|
+
try:
|
|
153
|
+
curs = db.cursor()
|
|
154
|
+
curs.execute(sql)
|
|
155
|
+
row = curs.fetchone()
|
|
156
|
+
curs.close()
|
|
157
|
+
if row and row[0]:
|
|
158
|
+
version_str = str(row[0]).split("\n")[0].strip()
|
|
159
|
+
break
|
|
160
|
+
except Exception:
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
# Build the connection descriptor.
|
|
164
|
+
if db.server_name:
|
|
165
|
+
port_part = f":{db.port}" if db.port else ""
|
|
166
|
+
location = f"{db.server_name}{port_part}/{db.db_name or ''}"
|
|
167
|
+
else:
|
|
168
|
+
location = db.db_name or "<in-memory>"
|
|
169
|
+
|
|
170
|
+
if version_str:
|
|
171
|
+
_console.print(
|
|
172
|
+
f"[bold green]Connected[/bold green] to [bold]{dbms_id}[/bold] "
|
|
173
|
+
f"[dim]{version_str}[/dim] at [cyan]{location}[/cyan]",
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
_console.print(
|
|
177
|
+
f"[bold green]Connected[/bold green] to [bold]{dbms_id}[/bold] at [cyan]{location}[/cyan]",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
db.close()
|
|
181
|
+
raise SystemExit(0)
|
|
182
|
+
|
|
183
|
+
|
|
122
184
|
# ---------------------------------------------------------------------------
|
|
123
185
|
# Core execution (split from argument parsing for testability)
|
|
124
186
|
# ---------------------------------------------------------------------------
|
|
@@ -150,6 +212,8 @@ def _run(
|
|
|
150
212
|
output_dir: str | None = None,
|
|
151
213
|
progress: bool = False,
|
|
152
214
|
profile: bool = False,
|
|
215
|
+
ping: bool = False,
|
|
216
|
+
lint: bool = False,
|
|
153
217
|
) -> None:
|
|
154
218
|
"""Initialise state, connect to the database, load the script, and run it.
|
|
155
219
|
|
|
@@ -157,6 +221,17 @@ def _run(
|
|
|
157
221
|
without going through the Typer CLI layer. All parameters mirror the
|
|
158
222
|
corresponding CLI options; see [Syntax & Options](../syntax.md) for
|
|
159
223
|
descriptions.
|
|
224
|
+
|
|
225
|
+
When *ping* is ``True``, the function connects to the database, prints
|
|
226
|
+
connection details (DBMS name, server version, and location), and calls
|
|
227
|
+
:func:`_ping_db` which raises ``SystemExit(0)``. No script is loaded or
|
|
228
|
+
executed. *script_name* and *command* may both be ``None`` in ping mode.
|
|
229
|
+
|
|
230
|
+
When *lint* is ``True``, the script is parsed and statically analysed for
|
|
231
|
+
structural issues (unmatched IF/ENDIF/LOOP/BATCH blocks, potentially
|
|
232
|
+
undefined variables, missing INCLUDE files, empty scripts) without
|
|
233
|
+
connecting to a database or executing anything. Exits with code 0 if no
|
|
234
|
+
errors were found, or code 1 if errors were found.
|
|
160
235
|
"""
|
|
161
236
|
import execsql.state as _state
|
|
162
237
|
|
|
@@ -286,10 +361,10 @@ def _run(
|
|
|
286
361
|
if progress:
|
|
287
362
|
conf.show_progress = True
|
|
288
363
|
|
|
289
|
-
# Positional arguments after the script name (or all positionals in inline mode)
|
|
364
|
+
# Positional arguments after the script name (or all positionals in inline/ping mode)
|
|
290
365
|
# off=1: script file occupies positional[0]; connection args start at [1]
|
|
291
|
-
# off=0: no script file; all positionals are connection args
|
|
292
|
-
off = 0 if command is not None else 1
|
|
366
|
+
# off=0: no script file; all positionals are connection args (inline -c or --ping)
|
|
367
|
+
off = 0 if (command is not None or ping) else 1
|
|
293
368
|
if len(positional) == off + 1:
|
|
294
369
|
if conf.db_type in ("a", "l", "k"):
|
|
295
370
|
conf.db_file = positional[off]
|
|
@@ -403,12 +478,13 @@ def _run(
|
|
|
403
478
|
)
|
|
404
479
|
|
|
405
480
|
# ------------------------------------------------------------------
|
|
406
|
-
# Load the SQL script
|
|
481
|
+
# Load the SQL script (skipped in --ping and --dry-run with no script)
|
|
407
482
|
# ------------------------------------------------------------------
|
|
408
|
-
if
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
483
|
+
if not ping:
|
|
484
|
+
if command is not None:
|
|
485
|
+
read_sqlstring(command.replace("\\n", "\n").replace("\\t", "\t"), "<inline>")
|
|
486
|
+
else:
|
|
487
|
+
read_sqlfile(script_name)
|
|
412
488
|
|
|
413
489
|
# ------------------------------------------------------------------
|
|
414
490
|
# Dry-run: print command list and exit without connecting to DB
|
|
@@ -417,6 +493,16 @@ def _run(
|
|
|
417
493
|
_print_dry_run(_state.commandliststack[-1] if _state.commandliststack else None)
|
|
418
494
|
raise SystemExit(0)
|
|
419
495
|
|
|
496
|
+
# ------------------------------------------------------------------
|
|
497
|
+
# Lint: static analysis without connecting to DB
|
|
498
|
+
# ------------------------------------------------------------------
|
|
499
|
+
if lint:
|
|
500
|
+
cmdlist = _state.commandliststack[-1] if _state.commandliststack else None
|
|
501
|
+
issues = _lint_script(cmdlist, script_name)
|
|
502
|
+
label = script_name or "<inline>"
|
|
503
|
+
exit_code = _print_lint_results(issues, label)
|
|
504
|
+
raise SystemExit(exit_code)
|
|
505
|
+
|
|
420
506
|
# ------------------------------------------------------------------
|
|
421
507
|
# Start GUI console if requested
|
|
422
508
|
# ------------------------------------------------------------------
|
|
@@ -446,6 +532,12 @@ def _run(
|
|
|
446
532
|
_state.subvars.add_substitution("$DB_SERVER", db.server_name)
|
|
447
533
|
_state.subvars.add_substitution("$SYSTEM_CMD_EXIT_STATUS", "0")
|
|
448
534
|
|
|
535
|
+
# ------------------------------------------------------------------
|
|
536
|
+
# --ping: report connection details and exit (no script executed)
|
|
537
|
+
# ------------------------------------------------------------------
|
|
538
|
+
if ping:
|
|
539
|
+
_ping_db(db) # raises SystemExit(0) on success
|
|
540
|
+
|
|
449
541
|
# ------------------------------------------------------------------
|
|
450
542
|
# Execute the script
|
|
451
543
|
# ------------------------------------------------------------------
|
execsql/metacommands/__init__.py
CHANGED
|
@@ -100,6 +100,7 @@ from execsql.metacommands.debug import (
|
|
|
100
100
|
x_debug_write_subvars,
|
|
101
101
|
x_debug_write_config,
|
|
102
102
|
)
|
|
103
|
+
from execsql.metacommands.debug_repl import x_breakpoint
|
|
103
104
|
from execsql.metacommands.io import (
|
|
104
105
|
x_export,
|
|
105
106
|
x_export_query,
|
|
@@ -300,6 +301,8 @@ __all__ = [
|
|
|
300
301
|
"x_debug_log_config",
|
|
301
302
|
"x_debug_write_subvars",
|
|
302
303
|
"x_debug_write_config",
|
|
304
|
+
# debug repl handlers
|
|
305
|
+
"x_breakpoint",
|
|
303
306
|
# io handlers
|
|
304
307
|
"x_export",
|
|
305
308
|
"x_export_query",
|
|
@@ -71,6 +71,124 @@ def xf_hasrows(**kwargs: Any) -> bool:
|
|
|
71
71
|
return nrows > 0
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
def _row_count(queryname: str, sql_context: str, metacommandline: str) -> int:
|
|
75
|
+
"""Return the number of rows in *queryname*, raising ErrInfo on failure.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
queryname: Table or view name to count rows in.
|
|
79
|
+
sql_context: The SQL string to include in error messages.
|
|
80
|
+
metacommandline: The full metacommand line for error context.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Integer row count.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
ErrInfo: If the query fails or the result is not numeric.
|
|
87
|
+
"""
|
|
88
|
+
sql = f"select count(*) from {queryname};"
|
|
89
|
+
try:
|
|
90
|
+
_hdrs, rec = _state.dbs.current().select_data(sql)
|
|
91
|
+
except ErrInfo:
|
|
92
|
+
raise
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise ErrInfo("db", sql, exception_msg=exception_desc()) from e
|
|
95
|
+
try:
|
|
96
|
+
return int(rec[0][0])
|
|
97
|
+
except (IndexError, TypeError, ValueError) as e:
|
|
98
|
+
raise ErrInfo(
|
|
99
|
+
type="cmd",
|
|
100
|
+
command_text=metacommandline,
|
|
101
|
+
other_msg=f"Could not read row count for {queryname}.",
|
|
102
|
+
) from e
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _parse_row_count_n(raw: str, metacommandline: str) -> int:
|
|
106
|
+
"""Parse and return the numeric threshold N from the matched group.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
raw: The raw string captured by the regex group (``n``).
|
|
110
|
+
metacommandline: The full metacommand line for error context.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Integer value of *raw*.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
ErrInfo: If *raw* cannot be parsed as an integer.
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
return int(raw.strip())
|
|
120
|
+
except (ValueError, TypeError) as e:
|
|
121
|
+
raise ErrInfo(
|
|
122
|
+
type="cmd",
|
|
123
|
+
command_text=metacommandline,
|
|
124
|
+
other_msg=f"ROW_COUNT threshold must be an integer; got {raw!r}.",
|
|
125
|
+
) from e
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def xf_row_count_gt(**kwargs: Any) -> bool:
|
|
129
|
+
"""Return True if the row count of *queryname* is strictly greater than N.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
**kwargs: Named groups from the regex match, plus ``metacommandline``.
|
|
133
|
+
Required keys: ``queryname``, ``n``.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if ``count(*) > N``.
|
|
137
|
+
"""
|
|
138
|
+
queryname = kwargs["queryname"]
|
|
139
|
+
mcl = kwargs["metacommandline"]
|
|
140
|
+
n = _parse_row_count_n(kwargs["n"], mcl)
|
|
141
|
+
return _row_count(queryname, f"select count(*) from {queryname};", mcl) > n
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def xf_row_count_gte(**kwargs: Any) -> bool:
|
|
145
|
+
"""Return True if the row count of *queryname* is greater than or equal to N.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
**kwargs: Named groups from the regex match, plus ``metacommandline``.
|
|
149
|
+
Required keys: ``queryname``, ``n``.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
True if ``count(*) >= N``.
|
|
153
|
+
"""
|
|
154
|
+
queryname = kwargs["queryname"]
|
|
155
|
+
mcl = kwargs["metacommandline"]
|
|
156
|
+
n = _parse_row_count_n(kwargs["n"], mcl)
|
|
157
|
+
return _row_count(queryname, f"select count(*) from {queryname};", mcl) >= n
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def xf_row_count_eq(**kwargs: Any) -> bool:
|
|
161
|
+
"""Return True if the row count of *queryname* equals N exactly.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
**kwargs: Named groups from the regex match, plus ``metacommandline``.
|
|
165
|
+
Required keys: ``queryname``, ``n``.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
True if ``count(*) == N``.
|
|
169
|
+
"""
|
|
170
|
+
queryname = kwargs["queryname"]
|
|
171
|
+
mcl = kwargs["metacommandline"]
|
|
172
|
+
n = _parse_row_count_n(kwargs["n"], mcl)
|
|
173
|
+
return _row_count(queryname, f"select count(*) from {queryname};", mcl) == n
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def xf_row_count_lt(**kwargs: Any) -> bool:
|
|
177
|
+
"""Return True if the row count of *queryname* is strictly less than N.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
**kwargs: Named groups from the regex match, plus ``metacommandline``.
|
|
181
|
+
Required keys: ``queryname``, ``n``.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
True if ``count(*) < N``.
|
|
185
|
+
"""
|
|
186
|
+
queryname = kwargs["queryname"]
|
|
187
|
+
mcl = kwargs["metacommandline"]
|
|
188
|
+
n = _parse_row_count_n(kwargs["n"], mcl)
|
|
189
|
+
return _row_count(queryname, f"select count(*) from {queryname};", mcl) < n
|
|
190
|
+
|
|
191
|
+
|
|
74
192
|
def xf_sqlerror(**kwargs: Any) -> bool:
|
|
75
193
|
return _state.status.sql_error
|
|
76
194
|
|
|
@@ -495,6 +613,36 @@ def build_conditional_table() -> Any:
|
|
|
495
613
|
mcl.add(r"^\s*HASROWS\((?P<queryname>[^)]+)\)", xf_hasrows, description="HASROWS", category="condition")
|
|
496
614
|
mcl.add(r"^\s*HAS_ROWS\((?P<queryname>[^)]+)\)", xf_hasrows)
|
|
497
615
|
|
|
616
|
+
# ROW_COUNT comparisons — ROW_COUNT_GT/GTE/EQ/LT(table, N)
|
|
617
|
+
# Table name: unquoted, double-quoted, or single-quoted. N: integer literal.
|
|
618
|
+
_rc_table = r"(?P<queryname>[A-Za-z0-9_.\"'\[\]]+)"
|
|
619
|
+
_rc_n = r"(?P<n>\d+)"
|
|
620
|
+
_rc_sep = r"\s*,\s*"
|
|
621
|
+
mcl.add(
|
|
622
|
+
rf"^\s*ROW_COUNT_GT\s*\(\s*{_rc_table}{_rc_sep}{_rc_n}\s*\)",
|
|
623
|
+
xf_row_count_gt,
|
|
624
|
+
description="ROW_COUNT_GT",
|
|
625
|
+
category="condition",
|
|
626
|
+
)
|
|
627
|
+
mcl.add(
|
|
628
|
+
rf"^\s*ROW_COUNT_GTE\s*\(\s*{_rc_table}{_rc_sep}{_rc_n}\s*\)",
|
|
629
|
+
xf_row_count_gte,
|
|
630
|
+
description="ROW_COUNT_GTE",
|
|
631
|
+
category="condition",
|
|
632
|
+
)
|
|
633
|
+
mcl.add(
|
|
634
|
+
rf"^\s*ROW_COUNT_EQ\s*\(\s*{_rc_table}{_rc_sep}{_rc_n}\s*\)",
|
|
635
|
+
xf_row_count_eq,
|
|
636
|
+
description="ROW_COUNT_EQ",
|
|
637
|
+
category="condition",
|
|
638
|
+
)
|
|
639
|
+
mcl.add(
|
|
640
|
+
rf"^\s*ROW_COUNT_LT\s*\(\s*{_rc_table}{_rc_sep}{_rc_n}\s*\)",
|
|
641
|
+
xf_row_count_lt,
|
|
642
|
+
description="ROW_COUNT_LT",
|
|
643
|
+
category="condition",
|
|
644
|
+
)
|
|
645
|
+
|
|
498
646
|
# Status predicates
|
|
499
647
|
mcl.add(r"^\s*sql_error\(\s*\)", xf_sqlerror, description="SQL_ERROR", category="condition")
|
|
500
648
|
mcl.add(r"^\s*dialog_canceled\(\s*\)", xf_dialogcanceled, description="DIALOG_CANCELED", category="condition")
|
|
@@ -0,0 +1,223 @@
|
|
|
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
|
+
In non-interactive environments (CI, piped input, ``sys.stdin.isatty()`` is
|
|
17
|
+
``False``) the metacommand is silently skipped so automated pipelines are not
|
|
18
|
+
blocked.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import sys
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import execsql.state as _state
|
|
25
|
+
|
|
26
|
+
__all__ = ["x_breakpoint"]
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Public handler
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
_HELP_TEXT = """\
|
|
33
|
+
execsql debug REPL commands:
|
|
34
|
+
continue c Resume script execution
|
|
35
|
+
abort q quit Halt the script (exit 1)
|
|
36
|
+
vars List all substitution variables and their values
|
|
37
|
+
$VARNAME Print a single variable's value (also &VAR, @VAR)
|
|
38
|
+
SELECT ...; Run ad-hoc SQL against the current database
|
|
39
|
+
next n Execute the next statement then pause again (step mode)
|
|
40
|
+
stack Show the command-list stack (script name, line, depth)
|
|
41
|
+
help Show this help text
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def x_breakpoint(**kwargs: Any) -> None:
|
|
46
|
+
"""Pause execution and enter the interactive debug REPL.
|
|
47
|
+
|
|
48
|
+
If ``sys.stdin`` is not a TTY (CI, piped input), the metacommand is
|
|
49
|
+
silently skipped — scripts will not hang in automation.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
**kwargs: Keyword arguments injected by the dispatch table (unused).
|
|
53
|
+
"""
|
|
54
|
+
if not sys.stdin.isatty():
|
|
55
|
+
return
|
|
56
|
+
_debug_repl()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# REPL core
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _debug_repl() -> None:
|
|
65
|
+
"""Interactive read-eval-print loop for script debugging.
|
|
66
|
+
|
|
67
|
+
Reads commands from stdin until the user types ``continue`` or ``abort``,
|
|
68
|
+
or until EOF / KeyboardInterrupt.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
import readline as _readline # noqa: F401 — side-effect: enables history/arrow keys
|
|
72
|
+
except ImportError:
|
|
73
|
+
pass # readline not available on Windows; continue without it
|
|
74
|
+
|
|
75
|
+
_write("\n[Breakpoint] Script paused. Type 'help' for commands, 'continue' to resume.\n")
|
|
76
|
+
|
|
77
|
+
while True:
|
|
78
|
+
try:
|
|
79
|
+
line = input("execsql debug> ").strip()
|
|
80
|
+
except EOFError:
|
|
81
|
+
_write("\n")
|
|
82
|
+
return # Ctrl-D → continue
|
|
83
|
+
except KeyboardInterrupt:
|
|
84
|
+
_write("\n")
|
|
85
|
+
return # Ctrl-C → continue
|
|
86
|
+
|
|
87
|
+
if not line:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
lower = line.lower()
|
|
91
|
+
|
|
92
|
+
if lower in ("continue", "c"):
|
|
93
|
+
return
|
|
94
|
+
elif lower in ("abort", "q", "quit"):
|
|
95
|
+
raise SystemExit(1)
|
|
96
|
+
elif lower == "help":
|
|
97
|
+
_write(_HELP_TEXT)
|
|
98
|
+
elif lower == "vars":
|
|
99
|
+
_print_all_vars()
|
|
100
|
+
elif lower == "stack":
|
|
101
|
+
_print_stack()
|
|
102
|
+
elif lower in ("next", "n"):
|
|
103
|
+
_enable_step_mode()
|
|
104
|
+
return
|
|
105
|
+
elif line[0] in ("$", "&", "@"):
|
|
106
|
+
_print_var(line)
|
|
107
|
+
elif line.rstrip().endswith(";"):
|
|
108
|
+
_run_sql(line)
|
|
109
|
+
else:
|
|
110
|
+
_write(f"Unknown command: {line!r}. Type 'help' for available commands.\n")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# REPL command implementations
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _write(text: str) -> None:
|
|
119
|
+
"""Write *text* to the execsql output stream (falls back to stdout)."""
|
|
120
|
+
output = _state.output
|
|
121
|
+
if output is not None:
|
|
122
|
+
output.write(text)
|
|
123
|
+
else:
|
|
124
|
+
sys.stdout.write(text)
|
|
125
|
+
sys.stdout.flush()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _print_all_vars() -> None:
|
|
129
|
+
"""Print all substitution variables and their current values."""
|
|
130
|
+
subvars = _state.subvars
|
|
131
|
+
if subvars is None:
|
|
132
|
+
_write(" (no substitution variables defined)\n")
|
|
133
|
+
return
|
|
134
|
+
items = subvars.substitutions # list of (name, value) tuples
|
|
135
|
+
if not items:
|
|
136
|
+
_write(" (no substitution variables defined)\n")
|
|
137
|
+
return
|
|
138
|
+
# Compute column width for aligned output.
|
|
139
|
+
max_name = max((len(name) for name, _ in items), default=0)
|
|
140
|
+
for name, value in sorted(items):
|
|
141
|
+
_write(f" {name:<{max_name}} = {value!r}\n")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _print_var(varname: str) -> None:
|
|
145
|
+
"""Print the value of a single substitution variable.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
varname: The variable reference as typed by the user, e.g. ``$FOO``.
|
|
149
|
+
"""
|
|
150
|
+
subvars = _state.subvars
|
|
151
|
+
if subvars is None:
|
|
152
|
+
_write(f" {varname}: (substitution variables not initialised)\n")
|
|
153
|
+
return
|
|
154
|
+
# varvalue() expects the name with its prefix (e.g. "$foo"); it lowercases internally.
|
|
155
|
+
value = subvars.varvalue(varname)
|
|
156
|
+
if value is None:
|
|
157
|
+
_write(f" {varname}: (undefined)\n")
|
|
158
|
+
else:
|
|
159
|
+
_write(f" {varname} = {value!r}\n")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _print_stack() -> None:
|
|
163
|
+
"""Print the current command-list stack (script name, line number, depth)."""
|
|
164
|
+
stack = _state.commandliststack
|
|
165
|
+
if not stack:
|
|
166
|
+
_write(" (command list stack is empty)\n")
|
|
167
|
+
return
|
|
168
|
+
_write(f" Stack depth: {len(stack)}\n")
|
|
169
|
+
for depth, cmdlist in enumerate(stack):
|
|
170
|
+
listname = getattr(cmdlist, "listname", "<unknown>")
|
|
171
|
+
cmdptr = getattr(cmdlist, "cmdptr", 0)
|
|
172
|
+
_write(f" [{depth}] {listname} (cursor at index {cmdptr})\n")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _run_sql(sql: str) -> None:
|
|
176
|
+
"""Execute ad-hoc SQL against the current database and pretty-print the results.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
sql: A complete SQL statement ending with a semicolon.
|
|
180
|
+
"""
|
|
181
|
+
dbs = _state.dbs
|
|
182
|
+
if dbs is None:
|
|
183
|
+
_write(" (no database connection is active)\n")
|
|
184
|
+
return
|
|
185
|
+
db = dbs.current()
|
|
186
|
+
if db is None:
|
|
187
|
+
_write(" (no database connection is active)\n")
|
|
188
|
+
return
|
|
189
|
+
try:
|
|
190
|
+
colnames, rows = db.select_data(sql)
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
_write(f" SQL error: {exc}\n")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
if not colnames:
|
|
196
|
+
_write(" (query returned no columns)\n")
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
# Build a simple text table.
|
|
200
|
+
col_widths = [len(c) for c in colnames]
|
|
201
|
+
str_rows: list[list[str]] = []
|
|
202
|
+
for row in rows:
|
|
203
|
+
str_row = [str(v) if v is not None else "NULL" for v in row]
|
|
204
|
+
str_rows.append(str_row)
|
|
205
|
+
for i, cell in enumerate(str_row):
|
|
206
|
+
col_widths[i] = max(col_widths[i], len(cell))
|
|
207
|
+
|
|
208
|
+
sep = "+-" + "-+-".join("-" * w for w in col_widths) + "-+"
|
|
209
|
+
header = "| " + " | ".join(c.ljust(col_widths[i]) for i, c in enumerate(colnames)) + " |"
|
|
210
|
+
_write(sep + "\n")
|
|
211
|
+
_write(header + "\n")
|
|
212
|
+
_write(sep + "\n")
|
|
213
|
+
for str_row in str_rows:
|
|
214
|
+
data_line = "| " + " | ".join(cell.ljust(col_widths[i]) for i, cell in enumerate(str_row)) + " |"
|
|
215
|
+
_write(data_line + "\n")
|
|
216
|
+
_write(sep + "\n")
|
|
217
|
+
row_word = "row" if len(str_rows) == 1 else "rows"
|
|
218
|
+
_write(f" ({len(str_rows)} {row_word})\n")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _enable_step_mode() -> None:
|
|
222
|
+
"""Activate step mode so the engine re-enters the REPL after the next statement."""
|
|
223
|
+
_state.step_mode = True
|
execsql/metacommands/dispatch.py
CHANGED
|
@@ -100,6 +100,7 @@ from execsql.metacommands.debug import (
|
|
|
100
100
|
x_debug_write_odbc_drivers,
|
|
101
101
|
x_debug_write_subvars,
|
|
102
102
|
)
|
|
103
|
+
from execsql.metacommands.debug_repl import x_breakpoint
|
|
103
104
|
from execsql.metacommands.io import (
|
|
104
105
|
x_cd,
|
|
105
106
|
x_copy,
|
|
@@ -1690,6 +1691,17 @@ def build_dispatch_table() -> MetaCommandList:
|
|
|
1690
1691
|
run_when_false=False,
|
|
1691
1692
|
)
|
|
1692
1693
|
|
|
1694
|
+
# ------------------------------------------------------------------
|
|
1695
|
+
# BREAKPOINT
|
|
1696
|
+
# ------------------------------------------------------------------
|
|
1697
|
+
mcl.add(
|
|
1698
|
+
r"^\s*BREAKPOINT\s*$",
|
|
1699
|
+
x_breakpoint,
|
|
1700
|
+
description="BREAKPOINT",
|
|
1701
|
+
category="action",
|
|
1702
|
+
run_when_false=False,
|
|
1703
|
+
)
|
|
1704
|
+
|
|
1693
1705
|
# ------------------------------------------------------------------
|
|
1694
1706
|
# IF / ORIF / ANDIF / ELSEIF / ELSE / ENDIF
|
|
1695
1707
|
# ------------------------------------------------------------------
|
execsql/script/engine.py
CHANGED
|
@@ -506,6 +506,11 @@ class CommandList:
|
|
|
506
506
|
cmditem.command.commandline()[:100],
|
|
507
507
|
),
|
|
508
508
|
)
|
|
509
|
+
if _state.step_mode:
|
|
510
|
+
_state.step_mode = False
|
|
511
|
+
from execsql.metacommands.debug_repl import _debug_repl
|
|
512
|
+
|
|
513
|
+
_debug_repl()
|
|
509
514
|
self.cmdptr += 1
|
|
510
515
|
|
|
511
516
|
def run_next(self) -> None:
|
execsql/state.py
CHANGED
|
@@ -96,6 +96,8 @@ __all__ = [
|
|
|
96
96
|
"gui_manager_thread",
|
|
97
97
|
# Profiling
|
|
98
98
|
"profile_data",
|
|
99
|
+
# Debug REPL
|
|
100
|
+
"step_mode",
|
|
99
101
|
# Version
|
|
100
102
|
"primary_vno",
|
|
101
103
|
"secondary_vno",
|
|
@@ -195,6 +197,8 @@ _CONTEXT_ATTRS: frozenset[str] = frozenset(
|
|
|
195
197
|
"gui_manager_thread",
|
|
196
198
|
# Profiling
|
|
197
199
|
"profile_data",
|
|
200
|
+
# Debug REPL
|
|
201
|
+
"step_mode",
|
|
198
202
|
},
|
|
199
203
|
)
|
|
200
204
|
|
|
@@ -248,6 +252,8 @@ class RuntimeContext:
|
|
|
248
252
|
"gui_manager_thread",
|
|
249
253
|
# Profiling
|
|
250
254
|
"profile_data",
|
|
255
|
+
# Debug REPL
|
|
256
|
+
"step_mode",
|
|
251
257
|
)
|
|
252
258
|
|
|
253
259
|
def __init__(self) -> None:
|
|
@@ -299,6 +305,9 @@ class RuntimeContext:
|
|
|
299
305
|
# Each entry: (source, line_no, command_type, elapsed_secs, command_text_preview)
|
|
300
306
|
self.profile_data: list[tuple] | None = None
|
|
301
307
|
|
|
308
|
+
# Debug REPL — True after a ``next`` command; engine re-enters REPL after next statement.
|
|
309
|
+
self.step_mode: bool = False
|
|
310
|
+
|
|
302
311
|
|
|
303
312
|
# ---------------------------------------------------------------------------
|
|
304
313
|
# Module proxy — transparently delegates context attr access to _ctx
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.10.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
|
|
@@ -221,6 +221,7 @@ execsql script.sql # read connection from config file
|
|
|
221
221
|
| `-w` | Skip password prompt when a username is supplied |
|
|
222
222
|
| `--dsn URL` | Connection string (e.g. `postgresql://user:pass@host/db`) |
|
|
223
223
|
| `--dry-run` | Parse the script and report commands without executing |
|
|
224
|
+
| `--lint` | Static analysis: check structure and warn on issues (no DB) |
|
|
224
225
|
| `--progress` | Show a progress bar for long-running IMPORT operations |
|
|
225
226
|
| `--dump-keywords` | Print metacommand keywords as JSON and exit |
|
|
226
227
|
| `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
|
|
@@ -7,12 +7,13 @@ execsql/format.py,sha256=-6iknDddqbkapMo4NKmT5LAynDLqMW5kHgDWRg0KSws,11990
|
|
|
7
7
|
execsql/models.py,sha256=DxkGp9iWbuZDWPGmnxZp9mvEeyOwxEJNx94fxQQiLfQ,13538
|
|
8
8
|
execsql/parser.py,sha256=mbNSMiAMR1NvNvFtQAZq6nxBOupMGJZXSimLWLtZeNs,15537
|
|
9
9
|
execsql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
execsql/state.py,sha256=
|
|
10
|
+
execsql/state.py,sha256=ovUQOr78R3LzsDUzBLL9Bq9ZdHiiFVlBvCNzilQ-K-s,15055
|
|
11
11
|
execsql/types.py,sha256=HVWb4umIB9lpxCGgqk3xy1hoGYPfN39xci5mHF0Izq4,31882
|
|
12
|
-
execsql/cli/__init__.py,sha256=
|
|
12
|
+
execsql/cli/__init__.py,sha256=Y4lFKvKWyjtMgSLsmACHYG33DFpeAQxddIVrlKURwpg,16081
|
|
13
13
|
execsql/cli/dsn.py,sha256=svaZtrUXFRL2W5G6FRRiKtR6kehOp7urrVhIx_642Z8,2820
|
|
14
14
|
execsql/cli/help.py,sha256=Sn_TgSJiQeBx-xZH0fuP5OvR_wasSTumjWF9UHfIX5k,5414
|
|
15
|
-
execsql/cli/
|
|
15
|
+
execsql/cli/lint.py,sha256=KluYROdjGJNUrVbO3cJym-H296zbih4no-F10HF0P4U,16165
|
|
16
|
+
execsql/cli/run.py,sha256=i0ip8tm21Sm4EFbfBdcekETmiABzrRfeP84SqA4IF68,30158
|
|
16
17
|
execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
|
|
17
18
|
execsql/db/access.py,sha256=L79gUnAnnM9EJ_f4k42jr7DI0qGcKtLOnJTlBC7uPm0,17879
|
|
18
19
|
execsql/db/base.py,sha256=hfMFj8fXY0T1aXLvWJHqb0aU4EQUDFOc-YrS29HH8U4,30405
|
|
@@ -58,13 +59,14 @@ execsql/importers/csv.py,sha256=Mu848WNzuhVO1ade-WurPyxqGOuVNRO8UwRF3-bav_I,4845
|
|
|
58
59
|
execsql/importers/feather.py,sha256=g2B69d2uv9vmnXcmjFyTVsMP40LYEzFYkhk3gD26mGw,1900
|
|
59
60
|
execsql/importers/ods.py,sha256=MJsdsjropzCvxAA3DDZfAL_AnmZ4yij7DnrjGyDJqHQ,2843
|
|
60
61
|
execsql/importers/xls.py,sha256=e0Zfe47ZiCpA1Ae3XDJ1ko3sCiH3-8U6XLKi6NvD0jQ,3683
|
|
61
|
-
execsql/metacommands/__init__.py,sha256=
|
|
62
|
-
execsql/metacommands/conditions.py,sha256=
|
|
62
|
+
execsql/metacommands/__init__.py,sha256=TT1ARHgHltHqZ7qx4Y62o1h_GOPvUztZKCem-wAE560,11215
|
|
63
|
+
execsql/metacommands/conditions.py,sha256=QUpevCHce9kbKpt6XpHkc73q3bYAIfaBin0b6eaJnYQ,29259
|
|
63
64
|
execsql/metacommands/connect.py,sha256=Nsm0D91i3RX-R2rzQQ-Br-gULaI6Uvdn9fqb7DOAVfE,14804
|
|
64
65
|
execsql/metacommands/control.py,sha256=CBCg0ZKSR-BGejBW5cXwk6aJ9VrYBzCg9C40ofi8qi8,8776
|
|
65
66
|
execsql/metacommands/data.py,sha256=tRQBGTAuW-eJ2tBNWaoZI9OjTyNNyHJISo7gOdL-sm8,11370
|
|
66
67
|
execsql/metacommands/debug.py,sha256=nmfQ2ijUbTQO3drnyV9EzFueGSTfMl-CddP_NlQyI14,8178
|
|
67
|
-
execsql/metacommands/
|
|
68
|
+
execsql/metacommands/debug_repl.py,sha256=fjf8O25dQs-Nrl_Uw_0RahKSttkty4cbzUClNsZuV18,7303
|
|
69
|
+
execsql/metacommands/dispatch.py,sha256=1Mae6yqrea6wViFLBsvVt33Zgx4xP8tnhOuB_aQC89c,84054
|
|
68
70
|
execsql/metacommands/io.py,sha256=Duh60caM4go9JczbGYNMKKYpcMimwPzF6EQ_tshKxdE,2971
|
|
69
71
|
execsql/metacommands/io_export.py,sha256=7lkCSnPhXy9FVau9_hT1u68NOVdG2DsWmvUh9hM1QWI,18359
|
|
70
72
|
execsql/metacommands/io_fileops.py,sha256=RKqbWPTYiwiqCZYG-lpih0w1JVOY4RBFdWr3BJb_pnY,9669
|
|
@@ -75,7 +77,7 @@ execsql/metacommands/script_ext.py,sha256=TUgAldB2LSJAwZrCvDDi804hQ1d9BDQD2GDqHN
|
|
|
75
77
|
execsql/metacommands/system.py,sha256=sUR5kLL7idTVg8WXIMdd-Kv7nkERIiaeL0beWsz8NyY,7293
|
|
76
78
|
execsql/script/__init__.py,sha256=pIo0EJ7-vg67rSMbOvbri_BOUgLoGoSEUfJgxUN7ZS0,3380
|
|
77
79
|
execsql/script/control.py,sha256=s-1eZdGARM6H1FwZ6VDdO_f50j7bvvRtTHesfUm9tbc,6144
|
|
78
|
-
execsql/script/engine.py,sha256=
|
|
80
|
+
execsql/script/engine.py,sha256=2WcOfYEOwO7L_NQAd3vk_c2wk1VZKJYSakSl07FBUts,40390
|
|
79
81
|
execsql/script/variables.py,sha256=MOT9XEHucpuuuHQZM5bklxGMBQcwHzwTBxd0q3aO0XY,11641
|
|
80
82
|
execsql/utils/__init__.py,sha256=0uR6JwVJQRX3vceByNBduCAf5dd5assKjeqJUWvpZoA,278
|
|
81
83
|
execsql/utils/auth.py,sha256=onXzNkNZQZxGC5w7eey06sjvAIAX_Lf9g7nUJtcsel0,7009
|
|
@@ -89,24 +91,24 @@ execsql/utils/numeric.py,sha256=xh02ANSRk3nUpQ-rtm66ILoMqoi7HtzCoRMIOT9U8QI,1570
|
|
|
89
91
|
execsql/utils/regex.py,sha256=diEzTZqU_HHwVMadPAvN1Vgzhl7I03eVaEFGCXyGGL8,3770
|
|
90
92
|
execsql/utils/strings.py,sha256=5Dvzrk-9SIw2lpxXZQkiJbNyo1sy7iXXAtSULlZ0KG8,8488
|
|
91
93
|
execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
|
|
92
|
-
execsql2-2.
|
|
93
|
-
execsql2-2.
|
|
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.
|
|
94
|
+
execsql2-2.10.0.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
|
|
95
|
+
execsql2-2.10.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
|
|
96
|
+
execsql2-2.10.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
|
|
97
|
+
execsql2-2.10.0.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
|
|
98
|
+
execsql2-2.10.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
|
|
99
|
+
execsql2-2.10.0.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
|
|
100
|
+
execsql2-2.10.0.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
|
|
101
|
+
execsql2-2.10.0.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
|
|
102
|
+
execsql2-2.10.0.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
|
|
103
|
+
execsql2-2.10.0.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
|
|
104
|
+
execsql2-2.10.0.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
|
|
105
|
+
execsql2-2.10.0.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
|
|
106
|
+
execsql2-2.10.0.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
|
|
107
|
+
execsql2-2.10.0.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
|
|
108
|
+
execsql2-2.10.0.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
|
|
109
|
+
execsql2-2.10.0.dist-info/METADATA,sha256=V4hDmO7GKgx0ZRu8BsVES5C6oyRIUus-MIu7k7RJlfI,16956
|
|
110
|
+
execsql2-2.10.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
111
|
+
execsql2-2.10.0.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
|
|
112
|
+
execsql2-2.10.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
|
|
113
|
+
execsql2-2.10.0.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
|
|
114
|
+
execsql2-2.10.0.dist-info/RECORD,,
|
|
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
|
|
File without changes
|
|
File without changes
|