execsql2 2.17.0__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/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 +181 -222
- 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.17.0.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.17.0.dist-info/RECORD +0 -124
- execsql2-2.17.0.dist-info/licenses/NOTICE +0 -10
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/WHEEL +0 -0
- {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/entry_points.txt +0 -0
- {execsql2-2.17.0.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:
|
|
@@ -376,12 +403,15 @@ def _execute_node(
|
|
|
376
403
|
_execute_loop(ctx, node, localvars)
|
|
377
404
|
|
|
378
405
|
elif isinstance(node, BatchBlock):
|
|
406
|
+
ctx.last_command = _FakeScriptCmd(node)
|
|
379
407
|
_execute_batch(ctx, node, localvars, in_loop=in_loop)
|
|
380
408
|
|
|
381
409
|
elif isinstance(node, ScriptBlock):
|
|
410
|
+
ctx.last_command = _FakeScriptCmd(node)
|
|
382
411
|
_register_script_block(ctx, node)
|
|
383
412
|
|
|
384
413
|
elif isinstance(node, SqlBlock):
|
|
414
|
+
ctx.last_command = _FakeScriptCmd(node)
|
|
385
415
|
_execute_sql_block(ctx, node, localvars, in_loop=in_loop)
|
|
386
416
|
|
|
387
417
|
elif isinstance(node, IncludeDirective):
|
|
@@ -407,20 +437,48 @@ def _execute_if(
|
|
|
407
437
|
*,
|
|
408
438
|
in_loop: bool = False,
|
|
409
439
|
) -> None:
|
|
410
|
-
"""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
|
+
|
|
411
448
|
if _eval_condition(ctx, node.condition, node.condition_modifiers):
|
|
412
|
-
|
|
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()
|
|
413
457
|
return
|
|
414
458
|
|
|
415
459
|
# Try ELSEIF clauses
|
|
416
460
|
for clause in node.elseif_clauses:
|
|
417
461
|
if _eval_condition(ctx, clause.condition, clause.condition_modifiers):
|
|
418
|
-
|
|
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()
|
|
419
470
|
return
|
|
420
471
|
|
|
421
472
|
# ELSE branch
|
|
422
473
|
if node.else_body:
|
|
423
|
-
|
|
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()
|
|
424
482
|
|
|
425
483
|
|
|
426
484
|
def _execute_loop(
|
|
@@ -429,29 +487,39 @@ def _execute_loop(
|
|
|
429
487
|
localvars: SubVarSet | None = None,
|
|
430
488
|
) -> None:
|
|
431
489
|
"""Execute a LOOP WHILE or LOOP UNTIL block."""
|
|
490
|
+
from execsql.state import ExecFrame
|
|
491
|
+
|
|
432
492
|
# Convert deferred vars in the condition — they re-evaluate each iteration
|
|
433
493
|
condition = _convert_deferred_vars(node.condition)
|
|
434
494
|
|
|
435
|
-
if node.loop_type == "WHILE"
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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()
|
|
455
523
|
|
|
456
524
|
|
|
457
525
|
def _execute_batch(
|
|
@@ -462,10 +530,17 @@ def _execute_batch(
|
|
|
462
530
|
in_loop: bool = False,
|
|
463
531
|
) -> None:
|
|
464
532
|
"""Execute a BEGIN BATCH / END BATCH block."""
|
|
533
|
+
from execsql.state import ExecFrame
|
|
534
|
+
|
|
465
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
|
+
)
|
|
466
540
|
try:
|
|
467
541
|
_execute_nodes(ctx, node.body, node.span.file, localvars, in_loop=in_loop)
|
|
468
542
|
finally:
|
|
543
|
+
ctx.ast_exec_stack.pop()
|
|
469
544
|
if ctx.status.batch.in_batch():
|
|
470
545
|
ctx.status.batch.end_batch()
|
|
471
546
|
|
|
@@ -498,135 +573,9 @@ def _pre_register_scripts(ctx: RuntimeContext, nodes: list[Node]) -> None:
|
|
|
498
573
|
|
|
499
574
|
|
|
500
575
|
def _register_script_block(ctx: RuntimeContext, node: ScriptBlock) -> None:
|
|
501
|
-
"""Register a named SCRIPT block.
|
|
502
|
-
|
|
503
|
-
Stores the AST node in ``ctx.ast_scripts`` for native execution, and also
|
|
504
|
-
builds a legacy ``CommandList`` in ``ctx.savedscripts`` so that dispatch
|
|
505
|
-
table handlers (e.g., ON ERROR_HALT EXECUTE SCRIPT) still work.
|
|
506
|
-
"""
|
|
507
|
-
from execsql.script.engine import CommandList
|
|
508
|
-
|
|
509
|
-
# AST-native registry
|
|
576
|
+
"""Register a named SCRIPT block in the AST script registry."""
|
|
510
577
|
ctx.ast_scripts[node.name] = node
|
|
511
578
|
|
|
512
|
-
# Legacy compatibility — flatten to CommandList for dispatch table
|
|
513
|
-
cmdlist = _flatten_for_legacy(node.body, node.span.file)
|
|
514
|
-
cl = CommandList(cmdlist, node.name, node.param_names)
|
|
515
|
-
ctx.savedscripts[node.name] = cl
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
def _flatten_for_legacy(nodes: list[Node], source: str) -> list:
|
|
519
|
-
"""Convert AST nodes to flat ScriptCmd list for legacy compatibility."""
|
|
520
|
-
from execsql.script.engine import MetacommandStmt, ScriptCmd, SqlStmt
|
|
521
|
-
|
|
522
|
-
result = []
|
|
523
|
-
for node in nodes:
|
|
524
|
-
if isinstance(node, SqlStatement):
|
|
525
|
-
text = re.sub(r"\s*;(\s*;\s*)+$", ";", node.text)
|
|
526
|
-
result.append(
|
|
527
|
-
ScriptCmd(node.span.file, node.span.start_line, "sql", SqlStmt(text)),
|
|
528
|
-
)
|
|
529
|
-
elif isinstance(node, MetaCommandStatement):
|
|
530
|
-
result.append(
|
|
531
|
-
ScriptCmd(node.span.file, node.span.start_line, "cmd", MetacommandStmt(node.command)),
|
|
532
|
-
)
|
|
533
|
-
elif isinstance(node, IfBlock):
|
|
534
|
-
result.append(
|
|
535
|
-
ScriptCmd(
|
|
536
|
-
node.span.file,
|
|
537
|
-
node.span.start_line,
|
|
538
|
-
"cmd",
|
|
539
|
-
MetacommandStmt(f"IF ({node.condition})"),
|
|
540
|
-
),
|
|
541
|
-
)
|
|
542
|
-
# Emit ANDIF/ORIF condition modifiers after the IF
|
|
543
|
-
for mod in node.condition_modifiers:
|
|
544
|
-
keyword = "ANDIF" if mod.kind == "AND" else "ORIF"
|
|
545
|
-
result.append(
|
|
546
|
-
ScriptCmd(
|
|
547
|
-
mod.span.file,
|
|
548
|
-
mod.span.start_line,
|
|
549
|
-
"cmd",
|
|
550
|
-
MetacommandStmt(f"{keyword} ({mod.condition})"),
|
|
551
|
-
),
|
|
552
|
-
)
|
|
553
|
-
result.extend(_flatten_for_legacy(node.body, source))
|
|
554
|
-
for clause in node.elseif_clauses:
|
|
555
|
-
result.append(
|
|
556
|
-
ScriptCmd(
|
|
557
|
-
clause.span.file,
|
|
558
|
-
clause.span.start_line,
|
|
559
|
-
"cmd",
|
|
560
|
-
MetacommandStmt(f"ELSEIF ({clause.condition})"),
|
|
561
|
-
),
|
|
562
|
-
)
|
|
563
|
-
result.extend(_flatten_for_legacy(clause.body, source))
|
|
564
|
-
if node.else_body:
|
|
565
|
-
result.append(
|
|
566
|
-
ScriptCmd(
|
|
567
|
-
node.span.file,
|
|
568
|
-
node.else_span.start_line if node.else_span else node.span.start_line,
|
|
569
|
-
"cmd",
|
|
570
|
-
MetacommandStmt("ELSE"),
|
|
571
|
-
),
|
|
572
|
-
)
|
|
573
|
-
result.extend(_flatten_for_legacy(node.else_body, source))
|
|
574
|
-
result.append(
|
|
575
|
-
ScriptCmd(
|
|
576
|
-
node.span.file,
|
|
577
|
-
node.span.effective_end_line,
|
|
578
|
-
"cmd",
|
|
579
|
-
MetacommandStmt("ENDIF"),
|
|
580
|
-
),
|
|
581
|
-
)
|
|
582
|
-
elif isinstance(node, LoopBlock):
|
|
583
|
-
result.append(
|
|
584
|
-
ScriptCmd(
|
|
585
|
-
node.span.file,
|
|
586
|
-
node.span.start_line,
|
|
587
|
-
"cmd",
|
|
588
|
-
MetacommandStmt(f"LOOP {node.loop_type} ({node.condition})"),
|
|
589
|
-
),
|
|
590
|
-
)
|
|
591
|
-
result.extend(_flatten_for_legacy(node.body, source))
|
|
592
|
-
result.append(
|
|
593
|
-
ScriptCmd(
|
|
594
|
-
node.span.file,
|
|
595
|
-
node.span.effective_end_line,
|
|
596
|
-
"cmd",
|
|
597
|
-
MetacommandStmt("END LOOP"),
|
|
598
|
-
),
|
|
599
|
-
)
|
|
600
|
-
elif isinstance(node, BatchBlock):
|
|
601
|
-
result.append(
|
|
602
|
-
ScriptCmd(node.span.file, node.span.start_line, "cmd", MetacommandStmt("BEGIN BATCH")),
|
|
603
|
-
)
|
|
604
|
-
result.extend(_flatten_for_legacy(node.body, source))
|
|
605
|
-
result.append(
|
|
606
|
-
ScriptCmd(node.span.file, node.span.effective_end_line, "cmd", MetacommandStmt("END BATCH")),
|
|
607
|
-
)
|
|
608
|
-
elif isinstance(node, SqlBlock):
|
|
609
|
-
result.extend(_flatten_for_legacy(node.body, source))
|
|
610
|
-
elif isinstance(node, IncludeDirective):
|
|
611
|
-
if node.is_execute_script:
|
|
612
|
-
parts = ["EXECUTE SCRIPT"]
|
|
613
|
-
if node.if_exists:
|
|
614
|
-
parts.append("IF EXISTS")
|
|
615
|
-
parts.append(node.target)
|
|
616
|
-
if node.arguments:
|
|
617
|
-
parts.append(f"WITH ARGS ({node.arguments})")
|
|
618
|
-
if node.loop_type:
|
|
619
|
-
parts.append(f"{node.loop_type} ({node.loop_condition})")
|
|
620
|
-
result.append(
|
|
621
|
-
ScriptCmd(node.span.file, node.span.start_line, "cmd", MetacommandStmt(" ".join(parts))),
|
|
622
|
-
)
|
|
623
|
-
else:
|
|
624
|
-
prefix = "INCLUDE IF EXISTS" if node.if_exists else "INCLUDE"
|
|
625
|
-
result.append(
|
|
626
|
-
ScriptCmd(node.span.file, node.span.start_line, "cmd", MetacommandStmt(f"{prefix} {node.target}")),
|
|
627
|
-
)
|
|
628
|
-
return result
|
|
629
|
-
|
|
630
579
|
|
|
631
580
|
def _execute_sql_block(
|
|
632
581
|
ctx: RuntimeContext,
|
|
@@ -666,22 +615,12 @@ def _execute_include(
|
|
|
666
615
|
_execute_script_native(ctx, node, ctx.ast_scripts[target], localvars)
|
|
667
616
|
return
|
|
668
617
|
|
|
669
|
-
# Target not
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
raise ErrInfo(
|
|
673
|
-
"cmd",
|
|
674
|
-
other_msg=f"There is no SCRIPT named {node.target}.",
|
|
675
|
-
)
|
|
676
|
-
if node.if_exists and target not in ctx.savedscripts:
|
|
677
|
-
return # IF EXISTS — skip silently
|
|
678
|
-
|
|
679
|
-
# Target is in savedscripts but not ast_scripts — this shouldn't
|
|
680
|
-
# happen when the AST executor is the only engine, but handle it
|
|
681
|
-
# gracefully by raising an error.
|
|
618
|
+
# Target not registered — IF EXISTS silently skips; otherwise error.
|
|
619
|
+
if node.if_exists:
|
|
620
|
+
return
|
|
682
621
|
raise ErrInfo(
|
|
683
622
|
"cmd",
|
|
684
|
-
other_msg=f"SCRIPT {node.target}
|
|
623
|
+
other_msg=f"There is no SCRIPT named {node.target}.",
|
|
685
624
|
)
|
|
686
625
|
|
|
687
626
|
# --- INCLUDE (file inclusion) — parse and execute natively ---
|
|
@@ -696,10 +635,11 @@ def _execute_script_native(
|
|
|
696
635
|
) -> None:
|
|
697
636
|
"""Execute a SCRIPT block natively through the AST executor.
|
|
698
637
|
|
|
699
|
-
Pushes a :class
|
|
700
|
-
|
|
701
|
-
prompt handlers, etc.) can access
|
|
702
|
-
|
|
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
|
|
703
643
|
is popped on exit (including on error) via ``try/finally``.
|
|
704
644
|
"""
|
|
705
645
|
from execsql.script.variables import ScriptArgSubVarSet
|
|
@@ -754,7 +694,7 @@ def _execute_script_native(
|
|
|
754
694
|
if pdef.default is not None:
|
|
755
695
|
paramvals.add_substitution(f"#{pdef.name}", pdef.default)
|
|
756
696
|
|
|
757
|
-
# Push a
|
|
697
|
+
# Push a SCRIPT-kind ExecFrame onto ast_exec_stack so that:
|
|
758
698
|
# - get_subvarset() can find ~local and +outer-scope variables
|
|
759
699
|
# - xf_sub_defined/xf_sub_empty can check ~local and #param variables
|
|
760
700
|
# - current_script_line() returns meaningful source location
|
|
@@ -765,9 +705,11 @@ def _execute_script_native(
|
|
|
765
705
|
script_block.span.file,
|
|
766
706
|
script_block.span.start_line,
|
|
767
707
|
paramnames=script_block.param_names,
|
|
708
|
+
kind="script",
|
|
768
709
|
)
|
|
769
710
|
if paramvals is not None:
|
|
770
711
|
frame.paramvals = paramvals
|
|
712
|
+
frame.params = dict(paramvals.substitutions)
|
|
771
713
|
|
|
772
714
|
try:
|
|
773
715
|
|
|
@@ -787,12 +729,14 @@ def _execute_script_native(
|
|
|
787
729
|
expanded = substitute_vars(condition, effective_locals, ctx=ctx)
|
|
788
730
|
if not xcmd_test(expanded):
|
|
789
731
|
break
|
|
732
|
+
frame.iteration += 1
|
|
790
733
|
try:
|
|
791
734
|
_run_body()
|
|
792
735
|
except _BreakLoop:
|
|
793
736
|
break
|
|
794
737
|
elif node.loop_type == "UNTIL":
|
|
795
738
|
while True:
|
|
739
|
+
frame.iteration += 1
|
|
796
740
|
try:
|
|
797
741
|
_run_body()
|
|
798
742
|
except _BreakLoop:
|
|
@@ -802,7 +746,13 @@ def _execute_script_native(
|
|
|
802
746
|
if xcmd_test(expanded):
|
|
803
747
|
break
|
|
804
748
|
else:
|
|
805
|
-
|
|
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
|
|
806
756
|
finally:
|
|
807
757
|
_pop_frame(ctx)
|
|
808
758
|
|
|
@@ -867,10 +817,17 @@ def _execute_include_native(
|
|
|
867
817
|
_pre_register_scripts(ctx, included_tree.body)
|
|
868
818
|
|
|
869
819
|
# Execute with include-chain tracking
|
|
820
|
+
from execsql.state import ExecFrame
|
|
821
|
+
|
|
870
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
|
+
)
|
|
871
827
|
try:
|
|
872
828
|
_execute_nodes(ctx, included_tree.body, included_tree.source, localvars)
|
|
873
829
|
finally:
|
|
830
|
+
ctx.ast_exec_stack.pop()
|
|
874
831
|
ctx.include_chain.pop()
|
|
875
832
|
|
|
876
833
|
|
|
@@ -975,6 +932,8 @@ def execute(script: Script, *, ctx: RuntimeContext | None = None) -> None:
|
|
|
975
932
|
with active_context(ctx):
|
|
976
933
|
ctx.ast_scripts.clear()
|
|
977
934
|
ctx.include_chain.clear()
|
|
935
|
+
ctx.ast_exec_stack.clear()
|
|
936
|
+
ctx.last_command = None
|
|
978
937
|
# Seed the include chain with the main script to catch self-includes.
|
|
979
938
|
if script.source != "<inline>":
|
|
980
939
|
try:
|
|
@@ -986,11 +945,11 @@ def execute(script: Script, *, ctx: RuntimeContext | None = None) -> None:
|
|
|
986
945
|
# The legacy engine registered scripts at parse time (two-pass);
|
|
987
946
|
# the AST executor must do an explicit pre-scan.
|
|
988
947
|
_pre_register_scripts(ctx, script.body)
|
|
989
|
-
# Push a root frame so
|
|
990
|
-
# execution. This ensures get_subvarset(),
|
|
991
|
-
# xf_sub_defined(), the REPL, and
|
|
992
|
-
#
|
|
993
|
-
_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")
|
|
994
953
|
try:
|
|
995
954
|
_execute_nodes(ctx, script.body, script.source)
|
|
996
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!"!
|