execsql2 2.16.18__py3-none-any.whl → 2.17.2__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/__init__.py +6 -2
- execsql/api.py +25 -6
- execsql/cli/__init__.py +5 -3
- execsql/cli/lint.py +30 -34
- execsql/cli/run.py +10 -0
- execsql/config.py +145 -92
- execsql/db/access.py +54 -40
- execsql/db/base.py +33 -6
- execsql/db/firebird.py +3 -1
- execsql/db/mysql.py +4 -3
- execsql/db/oracle.py +36 -14
- execsql/db/postgres.py +8 -6
- execsql/db/sqlite.py +5 -2
- execsql/db/sqlserver.py +8 -6
- execsql/debug/repl.py +59 -21
- execsql/exceptions.py +19 -4
- execsql/exporters/base.py +3 -2
- execsql/exporters/delimited.py +2 -3
- execsql/exporters/feather.py +3 -3
- execsql/exporters/ods.py +1 -1
- execsql/exporters/xls.py +12 -4
- execsql/exporters/xlsx.py +1 -1
- execsql/gui/desktop.py +129 -15
- execsql/importers/__init__.py +1 -1
- execsql/importers/ods.py +1 -1
- execsql/importers/xls.py +1 -1
- execsql/metacommands/__init__.py +34 -5
- execsql/metacommands/conditions.py +26 -14
- execsql/metacommands/connect.py +21 -14
- execsql/metacommands/control.py +55 -68
- execsql/metacommands/data.py +25 -9
- execsql/metacommands/debug.py +132 -77
- execsql/metacommands/io_export.py +14 -2
- execsql/metacommands/io_import.py +11 -2
- execsql/metacommands/io_write.py +113 -11
- execsql/metacommands/prompt.py +46 -32
- execsql/metacommands/script_ext.py +63 -34
- execsql/metacommands/system.py +4 -3
- execsql/metacommands/upsert.py +0 -29
- execsql/script/__init__.py +28 -37
- execsql/script/ast.py +7 -7
- execsql/script/control.py +4 -101
- execsql/script/engine.py +37 -251
- execsql/script/executor.py +193 -230
- execsql/script/parser.py +1 -3
- execsql/script/variables.py +8 -3
- execsql/state.py +125 -37
- execsql/utils/errors.py +0 -2
- execsql/utils/fileio.py +47 -3
- execsql/utils/mail.py +3 -2
- execsql/utils/strings.py +5 -5
- {execsql2-2.16.18.dist-info → execsql2-2.17.2.dist-info}/METADATA +42 -36
- execsql2-2.17.2.dist-info/RECORD +124 -0
- execsql2-2.17.2.dist-info/licenses/NOTICE +11 -0
- execsql2-2.16.18.dist-info/RECORD +0 -124
- execsql2-2.16.18.dist-info/licenses/NOTICE +0 -10
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.16.18.dist-info → execsql2-2.17.2.dist-info}/WHEEL +0 -0
- {execsql2-2.16.18.dist-info → execsql2-2.17.2.dist-info}/entry_points.txt +0 -0
- {execsql2-2.16.18.dist-info → execsql2-2.17.2.dist-info}/licenses/LICENSE.txt +0 -0
execsql/metacommands/debug.py
CHANGED
|
@@ -3,12 +3,24 @@ from __future__ import annotations
|
|
|
3
3
|
"""
|
|
4
4
|
Debug metacommand handlers for execsql.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
``
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
Handlers for the ``DEBUG …`` family of introspection metacommands and
|
|
7
|
+
``SHOW SCRIPTS`` script-block listing:
|
|
8
|
+
|
|
9
|
+
- ``x_debug_log_subvars`` / ``x_debug_write_subvars`` — dump substitution
|
|
10
|
+
variables to the log file or to terminal/file (``DEBUG LOG SUBVARS`` /
|
|
11
|
+
``DEBUG WRITE SUBVARS``). Both accept optional ``LOCAL`` / ``USER``
|
|
12
|
+
qualifiers.
|
|
13
|
+
- ``x_debug_log_config`` / ``x_debug_write_config`` — dump the merged
|
|
14
|
+
configuration.
|
|
15
|
+
- ``x_debug_write_odbc_drivers`` — list installed ODBC drivers.
|
|
16
|
+
- ``x_debug_write_metacommands`` — dump the registered metacommand list
|
|
17
|
+
(``DEBUG WRITE METACOMMANDLIST TO <file>``).
|
|
18
|
+
- ``x_debug_commandliststack`` — dump the current execution stack
|
|
19
|
+
(``DEBUG WRITE COMMANDLISTSTACK``).
|
|
20
|
+
- ``x_debug_iflevels`` — dump the nested IF condition state
|
|
21
|
+
(``DEBUG WRITE IFLEVELS``).
|
|
22
|
+
- ``x_show_scripts`` — list registered ``BEGIN SCRIPT`` blocks
|
|
23
|
+
(``SHOW SCRIPTS [<name>]``), with optional name argument for detail.
|
|
12
24
|
"""
|
|
13
25
|
|
|
14
26
|
from pathlib import Path
|
|
@@ -34,23 +46,65 @@ def x_debug_write_metacommands(**kwargs: Any) -> None:
|
|
|
34
46
|
|
|
35
47
|
|
|
36
48
|
def x_debug_commandliststack(**kwargs: Any) -> None:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
"""Dump the unified AST execution stack.
|
|
50
|
+
|
|
51
|
+
Shows every nesting construct the executor is currently inside:
|
|
52
|
+
``<main>`` script, ``EXECUTE SCRIPT`` calls, ``INCLUDE``'d files,
|
|
53
|
+
``IF``/``ELSEIF``/``ELSE`` branches, ``LOOP`` iterations (with iteration
|
|
54
|
+
count), and ``BATCH`` blocks. Each frame shows source file and line.
|
|
55
|
+
|
|
56
|
+
The legacy ``commandliststack`` is reported separately as a secondary view
|
|
57
|
+
because it only records SCRIPT call frames; the unified ``ast_exec_stack``
|
|
58
|
+
is the authoritative debugger view.
|
|
59
|
+
"""
|
|
60
|
+
stack = getattr(_state, "ast_exec_stack", None) or []
|
|
61
|
+
_state.output.write(f"Execution Stack (depth: {len(stack)}):\n")
|
|
62
|
+
if not stack:
|
|
63
|
+
_state.output.write(" (empty)\n")
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
for depth, frame in enumerate(stack):
|
|
67
|
+
kind_label = frame.kind.upper().replace("LOOP_", "LOOP ")
|
|
68
|
+
# Right-hand description per kind
|
|
69
|
+
if frame.kind in ("if", "elseif"):
|
|
70
|
+
desc = f"{kind_label} {frame.label}"
|
|
71
|
+
elif frame.kind == "else":
|
|
72
|
+
desc = kind_label
|
|
73
|
+
elif frame.kind in ("loop_while", "loop_until"):
|
|
74
|
+
desc = f"{kind_label} {frame.label} iter={frame.iteration}"
|
|
75
|
+
elif frame.kind == "script":
|
|
76
|
+
params = ""
|
|
77
|
+
if frame.params:
|
|
78
|
+
params = "(" + ", ".join(f"{k}={v!r}" for k, v in frame.params.items()) + ")"
|
|
79
|
+
iter_suffix = f" iter={frame.iteration}" if frame.iteration else ""
|
|
80
|
+
desc = f"SCRIPT {frame.label}{params}{iter_suffix}"
|
|
81
|
+
elif frame.kind == "include":
|
|
82
|
+
desc = f"INCLUDE {frame.label}"
|
|
83
|
+
elif frame.kind == "batch":
|
|
84
|
+
desc = "BATCH"
|
|
85
|
+
elif frame.kind == "main":
|
|
86
|
+
desc = frame.label or "<main>"
|
|
87
|
+
else:
|
|
88
|
+
desc = f"{kind_label} {frame.label}"
|
|
89
|
+
src = f" {frame.source}:{frame.line}" if frame.source and frame.line else ""
|
|
90
|
+
_state.output.write(f" [{depth}] {desc}{src}\n")
|
|
44
91
|
return None
|
|
45
92
|
|
|
46
93
|
|
|
47
94
|
def x_debug_iflevels(**kwargs: Any) -> None:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
else
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
95
|
+
# Filter the unified AST exec stack to just IF/ELSEIF/ELSE frames.
|
|
96
|
+
stack = getattr(_state, "ast_exec_stack", None) or []
|
|
97
|
+
if_frames = [f for f in stack if f.kind in ("if", "elseif", "else")]
|
|
98
|
+
if not if_frames:
|
|
99
|
+
_state.output.write("If levels: (no active IF block)\n")
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
_state.output.write(f"If levels (depth {len(if_frames)}):\n")
|
|
103
|
+
for depth, frame in enumerate(if_frames):
|
|
104
|
+
kind_label = frame.kind.upper()
|
|
105
|
+
label = f"{kind_label} {frame.label}".strip() if frame.label else kind_label
|
|
106
|
+
src = f" {frame.source}:{frame.line}" if frame.source and frame.line else ""
|
|
107
|
+
_state.output.write(f" [{depth}] {label}{src}\n")
|
|
54
108
|
return None
|
|
55
109
|
|
|
56
110
|
|
|
@@ -61,13 +115,13 @@ def x_debug_write_odbc_drivers(**kwargs: Any) -> None:
|
|
|
61
115
|
fatal_error("The pyodbc module is required.")
|
|
62
116
|
output_dest = kwargs["filename"]
|
|
63
117
|
append = kwargs["append"]
|
|
118
|
+
if output_dest is not None and output_dest != "stdout" and append is None:
|
|
119
|
+
filewriter_open_as_new(output_dest)
|
|
64
120
|
|
|
65
121
|
def write(txt: str) -> None:
|
|
66
122
|
if output_dest is None or output_dest == "stdout":
|
|
67
123
|
_state.output.write(txt)
|
|
68
124
|
else:
|
|
69
|
-
if not append:
|
|
70
|
-
filewriter_open_as_new(output_dest)
|
|
71
125
|
filewriter_write(output_dest, txt)
|
|
72
126
|
|
|
73
127
|
for d in pyodbc.drivers():
|
|
@@ -77,8 +131,9 @@ def x_debug_write_odbc_drivers(**kwargs: Any) -> None:
|
|
|
77
131
|
def x_debug_log_subvars(**kwargs: Any) -> None:
|
|
78
132
|
local = kwargs["local"]
|
|
79
133
|
user = kwargs["user"]
|
|
80
|
-
|
|
81
|
-
|
|
134
|
+
localvars = _state.current_localvars()
|
|
135
|
+
if localvars is not None:
|
|
136
|
+
for s in localvars.substitutions:
|
|
82
137
|
_state.exec_log.log_status_info(f"Substitution [{s[0]}] = [{s[1]}]")
|
|
83
138
|
if local is None:
|
|
84
139
|
for s in _state.subvars.substitutions:
|
|
@@ -86,51 +141,57 @@ def x_debug_log_subvars(**kwargs: Any) -> None:
|
|
|
86
141
|
_state.exec_log.log_status_info(f"Substitution [{s[0]}] = [{s[1]}]")
|
|
87
142
|
|
|
88
143
|
|
|
89
|
-
|
|
144
|
+
_SENSITIVE_ATTRS = frozenset({"smtp_password", "passwd", "password"})
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _config_lines() -> list[str]:
|
|
148
|
+
"""Return the merged config as a list of ``"[<section>] <key> = <value>"`` lines.
|
|
149
|
+
|
|
150
|
+
Pulled directly from ``ConfigData._schema``, the registry that ``ConfigData``
|
|
151
|
+
populates each time it reads an option from the INI files. Adding a new
|
|
152
|
+
option to ``ConfigData.__init__`` automatically makes it appear here —
|
|
153
|
+
nothing in this file needs updating.
|
|
154
|
+
|
|
155
|
+
Lines are grouped by section in the order each section was first registered
|
|
156
|
+
so the dump matches the natural reading order of ``execsql.conf``.
|
|
157
|
+
|
|
158
|
+
Sensitive values (passwords) are redacted.
|
|
159
|
+
"""
|
|
160
|
+
from execsql.config import ConfigData
|
|
161
|
+
|
|
90
162
|
conf = _state.conf
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
_state.exec_log.log_status_info(f"Config; Template processor = {conf.template_processor}")
|
|
124
|
-
_state.exec_log.log_status_info(f"Config; SMTP host = {conf.smtp_host}")
|
|
125
|
-
_state.exec_log.log_status_info(f"Config; SMTP port = {conf.smtp_port}")
|
|
126
|
-
_state.exec_log.log_status_info(f"Config; SMTP username = {conf.smtp_username}")
|
|
127
|
-
_state.exec_log.log_status_info(f"Config; SMTP use SSL = {conf.smtp_ssl}")
|
|
128
|
-
_state.exec_log.log_status_info(f"Config; SMTP use TLS = {conf.smtp_tls}")
|
|
129
|
-
_state.exec_log.log_status_info(f"Config; Email format = {conf.email_format}")
|
|
130
|
-
_state.exec_log.log_status_info(f"Config; Email CSS = {conf.email_css}")
|
|
131
|
-
_state.exec_log.log_status_info(f"Config; Zip buffer size (Mb) = {conf.zip_buffer_mb}")
|
|
132
|
-
_state.exec_log.log_status_info(f"Config; DAO flush delay (seconds) = {conf.dao_flush_delay_secs}")
|
|
133
|
-
_state.exec_log.log_status_info(f"Config; Configuration files read = {', '.join(conf.files_read)}")
|
|
163
|
+
if conf is None:
|
|
164
|
+
return ["(no config loaded)"]
|
|
165
|
+
|
|
166
|
+
# Group attrs by section, preserving registration order within each section.
|
|
167
|
+
by_section: dict[str, list[tuple[str, str, str]]] = {}
|
|
168
|
+
section_order: list[str] = []
|
|
169
|
+
for attr, (section, ini_key, type_label) in ConfigData._schema.items():
|
|
170
|
+
if section not in by_section:
|
|
171
|
+
by_section[section] = []
|
|
172
|
+
section_order.append(section)
|
|
173
|
+
by_section[section].append((attr, ini_key, type_label))
|
|
174
|
+
|
|
175
|
+
lines: list[str] = []
|
|
176
|
+
for section in section_order:
|
|
177
|
+
lines.append(f"[{section}]")
|
|
178
|
+
for attr, ini_key, _type_label in by_section[section]:
|
|
179
|
+
value = getattr(conf, attr, "(unset)")
|
|
180
|
+
if attr in _SENSITIVE_ATTRS and value:
|
|
181
|
+
value = "***"
|
|
182
|
+
lines.append(f" {ini_key} = {value} (attr: {attr})")
|
|
183
|
+
lines.append("")
|
|
184
|
+
|
|
185
|
+
# Runtime-computed values that aren't read from the INI file.
|
|
186
|
+
lines.append("[runtime]")
|
|
187
|
+
files = getattr(conf, "files_read", None) or []
|
|
188
|
+
lines.append(f" files_read = {', '.join(files) if files else '(none)'}")
|
|
189
|
+
return lines
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def x_debug_log_config(**kwargs: Any) -> None:
|
|
193
|
+
for line in _config_lines():
|
|
194
|
+
_state.exec_log.log_status_info(line)
|
|
134
195
|
|
|
135
196
|
|
|
136
197
|
def x_debug_write_subvars(**kwargs: Any) -> None:
|
|
@@ -147,8 +208,9 @@ def x_debug_write_subvars(**kwargs: Any) -> None:
|
|
|
147
208
|
else:
|
|
148
209
|
filewriter_write(output_dest, txt)
|
|
149
210
|
|
|
150
|
-
|
|
151
|
-
|
|
211
|
+
localvars = _state.current_localvars()
|
|
212
|
+
if localvars is not None:
|
|
213
|
+
for s in localvars.substitutions:
|
|
152
214
|
write(f"Substitution [{s[0]}] = [{s[1]}]\n")
|
|
153
215
|
if local is None:
|
|
154
216
|
for s in _state.subvars.substitutions:
|
|
@@ -159,13 +221,6 @@ def x_debug_write_subvars(**kwargs: Any) -> None:
|
|
|
159
221
|
def x_debug_write_config(**kwargs: Any) -> None:
|
|
160
222
|
output_dest = kwargs["filename"]
|
|
161
223
|
append = kwargs["append"]
|
|
162
|
-
conf = _state.conf
|
|
163
|
-
lines = [
|
|
164
|
-
f"Config; Script encoding = {conf.script_encoding}",
|
|
165
|
-
f"Config; Output encoding = {conf.output_encoding}",
|
|
166
|
-
f"Config; Import encoding = {conf.import_encoding}",
|
|
167
|
-
f"Config; GUI level = {conf.gui_level}",
|
|
168
|
-
]
|
|
169
224
|
if output_dest is not None and output_dest != "stdout" and append is None:
|
|
170
225
|
filewriter_open_as_new(output_dest)
|
|
171
226
|
|
|
@@ -175,7 +230,7 @@ def x_debug_write_config(**kwargs: Any) -> None:
|
|
|
175
230
|
else:
|
|
176
231
|
filewriter_write(output_dest, txt)
|
|
177
232
|
|
|
178
|
-
for line in
|
|
233
|
+
for line in _config_lines():
|
|
179
234
|
write(f"{line}\n")
|
|
180
235
|
|
|
181
236
|
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
"""Export metacommand handlers.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
- ``x_export`` — EXPORT (table → file in any supported format).
|
|
4
|
+
- ``x_export_query`` — EXPORT QUERY (ad-hoc SELECT → file).
|
|
5
|
+
- ``x_export_with_template`` / ``x_export_query_with_template`` —
|
|
6
|
+
template-rendered EXPORT (Jinja or string.Template).
|
|
7
|
+
- ``x_export_ods_multiple`` / ``x_export_xlsx_multiple`` — multi-sheet
|
|
8
|
+
ODS / XLSX workbooks.
|
|
9
|
+
- ``x_export_metadata`` / ``x_export_metadata_table`` — EXPORT_METADATA
|
|
10
|
+
(dump the in-process export-record log).
|
|
11
|
+
- ``x_export_row_buffer`` — CONFIG EXPORT_ROW_BUFFER (set the per-export
|
|
12
|
+
row-buffer size).
|
|
13
|
+
|
|
14
|
+
Per-format dispatch happens inside ``x_export``'s
|
|
15
|
+
``if filefmt in (...): ... elif ...`` chain, which calls the
|
|
16
|
+
appropriate writer in :mod:`execsql.exporters`.
|
|
5
17
|
"""
|
|
6
18
|
|
|
7
19
|
from __future__ import annotations
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
"""Import metacommand handlers.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
- ``x_import`` — IMPORT (delimited text / CSV; dispatches by extension
|
|
4
|
+
for .ods/.xls/.xlsx in the same handler).
|
|
5
|
+
- ``x_import_file`` — IMPORT_FILE (insert a binary file blob as a row).
|
|
6
|
+
- ``x_import_ods`` / ``x_import_ods_pattern`` — IMPORT … FROM ODS,
|
|
7
|
+
single-sheet and SHEETS MATCHING <pattern>.
|
|
8
|
+
- ``x_import_xls`` / ``x_import_xls_pattern`` — same for XLS/XLSX.
|
|
9
|
+
- ``x_import_parquet`` — IMPORT … FROM PARQUET (via polars).
|
|
10
|
+
- ``x_import_feather`` — IMPORT … FROM FEATHER (via polars).
|
|
11
|
+
- ``x_import_json`` — IMPORT … FROM JSON (array of objects or NDJSON).
|
|
12
|
+
- ``x_import_row_buffer`` — CONFIG IMPORT_ROW_BUFFER.
|
|
13
|
+
- ``x_show_progress`` — CONFIG SHOW_PROGRESS (toggle the import progress bar).
|
|
5
14
|
"""
|
|
6
15
|
|
|
7
16
|
from __future__ import annotations
|
execsql/metacommands/io_write.py
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
"""WRITE metacommand handlers.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
- ``x_write`` — WRITE "<text>" [TEE TO <file>] (text output).
|
|
4
|
+
- ``x_writescript`` — WRITE SCRIPT <name> [TO <file>] (dump a named
|
|
5
|
+
script block).
|
|
6
|
+
- ``x_write_create_table`` — WRITE CREATE_TABLE … (emit a CREATE TABLE
|
|
7
|
+
statement inferred from a data source); plus the format-specific
|
|
8
|
+
variants ``x_write_create_table_ods``, ``x_write_create_table_xls``,
|
|
9
|
+
``x_write_create_table_alias``.
|
|
10
|
+
- ``x_write_prefix`` / ``x_write_suffix`` — CONFIG WRITE_PREFIX /
|
|
11
|
+
CONFIG WRITE_SUFFIX (set text prepended / appended to every WRITE).
|
|
5
12
|
"""
|
|
6
13
|
|
|
7
14
|
from __future__ import annotations
|
|
@@ -222,11 +229,107 @@ def x_write_suffix(**kwargs: Any) -> None:
|
|
|
222
229
|
return None
|
|
223
230
|
|
|
224
231
|
|
|
232
|
+
def _render_script_nodes(nodes: Any, emit: Any) -> None:
|
|
233
|
+
"""Walk a list of AST nodes and emit reconstructed source lines.
|
|
234
|
+
|
|
235
|
+
Every node type that can legally appear in a ``ScriptBlock.body`` is
|
|
236
|
+
handled — nested ``IfBlock`` / ``LoopBlock`` / ``BatchBlock`` /
|
|
237
|
+
``SqlBlock`` structures render with their delimiters; ``Comment`` and
|
|
238
|
+
``IncludeDirective`` (``INCLUDE`` / ``EXECUTE SCRIPT``) are preserved.
|
|
239
|
+
Unknown node types raise ``ErrInfo`` rather than silently disappear so
|
|
240
|
+
future AST additions can't quietly cause data loss.
|
|
241
|
+
"""
|
|
242
|
+
from execsql.exceptions import ErrInfo
|
|
243
|
+
from execsql.script.ast import (
|
|
244
|
+
BatchBlock,
|
|
245
|
+
Comment,
|
|
246
|
+
IfBlock,
|
|
247
|
+
IncludeDirective,
|
|
248
|
+
LoopBlock,
|
|
249
|
+
MetaCommandStatement,
|
|
250
|
+
ScriptBlock,
|
|
251
|
+
SqlBlock,
|
|
252
|
+
SqlStatement,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
for node in nodes:
|
|
256
|
+
if isinstance(node, SqlStatement):
|
|
257
|
+
emit(f"{node.text}\n")
|
|
258
|
+
elif isinstance(node, MetaCommandStatement):
|
|
259
|
+
emit(f"-- !x! {node.command}\n")
|
|
260
|
+
elif isinstance(node, Comment):
|
|
261
|
+
# Preserve comment text verbatim; the parser stores it including
|
|
262
|
+
# the leading "--" / "/* ... */" delimiters.
|
|
263
|
+
emit(f"{node.text}\n")
|
|
264
|
+
elif isinstance(node, IncludeDirective):
|
|
265
|
+
kw = "EXECUTE SCRIPT" if node.is_execute_script else "INCLUDE"
|
|
266
|
+
if_exists = " IF EXISTS" if node.if_exists else ""
|
|
267
|
+
args = f" ({node.arguments})" if node.arguments else ""
|
|
268
|
+
loop = f" LOOP {node.loop_type} ({node.loop_condition})" if node.loop_type else ""
|
|
269
|
+
emit(f"-- !x! {kw}{if_exists} {node.target}{args}{loop}\n")
|
|
270
|
+
elif isinstance(node, IfBlock):
|
|
271
|
+
emit(f"-- !x! IF ({node.condition})\n")
|
|
272
|
+
for mod in node.condition_modifiers:
|
|
273
|
+
kw = "ANDIF" if mod.kind == "AND" else "ORIF"
|
|
274
|
+
emit(f"-- !x! {kw} ({mod.condition})\n")
|
|
275
|
+
_render_script_nodes(node.body, emit)
|
|
276
|
+
for clause in node.elseif_clauses:
|
|
277
|
+
emit(f"-- !x! ELSEIF ({clause.condition})\n")
|
|
278
|
+
for mod in clause.condition_modifiers:
|
|
279
|
+
kw = "ANDIF" if mod.kind == "AND" else "ORIF"
|
|
280
|
+
emit(f"-- !x! {kw} ({mod.condition})\n")
|
|
281
|
+
_render_script_nodes(clause.body, emit)
|
|
282
|
+
if node.else_body:
|
|
283
|
+
emit("-- !x! ELSE\n")
|
|
284
|
+
_render_script_nodes(node.else_body, emit)
|
|
285
|
+
emit("-- !x! ENDIF\n")
|
|
286
|
+
elif isinstance(node, LoopBlock):
|
|
287
|
+
emit(f"-- !x! LOOP {node.loop_type} ({node.condition})\n")
|
|
288
|
+
_render_script_nodes(node.body, emit)
|
|
289
|
+
emit("-- !x! END LOOP\n")
|
|
290
|
+
elif isinstance(node, BatchBlock):
|
|
291
|
+
emit("-- !x! BEGIN BATCH\n")
|
|
292
|
+
_render_script_nodes(node.body, emit)
|
|
293
|
+
emit("-- !x! END BATCH\n")
|
|
294
|
+
elif isinstance(node, SqlBlock):
|
|
295
|
+
emit("-- !x! BEGIN SQL\n")
|
|
296
|
+
_render_script_nodes(node.body, emit)
|
|
297
|
+
emit("-- !x! END SQL\n")
|
|
298
|
+
elif isinstance(node, ScriptBlock):
|
|
299
|
+
# Nested SCRIPT definition — emit recursively with its own
|
|
300
|
+
# BEGIN/END SCRIPT framing.
|
|
301
|
+
param_names = node.param_names
|
|
302
|
+
if param_names:
|
|
303
|
+
emit(f"-- !x! BEGIN SCRIPT {node.name} ({', '.join(param_names)})\n")
|
|
304
|
+
else:
|
|
305
|
+
emit(f"-- !x! BEGIN SCRIPT {node.name}\n")
|
|
306
|
+
_render_script_nodes(node.body, emit)
|
|
307
|
+
emit(f"-- !x! END SCRIPT {node.name}\n")
|
|
308
|
+
else:
|
|
309
|
+
raise ErrInfo(
|
|
310
|
+
type="cmd",
|
|
311
|
+
other_msg=f"WRITE SCRIPT cannot render AST node of type {type(node).__name__}.",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
225
315
|
def x_writescript(**kwargs: Any) -> None:
|
|
226
|
-
|
|
316
|
+
"""Dump a registered SCRIPT block's source to stdout or a file.
|
|
317
|
+
|
|
318
|
+
Reads from the AST script registry (``ctx.ast_scripts``) and walks the
|
|
319
|
+
full block tree — including nested IF / LOOP / BATCH structures —
|
|
320
|
+
reconstructing source between ``BEGIN SCRIPT`` and ``END SCRIPT``
|
|
321
|
+
delimiters so the output is re-includable.
|
|
322
|
+
"""
|
|
323
|
+
from execsql.exceptions import ErrInfo
|
|
324
|
+
|
|
325
|
+
script_id = kwargs["script_id"].lower()
|
|
227
326
|
output_dest = kwargs["filename"]
|
|
228
327
|
append = kwargs["append"]
|
|
229
328
|
|
|
329
|
+
block = _state.ast_scripts.get(script_id)
|
|
330
|
+
if block is None:
|
|
331
|
+
raise ErrInfo("cmd", other_msg=f"There is no SCRIPT named {script_id}.")
|
|
332
|
+
|
|
230
333
|
def write(txt: str) -> None:
|
|
231
334
|
if output_dest is None or output_dest == "stdout":
|
|
232
335
|
_state.output.write(txt)
|
|
@@ -237,12 +340,11 @@ def x_writescript(**kwargs: Any) -> None:
|
|
|
237
340
|
check_dir(output_dest)
|
|
238
341
|
if not append:
|
|
239
342
|
filewriter_open_as_new(output_dest)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
343
|
+
|
|
344
|
+
param_names = block.param_names
|
|
345
|
+
if param_names:
|
|
346
|
+
write(f"-- !x! BEGIN SCRIPT {script_id} ({', '.join(param_names)})\n")
|
|
243
347
|
else:
|
|
244
|
-
write(f"BEGIN SCRIPT {script_id}\n")
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
write(f"{line}\n")
|
|
248
|
-
write(f"END SCRIPT {script_id}\n")
|
|
348
|
+
write(f"-- !x! BEGIN SCRIPT {script_id}\n")
|
|
349
|
+
_render_script_nodes(block.body, write)
|
|
350
|
+
write(f"-- !x! END SCRIPT {script_id}\n")
|