execsql2 2.15.8__py3-none-any.whl → 2.16.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/__init__.py +8 -3
- execsql/api.py +580 -0
- execsql/cli/__init__.py +123 -0
- execsql/cli/lint_ast.py +439 -0
- execsql/cli/run.py +113 -102
- execsql/config.py +29 -4
- execsql/db/access.py +1 -0
- execsql/db/base.py +4 -1
- execsql/db/dsn.py +3 -2
- execsql/db/duckdb.py +1 -1
- execsql/db/factory.py +3 -0
- execsql/db/firebird.py +2 -1
- execsql/db/mysql.py +2 -1
- execsql/db/oracle.py +2 -1
- execsql/db/postgres.py +2 -1
- execsql/db/sqlite.py +1 -1
- execsql/db/sqlserver.py +3 -2
- execsql/debug/repl.py +27 -10
- execsql/exporters/base.py +6 -4
- execsql/exporters/delimited.py +11 -3
- execsql/exporters/pretty.py +9 -12
- execsql/gui/tui.py +59 -2
- execsql/metacommands/__init__.py +3 -0
- execsql/metacommands/conditions.py +20 -2
- execsql/metacommands/connect.py +1 -1
- execsql/metacommands/control.py +8 -14
- execsql/metacommands/debug.py +6 -4
- execsql/metacommands/io_export.py +117 -315
- execsql/metacommands/io_fileops.py +7 -13
- execsql/metacommands/io_write.py +1 -1
- execsql/metacommands/script_ext.py +8 -5
- execsql/metacommands/upsert.py +40 -0
- execsql/models.py +8 -12
- execsql/plugins.py +414 -0
- execsql/script/__init__.py +36 -12
- execsql/script/ast.py +562 -0
- execsql/script/engine.py +59 -368
- execsql/script/executor.py +833 -0
- execsql/script/parser.py +663 -0
- execsql/script/variables.py +11 -0
- execsql/state.py +55 -2
- execsql/utils/crypto.py +14 -10
- execsql/utils/errors.py +31 -8
- execsql/utils/gui.py +139 -17
- execsql/utils/mail.py +15 -12
- {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/METADATA +59 -1
- {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/RECORD +66 -60
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/WHEEL +0 -0
- {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
"""AST-based script executor for execsql.
|
|
2
|
+
|
|
3
|
+
Walks a :class:`~execsql.script.ast.Script` tree and executes each node,
|
|
4
|
+
replacing the flat ``CommandList.run_next()`` loop for scripts parsed via
|
|
5
|
+
the AST parser.
|
|
6
|
+
|
|
7
|
+
Design:
|
|
8
|
+
- **The executor owns control flow.** IF conditions, LOOP iteration,
|
|
9
|
+
and BATCH boundaries are driven by the tree structure — no
|
|
10
|
+
``if_stack`` or ``compiling_loop`` needed.
|
|
11
|
+
- **SQL and metacommands delegate to the existing runtime.** SQL is
|
|
12
|
+
executed via the current database connection; metacommands are
|
|
13
|
+
dispatched through ``ctx.metacommandlist.eval()``. All 200+
|
|
14
|
+
metacommand handlers work unchanged.
|
|
15
|
+
- **Variable substitution** uses the existing ``substitute_vars()``.
|
|
16
|
+
- **RuntimeContext is passed explicitly** as ``ctx`` — the first
|
|
17
|
+
module migrated to instance-based context (Phase 2). The public
|
|
18
|
+
``execute()`` function defaults to ``get_context()`` if no ``ctx``
|
|
19
|
+
is provided, so callers that haven't migrated yet work unchanged.
|
|
20
|
+
|
|
21
|
+
Usage::
|
|
22
|
+
|
|
23
|
+
from execsql.script.executor import execute
|
|
24
|
+
from execsql.script.parser import parse_script
|
|
25
|
+
|
|
26
|
+
tree = parse_script("pipeline.sql")
|
|
27
|
+
execute(tree) # uses global context
|
|
28
|
+
|
|
29
|
+
# Or with an explicit context:
|
|
30
|
+
from execsql.state import RuntimeContext, get_context
|
|
31
|
+
ctx = get_context()
|
|
32
|
+
execute(tree, ctx=ctx)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import copy
|
|
38
|
+
import datetime
|
|
39
|
+
import os
|
|
40
|
+
import re
|
|
41
|
+
import time as _time
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import Any
|
|
44
|
+
|
|
45
|
+
from execsql.exceptions import ErrInfo
|
|
46
|
+
from execsql.script.ast import (
|
|
47
|
+
BatchBlock,
|
|
48
|
+
Comment,
|
|
49
|
+
ConditionModifier,
|
|
50
|
+
IfBlock,
|
|
51
|
+
IncludeDirective,
|
|
52
|
+
LoopBlock,
|
|
53
|
+
MetaCommandStatement,
|
|
54
|
+
Node,
|
|
55
|
+
Script,
|
|
56
|
+
ScriptBlock,
|
|
57
|
+
SqlBlock,
|
|
58
|
+
SqlStatement,
|
|
59
|
+
)
|
|
60
|
+
from execsql.script.engine import set_dynamic_system_vars, set_static_system_vars, substitute_vars
|
|
61
|
+
from execsql.script.variables import SubVarSet
|
|
62
|
+
from execsql.state import RuntimeContext, active_context, get_context, xcmd_test
|
|
63
|
+
from execsql.utils.errors import exception_desc, exit_now, stamp_errinfo
|
|
64
|
+
|
|
65
|
+
__all__ = ["execute"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Helpers
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
# Regex for deferred variable conversion: !{$VAR}! → !!$VAR!!
|
|
73
|
+
_DEFER_RX = re.compile(r"!\{([$@&~#+]?\w+)\}!")
|
|
74
|
+
|
|
75
|
+
# Compiled regex to match prefixed variables (for unsubstituted-var warnings)
|
|
76
|
+
_VARLIKE = re.compile(r"!![$@&~#]?\w+!!", re.I)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Legacy module-level alias — ``_ast_scripts`` is now ``ctx.ast_scripts``
|
|
80
|
+
# on the RuntimeContext. Kept as a comment for grep-ability.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _convert_deferred_vars(text: str) -> str:
|
|
84
|
+
"""Convert deferred substitution variables to regular ones.
|
|
85
|
+
|
|
86
|
+
In loop bodies, ``!{$VAR}!`` is converted to ``!!$VAR!!`` so that
|
|
87
|
+
variables are re-evaluated on each iteration instead of being captured
|
|
88
|
+
once at loop entry.
|
|
89
|
+
"""
|
|
90
|
+
return _DEFER_RX.sub(r"!!\1!!", text)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _eval_condition(
|
|
94
|
+
ctx: RuntimeContext,
|
|
95
|
+
condition: str,
|
|
96
|
+
modifiers: list[ConditionModifier] | None = None,
|
|
97
|
+
) -> bool:
|
|
98
|
+
"""Evaluate a condition string with optional ANDIF/ORIF modifiers."""
|
|
99
|
+
expanded = substitute_vars(condition, ctx=ctx)
|
|
100
|
+
result = xcmd_test(expanded)
|
|
101
|
+
|
|
102
|
+
if modifiers:
|
|
103
|
+
for mod in modifiers:
|
|
104
|
+
mod_expanded = substitute_vars(mod.condition, ctx=ctx)
|
|
105
|
+
mod_result = xcmd_test(mod_expanded)
|
|
106
|
+
if mod.kind == "AND":
|
|
107
|
+
result = result and mod_result
|
|
108
|
+
else: # OR
|
|
109
|
+
result = result or mod_result
|
|
110
|
+
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _set_command_vars(ctx: RuntimeContext, source: str, line_no: int) -> None:
|
|
115
|
+
"""Set per-command system variables (current script, line, time)."""
|
|
116
|
+
now = datetime.datetime.now()
|
|
117
|
+
ctx.subvars.add_substitution("$CURRENT_TIME", now.strftime("%Y-%m-%d %H:%M"))
|
|
118
|
+
ctx.subvars.add_substitution("$CURRENT_DATE", now.strftime("%Y-%m-%d"))
|
|
119
|
+
utcnow = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
120
|
+
ctx.subvars.add_substitution("$CURRENT_TIME_UTC", utcnow.strftime("%Y-%m-%d %H:%M"))
|
|
121
|
+
_p = Path(source)
|
|
122
|
+
ctx.subvars.add_substitution("$CURRENT_SCRIPT", source)
|
|
123
|
+
ctx.subvars.add_substitution("$CURRENT_SCRIPT_PATH", str(_p.resolve().parent) + os.sep)
|
|
124
|
+
ctx.subvars.add_substitution("$CURRENT_SCRIPT_NAME", _p.name)
|
|
125
|
+
ctx.subvars.add_substitution("$CURRENT_SCRIPT_LINE", str(line_no))
|
|
126
|
+
ctx.subvars.add_substitution("$SCRIPT_LINE", str(line_no))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# SQL execution (bypasses SqlStmt.run's if_stack check)
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _exec_sql(
|
|
135
|
+
ctx: RuntimeContext,
|
|
136
|
+
text: str,
|
|
137
|
+
source: str,
|
|
138
|
+
line_no: int,
|
|
139
|
+
localvars: SubVarSet | None = None,
|
|
140
|
+
commit: bool = True,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Execute a SQL statement against the current database."""
|
|
143
|
+
ctx.status.sql_error = False
|
|
144
|
+
if ctx.status.batch.in_batch():
|
|
145
|
+
ctx.status.batch.using_db(ctx.dbs.current())
|
|
146
|
+
cmd = substitute_vars(text, localvars, ctx=ctx)
|
|
147
|
+
if _VARLIKE.search(cmd):
|
|
148
|
+
ctx.output.write(
|
|
149
|
+
f"Warning: There is a potential un-substituted variable in the command\n {cmd}\n",
|
|
150
|
+
)
|
|
151
|
+
e = None
|
|
152
|
+
try:
|
|
153
|
+
db = ctx.dbs.current()
|
|
154
|
+
if ctx.conf.log_sql and ctx.exec_log:
|
|
155
|
+
ctx.exec_log.log_sql_query(cmd, db.name(), line_no)
|
|
156
|
+
db.execute(cmd)
|
|
157
|
+
if commit:
|
|
158
|
+
db.commit()
|
|
159
|
+
except ErrInfo as errinfo:
|
|
160
|
+
e = errinfo
|
|
161
|
+
except SystemExit:
|
|
162
|
+
raise
|
|
163
|
+
except Exception:
|
|
164
|
+
e = ErrInfo(type="exception", exception_msg=exception_desc())
|
|
165
|
+
if e:
|
|
166
|
+
stamp_errinfo(e)
|
|
167
|
+
ctx.subvars.add_substitution("$LAST_ERROR", cmd)
|
|
168
|
+
ctx.subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
|
|
169
|
+
ctx.status.sql_error = True
|
|
170
|
+
if ctx.exec_log is not None:
|
|
171
|
+
ctx.exec_log.log_status_info(f"SQL error: {e.errmsg()}")
|
|
172
|
+
if ctx.status.halt_on_err:
|
|
173
|
+
exit_now(1, e)
|
|
174
|
+
return
|
|
175
|
+
ctx.subvars.add_substitution("$LAST_SQL", cmd)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# Metacommand execution (bypasses MetacommandStmt.run's if_stack check)
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _exec_metacommand(
|
|
184
|
+
ctx: RuntimeContext,
|
|
185
|
+
command: str,
|
|
186
|
+
source: str,
|
|
187
|
+
line_no: int,
|
|
188
|
+
localvars: SubVarSet | None = None,
|
|
189
|
+
) -> Any:
|
|
190
|
+
"""Dispatch a metacommand through the dispatch table."""
|
|
191
|
+
cmd = substitute_vars(command, localvars, ctx=ctx)
|
|
192
|
+
if _VARLIKE.search(cmd):
|
|
193
|
+
ctx.output.write(
|
|
194
|
+
f"Warning: There is a potential un-substituted variable in the command\n {cmd}\n",
|
|
195
|
+
)
|
|
196
|
+
e = None
|
|
197
|
+
try:
|
|
198
|
+
applies, result = ctx.metacommandlist.eval(cmd)
|
|
199
|
+
if applies:
|
|
200
|
+
return result
|
|
201
|
+
except ErrInfo as errinfo:
|
|
202
|
+
e = errinfo
|
|
203
|
+
except SystemExit:
|
|
204
|
+
raise
|
|
205
|
+
except Exception:
|
|
206
|
+
e = ErrInfo(type="exception", exception_msg=exception_desc())
|
|
207
|
+
if e:
|
|
208
|
+
stamp_errinfo(e)
|
|
209
|
+
ctx.status.metacommand_error = True
|
|
210
|
+
ctx.subvars.add_substitution("$LAST_ERROR", cmd)
|
|
211
|
+
ctx.subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
|
|
212
|
+
if ctx.exec_log is not None:
|
|
213
|
+
ctx.exec_log.log_status_info(f"Metacommand error: {e.errmsg()}")
|
|
214
|
+
if ctx.status.halt_on_metacommand_err:
|
|
215
|
+
raise e
|
|
216
|
+
return None
|
|
217
|
+
# No handler matched — truly unknown metacommand
|
|
218
|
+
ctx.status.metacommand_error = True
|
|
219
|
+
raise ErrInfo(type="cmd", command_text=cmd, other_msg="Unknown metacommand")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
# Core tree walker
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _execute_nodes(
|
|
228
|
+
ctx: RuntimeContext,
|
|
229
|
+
nodes: list[Node],
|
|
230
|
+
source: str,
|
|
231
|
+
localvars: SubVarSet | None = None,
|
|
232
|
+
*,
|
|
233
|
+
in_loop: bool = False,
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Execute a list of AST nodes sequentially."""
|
|
236
|
+
for node in nodes:
|
|
237
|
+
if isinstance(node, Comment):
|
|
238
|
+
continue # Comments have no runtime semantics
|
|
239
|
+
set_dynamic_system_vars(ctx)
|
|
240
|
+
_set_command_vars(ctx, node.span.file, node.span.start_line)
|
|
241
|
+
|
|
242
|
+
# Debug step mode
|
|
243
|
+
if ctx.step_mode:
|
|
244
|
+
ctx.step_mode = False
|
|
245
|
+
from execsql.debug.repl import _debug_repl
|
|
246
|
+
|
|
247
|
+
_debug_repl(step=True)
|
|
248
|
+
|
|
249
|
+
# Profiling
|
|
250
|
+
profiling = ctx.profile_data is not None
|
|
251
|
+
if profiling:
|
|
252
|
+
t0 = _time.perf_counter()
|
|
253
|
+
|
|
254
|
+
_execute_node(ctx, node, localvars, in_loop=in_loop)
|
|
255
|
+
|
|
256
|
+
if profiling:
|
|
257
|
+
elapsed = _time.perf_counter() - t0
|
|
258
|
+
cmd_type = _node_cmd_type(node)
|
|
259
|
+
cmd_text = _node_cmd_text(node)[:100]
|
|
260
|
+
ctx.profile_data.append(
|
|
261
|
+
(node.span.file, node.span.start_line, cmd_type, elapsed, cmd_text),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
ctx.cmds_run += 1
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _execute_node(
|
|
268
|
+
ctx: RuntimeContext,
|
|
269
|
+
node: Node,
|
|
270
|
+
localvars: SubVarSet | None = None,
|
|
271
|
+
*,
|
|
272
|
+
in_loop: bool = False,
|
|
273
|
+
) -> None:
|
|
274
|
+
"""Execute a single AST node."""
|
|
275
|
+
if isinstance(node, SqlStatement):
|
|
276
|
+
text = node.text
|
|
277
|
+
if in_loop:
|
|
278
|
+
text = _convert_deferred_vars(text)
|
|
279
|
+
# Deduplicate trailing semicolons (matches SqlStmt.__init__)
|
|
280
|
+
text = re.sub(r"\s*;(\s*;\s*)+$", ";", text)
|
|
281
|
+
ctx.last_command = _FakeScriptCmd(node)
|
|
282
|
+
_exec_sql(
|
|
283
|
+
ctx,
|
|
284
|
+
text,
|
|
285
|
+
node.span.file,
|
|
286
|
+
node.span.start_line,
|
|
287
|
+
localvars,
|
|
288
|
+
commit=not ctx.status.batch.in_batch(),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
elif isinstance(node, MetaCommandStatement):
|
|
292
|
+
command = node.command
|
|
293
|
+
if in_loop:
|
|
294
|
+
command = _convert_deferred_vars(command)
|
|
295
|
+
# Intercept BREAK before dispatch — it controls loop flow
|
|
296
|
+
expanded = substitute_vars(command, localvars, ctx=ctx)
|
|
297
|
+
if _BREAK_RX.match(expanded):
|
|
298
|
+
raise _BreakLoop
|
|
299
|
+
ctx.last_command = _FakeScriptCmd(node)
|
|
300
|
+
_exec_metacommand(ctx, command, node.span.file, node.span.start_line, localvars)
|
|
301
|
+
|
|
302
|
+
elif isinstance(node, IfBlock):
|
|
303
|
+
_execute_if(ctx, node, localvars, in_loop=in_loop)
|
|
304
|
+
|
|
305
|
+
elif isinstance(node, LoopBlock):
|
|
306
|
+
_execute_loop(ctx, node, localvars)
|
|
307
|
+
|
|
308
|
+
elif isinstance(node, BatchBlock):
|
|
309
|
+
_execute_batch(ctx, node, localvars, in_loop=in_loop)
|
|
310
|
+
|
|
311
|
+
elif isinstance(node, ScriptBlock):
|
|
312
|
+
_register_script_block(ctx, node)
|
|
313
|
+
|
|
314
|
+
elif isinstance(node, SqlBlock):
|
|
315
|
+
_execute_sql_block(ctx, node, localvars, in_loop=in_loop)
|
|
316
|
+
|
|
317
|
+
elif isinstance(node, IncludeDirective):
|
|
318
|
+
_execute_include(ctx, node, localvars)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# ---------------------------------------------------------------------------
|
|
322
|
+
# Block executors
|
|
323
|
+
# ---------------------------------------------------------------------------
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _execute_if(
|
|
327
|
+
ctx: RuntimeContext,
|
|
328
|
+
node: IfBlock,
|
|
329
|
+
localvars: SubVarSet | None = None,
|
|
330
|
+
*,
|
|
331
|
+
in_loop: bool = False,
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Evaluate an IF block and execute the matching branch."""
|
|
334
|
+
if _eval_condition(ctx, node.condition, node.condition_modifiers):
|
|
335
|
+
_execute_nodes(ctx, node.body, node.span.file, localvars, in_loop=in_loop)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
# Try ELSEIF clauses
|
|
339
|
+
for clause in node.elseif_clauses:
|
|
340
|
+
expanded = substitute_vars(clause.condition, ctx=ctx)
|
|
341
|
+
if xcmd_test(expanded):
|
|
342
|
+
_execute_nodes(ctx, clause.body, node.span.file, localvars, in_loop=in_loop)
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
# ELSE branch
|
|
346
|
+
if node.else_body:
|
|
347
|
+
_execute_nodes(ctx, node.else_body, node.span.file, localvars, in_loop=in_loop)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _execute_loop(
|
|
351
|
+
ctx: RuntimeContext,
|
|
352
|
+
node: LoopBlock,
|
|
353
|
+
localvars: SubVarSet | None = None,
|
|
354
|
+
) -> None:
|
|
355
|
+
"""Execute a LOOP WHILE or LOOP UNTIL block."""
|
|
356
|
+
# Convert deferred vars in the condition — they re-evaluate each iteration
|
|
357
|
+
condition = _convert_deferred_vars(node.condition)
|
|
358
|
+
|
|
359
|
+
if node.loop_type == "WHILE":
|
|
360
|
+
while True:
|
|
361
|
+
expanded = substitute_vars(condition, ctx=ctx)
|
|
362
|
+
if not xcmd_test(expanded):
|
|
363
|
+
break
|
|
364
|
+
try:
|
|
365
|
+
_execute_nodes(ctx, node.body, node.span.file, localvars, in_loop=True)
|
|
366
|
+
except _BreakLoop:
|
|
367
|
+
break
|
|
368
|
+
else: # UNTIL
|
|
369
|
+
while True:
|
|
370
|
+
try:
|
|
371
|
+
_execute_nodes(ctx, node.body, node.span.file, localvars, in_loop=True)
|
|
372
|
+
except _BreakLoop:
|
|
373
|
+
break
|
|
374
|
+
expanded = substitute_vars(condition, ctx=ctx)
|
|
375
|
+
if xcmd_test(expanded):
|
|
376
|
+
break
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _execute_batch(
|
|
380
|
+
ctx: RuntimeContext,
|
|
381
|
+
node: BatchBlock,
|
|
382
|
+
localvars: SubVarSet | None = None,
|
|
383
|
+
*,
|
|
384
|
+
in_loop: bool = False,
|
|
385
|
+
) -> None:
|
|
386
|
+
"""Execute a BEGIN BATCH / END BATCH block."""
|
|
387
|
+
ctx.status.batch.new_batch()
|
|
388
|
+
try:
|
|
389
|
+
_execute_nodes(ctx, node.body, node.span.file, localvars, in_loop=in_loop)
|
|
390
|
+
finally:
|
|
391
|
+
if ctx.status.batch.in_batch():
|
|
392
|
+
ctx.status.batch.end_batch()
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _register_script_block(ctx: RuntimeContext, node: ScriptBlock) -> None:
|
|
396
|
+
"""Register a named SCRIPT block.
|
|
397
|
+
|
|
398
|
+
Stores the AST node in ``ctx.ast_scripts`` for native execution, and also
|
|
399
|
+
builds a legacy ``CommandList`` in ``ctx.savedscripts`` so that dispatch
|
|
400
|
+
table handlers (e.g., ON ERROR_HALT EXECUTE SCRIPT) still work.
|
|
401
|
+
"""
|
|
402
|
+
from execsql.script.engine import CommandList
|
|
403
|
+
|
|
404
|
+
# AST-native registry
|
|
405
|
+
ctx.ast_scripts[node.name] = node
|
|
406
|
+
|
|
407
|
+
# Legacy compatibility — flatten to CommandList for dispatch table
|
|
408
|
+
cmdlist = _flatten_for_legacy(node.body, node.span.file)
|
|
409
|
+
cl = CommandList(cmdlist, node.name, node.param_names)
|
|
410
|
+
ctx.savedscripts[node.name] = cl
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _flatten_for_legacy(nodes: list[Node], source: str) -> list:
|
|
414
|
+
"""Convert AST nodes to flat ScriptCmd list for legacy compatibility."""
|
|
415
|
+
from execsql.script.engine import MetacommandStmt, ScriptCmd, SqlStmt
|
|
416
|
+
|
|
417
|
+
result = []
|
|
418
|
+
for node in nodes:
|
|
419
|
+
if isinstance(node, SqlStatement):
|
|
420
|
+
text = re.sub(r"\s*;(\s*;\s*)+$", ";", node.text)
|
|
421
|
+
result.append(
|
|
422
|
+
ScriptCmd(node.span.file, node.span.start_line, "sql", SqlStmt(text)),
|
|
423
|
+
)
|
|
424
|
+
elif isinstance(node, MetaCommandStatement):
|
|
425
|
+
result.append(
|
|
426
|
+
ScriptCmd(node.span.file, node.span.start_line, "cmd", MetacommandStmt(node.command)),
|
|
427
|
+
)
|
|
428
|
+
elif isinstance(node, IfBlock):
|
|
429
|
+
result.append(
|
|
430
|
+
ScriptCmd(
|
|
431
|
+
node.span.file,
|
|
432
|
+
node.span.start_line,
|
|
433
|
+
"cmd",
|
|
434
|
+
MetacommandStmt(f"IF ({node.condition})"),
|
|
435
|
+
),
|
|
436
|
+
)
|
|
437
|
+
# Emit ANDIF/ORIF condition modifiers after the IF
|
|
438
|
+
for mod in node.condition_modifiers:
|
|
439
|
+
keyword = "ANDIF" if mod.kind == "AND" else "ORIF"
|
|
440
|
+
result.append(
|
|
441
|
+
ScriptCmd(
|
|
442
|
+
mod.span.file,
|
|
443
|
+
mod.span.start_line,
|
|
444
|
+
"cmd",
|
|
445
|
+
MetacommandStmt(f"{keyword} ({mod.condition})"),
|
|
446
|
+
),
|
|
447
|
+
)
|
|
448
|
+
result.extend(_flatten_for_legacy(node.body, source))
|
|
449
|
+
for clause in node.elseif_clauses:
|
|
450
|
+
result.append(
|
|
451
|
+
ScriptCmd(
|
|
452
|
+
clause.span.file,
|
|
453
|
+
clause.span.start_line,
|
|
454
|
+
"cmd",
|
|
455
|
+
MetacommandStmt(f"ELSEIF ({clause.condition})"),
|
|
456
|
+
),
|
|
457
|
+
)
|
|
458
|
+
result.extend(_flatten_for_legacy(clause.body, source))
|
|
459
|
+
if node.else_body:
|
|
460
|
+
result.append(
|
|
461
|
+
ScriptCmd(
|
|
462
|
+
node.span.file,
|
|
463
|
+
node.else_span.start_line if node.else_span else node.span.start_line,
|
|
464
|
+
"cmd",
|
|
465
|
+
MetacommandStmt("ELSE"),
|
|
466
|
+
),
|
|
467
|
+
)
|
|
468
|
+
result.extend(_flatten_for_legacy(node.else_body, source))
|
|
469
|
+
result.append(
|
|
470
|
+
ScriptCmd(
|
|
471
|
+
node.span.file,
|
|
472
|
+
node.span.effective_end_line,
|
|
473
|
+
"cmd",
|
|
474
|
+
MetacommandStmt("ENDIF"),
|
|
475
|
+
),
|
|
476
|
+
)
|
|
477
|
+
elif isinstance(node, LoopBlock):
|
|
478
|
+
result.append(
|
|
479
|
+
ScriptCmd(
|
|
480
|
+
node.span.file,
|
|
481
|
+
node.span.start_line,
|
|
482
|
+
"cmd",
|
|
483
|
+
MetacommandStmt(f"LOOP {node.loop_type} ({node.condition})"),
|
|
484
|
+
),
|
|
485
|
+
)
|
|
486
|
+
result.extend(_flatten_for_legacy(node.body, source))
|
|
487
|
+
result.append(
|
|
488
|
+
ScriptCmd(
|
|
489
|
+
node.span.file,
|
|
490
|
+
node.span.effective_end_line,
|
|
491
|
+
"cmd",
|
|
492
|
+
MetacommandStmt("END LOOP"),
|
|
493
|
+
),
|
|
494
|
+
)
|
|
495
|
+
elif isinstance(node, BatchBlock):
|
|
496
|
+
result.append(
|
|
497
|
+
ScriptCmd(node.span.file, node.span.start_line, "cmd", MetacommandStmt("BEGIN BATCH")),
|
|
498
|
+
)
|
|
499
|
+
result.extend(_flatten_for_legacy(node.body, source))
|
|
500
|
+
result.append(
|
|
501
|
+
ScriptCmd(node.span.file, node.span.effective_end_line, "cmd", MetacommandStmt("END BATCH")),
|
|
502
|
+
)
|
|
503
|
+
elif isinstance(node, SqlBlock):
|
|
504
|
+
result.extend(_flatten_for_legacy(node.body, source))
|
|
505
|
+
elif isinstance(node, IncludeDirective):
|
|
506
|
+
if node.is_execute_script:
|
|
507
|
+
parts = ["EXECUTE SCRIPT"]
|
|
508
|
+
if node.if_exists:
|
|
509
|
+
parts.append("IF EXISTS")
|
|
510
|
+
parts.append(node.target)
|
|
511
|
+
if node.arguments:
|
|
512
|
+
parts.append(f"WITH ARGS ({node.arguments})")
|
|
513
|
+
if node.loop_type:
|
|
514
|
+
parts.append(f"{node.loop_type} ({node.loop_condition})")
|
|
515
|
+
result.append(
|
|
516
|
+
ScriptCmd(node.span.file, node.span.start_line, "cmd", MetacommandStmt(" ".join(parts))),
|
|
517
|
+
)
|
|
518
|
+
else:
|
|
519
|
+
prefix = "INCLUDE IF EXISTS" if node.if_exists else "INCLUDE"
|
|
520
|
+
result.append(
|
|
521
|
+
ScriptCmd(node.span.file, node.span.start_line, "cmd", MetacommandStmt(f"{prefix} {node.target}")),
|
|
522
|
+
)
|
|
523
|
+
return result
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _execute_sql_block(
|
|
527
|
+
ctx: RuntimeContext,
|
|
528
|
+
node: SqlBlock,
|
|
529
|
+
localvars: SubVarSet | None = None,
|
|
530
|
+
*,
|
|
531
|
+
in_loop: bool = False,
|
|
532
|
+
) -> None:
|
|
533
|
+
"""Execute a BEGIN SQL / END SQL block."""
|
|
534
|
+
_execute_nodes(ctx, node.body, node.span.file, localvars, in_loop=in_loop)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _execute_include(
|
|
538
|
+
ctx: RuntimeContext,
|
|
539
|
+
node: IncludeDirective,
|
|
540
|
+
localvars: SubVarSet | None = None,
|
|
541
|
+
) -> None:
|
|
542
|
+
"""Execute an INCLUDE or EXECUTE SCRIPT directive.
|
|
543
|
+
|
|
544
|
+
**INCLUDE** is handled natively: the target file is parsed by the AST
|
|
545
|
+
parser and executed through the AST executor with circular-include
|
|
546
|
+
detection.
|
|
547
|
+
|
|
548
|
+
**EXECUTE SCRIPT** is handled natively when the target is in
|
|
549
|
+
``ctx.ast_scripts``: arguments are parsed, a local variable overlay
|
|
550
|
+
is created, and the body is executed through the AST executor.
|
|
551
|
+
WHILE/UNTIL loops are handled natively too.
|
|
552
|
+
"""
|
|
553
|
+
if node.is_execute_script:
|
|
554
|
+
target = node.target.lower()
|
|
555
|
+
|
|
556
|
+
# Native path: target is in our AST registry
|
|
557
|
+
if target in ctx.ast_scripts:
|
|
558
|
+
_execute_script_native(ctx, node, ctx.ast_scripts[target], localvars)
|
|
559
|
+
return
|
|
560
|
+
|
|
561
|
+
# Target not in AST registry — might be defined in an INCLUDE'd file
|
|
562
|
+
# that hasn't been loaded yet. Fall through to legacy dispatch.
|
|
563
|
+
if not node.if_exists and target not in ctx.savedscripts:
|
|
564
|
+
raise ErrInfo(
|
|
565
|
+
"cmd",
|
|
566
|
+
other_msg=f"There is no SCRIPT named {node.target}.",
|
|
567
|
+
)
|
|
568
|
+
if node.if_exists and target not in ctx.savedscripts:
|
|
569
|
+
return # IF EXISTS — skip silently
|
|
570
|
+
|
|
571
|
+
# Target is in savedscripts but not ast_scripts — this shouldn't
|
|
572
|
+
# happen when the AST executor is the only engine, but handle it
|
|
573
|
+
# gracefully by raising an error.
|
|
574
|
+
raise ErrInfo(
|
|
575
|
+
"cmd",
|
|
576
|
+
other_msg=f"SCRIPT {node.target} is not registered in the AST executor.",
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# --- INCLUDE (file inclusion) — parse and execute natively ---
|
|
580
|
+
_execute_include_native(ctx, node, localvars)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _execute_script_native(
|
|
584
|
+
ctx: RuntimeContext,
|
|
585
|
+
node: IncludeDirective,
|
|
586
|
+
script_block: ScriptBlock,
|
|
587
|
+
localvars: SubVarSet | None = None,
|
|
588
|
+
) -> None:
|
|
589
|
+
"""Execute a SCRIPT block natively through the AST executor."""
|
|
590
|
+
from execsql.script.variables import ScriptArgSubVarSet
|
|
591
|
+
from execsql.utils.strings import wo_quotes
|
|
592
|
+
|
|
593
|
+
# Parse arguments (replicates ScriptExecSpec logic)
|
|
594
|
+
script_localvars = None
|
|
595
|
+
if node.arguments is not None:
|
|
596
|
+
args_rx = re.compile(
|
|
597
|
+
r'(?P<param>#?\w+)\s*=\s*(?P<arg>(?:(?:[^"\'\[][^,\)]*)|(?:"[^"]*")|(?:\'[^\']*\')|(?:\[[^\]]*\])))',
|
|
598
|
+
re.I,
|
|
599
|
+
)
|
|
600
|
+
all_args = re.findall(args_rx, node.arguments)
|
|
601
|
+
all_cleaned_args = [(ae[0], wo_quotes(ae[1])) for ae in all_args]
|
|
602
|
+
all_prepared_args = [(ae[0] if ae[0][0] == "#" else "#" + ae[0], ae[1]) for ae in all_cleaned_args]
|
|
603
|
+
scriptvarset = ScriptArgSubVarSet()
|
|
604
|
+
for param, arg in all_prepared_args:
|
|
605
|
+
scriptvarset.add_substitution(param, arg)
|
|
606
|
+
|
|
607
|
+
# Validate parameter names match
|
|
608
|
+
if script_block.param_names is not None:
|
|
609
|
+
passed_names = [p[0].lstrip("#") for p in all_prepared_args]
|
|
610
|
+
if not all(p in passed_names for p in script_block.param_names):
|
|
611
|
+
raise ErrInfo(
|
|
612
|
+
"error",
|
|
613
|
+
other_msg=f"Formal and actual parameter name mismatch in call to {script_block.name}.",
|
|
614
|
+
)
|
|
615
|
+
script_localvars = scriptvarset
|
|
616
|
+
else:
|
|
617
|
+
if script_block.param_names is not None:
|
|
618
|
+
raise ErrInfo(
|
|
619
|
+
"error",
|
|
620
|
+
other_msg=(
|
|
621
|
+
f"Missing expected parameters ({', '.join(script_block.param_names)}) "
|
|
622
|
+
f"in call to {script_block.name}."
|
|
623
|
+
),
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
# Merge script-local vars with any existing local vars
|
|
627
|
+
merged = script_localvars
|
|
628
|
+
|
|
629
|
+
def _run_body() -> None:
|
|
630
|
+
# Deep-copy the body to avoid mutation across iterations
|
|
631
|
+
body = copy.deepcopy(script_block.body)
|
|
632
|
+
_execute_nodes(ctx, body, script_block.span.file, merged, in_loop=False)
|
|
633
|
+
|
|
634
|
+
# Handle WHILE/UNTIL loops
|
|
635
|
+
# Convert deferred vars once — node.loop_condition is immutable after parsing
|
|
636
|
+
if node.loop_type is not None:
|
|
637
|
+
condition = _convert_deferred_vars(node.loop_condition)
|
|
638
|
+
|
|
639
|
+
if node.loop_type == "WHILE":
|
|
640
|
+
while True:
|
|
641
|
+
expanded = substitute_vars(condition, ctx=ctx)
|
|
642
|
+
if not xcmd_test(expanded):
|
|
643
|
+
break
|
|
644
|
+
try:
|
|
645
|
+
_run_body()
|
|
646
|
+
except _BreakLoop:
|
|
647
|
+
break
|
|
648
|
+
elif node.loop_type == "UNTIL":
|
|
649
|
+
while True:
|
|
650
|
+
try:
|
|
651
|
+
_run_body()
|
|
652
|
+
except _BreakLoop:
|
|
653
|
+
break
|
|
654
|
+
expanded = substitute_vars(condition, ctx=ctx)
|
|
655
|
+
if xcmd_test(expanded):
|
|
656
|
+
break
|
|
657
|
+
else:
|
|
658
|
+
_run_body()
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _execute_include_native(
|
|
662
|
+
ctx: RuntimeContext,
|
|
663
|
+
node: IncludeDirective,
|
|
664
|
+
localvars: SubVarSet | None = None,
|
|
665
|
+
) -> None:
|
|
666
|
+
"""Parse an INCLUDE'd file with the AST parser and execute it natively.
|
|
667
|
+
|
|
668
|
+
Handles tilde expansion, IF EXISTS, logging, and circular-include
|
|
669
|
+
detection via ``ctx.include_chain``.
|
|
670
|
+
"""
|
|
671
|
+
from execsql.script.parser import parse_script
|
|
672
|
+
from execsql.utils.errors import file_size_date
|
|
673
|
+
|
|
674
|
+
# Substitute variables in the target path
|
|
675
|
+
target = substitute_vars(node.target, localvars, ctx=ctx).strip()
|
|
676
|
+
|
|
677
|
+
# Tilde expansion (matches x_include legacy handler)
|
|
678
|
+
if len(target) > 1 and target[0] == "~" and target[1] == os.sep:
|
|
679
|
+
target = str(Path.home() / target[2:])
|
|
680
|
+
|
|
681
|
+
target_path = Path(target)
|
|
682
|
+
|
|
683
|
+
# IF EXISTS handling
|
|
684
|
+
if node.if_exists:
|
|
685
|
+
if not target_path.is_file():
|
|
686
|
+
return
|
|
687
|
+
else:
|
|
688
|
+
if not target_path.is_file():
|
|
689
|
+
raise ErrInfo(type="error", other_msg=f"File {target} does not exist.")
|
|
690
|
+
|
|
691
|
+
# Resolve to absolute for consistent circular-include detection
|
|
692
|
+
resolved = str(target_path.resolve())
|
|
693
|
+
|
|
694
|
+
# Circular include detection
|
|
695
|
+
if resolved in ctx.include_chain:
|
|
696
|
+
chain = " → ".join(ctx.include_chain + [resolved])
|
|
697
|
+
raise ErrInfo(
|
|
698
|
+
type="error",
|
|
699
|
+
other_msg=f"Circular INCLUDE detected: {chain}",
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
# Log the include (matching legacy read_sqlfile behavior)
|
|
703
|
+
if ctx.exec_log:
|
|
704
|
+
sz, dt = file_size_date(target)
|
|
705
|
+
ctx.exec_log.log_status_info(f"Reading script file {target} (size: {sz}; date: {dt})")
|
|
706
|
+
|
|
707
|
+
# Parse with AST parser
|
|
708
|
+
encoding = ctx.conf.script_encoding if ctx.conf else "utf-8"
|
|
709
|
+
included_tree = parse_script(target, encoding=encoding)
|
|
710
|
+
|
|
711
|
+
# Execute with include-chain tracking
|
|
712
|
+
ctx.include_chain.append(resolved)
|
|
713
|
+
try:
|
|
714
|
+
_execute_nodes(ctx, included_tree.body, included_tree.source, localvars)
|
|
715
|
+
finally:
|
|
716
|
+
ctx.include_chain.pop()
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
# ---------------------------------------------------------------------------
|
|
720
|
+
# BREAK support
|
|
721
|
+
# ---------------------------------------------------------------------------
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
class _BreakLoop(Exception):
|
|
725
|
+
"""Raised by BREAK metacommand to exit the innermost loop."""
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
_BREAK_RX = re.compile(r"^\s*BREAK\s*$", re.I)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
# ---------------------------------------------------------------------------
|
|
732
|
+
# Fake ScriptCmd for ctx.last_command compatibility
|
|
733
|
+
# ---------------------------------------------------------------------------
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
class _FakeScriptCmd:
|
|
737
|
+
"""Minimal stand-in for ScriptCmd to satisfy ctx.last_command readers."""
|
|
738
|
+
|
|
739
|
+
__slots__ = ("source", "line_no", "source_dir", "source_name", "command", "command_type")
|
|
740
|
+
|
|
741
|
+
def __init__(self, node: Node) -> None:
|
|
742
|
+
self.source = node.span.file
|
|
743
|
+
self.line_no = node.span.start_line
|
|
744
|
+
_p = Path(node.span.file)
|
|
745
|
+
self.source_dir = str(_p.resolve().parent) + os.sep
|
|
746
|
+
self.source_name = _p.name
|
|
747
|
+
self.command_type = "sql" if isinstance(node, SqlStatement) else "cmd"
|
|
748
|
+
if isinstance(node, SqlStatement):
|
|
749
|
+
self.command = type("_cmd", (), {"statement": node.text, "commandline": lambda self: self.statement})()
|
|
750
|
+
elif isinstance(node, MetaCommandStatement):
|
|
751
|
+
self.command = type(
|
|
752
|
+
"_cmd",
|
|
753
|
+
(),
|
|
754
|
+
{"statement": node.command, "commandline": lambda self: "-- !x! " + self.statement},
|
|
755
|
+
)()
|
|
756
|
+
else:
|
|
757
|
+
self.command = type("_cmd", (), {"statement": "", "commandline": lambda self: ""})()
|
|
758
|
+
|
|
759
|
+
def current_script_line(self) -> tuple:
|
|
760
|
+
return (self.source, self.line_no)
|
|
761
|
+
|
|
762
|
+
def commandline(self) -> str:
|
|
763
|
+
return self.command.commandline()
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
# ---------------------------------------------------------------------------
|
|
767
|
+
# Node type/text helpers for profiling
|
|
768
|
+
# ---------------------------------------------------------------------------
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _node_cmd_type(node: Node) -> str:
|
|
772
|
+
if isinstance(node, SqlStatement):
|
|
773
|
+
return "sql"
|
|
774
|
+
return "cmd"
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _node_cmd_text(node: Node) -> str:
|
|
778
|
+
if isinstance(node, SqlStatement):
|
|
779
|
+
return node.text
|
|
780
|
+
if isinstance(node, MetaCommandStatement):
|
|
781
|
+
return "-- !x! " + node.command
|
|
782
|
+
if isinstance(node, IfBlock):
|
|
783
|
+
return f"-- !x! IF ({node.condition})"
|
|
784
|
+
if isinstance(node, LoopBlock):
|
|
785
|
+
return f"-- !x! LOOP {node.loop_type} ({node.condition})"
|
|
786
|
+
if isinstance(node, BatchBlock):
|
|
787
|
+
return "-- !x! BEGIN BATCH"
|
|
788
|
+
if isinstance(node, ScriptBlock):
|
|
789
|
+
return f"-- !x! BEGIN SCRIPT {node.name}"
|
|
790
|
+
if isinstance(node, IncludeDirective):
|
|
791
|
+
if node.is_execute_script:
|
|
792
|
+
return f"-- !x! EXECUTE SCRIPT {node.target}"
|
|
793
|
+
return f"-- !x! INCLUDE {node.target}"
|
|
794
|
+
return repr(node)
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
# ---------------------------------------------------------------------------
|
|
798
|
+
# Public entry point
|
|
799
|
+
# ---------------------------------------------------------------------------
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def execute(script: Script, *, ctx: RuntimeContext | None = None) -> None:
|
|
803
|
+
"""Execute an AST-parsed script.
|
|
804
|
+
|
|
805
|
+
Args:
|
|
806
|
+
script: The parsed :class:`Script` tree to execute.
|
|
807
|
+
ctx: The :class:`RuntimeContext` to use. Defaults to the global
|
|
808
|
+
context via :func:`get_context` if not provided.
|
|
809
|
+
"""
|
|
810
|
+
if ctx is None:
|
|
811
|
+
ctx = get_context()
|
|
812
|
+
|
|
813
|
+
# Activate this context so all _state.foo accesses in metacommand
|
|
814
|
+
# handlers, database adapters, and other legacy code resolve against
|
|
815
|
+
# it. This gives full isolation without modifying 200+ handler
|
|
816
|
+
# function signatures.
|
|
817
|
+
with active_context(ctx):
|
|
818
|
+
ctx.ast_scripts.clear()
|
|
819
|
+
ctx.include_chain.clear()
|
|
820
|
+
# Seed the include chain with the main script to catch self-includes.
|
|
821
|
+
if script.source != "<inline>":
|
|
822
|
+
try:
|
|
823
|
+
ctx.include_chain.append(str(Path(script.source).resolve()))
|
|
824
|
+
except (OSError, ValueError):
|
|
825
|
+
ctx.include_chain.append(script.source)
|
|
826
|
+
set_static_system_vars(ctx)
|
|
827
|
+
try:
|
|
828
|
+
_execute_nodes(ctx, script.body, script.source)
|
|
829
|
+
except _BreakLoop as exc:
|
|
830
|
+
raise ErrInfo(
|
|
831
|
+
type="cmd",
|
|
832
|
+
other_msg="BREAK metacommand outside of a LOOP block.",
|
|
833
|
+
) from exc
|