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/script/executor.py
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
"""AST-based script executor for execsql.
|
|
2
2
|
|
|
3
|
-
Walks a :class:`~execsql.script.ast.Script` tree and executes each node
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Walks a :class:`~execsql.script.ast.Script` tree and executes each node.
|
|
4
|
+
This is the only execution engine; the parser produces the tree, this
|
|
5
|
+
module runs it.
|
|
6
6
|
|
|
7
7
|
Design:
|
|
8
|
-
- **
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- **SQL and metacommands delegate to the existing runtime.**
|
|
12
|
-
|
|
13
|
-
dispatched through ``ctx.metacommandlist.eval()
|
|
14
|
-
|
|
15
|
-
- **Variable substitution** uses
|
|
16
|
-
- **
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
- **Control flow is tree-driven.** IF conditions, LOOP iteration, and
|
|
9
|
+
BATCH boundaries are resolved by walking nested nodes, not by
|
|
10
|
+
runtime state flags.
|
|
11
|
+
- **SQL and metacommands delegate to the existing runtime.** SQL
|
|
12
|
+
runs against the active database connection; metacommands are
|
|
13
|
+
dispatched through ``ctx.metacommandlist.eval()`` (the same
|
|
14
|
+
~225-entry dispatch table the rest of the codebase uses).
|
|
15
|
+
- **Variable substitution** uses :func:`execsql.script.engine.substitute_vars`.
|
|
16
|
+
- **Context is explicit.** :func:`execute` takes a
|
|
17
|
+
:class:`~execsql.state.RuntimeContext` via the ``ctx`` keyword;
|
|
18
|
+
when omitted it falls back to :func:`~execsql.state.get_context`.
|
|
19
|
+
For tracking and error reporting the executor pushes
|
|
20
|
+
:class:`~execsql.state.ExecFrame` records onto
|
|
21
|
+
``ctx.ast_exec_stack`` as it descends into IF / LOOP / BATCH /
|
|
22
|
+
INCLUDE'd files / ``EXECUTE SCRIPT`` calls. Scope frames
|
|
23
|
+
(``main``, ``script``) carry the active ``localvars`` and
|
|
24
|
+
``paramvals``; block frames cache a ``scope_ref`` to the
|
|
25
|
+
enclosing scope for O(1) variable lookup.
|
|
20
26
|
|
|
21
27
|
Usage::
|
|
22
28
|
|
|
@@ -24,12 +30,13 @@ Usage::
|
|
|
24
30
|
from execsql.script.parser import parse_script
|
|
25
31
|
|
|
26
32
|
tree = parse_script("pipeline.sql")
|
|
27
|
-
execute(tree) # uses
|
|
33
|
+
execute(tree) # uses the thread-local context
|
|
28
34
|
|
|
29
35
|
# Or with an explicit context:
|
|
30
|
-
from execsql.state import RuntimeContext,
|
|
31
|
-
ctx =
|
|
32
|
-
|
|
36
|
+
from execsql.state import RuntimeContext, active_context
|
|
37
|
+
ctx = RuntimeContext()
|
|
38
|
+
with active_context(ctx):
|
|
39
|
+
execute(tree, ctx=ctx)
|
|
33
40
|
"""
|
|
34
41
|
|
|
35
42
|
from __future__ import annotations
|
|
@@ -57,9 +64,9 @@ from execsql.script.ast import (
|
|
|
57
64
|
SqlBlock,
|
|
58
65
|
SqlStatement,
|
|
59
66
|
)
|
|
60
|
-
from execsql.script.engine import
|
|
67
|
+
from execsql.script.engine import set_dynamic_system_vars, set_static_system_vars, substitute_vars
|
|
61
68
|
from execsql.script.variables import SubVarSet
|
|
62
|
-
from execsql.state import RuntimeContext, active_context, get_context, xcmd_test
|
|
69
|
+
from execsql.state import ExecFrame, RuntimeContext, active_context, get_context, xcmd_test
|
|
63
70
|
from execsql.utils.errors import exception_desc, exit_now, stamp_errinfo
|
|
64
71
|
|
|
65
72
|
__all__ = ["execute"]
|
|
@@ -81,18 +88,21 @@ _VARLIKE = re.compile(r"!![$@&~#]?\w+!!", re.I)
|
|
|
81
88
|
|
|
82
89
|
|
|
83
90
|
def _stack_localvars(ctx: RuntimeContext) -> SubVarSet | None:
|
|
84
|
-
"""Build the merged local+param
|
|
91
|
+
"""Build the merged ``~`` local + ``#`` param overlay for the current scope.
|
|
85
92
|
|
|
86
|
-
Returns ``
|
|
87
|
-
|
|
88
|
-
the
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
Returns ``localvars.merge(paramvals)`` for the innermost SCRIPT or
|
|
94
|
+
``<main>`` scope frame, or ``None`` if the exec stack is empty. This is
|
|
95
|
+
the canonical "current variable scope" used by ``substitute_vars`` and
|
|
96
|
+
``get_subvarset`` so that ``x_sub``, ``x_rm_sub``, condition predicates,
|
|
97
|
+
and prompt handlers all see the same overlay.
|
|
91
98
|
"""
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return
|
|
95
|
-
|
|
99
|
+
scope = ctx.current_scope()
|
|
100
|
+
if scope is None:
|
|
101
|
+
return None
|
|
102
|
+
localvars = scope.localvars
|
|
103
|
+
if localvars is None:
|
|
104
|
+
return None
|
|
105
|
+
return localvars.merge(scope.paramvals)
|
|
96
106
|
|
|
97
107
|
|
|
98
108
|
def _push_frame(
|
|
@@ -102,26 +112,43 @@ def _push_frame(
|
|
|
102
112
|
line_no: int = 0,
|
|
103
113
|
*,
|
|
104
114
|
paramnames: list[str] | None = None,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
``
|
|
110
|
-
|
|
111
|
-
``
|
|
115
|
+
kind: str = "script",
|
|
116
|
+
) -> ExecFrame:
|
|
117
|
+
"""Push a scope frame onto the unified AST execution stack.
|
|
118
|
+
|
|
119
|
+
Creates an :class:`ExecFrame` of ``kind`` ``"main"`` or ``"script"`` with a
|
|
120
|
+
fresh :class:`LocalSubVarSet` and the declared ``paramnames``. Returns
|
|
121
|
+
the frame so the caller can set ``paramvals`` after parsing the call's
|
|
122
|
+
arguments.
|
|
112
123
|
"""
|
|
113
|
-
from execsql.script.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
from execsql.script.variables import LocalSubVarSet
|
|
125
|
+
|
|
126
|
+
frame = ExecFrame(
|
|
127
|
+
kind=kind,
|
|
128
|
+
label=name,
|
|
129
|
+
source=source,
|
|
130
|
+
line=line_no or None,
|
|
131
|
+
localvars=LocalSubVarSet(),
|
|
132
|
+
paramnames=paramnames,
|
|
133
|
+
)
|
|
134
|
+
ctx.ast_exec_stack.append(frame)
|
|
118
135
|
return frame
|
|
119
136
|
|
|
120
137
|
|
|
121
138
|
def _pop_frame(ctx: RuntimeContext) -> None:
|
|
122
|
-
"""Pop the top frame from the
|
|
123
|
-
if ctx.
|
|
124
|
-
ctx.
|
|
139
|
+
"""Pop the top scope frame from the AST execution stack."""
|
|
140
|
+
if ctx.ast_exec_stack:
|
|
141
|
+
ctx.ast_exec_stack.pop()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _push_block_frame(ctx: RuntimeContext, frame: ExecFrame) -> None:
|
|
145
|
+
"""Push a non-scope frame (if/loop/batch/include) onto the exec stack.
|
|
146
|
+
|
|
147
|
+
Caches a reference to the enclosing SCRIPT/main scope on the frame so
|
|
148
|
+
that ``current_localvars()`` stays O(1) regardless of nesting depth.
|
|
149
|
+
"""
|
|
150
|
+
frame.scope_ref = ctx.current_scope()
|
|
151
|
+
ctx.ast_exec_stack.append(frame)
|
|
125
152
|
|
|
126
153
|
|
|
127
154
|
def _convert_deferred_vars(text: str) -> str:
|
|
@@ -243,15 +270,17 @@ def _exec_sql(
|
|
|
243
270
|
|
|
244
271
|
def _exec_metacommand(
|
|
245
272
|
ctx: RuntimeContext,
|
|
246
|
-
|
|
273
|
+
cmd: str,
|
|
247
274
|
source: str,
|
|
248
275
|
line_no: int,
|
|
249
|
-
localvars: SubVarSet | None = None,
|
|
250
276
|
) -> Any:
|
|
251
|
-
"""Dispatch a metacommand through the dispatch table.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
277
|
+
"""Dispatch a metacommand through the dispatch table.
|
|
278
|
+
|
|
279
|
+
*cmd* must already have ``!!$VAR!!`` substitution applied. The caller is
|
|
280
|
+
responsible for expansion so that side-effecting substitutions (counter
|
|
281
|
+
increments, ``$RANDOM``, ``$UUID``) are evaluated exactly once per
|
|
282
|
+
metacommand reference.
|
|
283
|
+
"""
|
|
255
284
|
if _VARLIKE.search(cmd):
|
|
256
285
|
ctx.output.write(
|
|
257
286
|
f"Warning: There is a potential un-substituted variable in the command\n {cmd}\n",
|
|
@@ -355,13 +384,15 @@ def _execute_node(
|
|
|
355
384
|
command = node.command
|
|
356
385
|
if in_loop:
|
|
357
386
|
command = _convert_deferred_vars(command)
|
|
358
|
-
#
|
|
387
|
+
# Substitute once: the same expanded text is used for BREAK detection
|
|
388
|
+
# and dispatch. Calling substitute_vars twice would double-increment
|
|
389
|
+
# !!$COUNTER_N!! and re-roll !!$RANDOM!!/!!$UUID!! references.
|
|
359
390
|
effective_locals = _stack_localvars(ctx) or localvars
|
|
360
391
|
expanded = substitute_vars(command, effective_locals, ctx=ctx)
|
|
361
392
|
if _BREAK_RX.match(expanded):
|
|
362
393
|
raise _BreakLoop
|
|
363
394
|
ctx.last_command = _FakeScriptCmd(node)
|
|
364
|
-
_exec_metacommand(ctx,
|
|
395
|
+
_exec_metacommand(ctx, expanded, node.span.file, node.span.start_line)
|
|
365
396
|
|
|
366
397
|
elif isinstance(node, IfBlock):
|
|
367
398
|
ctx.last_command = _FakeScriptCmd(node)
|
|
@@ -372,12 +403,15 @@ def _execute_node(
|
|
|
372
403
|
_execute_loop(ctx, node, localvars)
|
|
373
404
|
|
|
374
405
|
elif isinstance(node, BatchBlock):
|
|
406
|
+
ctx.last_command = _FakeScriptCmd(node)
|
|
375
407
|
_execute_batch(ctx, node, localvars, in_loop=in_loop)
|
|
376
408
|
|
|
377
409
|
elif isinstance(node, ScriptBlock):
|
|
410
|
+
ctx.last_command = _FakeScriptCmd(node)
|
|
378
411
|
_register_script_block(ctx, node)
|
|
379
412
|
|
|
380
413
|
elif isinstance(node, SqlBlock):
|
|
414
|
+
ctx.last_command = _FakeScriptCmd(node)
|
|
381
415
|
_execute_sql_block(ctx, node, localvars, in_loop=in_loop)
|
|
382
416
|
|
|
383
417
|
elif isinstance(node, IncludeDirective):
|
|
@@ -403,20 +437,48 @@ def _execute_if(
|
|
|
403
437
|
*,
|
|
404
438
|
in_loop: bool = False,
|
|
405
439
|
) -> None:
|
|
406
|
-
"""Evaluate an IF block and execute the matching branch.
|
|
440
|
+
"""Evaluate an IF block and execute the matching branch.
|
|
441
|
+
|
|
442
|
+
Pushes an :class:`ExecFrame` onto ``ctx.ast_exec_stack`` for the active
|
|
443
|
+
branch while its body executes, so the debug REPL and
|
|
444
|
+
``DEBUG WRITE COMMANDLISTSTACK`` see the current IF nesting.
|
|
445
|
+
"""
|
|
446
|
+
from execsql.state import ExecFrame
|
|
447
|
+
|
|
407
448
|
if _eval_condition(ctx, node.condition, node.condition_modifiers):
|
|
408
|
-
|
|
449
|
+
_push_block_frame(
|
|
450
|
+
ctx,
|
|
451
|
+
ExecFrame(kind="if", label=node.condition, source=node.span.file, line=node.span.start_line),
|
|
452
|
+
)
|
|
453
|
+
try:
|
|
454
|
+
_execute_nodes(ctx, node.body, node.span.file, localvars, in_loop=in_loop)
|
|
455
|
+
finally:
|
|
456
|
+
ctx.ast_exec_stack.pop()
|
|
409
457
|
return
|
|
410
458
|
|
|
411
459
|
# Try ELSEIF clauses
|
|
412
460
|
for clause in node.elseif_clauses:
|
|
413
461
|
if _eval_condition(ctx, clause.condition, clause.condition_modifiers):
|
|
414
|
-
|
|
462
|
+
_push_block_frame(
|
|
463
|
+
ctx,
|
|
464
|
+
ExecFrame(kind="elseif", label=clause.condition, source=node.span.file, line=node.span.start_line),
|
|
465
|
+
)
|
|
466
|
+
try:
|
|
467
|
+
_execute_nodes(ctx, clause.body, node.span.file, localvars, in_loop=in_loop)
|
|
468
|
+
finally:
|
|
469
|
+
ctx.ast_exec_stack.pop()
|
|
415
470
|
return
|
|
416
471
|
|
|
417
472
|
# ELSE branch
|
|
418
473
|
if node.else_body:
|
|
419
|
-
|
|
474
|
+
_push_block_frame(
|
|
475
|
+
ctx,
|
|
476
|
+
ExecFrame(kind="else", label="", source=node.span.file, line=node.span.start_line),
|
|
477
|
+
)
|
|
478
|
+
try:
|
|
479
|
+
_execute_nodes(ctx, node.else_body, node.span.file, localvars, in_loop=in_loop)
|
|
480
|
+
finally:
|
|
481
|
+
ctx.ast_exec_stack.pop()
|
|
420
482
|
|
|
421
483
|
|
|
422
484
|
def _execute_loop(
|
|
@@ -425,29 +487,39 @@ def _execute_loop(
|
|
|
425
487
|
localvars: SubVarSet | None = None,
|
|
426
488
|
) -> None:
|
|
427
489
|
"""Execute a LOOP WHILE or LOOP UNTIL block."""
|
|
490
|
+
from execsql.state import ExecFrame
|
|
491
|
+
|
|
428
492
|
# Convert deferred vars in the condition — they re-evaluate each iteration
|
|
429
493
|
condition = _convert_deferred_vars(node.condition)
|
|
430
494
|
|
|
431
|
-
if node.loop_type == "WHILE"
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
495
|
+
kind = "loop_while" if node.loop_type == "WHILE" else "loop_until"
|
|
496
|
+
frame = ExecFrame(kind=kind, label=node.condition, source=node.span.file, line=node.span.start_line, iteration=0)
|
|
497
|
+
_push_block_frame(ctx, frame)
|
|
498
|
+
try:
|
|
499
|
+
if node.loop_type == "WHILE":
|
|
500
|
+
while True:
|
|
501
|
+
effective_locals = _stack_localvars(ctx)
|
|
502
|
+
expanded = substitute_vars(condition, effective_locals, ctx=ctx)
|
|
503
|
+
if not xcmd_test(expanded):
|
|
504
|
+
break
|
|
505
|
+
frame.iteration += 1
|
|
506
|
+
try:
|
|
507
|
+
_execute_nodes(ctx, node.body, node.span.file, localvars, in_loop=True)
|
|
508
|
+
except _BreakLoop:
|
|
509
|
+
break
|
|
510
|
+
else: # UNTIL
|
|
511
|
+
while True:
|
|
512
|
+
frame.iteration += 1
|
|
513
|
+
try:
|
|
514
|
+
_execute_nodes(ctx, node.body, node.span.file, localvars, in_loop=True)
|
|
515
|
+
except _BreakLoop:
|
|
516
|
+
break
|
|
517
|
+
effective_locals = _stack_localvars(ctx)
|
|
518
|
+
expanded = substitute_vars(condition, effective_locals, ctx=ctx)
|
|
519
|
+
if xcmd_test(expanded):
|
|
520
|
+
break
|
|
521
|
+
finally:
|
|
522
|
+
ctx.ast_exec_stack.pop()
|
|
451
523
|
|
|
452
524
|
|
|
453
525
|
def _execute_batch(
|
|
@@ -458,10 +530,17 @@ def _execute_batch(
|
|
|
458
530
|
in_loop: bool = False,
|
|
459
531
|
) -> None:
|
|
460
532
|
"""Execute a BEGIN BATCH / END BATCH block."""
|
|
533
|
+
from execsql.state import ExecFrame
|
|
534
|
+
|
|
461
535
|
ctx.status.batch.new_batch()
|
|
536
|
+
_push_block_frame(
|
|
537
|
+
ctx,
|
|
538
|
+
ExecFrame(kind="batch", label="", source=node.span.file, line=node.span.start_line),
|
|
539
|
+
)
|
|
462
540
|
try:
|
|
463
541
|
_execute_nodes(ctx, node.body, node.span.file, localvars, in_loop=in_loop)
|
|
464
542
|
finally:
|
|
543
|
+
ctx.ast_exec_stack.pop()
|
|
465
544
|
if ctx.status.batch.in_batch():
|
|
466
545
|
ctx.status.batch.end_batch()
|
|
467
546
|
|
|
@@ -494,135 +573,9 @@ def _pre_register_scripts(ctx: RuntimeContext, nodes: list[Node]) -> None:
|
|
|
494
573
|
|
|
495
574
|
|
|
496
575
|
def _register_script_block(ctx: RuntimeContext, node: ScriptBlock) -> None:
|
|
497
|
-
"""Register a named SCRIPT block.
|
|
498
|
-
|
|
499
|
-
Stores the AST node in ``ctx.ast_scripts`` for native execution, and also
|
|
500
|
-
builds a legacy ``CommandList`` in ``ctx.savedscripts`` so that dispatch
|
|
501
|
-
table handlers (e.g., ON ERROR_HALT EXECUTE SCRIPT) still work.
|
|
502
|
-
"""
|
|
503
|
-
from execsql.script.engine import CommandList
|
|
504
|
-
|
|
505
|
-
# AST-native registry
|
|
576
|
+
"""Register a named SCRIPT block in the AST script registry."""
|
|
506
577
|
ctx.ast_scripts[node.name] = node
|
|
507
578
|
|
|
508
|
-
# Legacy compatibility — flatten to CommandList for dispatch table
|
|
509
|
-
cmdlist = _flatten_for_legacy(node.body, node.span.file)
|
|
510
|
-
cl = CommandList(cmdlist, node.name, node.param_names)
|
|
511
|
-
ctx.savedscripts[node.name] = cl
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
def _flatten_for_legacy(nodes: list[Node], source: str) -> list:
|
|
515
|
-
"""Convert AST nodes to flat ScriptCmd list for legacy compatibility."""
|
|
516
|
-
from execsql.script.engine import MetacommandStmt, ScriptCmd, SqlStmt
|
|
517
|
-
|
|
518
|
-
result = []
|
|
519
|
-
for node in nodes:
|
|
520
|
-
if isinstance(node, SqlStatement):
|
|
521
|
-
text = re.sub(r"\s*;(\s*;\s*)+$", ";", node.text)
|
|
522
|
-
result.append(
|
|
523
|
-
ScriptCmd(node.span.file, node.span.start_line, "sql", SqlStmt(text)),
|
|
524
|
-
)
|
|
525
|
-
elif isinstance(node, MetaCommandStatement):
|
|
526
|
-
result.append(
|
|
527
|
-
ScriptCmd(node.span.file, node.span.start_line, "cmd", MetacommandStmt(node.command)),
|
|
528
|
-
)
|
|
529
|
-
elif isinstance(node, IfBlock):
|
|
530
|
-
result.append(
|
|
531
|
-
ScriptCmd(
|
|
532
|
-
node.span.file,
|
|
533
|
-
node.span.start_line,
|
|
534
|
-
"cmd",
|
|
535
|
-
MetacommandStmt(f"IF ({node.condition})"),
|
|
536
|
-
),
|
|
537
|
-
)
|
|
538
|
-
# Emit ANDIF/ORIF condition modifiers after the IF
|
|
539
|
-
for mod in node.condition_modifiers:
|
|
540
|
-
keyword = "ANDIF" if mod.kind == "AND" else "ORIF"
|
|
541
|
-
result.append(
|
|
542
|
-
ScriptCmd(
|
|
543
|
-
mod.span.file,
|
|
544
|
-
mod.span.start_line,
|
|
545
|
-
"cmd",
|
|
546
|
-
MetacommandStmt(f"{keyword} ({mod.condition})"),
|
|
547
|
-
),
|
|
548
|
-
)
|
|
549
|
-
result.extend(_flatten_for_legacy(node.body, source))
|
|
550
|
-
for clause in node.elseif_clauses:
|
|
551
|
-
result.append(
|
|
552
|
-
ScriptCmd(
|
|
553
|
-
clause.span.file,
|
|
554
|
-
clause.span.start_line,
|
|
555
|
-
"cmd",
|
|
556
|
-
MetacommandStmt(f"ELSEIF ({clause.condition})"),
|
|
557
|
-
),
|
|
558
|
-
)
|
|
559
|
-
result.extend(_flatten_for_legacy(clause.body, source))
|
|
560
|
-
if node.else_body:
|
|
561
|
-
result.append(
|
|
562
|
-
ScriptCmd(
|
|
563
|
-
node.span.file,
|
|
564
|
-
node.else_span.start_line if node.else_span else node.span.start_line,
|
|
565
|
-
"cmd",
|
|
566
|
-
MetacommandStmt("ELSE"),
|
|
567
|
-
),
|
|
568
|
-
)
|
|
569
|
-
result.extend(_flatten_for_legacy(node.else_body, source))
|
|
570
|
-
result.append(
|
|
571
|
-
ScriptCmd(
|
|
572
|
-
node.span.file,
|
|
573
|
-
node.span.effective_end_line,
|
|
574
|
-
"cmd",
|
|
575
|
-
MetacommandStmt("ENDIF"),
|
|
576
|
-
),
|
|
577
|
-
)
|
|
578
|
-
elif isinstance(node, LoopBlock):
|
|
579
|
-
result.append(
|
|
580
|
-
ScriptCmd(
|
|
581
|
-
node.span.file,
|
|
582
|
-
node.span.start_line,
|
|
583
|
-
"cmd",
|
|
584
|
-
MetacommandStmt(f"LOOP {node.loop_type} ({node.condition})"),
|
|
585
|
-
),
|
|
586
|
-
)
|
|
587
|
-
result.extend(_flatten_for_legacy(node.body, source))
|
|
588
|
-
result.append(
|
|
589
|
-
ScriptCmd(
|
|
590
|
-
node.span.file,
|
|
591
|
-
node.span.effective_end_line,
|
|
592
|
-
"cmd",
|
|
593
|
-
MetacommandStmt("END LOOP"),
|
|
594
|
-
),
|
|
595
|
-
)
|
|
596
|
-
elif isinstance(node, BatchBlock):
|
|
597
|
-
result.append(
|
|
598
|
-
ScriptCmd(node.span.file, node.span.start_line, "cmd", MetacommandStmt("BEGIN BATCH")),
|
|
599
|
-
)
|
|
600
|
-
result.extend(_flatten_for_legacy(node.body, source))
|
|
601
|
-
result.append(
|
|
602
|
-
ScriptCmd(node.span.file, node.span.effective_end_line, "cmd", MetacommandStmt("END BATCH")),
|
|
603
|
-
)
|
|
604
|
-
elif isinstance(node, SqlBlock):
|
|
605
|
-
result.extend(_flatten_for_legacy(node.body, source))
|
|
606
|
-
elif isinstance(node, IncludeDirective):
|
|
607
|
-
if node.is_execute_script:
|
|
608
|
-
parts = ["EXECUTE SCRIPT"]
|
|
609
|
-
if node.if_exists:
|
|
610
|
-
parts.append("IF EXISTS")
|
|
611
|
-
parts.append(node.target)
|
|
612
|
-
if node.arguments:
|
|
613
|
-
parts.append(f"WITH ARGS ({node.arguments})")
|
|
614
|
-
if node.loop_type:
|
|
615
|
-
parts.append(f"{node.loop_type} ({node.loop_condition})")
|
|
616
|
-
result.append(
|
|
617
|
-
ScriptCmd(node.span.file, node.span.start_line, "cmd", MetacommandStmt(" ".join(parts))),
|
|
618
|
-
)
|
|
619
|
-
else:
|
|
620
|
-
prefix = "INCLUDE IF EXISTS" if node.if_exists else "INCLUDE"
|
|
621
|
-
result.append(
|
|
622
|
-
ScriptCmd(node.span.file, node.span.start_line, "cmd", MetacommandStmt(f"{prefix} {node.target}")),
|
|
623
|
-
)
|
|
624
|
-
return result
|
|
625
|
-
|
|
626
579
|
|
|
627
580
|
def _execute_sql_block(
|
|
628
581
|
ctx: RuntimeContext,
|
|
@@ -662,22 +615,12 @@ def _execute_include(
|
|
|
662
615
|
_execute_script_native(ctx, node, ctx.ast_scripts[target], localvars)
|
|
663
616
|
return
|
|
664
617
|
|
|
665
|
-
# Target not
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
raise ErrInfo(
|
|
669
|
-
"cmd",
|
|
670
|
-
other_msg=f"There is no SCRIPT named {node.target}.",
|
|
671
|
-
)
|
|
672
|
-
if node.if_exists and target not in ctx.savedscripts:
|
|
673
|
-
return # IF EXISTS — skip silently
|
|
674
|
-
|
|
675
|
-
# Target is in savedscripts but not ast_scripts — this shouldn't
|
|
676
|
-
# happen when the AST executor is the only engine, but handle it
|
|
677
|
-
# gracefully by raising an error.
|
|
618
|
+
# Target not registered — IF EXISTS silently skips; otherwise error.
|
|
619
|
+
if node.if_exists:
|
|
620
|
+
return
|
|
678
621
|
raise ErrInfo(
|
|
679
622
|
"cmd",
|
|
680
|
-
other_msg=f"SCRIPT {node.target}
|
|
623
|
+
other_msg=f"There is no SCRIPT named {node.target}.",
|
|
681
624
|
)
|
|
682
625
|
|
|
683
626
|
# --- INCLUDE (file inclusion) — parse and execute natively ---
|
|
@@ -692,10 +635,11 @@ def _execute_script_native(
|
|
|
692
635
|
) -> None:
|
|
693
636
|
"""Execute a SCRIPT block natively through the AST executor.
|
|
694
637
|
|
|
695
|
-
Pushes a :class
|
|
696
|
-
|
|
697
|
-
prompt handlers, etc.) can access
|
|
698
|
-
|
|
638
|
+
Pushes a SCRIPT-kind :class:`~execsql.state.ExecFrame` onto
|
|
639
|
+
``ctx.ast_exec_stack`` so that metacommand handlers (``x_sub``,
|
|
640
|
+
``x_rm_sub``, ``xf_sub_defined``, prompt handlers, etc.) can access
|
|
641
|
+
``~`` local variables and ``#`` script arguments via
|
|
642
|
+
``ctx.current_localvars()`` / ``ctx.current_paramvals()``. The frame
|
|
699
643
|
is popped on exit (including on error) via ``try/finally``.
|
|
700
644
|
"""
|
|
701
645
|
from execsql.script.variables import ScriptArgSubVarSet
|
|
@@ -750,7 +694,7 @@ def _execute_script_native(
|
|
|
750
694
|
if pdef.default is not None:
|
|
751
695
|
paramvals.add_substitution(f"#{pdef.name}", pdef.default)
|
|
752
696
|
|
|
753
|
-
# Push a
|
|
697
|
+
# Push a SCRIPT-kind ExecFrame onto ast_exec_stack so that:
|
|
754
698
|
# - get_subvarset() can find ~local and +outer-scope variables
|
|
755
699
|
# - xf_sub_defined/xf_sub_empty can check ~local and #param variables
|
|
756
700
|
# - current_script_line() returns meaningful source location
|
|
@@ -761,9 +705,11 @@ def _execute_script_native(
|
|
|
761
705
|
script_block.span.file,
|
|
762
706
|
script_block.span.start_line,
|
|
763
707
|
paramnames=script_block.param_names,
|
|
708
|
+
kind="script",
|
|
764
709
|
)
|
|
765
710
|
if paramvals is not None:
|
|
766
711
|
frame.paramvals = paramvals
|
|
712
|
+
frame.params = dict(paramvals.substitutions)
|
|
767
713
|
|
|
768
714
|
try:
|
|
769
715
|
|
|
@@ -783,12 +729,14 @@ def _execute_script_native(
|
|
|
783
729
|
expanded = substitute_vars(condition, effective_locals, ctx=ctx)
|
|
784
730
|
if not xcmd_test(expanded):
|
|
785
731
|
break
|
|
732
|
+
frame.iteration += 1
|
|
786
733
|
try:
|
|
787
734
|
_run_body()
|
|
788
735
|
except _BreakLoop:
|
|
789
736
|
break
|
|
790
737
|
elif node.loop_type == "UNTIL":
|
|
791
738
|
while True:
|
|
739
|
+
frame.iteration += 1
|
|
792
740
|
try:
|
|
793
741
|
_run_body()
|
|
794
742
|
except _BreakLoop:
|
|
@@ -798,7 +746,13 @@ def _execute_script_native(
|
|
|
798
746
|
if xcmd_test(expanded):
|
|
799
747
|
break
|
|
800
748
|
else:
|
|
801
|
-
|
|
749
|
+
try:
|
|
750
|
+
_run_body()
|
|
751
|
+
except _BreakLoop as exc:
|
|
752
|
+
raise ErrInfo(
|
|
753
|
+
type="cmd",
|
|
754
|
+
other_msg=f"BREAK metacommand inside SCRIPT '{script_block.name}' but not inside a LOOP.",
|
|
755
|
+
) from exc
|
|
802
756
|
finally:
|
|
803
757
|
_pop_frame(ctx)
|
|
804
758
|
|
|
@@ -863,10 +817,17 @@ def _execute_include_native(
|
|
|
863
817
|
_pre_register_scripts(ctx, included_tree.body)
|
|
864
818
|
|
|
865
819
|
# Execute with include-chain tracking
|
|
820
|
+
from execsql.state import ExecFrame
|
|
821
|
+
|
|
866
822
|
ctx.include_chain.append(resolved)
|
|
823
|
+
_push_block_frame(
|
|
824
|
+
ctx,
|
|
825
|
+
ExecFrame(kind="include", label=Path(resolved).name, source=resolved, line=1),
|
|
826
|
+
)
|
|
867
827
|
try:
|
|
868
828
|
_execute_nodes(ctx, included_tree.body, included_tree.source, localvars)
|
|
869
829
|
finally:
|
|
830
|
+
ctx.ast_exec_stack.pop()
|
|
870
831
|
ctx.include_chain.pop()
|
|
871
832
|
|
|
872
833
|
|
|
@@ -971,6 +932,8 @@ def execute(script: Script, *, ctx: RuntimeContext | None = None) -> None:
|
|
|
971
932
|
with active_context(ctx):
|
|
972
933
|
ctx.ast_scripts.clear()
|
|
973
934
|
ctx.include_chain.clear()
|
|
935
|
+
ctx.ast_exec_stack.clear()
|
|
936
|
+
ctx.last_command = None
|
|
974
937
|
# Seed the include chain with the main script to catch self-includes.
|
|
975
938
|
if script.source != "<inline>":
|
|
976
939
|
try:
|
|
@@ -982,11 +945,11 @@ def execute(script: Script, *, ctx: RuntimeContext | None = None) -> None:
|
|
|
982
945
|
# The legacy engine registered scripts at parse time (two-pass);
|
|
983
946
|
# the AST executor must do an explicit pre-scan.
|
|
984
947
|
_pre_register_scripts(ctx, script.body)
|
|
985
|
-
# Push a root frame so
|
|
986
|
-
# execution. This ensures get_subvarset(),
|
|
987
|
-
# xf_sub_defined(), the REPL, and
|
|
988
|
-
#
|
|
989
|
-
_push_frame(ctx, "<main>", script.source)
|
|
948
|
+
# Push a root <main> scope frame so the AST exec stack is never
|
|
949
|
+
# empty during execution. This ensures get_subvarset(),
|
|
950
|
+
# current_script_line(), xf_sub_defined(), the REPL, and every
|
|
951
|
+
# variable-scoping reader works correctly even at the top level.
|
|
952
|
+
_push_frame(ctx, "<main>", script.source, line_no=1, kind="main")
|
|
990
953
|
try:
|
|
991
954
|
_execute_nodes(ctx, script.body, script.source)
|
|
992
955
|
except _BreakLoop as exc:
|
execsql/script/parser.py
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"""AST-producing parser for execsql scripts.
|
|
2
2
|
|
|
3
3
|
Converts raw ``.sql`` script text into a :class:`~execsql.script.ast.Script`
|
|
4
|
-
tree
|
|
5
|
-
:mod:`execsql.script.engine` — during the transition both paths coexist and
|
|
6
|
-
can be compared for correctness.
|
|
4
|
+
tree consumed by :func:`execsql.script.executor.execute`.
|
|
7
5
|
|
|
8
6
|
The parser is a single-pass, line-oriented state machine that tracks:
|
|
9
7
|
|
execsql/script/variables.py
CHANGED
|
@@ -248,8 +248,12 @@ class SubVarSet:
|
|
|
248
248
|
sub = sub.replace("\\", "\\\\")
|
|
249
249
|
quote = m.group("q")
|
|
250
250
|
if quote == "'":
|
|
251
|
-
|
|
251
|
+
# Wrap value in single quotes, doubling any embedded
|
|
252
|
+
# apostrophe — produces a SQL string literal.
|
|
253
|
+
sub = "'" + sub.replace("'", "''") + "'"
|
|
252
254
|
elif quote == '"':
|
|
255
|
+
# Wrap value in double quotes — produces a SQL quoted
|
|
256
|
+
# identifier or quoted metacommand argument.
|
|
253
257
|
sub = '"' + sub + '"'
|
|
254
258
|
return command_str[: m.start()] + sub + command_str[m.end() :], True
|
|
255
259
|
# Token found but variable not defined — skip it and keep searching.
|
|
@@ -277,12 +281,13 @@ class SubVarSet:
|
|
|
277
281
|
idx = cmd_lower.find(token)
|
|
278
282
|
if idx != -1:
|
|
279
283
|
return command_str[:idx] + sub + command_str[idx + len(token) :], True
|
|
280
|
-
# Single-quote-
|
|
284
|
+
# Single-quote-wrapped token: !'!varname!'!
|
|
281
285
|
tokenq = f"!'!{varname}!'!"
|
|
282
286
|
idxq = cmd_lower.find(tokenq)
|
|
283
287
|
if idxq != -1:
|
|
288
|
+
wrapped = "'" + sub.replace("'", "''") + "'"
|
|
284
289
|
return (
|
|
285
|
-
command_str[:idxq] +
|
|
290
|
+
command_str[:idxq] + wrapped + command_str[idxq + len(tokenq) :],
|
|
286
291
|
True,
|
|
287
292
|
)
|
|
288
293
|
# Double-quote-wrapped token: !"!varname!"!
|