execsql2 2.23.1__py3-none-any.whl → 2.24.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 +17 -586
- execsql/cli/application.py +309 -0
- execsql/cli/commands/__init__.py +13 -0
- execsql/cli/commands/config.py +205 -0
- execsql/cli/commands/format.py +100 -0
- execsql/cli/commands/init.py +135 -0
- execsql/cli/commands/lint.py +233 -0
- execsql/cli/commands/listing.py +81 -0
- execsql/cli/commands/run.py +451 -0
- execsql/cli/dispatch.py +66 -0
- execsql/cli/help.py +175 -24
- execsql/cli/lint.py +513 -76
- execsql/cli/options.py +108 -0
- execsql/cli/run.py +41 -14
- execsql/config.py +251 -154
- execsql/config_validation.py +282 -0
- execsql/data/execsql.conf.template +24 -1
- execsql/data/script_header.sql +23 -0
- execsql/db/factory.py +2 -2
- execsql/debug/repl.py +51 -193
- execsql/exceptions.py +9 -0
- execsql/format.py +87 -75
- execsql/interactive.py +232 -0
- execsql/manifest.py +189 -0
- execsql/metacommands/conditions.py +1 -1
- execsql/metacommands/control.py +2 -3
- execsql/metacommands/effects.py +124 -0
- execsql/script/__init__.py +0 -9
- execsql/script/ast.py +1 -1
- execsql/script/engine.py +6 -93
- execsql/script/executor.py +153 -34
- execsql/state.py +11 -2
- execsql/state.pyi +3 -0
- execsql/utils/color.py +18 -0
- execsql/utils/errors.py +11 -1
- {execsql2-2.23.1.data → execsql2-2.24.0.data}/data/execsql2_extras/README.md +1 -1
- {execsql2-2.23.1.dist-info → execsql2-2.24.0.dist-info}/METADATA +111 -58
- {execsql2-2.23.1.dist-info → execsql2-2.24.0.dist-info}/RECORD +55 -39
- execsql2-2.24.0.dist-info/entry_points.txt +2 -0
- execsql2-2.23.1.dist-info/entry_points.txt +0 -3
- {execsql2-2.23.1.data → execsql2-2.24.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.23.1.data → execsql2-2.24.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.24.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.24.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.24.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.24.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.24.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.24.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.24.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.24.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.24.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.24.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.24.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.24.0.dist-info}/WHEEL +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.24.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.24.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""What a matched metacommand touches: files it reads, writes or deletes, and connections.
|
|
2
|
+
|
|
3
|
+
Used by ``run --manifest``, which records the metacommands a run executed.
|
|
4
|
+
It classifies from the handler a metacommand dispatched to and the groups its
|
|
5
|
+
regex captured, so any other reader of a script (a static inspector) can share
|
|
6
|
+
the same answer.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
__all__ = ["Effect", "effects"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Effect:
|
|
18
|
+
"""One thing a metacommand touches.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
role: ``"read"``, ``"write"`` or ``"delete"`` for a file;
|
|
22
|
+
``"connect"`` or ``"use"`` for a database.
|
|
23
|
+
target: The file path, or for a connection ``alias: server/db`` or
|
|
24
|
+
``alias: file``.
|
|
25
|
+
detail: For a connection, the DBMS and user. Never a password.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
role: str
|
|
29
|
+
target: str
|
|
30
|
+
detail: str = ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_IMPORTS = (
|
|
34
|
+
"x_import",
|
|
35
|
+
"x_import_file",
|
|
36
|
+
"x_import_feather",
|
|
37
|
+
"x_import_json",
|
|
38
|
+
"x_import_ods",
|
|
39
|
+
"x_import_ods_pattern",
|
|
40
|
+
"x_import_parquet",
|
|
41
|
+
"x_import_xls",
|
|
42
|
+
"x_import_xls_pattern",
|
|
43
|
+
"x_sub_ini",
|
|
44
|
+
"x_serve",
|
|
45
|
+
)
|
|
46
|
+
_EXPORTS = (
|
|
47
|
+
"x_export",
|
|
48
|
+
"x_export_query",
|
|
49
|
+
"x_export_metadata",
|
|
50
|
+
"x_export_with_template",
|
|
51
|
+
"x_export_query_with_template",
|
|
52
|
+
"x_export_ods_multiple",
|
|
53
|
+
"x_export_xlsx_multiple",
|
|
54
|
+
)
|
|
55
|
+
_WRITES = (
|
|
56
|
+
"x_write",
|
|
57
|
+
"x_writescript",
|
|
58
|
+
"x_consolesave",
|
|
59
|
+
"x_cancel_halt_write",
|
|
60
|
+
"x_error_halt_write",
|
|
61
|
+
"x_debug_write_config",
|
|
62
|
+
"x_debug_write_metacommands",
|
|
63
|
+
"x_debug_write_odbc_drivers",
|
|
64
|
+
"x_debug_write_subvars",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
#: Handler name → {regex group: role}. Groups not listed (passwords,
|
|
68
|
+
#: encodings, prompt paths) are never reported.
|
|
69
|
+
FILE_ROLES: dict[str, dict[str, str]] = {
|
|
70
|
+
**{name: {"filename": "read"} for name in _IMPORTS},
|
|
71
|
+
**{name: {"filename": "write", "zipfilename": "write", "template": "read"} for name in _EXPORTS},
|
|
72
|
+
**{name: {"filename": "write"} for name in _WRITES},
|
|
73
|
+
**{
|
|
74
|
+
name: {"filename": "read", "outfile": "write"}
|
|
75
|
+
for name in ("x_write_create_table", "x_write_create_table_ods", "x_write_create_table_xls")
|
|
76
|
+
},
|
|
77
|
+
"x_write_create_table_alias": {"filename": "read"},
|
|
78
|
+
**{
|
|
79
|
+
name: {"att_file": "read", "msg_file": "read"}
|
|
80
|
+
for name in ("x_email", "x_cancel_halt_email", "x_error_halt_email")
|
|
81
|
+
},
|
|
82
|
+
"x_zip": {"filename": "read", "zipfilename": "write"},
|
|
83
|
+
"x_rm_file": {"filename": "delete"},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
DBMS = {
|
|
87
|
+
"pg": "PostgreSQL",
|
|
88
|
+
"mysql": "MySQL",
|
|
89
|
+
"ssvr": "SQL Server",
|
|
90
|
+
"ora": "Oracle",
|
|
91
|
+
"fb": "Firebird",
|
|
92
|
+
"sqlite": "SQLite",
|
|
93
|
+
"duckdb": "DuckDB",
|
|
94
|
+
"access": "MS Access",
|
|
95
|
+
"dsn": "ODBC DSN",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _connection(handler: str, groups: dict[str, str | None]) -> Effect | None:
|
|
100
|
+
if handler == "x_use":
|
|
101
|
+
return Effect("use", groups.get("db_alias") or "")
|
|
102
|
+
if not handler.startswith("x_connect_"):
|
|
103
|
+
return None
|
|
104
|
+
kind = handler.removeprefix("x_connect_").removeprefix("user_")
|
|
105
|
+
target = groups.get("filename") or groups.get("dsn") or ""
|
|
106
|
+
if groups.get("server"):
|
|
107
|
+
target = f"{groups['server']}/{groups.get('db_name') or ''}"
|
|
108
|
+
detail = DBMS.get(kind, kind)
|
|
109
|
+
if groups.get("user"):
|
|
110
|
+
detail += f", user {groups['user']}"
|
|
111
|
+
return Effect("connect", f"{groups.get('db_alias') or ''}: {target}".strip(), detail)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def effects(handler: str, groups: dict[str, str | None]) -> list[Effect]:
|
|
115
|
+
"""Everything a metacommand dispatched to *handler* with *groups* touches."""
|
|
116
|
+
connection = _connection(handler, groups)
|
|
117
|
+
if connection is not None:
|
|
118
|
+
return [connection]
|
|
119
|
+
found = []
|
|
120
|
+
for group, role in FILE_ROLES.get(handler, {}).items():
|
|
121
|
+
value = groups.get(group)
|
|
122
|
+
if value:
|
|
123
|
+
found.append(Effect(role, value.strip().strip('"')))
|
|
124
|
+
return found
|
execsql/script/__init__.py
CHANGED
|
@@ -23,9 +23,6 @@ Key classes:
|
|
|
23
23
|
handler function + flags).
|
|
24
24
|
- :class:`MetaCommandList` — ordered list of :class:`MetaCommand` entries
|
|
25
25
|
with a keyword index for fast dispatch.
|
|
26
|
-
- :class:`SqlStmt` / :class:`MetacommandStmt` / :class:`ScriptCmd` —
|
|
27
|
-
statement wrappers carried in the AST and used by ``ctx.last_command``
|
|
28
|
-
for source-location tracking.
|
|
29
26
|
- :class:`ScriptExecSpec` — specification for deferred script execution
|
|
30
27
|
(used by ``ON ERROR_HALT`` / ``ON CANCEL_HALT EXECUTE SCRIPT``).
|
|
31
28
|
|
|
@@ -46,10 +43,7 @@ from execsql.script.control import BatchLevels
|
|
|
46
43
|
from execsql.script.engine import (
|
|
47
44
|
MetaCommand,
|
|
48
45
|
MetaCommandList,
|
|
49
|
-
MetacommandStmt,
|
|
50
|
-
ScriptCmd,
|
|
51
46
|
ScriptExecSpec,
|
|
52
|
-
SqlStmt,
|
|
53
47
|
current_script_line,
|
|
54
48
|
set_dynamic_system_vars,
|
|
55
49
|
set_static_system_vars,
|
|
@@ -84,9 +78,6 @@ __all__ = [
|
|
|
84
78
|
"ScriptArgSubVarSet",
|
|
85
79
|
"MetaCommand",
|
|
86
80
|
"MetaCommandList",
|
|
87
|
-
"SqlStmt",
|
|
88
|
-
"MetacommandStmt",
|
|
89
|
-
"ScriptCmd",
|
|
90
81
|
"ScriptExecSpec",
|
|
91
82
|
"set_dynamic_system_vars",
|
|
92
83
|
"set_static_system_vars",
|
execsql/script/ast.py
CHANGED
|
@@ -6,7 +6,7 @@ Defines the node types that make up the execsql AST.
|
|
|
6
6
|
|
|
7
7
|
Design principles:
|
|
8
8
|
- Every node carries a :class:`SourceSpan` so that error messages,
|
|
9
|
-
|
|
9
|
+
``execsql lint``, and the formatter can report precise source locations.
|
|
10
10
|
- Block structures (IF, LOOP, BATCH, SCRIPT) are represented as nodes
|
|
11
11
|
whose ``body`` (and optional ``else_body``, ``elseif_clauses``) contain
|
|
12
12
|
child nodes, forming the tree structure.
|
execsql/script/engine.py
CHANGED
|
@@ -11,9 +11,6 @@ live in :mod:`execsql.script.parser` and
|
|
|
11
11
|
Classes:
|
|
12
12
|
- :class:`MetaCommand` — one entry in the metacommand dispatch table.
|
|
13
13
|
- :class:`MetaCommandList` — ordered list of :class:`MetaCommand` entries with a keyword index.
|
|
14
|
-
- :class:`SqlStmt` — wraps a single SQL string for execution.
|
|
15
|
-
- :class:`MetacommandStmt` — wraps a metacommand line for dispatch.
|
|
16
|
-
- :class:`ScriptCmd` — pairs a statement with its source-file location.
|
|
17
14
|
- :class:`ScriptExecSpec` — specification for deferred script execution.
|
|
18
15
|
|
|
19
16
|
Functions:
|
|
@@ -28,7 +25,7 @@ import os
|
|
|
28
25
|
import re
|
|
29
26
|
import uuid
|
|
30
27
|
from pathlib import Path
|
|
31
|
-
from typing import Any
|
|
28
|
+
from typing import Any
|
|
32
29
|
|
|
33
30
|
import execsql.state as _state
|
|
34
31
|
from execsql.exceptions import ErrInfo
|
|
@@ -38,9 +35,6 @@ from execsql.utils.errors import exception_desc
|
|
|
38
35
|
__all__ = [
|
|
39
36
|
"MetaCommand",
|
|
40
37
|
"MetaCommandList",
|
|
41
|
-
"SqlStmt",
|
|
42
|
-
"MetacommandStmt",
|
|
43
|
-
"ScriptCmd",
|
|
44
38
|
"ScriptExecSpec",
|
|
45
39
|
"set_system_vars",
|
|
46
40
|
"substitute_vars",
|
|
@@ -97,8 +91,9 @@ class MetaCommand:
|
|
|
97
91
|
raise
|
|
98
92
|
except ErrInfo as errinf:
|
|
99
93
|
er = errinf
|
|
100
|
-
except Exception:
|
|
94
|
+
except Exception as exc:
|
|
101
95
|
er = ErrInfo("cmd", command_text=cmd_str, exception_msg=exception_desc())
|
|
96
|
+
er.__cause__ = exc
|
|
102
97
|
if er:
|
|
103
98
|
if _state.status.halt_on_metacommand_err:
|
|
104
99
|
from execsql.utils.errors import exit_now
|
|
@@ -205,7 +200,7 @@ class MetaCommandList:
|
|
|
205
200
|
def keywords_by_category(self) -> dict[str, list[str]]:
|
|
206
201
|
"""Return ``{category: [keyword, ...]}`` from entries that have both.
|
|
207
202
|
|
|
208
|
-
Used by
|
|
203
|
+
Used by ``execsql list keywords`` to introspect the dispatch table.
|
|
209
204
|
"""
|
|
210
205
|
result: dict[str, list[str]] = {}
|
|
211
206
|
for mc in self._commands:
|
|
@@ -239,89 +234,6 @@ class MetaCommandList:
|
|
|
239
234
|
return None
|
|
240
235
|
|
|
241
236
|
|
|
242
|
-
# ---------------------------------------------------------------------------
|
|
243
|
-
# SqlStmt / MetacommandStmt
|
|
244
|
-
# ---------------------------------------------------------------------------
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
class SqlStmt:
|
|
248
|
-
"""A single SQL statement ready to be executed against the active database.
|
|
249
|
-
|
|
250
|
-
Data class only — the legacy ``.run()`` method was removed when the AST
|
|
251
|
-
executor became the sole engine. SQL execution now goes through
|
|
252
|
-
:func:`execsql.script.executor._exec_sql`.
|
|
253
|
-
"""
|
|
254
|
-
|
|
255
|
-
def __init__(self, sql_statement: str) -> None:
|
|
256
|
-
self.statement = re.sub(r"\s*;(\s*;\s*)+$", ";", sql_statement)
|
|
257
|
-
|
|
258
|
-
def __repr__(self) -> str:
|
|
259
|
-
return f"SqlStmt({self.statement})"
|
|
260
|
-
|
|
261
|
-
def commandline(self) -> str:
|
|
262
|
-
"""Return the raw SQL statement text."""
|
|
263
|
-
return self.statement
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
class MetacommandStmt:
|
|
267
|
-
"""A single execsql metacommand line.
|
|
268
|
-
|
|
269
|
-
Data class only — the legacy ``.run()`` method was removed when the AST
|
|
270
|
-
executor became the sole engine. Metacommand dispatch now goes through
|
|
271
|
-
:func:`execsql.script.executor._exec_metacommand`.
|
|
272
|
-
"""
|
|
273
|
-
|
|
274
|
-
def __init__(self, metacommand_statement: str) -> None:
|
|
275
|
-
self.statement = metacommand_statement
|
|
276
|
-
|
|
277
|
-
def __repr__(self) -> str:
|
|
278
|
-
return f"MetacommandStmt({self.statement})"
|
|
279
|
-
|
|
280
|
-
def commandline(self) -> str:
|
|
281
|
-
"""Return the metacommand line in its canonical ``-- !x! ...`` form."""
|
|
282
|
-
return "-- !x! " + self.statement
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
# ---------------------------------------------------------------------------
|
|
286
|
-
# ScriptCmd
|
|
287
|
-
# ---------------------------------------------------------------------------
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
class ScriptCmd:
|
|
291
|
-
"""A parsed script item: either a :class:`SqlStmt` or a :class:`MetacommandStmt`, with source location."""
|
|
292
|
-
|
|
293
|
-
# A SQL script object that is either a SQL statement or a metacommand.
|
|
294
|
-
def __init__(
|
|
295
|
-
self,
|
|
296
|
-
command_source_name: str,
|
|
297
|
-
command_line_no: int,
|
|
298
|
-
command_type: str,
|
|
299
|
-
script_command: Any,
|
|
300
|
-
) -> None:
|
|
301
|
-
self.source = command_source_name
|
|
302
|
-
self.line_no = command_line_no
|
|
303
|
-
self.command_type = command_type
|
|
304
|
-
self.command = script_command
|
|
305
|
-
# MIGRATION NOTE: differs from monolith (execsql.py) — source_dir and source_name are
|
|
306
|
-
# resolved once at construction rather than on every statement execution. For absolute
|
|
307
|
-
# paths (the common case) the result is identical. For relative paths the value is
|
|
308
|
-
# anchored to the CWD at script-load time rather than at each statement's execution time;
|
|
309
|
-
# the original per-statement resolve could yield inconsistent values across statements of
|
|
310
|
-
# the same script if a CD metacommand ran between them.
|
|
311
|
-
_p = Path(command_source_name)
|
|
312
|
-
self.source_dir: str = str(_p.resolve().parent) + os.sep
|
|
313
|
-
self.source_name: str = _p.name
|
|
314
|
-
|
|
315
|
-
def __repr__(self) -> str:
|
|
316
|
-
return f"ScriptCmd({self.source!r}, {self.line_no!r}, {self.command_type!r}, {repr(self.command)!r})"
|
|
317
|
-
|
|
318
|
-
def current_script_line(self) -> tuple[str, int]:
|
|
319
|
-
return (self.source, self.line_no)
|
|
320
|
-
|
|
321
|
-
def commandline(self) -> str:
|
|
322
|
-
return cast(str, self.command.statement if self.command_type == "sql" else "-- !x! " + self.command.statement)
|
|
323
|
-
|
|
324
|
-
|
|
325
237
|
# ---------------------------------------------------------------------------
|
|
326
238
|
# ScriptExecSpec
|
|
327
239
|
# ---------------------------------------------------------------------------
|
|
@@ -509,7 +421,8 @@ def current_script_line() -> tuple[str, int]:
|
|
|
509
421
|
"""Return ``(source_name, line_number)`` for the command currently executing.
|
|
510
422
|
|
|
511
423
|
Reads from ``_state.last_command``, which the AST executor updates on
|
|
512
|
-
every statement via
|
|
424
|
+
every statement via :class:`~execsql.script.executor.ExecutingStatement`.
|
|
425
|
+
Returns ``("", 0)``
|
|
513
426
|
when nothing has executed yet (e.g. during early initialization errors).
|
|
514
427
|
"""
|
|
515
428
|
last = getattr(_state, "last_command", None)
|
execsql/script/executor.py
CHANGED
|
@@ -48,6 +48,7 @@ import re
|
|
|
48
48
|
import time as _time
|
|
49
49
|
from pathlib import Path
|
|
50
50
|
from typing import Any, cast
|
|
51
|
+
from collections.abc import Callable
|
|
51
52
|
|
|
52
53
|
from execsql.exceptions import ErrInfo
|
|
53
54
|
from execsql.script.ast import (
|
|
@@ -64,12 +65,13 @@ from execsql.script.ast import (
|
|
|
64
65
|
SqlBlock,
|
|
65
66
|
SqlStatement,
|
|
66
67
|
)
|
|
68
|
+
from execsql import manifest as _manifest
|
|
67
69
|
from execsql.script.engine import set_dynamic_system_vars, set_static_system_vars, substitute_vars
|
|
68
70
|
from execsql.script.variables import SubVarSet
|
|
69
71
|
from execsql.state import ExecFrame, RuntimeContext, active_context, get_context, xcmd_test
|
|
70
72
|
from execsql.utils.errors import exception_desc, exit_now, stamp_errinfo
|
|
71
73
|
|
|
72
|
-
__all__ = ["execute"]
|
|
74
|
+
__all__ = ["execute", "execute_input"]
|
|
73
75
|
|
|
74
76
|
|
|
75
77
|
# ---------------------------------------------------------------------------
|
|
@@ -256,19 +258,24 @@ def _exec_sql(
|
|
|
256
258
|
f"Warning: There is a potential un-substituted variable in the command\n {cmd}\n",
|
|
257
259
|
)
|
|
258
260
|
e = None
|
|
261
|
+
result = None
|
|
259
262
|
try:
|
|
260
263
|
db = dbs.current()
|
|
261
264
|
if conf.log_sql and ctx.exec_log:
|
|
262
265
|
ctx.exec_log.log_sql_query(cmd, db.name(), line_no)
|
|
263
|
-
|
|
266
|
+
if ctx.prompt_input is not None:
|
|
267
|
+
result = db.execute(cmd, fetch=True)
|
|
268
|
+
else:
|
|
269
|
+
db.execute(cmd)
|
|
264
270
|
if commit:
|
|
265
271
|
db.commit()
|
|
266
272
|
except ErrInfo as errinfo:
|
|
267
273
|
e = errinfo
|
|
268
274
|
except SystemExit:
|
|
269
275
|
raise
|
|
270
|
-
except Exception:
|
|
276
|
+
except Exception as exc:
|
|
271
277
|
e = ErrInfo(type="exception", exception_msg=exception_desc())
|
|
278
|
+
e.__cause__ = exc # the driver's own message, which a prompt shows
|
|
272
279
|
if e:
|
|
273
280
|
stamp_errinfo(e)
|
|
274
281
|
subvars.add_substitution("$LAST_ERROR", cmd)
|
|
@@ -281,6 +288,8 @@ def _exec_sql(
|
|
|
281
288
|
status.error_history.append((source, line_no, cmd, e.errmsg()))
|
|
282
289
|
return
|
|
283
290
|
subvars.add_substitution("$LAST_SQL", cmd)
|
|
291
|
+
if ctx.prompt_input is not None:
|
|
292
|
+
ctx.prompt_input(result)
|
|
284
293
|
|
|
285
294
|
|
|
286
295
|
# ---------------------------------------------------------------------------
|
|
@@ -319,13 +328,16 @@ def _exec_metacommand(
|
|
|
319
328
|
try:
|
|
320
329
|
applies, result = metacommandlist.eval(cmd)
|
|
321
330
|
if applies:
|
|
331
|
+
if (manifest := _manifest.current()) is not None:
|
|
332
|
+
manifest.record_metacommand(cmd, source, line_no, metacommandlist)
|
|
322
333
|
return result
|
|
323
334
|
except ErrInfo as errinfo:
|
|
324
335
|
e = errinfo
|
|
325
336
|
except SystemExit:
|
|
326
337
|
raise
|
|
327
|
-
except Exception:
|
|
338
|
+
except Exception as exc:
|
|
328
339
|
e = ErrInfo(type="exception", exception_msg=exception_desc())
|
|
340
|
+
e.__cause__ = exc
|
|
329
341
|
if e:
|
|
330
342
|
stamp_errinfo(e)
|
|
331
343
|
status.metacommand_error = True
|
|
@@ -402,9 +414,9 @@ def _execute_node(
|
|
|
402
414
|
text = node.text
|
|
403
415
|
if in_loop:
|
|
404
416
|
text = _convert_deferred_vars(text)
|
|
405
|
-
# Deduplicate trailing semicolons
|
|
417
|
+
# Deduplicate trailing semicolons
|
|
406
418
|
text = re.sub(r"\s*;(\s*;\s*)+$", ";", text)
|
|
407
|
-
ctx.last_command =
|
|
419
|
+
ctx.last_command = ExecutingStatement(node)
|
|
408
420
|
_exec_sql(
|
|
409
421
|
ctx,
|
|
410
422
|
text,
|
|
@@ -413,6 +425,8 @@ def _execute_node(
|
|
|
413
425
|
localvars,
|
|
414
426
|
commit=not ctx.status.batch.in_batch(),
|
|
415
427
|
)
|
|
428
|
+
if (manifest := _manifest.current()) is not None:
|
|
429
|
+
manifest.record_sql()
|
|
416
430
|
|
|
417
431
|
elif isinstance(node, MetaCommandStatement):
|
|
418
432
|
command = node.command
|
|
@@ -425,31 +439,31 @@ def _execute_node(
|
|
|
425
439
|
expanded = substitute_vars(command, effective_locals, ctx=ctx)
|
|
426
440
|
if _BREAK_RX.match(expanded):
|
|
427
441
|
raise _BreakLoop
|
|
428
|
-
ctx.last_command =
|
|
442
|
+
ctx.last_command = ExecutingStatement(node)
|
|
429
443
|
_exec_metacommand(ctx, expanded, node.span.file, node.span.start_line)
|
|
430
444
|
|
|
431
445
|
elif isinstance(node, IfBlock):
|
|
432
|
-
ctx.last_command =
|
|
446
|
+
ctx.last_command = ExecutingStatement(node)
|
|
433
447
|
_execute_if(ctx, node, localvars, in_loop=in_loop)
|
|
434
448
|
|
|
435
449
|
elif isinstance(node, LoopBlock):
|
|
436
|
-
ctx.last_command =
|
|
450
|
+
ctx.last_command = ExecutingStatement(node)
|
|
437
451
|
_execute_loop(ctx, node, localvars)
|
|
438
452
|
|
|
439
453
|
elif isinstance(node, BatchBlock):
|
|
440
|
-
ctx.last_command =
|
|
454
|
+
ctx.last_command = ExecutingStatement(node)
|
|
441
455
|
_execute_batch(ctx, node, localvars, in_loop=in_loop)
|
|
442
456
|
|
|
443
457
|
elif isinstance(node, ScriptBlock):
|
|
444
|
-
ctx.last_command =
|
|
458
|
+
ctx.last_command = ExecutingStatement(node)
|
|
445
459
|
_register_script_block(ctx, node)
|
|
446
460
|
|
|
447
461
|
elif isinstance(node, SqlBlock):
|
|
448
|
-
ctx.last_command =
|
|
462
|
+
ctx.last_command = ExecutingStatement(node)
|
|
449
463
|
_execute_sql_block(ctx, node, localvars, in_loop=in_loop)
|
|
450
464
|
|
|
451
465
|
elif isinstance(node, IncludeDirective):
|
|
452
|
-
ctx.last_command =
|
|
466
|
+
ctx.last_command = ExecutingStatement(node)
|
|
453
467
|
_execute_include(ctx, node, localvars)
|
|
454
468
|
|
|
455
469
|
else:
|
|
@@ -861,6 +875,8 @@ def _execute_include_native(
|
|
|
861
875
|
# Parse with AST parser
|
|
862
876
|
encoding = ctx.conf.script_encoding if ctx.conf is not None else "utf-8"
|
|
863
877
|
included_tree = parse_script(target, encoding=encoding)
|
|
878
|
+
if (manifest := _manifest.current()) is not None:
|
|
879
|
+
manifest.record_include(target, node.span.file, node.span.start_line)
|
|
864
880
|
|
|
865
881
|
# Pre-register SCRIPT blocks in the included file so forward references work.
|
|
866
882
|
_pre_register_scripts(ctx, included_tree.body)
|
|
@@ -885,40 +901,104 @@ def _execute_include_native(
|
|
|
885
901
|
# ---------------------------------------------------------------------------
|
|
886
902
|
|
|
887
903
|
|
|
888
|
-
class _BreakLoop(
|
|
889
|
-
"""Raised by BREAK metacommand to exit the innermost loop.
|
|
904
|
+
class _BreakLoop(BaseException):
|
|
905
|
+
"""Raised by BREAK metacommand to exit the innermost loop.
|
|
906
|
+
|
|
907
|
+
A ``BaseException``, like ``KeyboardInterrupt``, so the ``except
|
|
908
|
+
Exception`` handlers it may pass on its way to the loop leave it alone:
|
|
909
|
+
a BREAK typed at a BREAKPOINT prompt travels out through the BREAKPOINT
|
|
910
|
+
metacommand's handler.
|
|
911
|
+
"""
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def _inside_loop(ctx: RuntimeContext) -> bool:
|
|
915
|
+
"""Whether a BREAK at the current statement would leave a loop within its scope."""
|
|
916
|
+
for frame in reversed(ctx.ast_exec_stack):
|
|
917
|
+
if frame.kind in ("loop_while", "loop_until"):
|
|
918
|
+
return True
|
|
919
|
+
if frame.kind in ("script", "main"):
|
|
920
|
+
return frame.kind == "script" and frame.iteration > 0
|
|
921
|
+
return False
|
|
890
922
|
|
|
891
923
|
|
|
892
924
|
_BREAK_RX = re.compile(r"^\s*BREAK\s*$", re.I)
|
|
893
925
|
|
|
894
926
|
|
|
895
927
|
# ---------------------------------------------------------------------------
|
|
896
|
-
#
|
|
928
|
+
# The statement the executor is currently on
|
|
897
929
|
# ---------------------------------------------------------------------------
|
|
898
930
|
|
|
899
931
|
|
|
900
|
-
class
|
|
901
|
-
"""
|
|
932
|
+
class _StatementText:
|
|
933
|
+
"""The text of one statement, in the two spellings its readers expect.
|
|
934
|
+
|
|
935
|
+
``statement`` is the raw text; ``commandline()`` is the text as it
|
|
936
|
+
appeared in the source, so a metacommand gets its ``-- !x!`` marker back.
|
|
937
|
+
"""
|
|
938
|
+
|
|
939
|
+
__slots__ = ("statement", "_prefix")
|
|
940
|
+
|
|
941
|
+
def __init__(self, statement: str, prefix: str = "") -> None:
|
|
942
|
+
self.statement = statement
|
|
943
|
+
self._prefix = prefix
|
|
902
944
|
|
|
903
|
-
|
|
945
|
+
def commandline(self) -> str:
|
|
946
|
+
return self._prefix + self.statement
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
class ExecutingStatement:
|
|
950
|
+
"""Where the executor is, as ``ctx.last_command`` exposes it.
|
|
951
|
+
|
|
952
|
+
The AST node is the single representation of a statement. This wraps one
|
|
953
|
+
rather than copying fields out of it, so there is nothing to keep in sync
|
|
954
|
+
and no second source of truth about what is executing. Error reporting
|
|
955
|
+
(:mod:`execsql.utils.errors`), the debug REPL, and ``api.run()`` read
|
|
956
|
+
these five attributes; everything is derived from ``node`` on access, so
|
|
957
|
+
constructing one costs a single small object per statement rather than
|
|
958
|
+
the two classes the previous shim built each time.
|
|
959
|
+
"""
|
|
960
|
+
|
|
961
|
+
__slots__ = ("node", "_source_dir")
|
|
904
962
|
|
|
905
963
|
def __init__(self, node: Node) -> None:
|
|
906
|
-
self.
|
|
907
|
-
self.
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
self.
|
|
964
|
+
self.node = node
|
|
965
|
+
self._source_dir: str | None = None
|
|
966
|
+
|
|
967
|
+
@property
|
|
968
|
+
def source(self) -> str:
|
|
969
|
+
return self.node.span.file
|
|
970
|
+
|
|
971
|
+
@property
|
|
972
|
+
def line_no(self) -> int:
|
|
973
|
+
return self.node.span.start_line
|
|
974
|
+
|
|
975
|
+
@property
|
|
976
|
+
def source_name(self) -> str:
|
|
977
|
+
return Path(self.node.span.file).name
|
|
978
|
+
|
|
979
|
+
@property
|
|
980
|
+
def source_dir(self) -> str:
|
|
981
|
+
# resolve() touches the filesystem, so it is done once per statement
|
|
982
|
+
# object rather than on every read.
|
|
983
|
+
if self._source_dir is None:
|
|
984
|
+
self._source_dir = str(Path(self.node.span.file).resolve().parent) + os.sep
|
|
985
|
+
return self._source_dir
|
|
986
|
+
|
|
987
|
+
@property
|
|
988
|
+
def command_type(self) -> str:
|
|
989
|
+
return "sql" if isinstance(self.node, SqlStatement) else "cmd"
|
|
990
|
+
|
|
991
|
+
@property
|
|
992
|
+
def command(self) -> _StatementText:
|
|
993
|
+
node = self.node
|
|
912
994
|
if isinstance(node, SqlStatement):
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
else:
|
|
921
|
-
self.command = type("_cmd", (), {"statement": "", "commandline": lambda self: ""})()
|
|
995
|
+
return _StatementText(node.text)
|
|
996
|
+
if isinstance(node, MetaCommandStatement):
|
|
997
|
+
return _StatementText(node.command, "-- !x! ")
|
|
998
|
+
return _StatementText("")
|
|
999
|
+
|
|
1000
|
+
def __repr__(self) -> str:
|
|
1001
|
+
return f"ExecutingStatement({self.source}:{self.line_no}, {self.command_type})"
|
|
922
1002
|
|
|
923
1003
|
def current_script_line(self) -> tuple[str, int]:
|
|
924
1004
|
return (self.source, self.line_no)
|
|
@@ -1008,3 +1088,42 @@ def execute(script: Script, *, ctx: RuntimeContext | None = None) -> None:
|
|
|
1008
1088
|
) from exc
|
|
1009
1089
|
finally:
|
|
1010
1090
|
_pop_frame(ctx)
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
# ---------------------------------------------------------------------------
|
|
1094
|
+
# Input typed at a prompt
|
|
1095
|
+
# ---------------------------------------------------------------------------
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def execute_input(
|
|
1099
|
+
script: Script,
|
|
1100
|
+
show: Callable[[tuple[list[str], list] | None], None],
|
|
1101
|
+
*,
|
|
1102
|
+
ctx: RuntimeContext | None = None,
|
|
1103
|
+
) -> None:
|
|
1104
|
+
"""Run input typed at a BREAKPOINT prompt as the next lines of the paused script.
|
|
1105
|
+
|
|
1106
|
+
The input runs as if it were written where the script paused: in its
|
|
1107
|
+
variable scope, its batch and its loop, so a BREAK leaves that loop.
|
|
1108
|
+
Unlike :func:`execute`, nothing is reset.
|
|
1109
|
+
|
|
1110
|
+
*show* receives the result of each SQL statement run. An error that
|
|
1111
|
+
would halt the run is raised to the caller instead (see ``exit_now``).
|
|
1112
|
+
The paused script's current statement is restored afterwards, so the
|
|
1113
|
+
debug REPL's ``.where`` still shows it.
|
|
1114
|
+
"""
|
|
1115
|
+
if ctx is None:
|
|
1116
|
+
ctx = get_context()
|
|
1117
|
+
with active_context(ctx):
|
|
1118
|
+
_pre_register_scripts(ctx, script.body)
|
|
1119
|
+
last_command, prompt_input = ctx.last_command, ctx.prompt_input
|
|
1120
|
+
ctx.prompt_input = show
|
|
1121
|
+
try:
|
|
1122
|
+
_execute_nodes(ctx, script.body, script.source)
|
|
1123
|
+
except _BreakLoop as exc:
|
|
1124
|
+
if not _inside_loop(ctx):
|
|
1125
|
+
raise ErrInfo(type="cmd", other_msg="BREAK metacommand outside of a LOOP block.") from exc
|
|
1126
|
+
raise
|
|
1127
|
+
finally:
|
|
1128
|
+
ctx.prompt_input = prompt_input
|
|
1129
|
+
ctx.last_command = last_command
|
execsql/state.py
CHANGED
|
@@ -33,6 +33,7 @@ import threading
|
|
|
33
33
|
import types
|
|
34
34
|
from dataclasses import dataclass
|
|
35
35
|
from typing import TYPE_CHECKING, Any
|
|
36
|
+
from collections.abc import Callable
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
@dataclass
|
|
@@ -96,10 +97,10 @@ if TYPE_CHECKING:
|
|
|
96
97
|
from execsql.script import (
|
|
97
98
|
CounterVars,
|
|
98
99
|
MetaCommandList,
|
|
99
|
-
ScriptCmd,
|
|
100
100
|
ScriptExecSpec,
|
|
101
101
|
SubVarSet,
|
|
102
102
|
)
|
|
103
|
+
from execsql.script.executor import ExecutingStatement
|
|
103
104
|
from execsql.utils.fileio import FileWriter, Logger, TempFileMgr
|
|
104
105
|
from execsql.utils.mail import MailSpec
|
|
105
106
|
from execsql.utils.timer import Timer
|
|
@@ -143,6 +144,7 @@ __all__ = [
|
|
|
143
144
|
"profile_data",
|
|
144
145
|
# Debug REPL
|
|
145
146
|
"step_mode",
|
|
147
|
+
"prompt_input",
|
|
146
148
|
# Version
|
|
147
149
|
"primary_vno",
|
|
148
150
|
"secondary_vno",
|
|
@@ -239,6 +241,7 @@ _CONTEXT_ATTRS: frozenset[str] = frozenset(
|
|
|
239
241
|
"profile_data",
|
|
240
242
|
# Debug REPL
|
|
241
243
|
"step_mode",
|
|
244
|
+
"prompt_input",
|
|
242
245
|
# AST executor
|
|
243
246
|
"ast_scripts",
|
|
244
247
|
"include_chain",
|
|
@@ -293,6 +296,7 @@ class RuntimeContext:
|
|
|
293
296
|
"profile_data",
|
|
294
297
|
# Debug REPL
|
|
295
298
|
"step_mode",
|
|
299
|
+
"prompt_input",
|
|
296
300
|
# AST executor
|
|
297
301
|
"ast_scripts",
|
|
298
302
|
"include_chain",
|
|
@@ -305,7 +309,7 @@ class RuntimeContext:
|
|
|
305
309
|
self.logfile_encoding: str = "utf8"
|
|
306
310
|
|
|
307
311
|
# Runtime flags
|
|
308
|
-
self.last_command:
|
|
312
|
+
self.last_command: ExecutingStatement | None = None
|
|
309
313
|
self.upass: str | None = None
|
|
310
314
|
self.err_halt_writespec: WriteSpec | None = None
|
|
311
315
|
self.err_halt_email: MailSpec | None = None
|
|
@@ -347,6 +351,11 @@ class RuntimeContext:
|
|
|
347
351
|
|
|
348
352
|
# Debug REPL — True after a ``next`` command; engine re-enters REPL after next statement.
|
|
349
353
|
self.step_mode: bool = False
|
|
354
|
+
# Set while input typed at a BREAKPOINT prompt runs:
|
|
355
|
+
# called with each SQL statement's result, as ``(column_names, rows)``
|
|
356
|
+
# or ``None``. An error that would halt the run instead returns to the
|
|
357
|
+
# prompt (see ``exit_now``).
|
|
358
|
+
self.prompt_input: Callable[[tuple[list[str], list] | None], None] | None = None
|
|
350
359
|
|
|
351
360
|
# AST executor — script block registry and include-chain tracking.
|
|
352
361
|
self.ast_scripts: dict = {}
|