execsql2 2.34.0__py3-none-any.whl → 2.37.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/api.py +17 -8
- execsql/cli/lint.py +54 -6
- execsql/cli/run.py +5 -10
- execsql/config.py +2 -0
- execsql/data/execsql.conf.template +17 -7
- execsql/db/base.py +12 -2
- execsql/db/duckdb.py +8 -1
- execsql/debug/repl.py +2 -2
- execsql/metacommands/connect.py +10 -9
- execsql/metacommands/data.py +7 -1
- execsql/metacommands/dispatch.py +7 -0
- execsql/metacommands/io_fileops.py +6 -3
- execsql/metacommands/io_write.py +3 -1
- execsql/metacommands/reference.py +1 -0
- execsql/metacommands/script_ext.py +13 -10
- execsql/metacommands/system.py +13 -6
- execsql/models.py +30 -4
- execsql/script/ast.py +21 -0
- execsql/script/control.py +63 -3
- execsql/script/executor.py +379 -229
- execsql/script/parser.py +102 -52
- execsql/state.py +23 -0
- execsql/utils/errors.py +24 -6
- execsql/utils/sigterm.py +154 -0
- {execsql2-2.34.0.dist-info → execsql2-2.37.0.dist-info}/METADATA +2 -2
- {execsql2-2.34.0.dist-info → execsql2-2.37.0.dist-info}/RECORD +43 -42
- {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.34.0.dist-info → execsql2-2.37.0.dist-info}/WHEEL +0 -0
- {execsql2-2.34.0.dist-info → execsql2-2.37.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.34.0.dist-info → execsql2-2.37.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.34.0.dist-info → execsql2-2.37.0.dist-info}/licenses/NOTICE +0 -0
execsql/script/executor.py
CHANGED
|
@@ -63,11 +63,13 @@ from execsql.script.ast import (
|
|
|
63
63
|
Script,
|
|
64
64
|
ScriptBlock,
|
|
65
65
|
SqlBlock,
|
|
66
|
+
SourceSpan,
|
|
66
67
|
SqlStatement,
|
|
67
68
|
)
|
|
68
69
|
from execsql import manifest as _manifest
|
|
70
|
+
from execsql.script.parser import substitute_directive
|
|
69
71
|
from execsql.script.engine import set_dynamic_system_vars, set_static_system_vars, substitute_vars
|
|
70
|
-
from execsql.script.variables import SubVarSet
|
|
72
|
+
from execsql.script.variables import ScriptArgSubVarSet, SubVarSet
|
|
71
73
|
from execsql.state import ExecFrame, RuntimeContext, active_context, get_context, xcmd_test
|
|
72
74
|
from execsql.utils.errors import exception_desc, exit_now, logged_error, stamp_errinfo
|
|
73
75
|
|
|
@@ -85,6 +87,14 @@ _DEFER_RX = re.compile(r"!\{([$@&~#+]?\w+)\}!")
|
|
|
85
87
|
_VARLIKE = re.compile(r"!![$@&~#]?\w+!!", re.I)
|
|
86
88
|
|
|
87
89
|
|
|
90
|
+
def _warn_unsubstituted(cmd: str) -> None:
|
|
91
|
+
"""Log a command that still looks like it holds a variable; print it too if ``write_warnings`` is on."""
|
|
92
|
+
from execsql.utils.errors import write_warning
|
|
93
|
+
from execsql.utils.fileio import redact
|
|
94
|
+
|
|
95
|
+
write_warning(f"There is a potential un-substituted variable in the command\n {redact(cmd)}")
|
|
96
|
+
|
|
97
|
+
|
|
88
98
|
def _stack_localvars(ctx: RuntimeContext) -> SubVarSet | None:
|
|
89
99
|
"""Build the merged ``~`` local + ``#`` param overlay for the current scope.
|
|
90
100
|
|
|
@@ -169,39 +179,64 @@ def _convert_deferred_vars(text: str) -> str:
|
|
|
169
179
|
return _DEFER_RX.sub(r"!!\1!!", text)
|
|
170
180
|
|
|
171
181
|
|
|
172
|
-
def
|
|
182
|
+
def _condition_holds(
|
|
173
183
|
ctx: RuntimeContext,
|
|
184
|
+
command: str,
|
|
185
|
+
span: SourceSpan,
|
|
174
186
|
condition: str,
|
|
175
187
|
modifiers: list[ConditionModifier] | None = None,
|
|
176
188
|
) -> bool:
|
|
177
|
-
"""Evaluate
|
|
189
|
+
"""Evaluate the condition of IF, ELSEIF, LOOP or EXECUTE SCRIPT … WHILE / UNTIL, with its ANDIF / ORIF modifiers.
|
|
178
190
|
|
|
179
191
|
Short-circuits ANDIF (stops on first False) and ORIF (stops on first True)
|
|
180
192
|
so that patterns like ``IF (sub_defined(x)) ANDIF (not sub_empty(x))``
|
|
181
193
|
don't evaluate ``sub_empty`` when ``x`` is undefined.
|
|
194
|
+
|
|
195
|
+
*command* is the metacommand as written, on the line *span* starts; an
|
|
196
|
+
error is reported there, or on the ANDIF / ORIF that raised it.
|
|
182
197
|
"""
|
|
183
198
|
effective_locals = _stack_localvars(ctx)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
continue
|
|
193
|
-
if mod.kind == "OR" and result:
|
|
194
|
-
continue
|
|
195
|
-
mod_expanded = substitute_vars(mod.condition, effective_locals, ctx=ctx)
|
|
196
|
-
mod_result = xcmd_test(mod_expanded)
|
|
197
|
-
if mod.kind == "AND":
|
|
198
|
-
result = result and mod_result
|
|
199
|
-
else: # OR
|
|
200
|
-
result = result or mod_result
|
|
201
|
-
|
|
199
|
+
result = _test(ctx, command, span, condition, effective_locals)
|
|
200
|
+
for mod in modifiers or []:
|
|
201
|
+
# Short-circuit: AND with False can't become True,
|
|
202
|
+
# OR with True can't become False.
|
|
203
|
+
if (mod.kind == "AND" and not result) or (mod.kind == "OR" and result):
|
|
204
|
+
continue
|
|
205
|
+
keyword = "ANDIF" if mod.kind == "AND" else "ORIF"
|
|
206
|
+
result = _test(ctx, f"{keyword} ({mod.condition})", mod.span, mod.condition, effective_locals)
|
|
202
207
|
return result
|
|
203
208
|
|
|
204
209
|
|
|
210
|
+
def _test(
|
|
211
|
+
ctx: RuntimeContext,
|
|
212
|
+
command: str,
|
|
213
|
+
span: SourceSpan,
|
|
214
|
+
condition: str,
|
|
215
|
+
localvars: SubVarSet | None,
|
|
216
|
+
) -> bool:
|
|
217
|
+
"""One condition of *command*: its value.
|
|
218
|
+
|
|
219
|
+
A condition that cannot be evaluated stops the run. Its error is raised
|
|
220
|
+
with the line and text of *command*, which may be an ELSEIF, ANDIF or
|
|
221
|
+
ORIF rather than the statement on record, or a loop condition evaluated
|
|
222
|
+
after its body.
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
return xcmd_test(substitute_vars(condition, localvars, ctx=ctx))
|
|
226
|
+
except Exception as exc:
|
|
227
|
+
e = _as_errinfo(exc)
|
|
228
|
+
if e.script_file is None:
|
|
229
|
+
e.script_file, e.script_line_no = span.file, span.start_line
|
|
230
|
+
e.cmd, e.cmdtype = command, "cmd"
|
|
231
|
+
# The run stops, except at a BREAKPOINT prompt, which carries on:
|
|
232
|
+
# a test that failed in the database (HASROWS) has rolled back the
|
|
233
|
+
# connection, so the batch must not commit.
|
|
234
|
+
_fail_open_batches(ctx, span.file, span.start_line)
|
|
235
|
+
if e is exc:
|
|
236
|
+
raise
|
|
237
|
+
raise e from exc
|
|
238
|
+
|
|
239
|
+
|
|
205
240
|
def _set_command_vars(ctx: RuntimeContext, source: str, line_no: int) -> None:
|
|
206
241
|
"""Set per-command system variables (current script, line, time)."""
|
|
207
242
|
assert ctx.subvars is not None
|
|
@@ -254,11 +289,7 @@ def _exec_sql(
|
|
|
254
289
|
effective_locals = _stack_localvars(ctx) or localvars
|
|
255
290
|
cmd = substitute_vars(text, effective_locals, ctx=ctx)
|
|
256
291
|
if _VARLIKE.search(cmd):
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
output.write(
|
|
260
|
-
f"Warning: There is a potential un-substituted variable in the command\n {redact(cmd)}\n",
|
|
261
|
-
)
|
|
292
|
+
_warn_unsubstituted(cmd)
|
|
262
293
|
e = None
|
|
263
294
|
result = None
|
|
264
295
|
try:
|
|
@@ -271,34 +302,97 @@ def _exec_sql(
|
|
|
271
302
|
db.execute(cmd)
|
|
272
303
|
if commit:
|
|
273
304
|
db.commit()
|
|
274
|
-
except ErrInfo as errinfo:
|
|
275
|
-
e = errinfo
|
|
276
|
-
except SystemExit:
|
|
277
|
-
raise
|
|
278
305
|
except Exception as exc:
|
|
279
|
-
e =
|
|
280
|
-
e.__cause__ = exc # the driver's own message, which a prompt shows
|
|
306
|
+
e = _as_errinfo(exc)
|
|
281
307
|
if e:
|
|
282
|
-
|
|
283
|
-
subvars.add_substitution("$LAST_ERROR", cmd)
|
|
284
|
-
subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
|
|
285
|
-
status.sql_error = True
|
|
286
|
-
if ctx.exec_log is not None:
|
|
287
|
-
ctx.exec_log.log_status_info(f"SQL error: {logged_error(e)}")
|
|
288
|
-
if status.halt_on_err:
|
|
289
|
-
exit_now(1, e)
|
|
290
|
-
status.error_history.append((source, line_no, cmd, e.errmsg()))
|
|
308
|
+
_sql_failed(ctx, e, cmd, source, line_no)
|
|
291
309
|
return
|
|
292
310
|
subvars.add_substitution("$LAST_SQL", cmd)
|
|
293
311
|
if ctx.prompt_input is not None:
|
|
294
312
|
ctx.prompt_input(result)
|
|
295
313
|
|
|
296
314
|
|
|
315
|
+
def _sql_failed(ctx: RuntimeContext, e: ErrInfo, cmd: str, source: str, line_no: int) -> None:
|
|
316
|
+
"""Report an SQL error: halt under ERROR_HALT ON, else record it and carry on.
|
|
317
|
+
|
|
318
|
+
Sets ``$LAST_ERROR`` to *cmd*, ``$ERROR_MESSAGE`` and ``SQL_ERROR()``.
|
|
319
|
+
"""
|
|
320
|
+
assert ctx.status is not None
|
|
321
|
+
assert ctx.subvars is not None
|
|
322
|
+
stamp_errinfo(e)
|
|
323
|
+
ctx.subvars.add_substitution("$LAST_ERROR", cmd)
|
|
324
|
+
ctx.subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
|
|
325
|
+
ctx.status.sql_error = True
|
|
326
|
+
if ctx.exec_log is not None:
|
|
327
|
+
ctx.exec_log.log_status_info(f"SQL error: {logged_error(e)}")
|
|
328
|
+
_fail_open_batches(ctx, source, line_no)
|
|
329
|
+
if ctx.status.halt_on_err:
|
|
330
|
+
exit_now(1, e)
|
|
331
|
+
ctx.status.error_history.append((source, line_no, cmd, e.errmsg()))
|
|
332
|
+
|
|
333
|
+
|
|
297
334
|
# ---------------------------------------------------------------------------
|
|
298
335
|
# Metacommand execution
|
|
299
336
|
# ---------------------------------------------------------------------------
|
|
300
337
|
|
|
301
338
|
|
|
339
|
+
def _as_errinfo(exc: Exception) -> ErrInfo:
|
|
340
|
+
"""*exc* as an :class:`ErrInfo`: itself if it is one, else an exception report.
|
|
341
|
+
|
|
342
|
+
The report keeps *exc* as its ``__cause__``: the driver's or parser's own
|
|
343
|
+
message, which a prompt shows. Call it in the ``except`` clause, where
|
|
344
|
+
:func:`exception_desc` can still see the exception.
|
|
345
|
+
"""
|
|
346
|
+
if isinstance(exc, ErrInfo):
|
|
347
|
+
return exc
|
|
348
|
+
e = ErrInfo(type="exception", exception_msg=exception_desc())
|
|
349
|
+
e.__cause__ = exc
|
|
350
|
+
return e
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _fail_open_batches(ctx: RuntimeContext, source: str, line_no: int) -> None:
|
|
354
|
+
"""Keep every open batch from committing after an error inside it.
|
|
355
|
+
|
|
356
|
+
Whatever failed (a statement, a metacommand, or SQL a metacommand ran),
|
|
357
|
+
the batch is not committed: a database error may already have rolled
|
|
358
|
+
back the batch's earlier work, and execsql cannot tell from here whether
|
|
359
|
+
it did.
|
|
360
|
+
"""
|
|
361
|
+
assert ctx.status is not None
|
|
362
|
+
if ctx.status.batch.in_batch():
|
|
363
|
+
ctx.status.batch.fail_enclosing(f"an error on line {line_no} of {source}")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _metacommand_failed(
|
|
367
|
+
ctx: RuntimeContext,
|
|
368
|
+
cmd: str,
|
|
369
|
+
e: ErrInfo,
|
|
370
|
+
source: str,
|
|
371
|
+
line_no: int,
|
|
372
|
+
*,
|
|
373
|
+
always_halt: bool = False,
|
|
374
|
+
) -> None:
|
|
375
|
+
"""Record a failed metacommand; stop the run if METACOMMAND_ERROR_HALT is on, or *always_halt*.
|
|
376
|
+
|
|
377
|
+
With it off the script carries on, and ``METACOMMAND_ERROR()``,
|
|
378
|
+
``$LAST_ERROR`` and ``$ERROR_MESSAGE`` say what failed.
|
|
379
|
+
"""
|
|
380
|
+
status = ctx.status
|
|
381
|
+
subvars = ctx.subvars
|
|
382
|
+
assert status is not None
|
|
383
|
+
assert subvars is not None
|
|
384
|
+
stamp_errinfo(e)
|
|
385
|
+
status.metacommand_error = True
|
|
386
|
+
subvars.add_substitution("$LAST_ERROR", cmd)
|
|
387
|
+
subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
|
|
388
|
+
if ctx.exec_log is not None:
|
|
389
|
+
ctx.exec_log.log_status_info(f"Metacommand error: {logged_error(e)}")
|
|
390
|
+
_fail_open_batches(ctx, source, line_no)
|
|
391
|
+
if status.halt_on_metacommand_err or always_halt:
|
|
392
|
+
exit_now(1, e)
|
|
393
|
+
status.error_history.append((source, line_no, cmd, e.errmsg()))
|
|
394
|
+
|
|
395
|
+
|
|
302
396
|
def _exec_metacommand(
|
|
303
397
|
ctx: RuntimeContext,
|
|
304
398
|
cmd: str,
|
|
@@ -323,11 +417,7 @@ def _exec_metacommand(
|
|
|
323
417
|
assert subvars is not None
|
|
324
418
|
|
|
325
419
|
if _VARLIKE.search(cmd):
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
output.write(
|
|
329
|
-
f"Warning: There is a potential un-substituted variable in the command\n {redact(cmd)}\n",
|
|
330
|
-
)
|
|
420
|
+
_warn_unsubstituted(cmd)
|
|
331
421
|
e = None
|
|
332
422
|
try:
|
|
333
423
|
applies, result = metacommandlist.eval(cmd)
|
|
@@ -335,28 +425,16 @@ def _exec_metacommand(
|
|
|
335
425
|
if (manifest := _manifest.current()) is not None:
|
|
336
426
|
manifest.record_metacommand(cmd, source, line_no, metacommandlist)
|
|
337
427
|
return result
|
|
338
|
-
except ErrInfo as errinfo:
|
|
339
|
-
e = errinfo
|
|
340
|
-
except SystemExit:
|
|
341
|
-
raise
|
|
342
428
|
except Exception as exc:
|
|
343
|
-
e =
|
|
344
|
-
e.__cause__ = exc
|
|
429
|
+
e = _as_errinfo(exc)
|
|
345
430
|
if e:
|
|
346
|
-
|
|
347
|
-
status.metacommand_error = True
|
|
348
|
-
subvars.add_substitution("$LAST_ERROR", cmd)
|
|
349
|
-
subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
|
|
350
|
-
if ctx.exec_log is not None:
|
|
351
|
-
ctx.exec_log.log_status_info(f"Metacommand error: {logged_error(e)}")
|
|
352
|
-
if status.halt_on_metacommand_err:
|
|
353
|
-
exit_now(1, e)
|
|
431
|
+
_metacommand_failed(ctx, cmd, e, source, line_no)
|
|
354
432
|
if (manifest := _manifest.current()) is not None:
|
|
355
433
|
manifest.record_metacommand(cmd, source, line_no, metacommandlist)
|
|
356
|
-
status.error_history.append((source, line_no, cmd, e.errmsg()))
|
|
357
434
|
return None
|
|
358
435
|
# No handler matched — truly unknown metacommand
|
|
359
436
|
status.metacommand_error = True
|
|
437
|
+
_fail_open_batches(ctx, source, line_no)
|
|
360
438
|
raise ErrInfo(type="cmd", command_text=cmd, other_msg="Unknown metacommand")
|
|
361
439
|
|
|
362
440
|
|
|
@@ -443,10 +521,22 @@ def _execute_node(
|
|
|
443
521
|
# !!$COUNTER_N!! and re-roll !!$RANDOM!!/!!$UUID!! references.
|
|
444
522
|
effective_locals = _stack_localvars(ctx) or localvars
|
|
445
523
|
expanded = substitute_vars(command, effective_locals, ctx=ctx)
|
|
446
|
-
if _BREAK_RX.match(expanded):
|
|
447
|
-
raise _BreakLoop
|
|
448
524
|
ctx.last_command = ExecutingStatement(node)
|
|
449
|
-
|
|
525
|
+
if not _BREAK_RX.match(expanded):
|
|
526
|
+
_exec_metacommand(ctx, expanded, node.span.file, node.span.start_line)
|
|
527
|
+
elif _break_has_target(ctx):
|
|
528
|
+
raise _BreakLoop
|
|
529
|
+
else:
|
|
530
|
+
_metacommand_failed(
|
|
531
|
+
ctx,
|
|
532
|
+
expanded,
|
|
533
|
+
ErrInfo(
|
|
534
|
+
type="cmd",
|
|
535
|
+
other_msg="BREAK has nothing to leave: it is not inside a LOOP, SCRIPT or included file.",
|
|
536
|
+
),
|
|
537
|
+
node.span.file,
|
|
538
|
+
node.span.start_line,
|
|
539
|
+
)
|
|
450
540
|
|
|
451
541
|
elif isinstance(node, IfBlock):
|
|
452
542
|
ctx.last_command = ExecutingStatement(node)
|
|
@@ -499,7 +589,7 @@ def _execute_if(
|
|
|
499
589
|
"""
|
|
500
590
|
from execsql.state import ExecFrame
|
|
501
591
|
|
|
502
|
-
if
|
|
592
|
+
if _condition_holds(ctx, node.command, node.span, node.condition, node.condition_modifiers):
|
|
503
593
|
_push_block_frame(
|
|
504
594
|
ctx,
|
|
505
595
|
ExecFrame(kind="if", label=node.condition, source=node.span.file, line=node.span.start_line),
|
|
@@ -512,7 +602,7 @@ def _execute_if(
|
|
|
512
602
|
|
|
513
603
|
# Try ELSEIF clauses
|
|
514
604
|
for clause in node.elseif_clauses:
|
|
515
|
-
if
|
|
605
|
+
if _condition_holds(ctx, clause.command, clause.span, clause.condition, clause.condition_modifiers):
|
|
516
606
|
_push_block_frame(
|
|
517
607
|
ctx,
|
|
518
608
|
ExecFrame(kind="elseif", label=clause.condition, source=node.span.file, line=node.span.start_line),
|
|
@@ -552,9 +642,8 @@ def _execute_loop(
|
|
|
552
642
|
try:
|
|
553
643
|
if node.loop_type == "WHILE":
|
|
554
644
|
while True:
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if not xcmd_test(expanded):
|
|
645
|
+
ctx.last_command = ExecutingStatement(node)
|
|
646
|
+
if not _condition_holds(ctx, node.command, node.span, condition):
|
|
558
647
|
break
|
|
559
648
|
frame.iteration += 1
|
|
560
649
|
try:
|
|
@@ -568,9 +657,8 @@ def _execute_loop(
|
|
|
568
657
|
_execute_nodes(ctx, node.body, node.span.file, localvars, in_loop=True)
|
|
569
658
|
except _BreakLoop:
|
|
570
659
|
break
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
if xcmd_test(expanded):
|
|
660
|
+
ctx.last_command = ExecutingStatement(node)
|
|
661
|
+
if _condition_holds(ctx, node.command, node.span, condition):
|
|
574
662
|
break
|
|
575
663
|
finally:
|
|
576
664
|
ctx.ast_exec_stack.pop()
|
|
@@ -583,7 +671,19 @@ def _execute_batch(
|
|
|
583
671
|
*,
|
|
584
672
|
in_loop: bool = False,
|
|
585
673
|
) -> None:
|
|
586
|
-
"""Execute a BEGIN BATCH / END BATCH block.
|
|
674
|
+
"""Execute a BEGIN BATCH / END BATCH block.
|
|
675
|
+
|
|
676
|
+
Reaching END BATCH commits the batch; a commit that fails is an SQL error
|
|
677
|
+
on the END BATCH line, after the batch's uncommitted databases are rolled
|
|
678
|
+
back, and every enclosing batch then rolls back at its own END BATCH
|
|
679
|
+
instead of committing. Leaving it any other way (BREAK,
|
|
680
|
+
an error, HALT) abandons it: see
|
|
681
|
+
:meth:`~execsql.script.control.BatchLevels.abandon_batch` for when that
|
|
682
|
+
rolls back and when an enclosing batch takes the statements over. A
|
|
683
|
+
halt usually gets there first: :func:`~execsql.utils.errors.exit_now`
|
|
684
|
+
abandons every open batch before an ON ERROR_HALT script runs, so
|
|
685
|
+
nothing is left to do here.
|
|
686
|
+
"""
|
|
587
687
|
from execsql.state import ExecFrame
|
|
588
688
|
|
|
589
689
|
status = ctx.status
|
|
@@ -593,12 +693,35 @@ def _execute_batch(
|
|
|
593
693
|
ctx,
|
|
594
694
|
ExecFrame(kind="batch", label="", source=node.span.file, line=node.span.start_line),
|
|
595
695
|
)
|
|
696
|
+
completed = False
|
|
596
697
|
try:
|
|
597
698
|
_execute_nodes(ctx, node.body, node.span.file, localvars, in_loop=in_loop)
|
|
699
|
+
completed = True
|
|
598
700
|
finally:
|
|
599
701
|
ctx.ast_exec_stack.pop()
|
|
600
|
-
if status.batch.in_batch():
|
|
702
|
+
if status.batch.in_batch() and not completed:
|
|
703
|
+
status.batch.abandon_batch()
|
|
704
|
+
if status.batch.in_batch():
|
|
705
|
+
end_line = node.span.effective_end_line
|
|
706
|
+
ctx.last_command = ExecutingStatement(
|
|
707
|
+
MetaCommandStatement(span=SourceSpan(node.span.file, end_line), command="END BATCH"),
|
|
708
|
+
)
|
|
709
|
+
failure = status.batch.failure()
|
|
710
|
+
if failure is not None:
|
|
711
|
+
status.batch.abandon_batch()
|
|
712
|
+
e = ErrInfo(type="error", other_msg=f"The batch was not committed: {failure}.")
|
|
713
|
+
_sql_failed(ctx, e, "END BATCH", node.span.file, end_line)
|
|
714
|
+
return
|
|
715
|
+
# The commit is the batch's last SQL: SQL_ERROR() right after END
|
|
716
|
+
# BATCH says whether it failed, as after any statement.
|
|
717
|
+
status.sql_error = False
|
|
718
|
+
try:
|
|
601
719
|
status.batch.end_batch()
|
|
720
|
+
except Exception as exc:
|
|
721
|
+
# end_batch has rolled back what it had not committed, which can
|
|
722
|
+
# include an enclosing batch's statements on a shared connection;
|
|
723
|
+
# _sql_failed keeps that batch from committing the rest.
|
|
724
|
+
_sql_failed(ctx, _as_errinfo(exc), "END BATCH", node.span.file, end_line)
|
|
602
725
|
|
|
603
726
|
|
|
604
727
|
def _pre_register_scripts(ctx: RuntimeContext, nodes: list[Node]) -> None:
|
|
@@ -649,107 +772,123 @@ def _execute_include(
|
|
|
649
772
|
node: IncludeDirective,
|
|
650
773
|
localvars: SubVarSet | None = None,
|
|
651
774
|
) -> None:
|
|
652
|
-
"""
|
|
653
|
-
|
|
654
|
-
**INCLUDE** is handled natively: the target file is parsed by the AST
|
|
655
|
-
parser and executed through the AST executor with circular-include
|
|
656
|
-
detection.
|
|
775
|
+
"""Run an INCLUDE or EXECUTE SCRIPT written in the script.
|
|
657
776
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
777
|
+
Variables in its file, script name and arguments are substituted once,
|
|
778
|
+
as in any other metacommand, so ``!!$COUNTER_1!!`` counts once and
|
|
779
|
+
``$LAST_ERROR`` holds the text that ran. An EXECUTE SCRIPT ``WHILE`` /
|
|
780
|
+
``UNTIL`` condition is substituted on each pass instead.
|
|
662
781
|
"""
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
effective_locals = _stack_localvars(ctx) or localvars
|
|
667
|
-
target = substitute_vars(node.target, effective_locals, ctx=ctx).strip().lower()
|
|
668
|
-
|
|
669
|
-
# Native path: target is in our AST registry
|
|
670
|
-
if target in ctx.ast_scripts:
|
|
671
|
-
_execute_script_native(ctx, node, ctx.ast_scripts[target], localvars)
|
|
672
|
-
return
|
|
673
|
-
|
|
674
|
-
# Target not registered — IF EXISTS silently skips; otherwise error.
|
|
675
|
-
if node.if_exists:
|
|
676
|
-
return
|
|
677
|
-
raise ErrInfo(
|
|
678
|
-
"cmd",
|
|
679
|
-
other_msg=f"There is no SCRIPT named {node.target}.",
|
|
680
|
-
)
|
|
681
|
-
|
|
682
|
-
# --- INCLUDE (file inclusion) — parse and execute natively ---
|
|
683
|
-
_execute_include_native(ctx, node, localvars)
|
|
782
|
+
effective_locals = _stack_localvars(ctx) or localvars
|
|
783
|
+
directive = substitute_directive(node, lambda text: substitute_vars(text, effective_locals, ctx=ctx))
|
|
784
|
+
_run_directive(ctx, directive, localvars)
|
|
684
785
|
|
|
685
786
|
|
|
686
|
-
def
|
|
787
|
+
def _run_directive(
|
|
687
788
|
ctx: RuntimeContext,
|
|
688
789
|
node: IncludeDirective,
|
|
689
|
-
script_block: ScriptBlock,
|
|
690
790
|
localvars: SubVarSet | None = None,
|
|
691
791
|
) -> None:
|
|
692
|
-
"""
|
|
792
|
+
"""Run an INCLUDE or EXECUTE SCRIPT whose variables have been substituted.
|
|
693
793
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
794
|
+
**INCLUDE** parses the target file with the AST parser and executes it
|
|
795
|
+
with circular-include detection. **EXECUTE SCRIPT** runs a SCRIPT
|
|
796
|
+
registered in ``ctx.ast_scripts`` with its arguments in a local variable
|
|
797
|
+
overlay, once or in a WHILE / UNTIL loop.
|
|
798
|
+
|
|
799
|
+
The INCLUDE or EXECUTE SCRIPT failing itself (no such file or script, a
|
|
800
|
+
missing argument) is a metacommand error; see :func:`_directive_failed`.
|
|
801
|
+
"""
|
|
802
|
+
if (manifest := _manifest.current()) is not None:
|
|
803
|
+
assert ctx.metacommandlist is not None
|
|
804
|
+
manifest.record_metacommand(node.command, node.span.file, node.span.start_line, ctx.metacommandlist)
|
|
805
|
+
|
|
806
|
+
if not node.is_execute_script:
|
|
807
|
+
_execute_include_native(ctx, node, localvars)
|
|
808
|
+
return
|
|
809
|
+
|
|
810
|
+
target = node.target.strip().lower()
|
|
811
|
+
script_block = ctx.ast_scripts.get(target)
|
|
812
|
+
if script_block is None:
|
|
813
|
+
# IF EXISTS silently skips a SCRIPT that is not defined.
|
|
814
|
+
if not node.if_exists:
|
|
815
|
+
_directive_failed(ctx, node, ErrInfo("cmd", other_msg=f"There is no SCRIPT named {node.target.strip()}."))
|
|
816
|
+
return
|
|
817
|
+
try:
|
|
818
|
+
paramvals = _script_args(node, script_block)
|
|
819
|
+
except ErrInfo as e:
|
|
820
|
+
_directive_failed(ctx, node, e)
|
|
821
|
+
return
|
|
822
|
+
_execute_script_native(ctx, node, script_block, paramvals)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _directive_failed(ctx: RuntimeContext, node: IncludeDirective, e: ErrInfo) -> None:
|
|
826
|
+
"""INCLUDE or EXECUTE SCRIPT itself failed (no such file or script, a missing argument): a metacommand error.
|
|
827
|
+
|
|
828
|
+
Errors raised by the statements an included file or script runs are
|
|
829
|
+
theirs, not the directive's, and are handled where they happen.
|
|
830
|
+
"""
|
|
831
|
+
_metacommand_failed(ctx, node.command, e, node.span.file, node.span.start_line)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _script_args(node: IncludeDirective, script_block: ScriptBlock) -> ScriptArgSubVarSet | None:
|
|
835
|
+
"""The arguments an EXECUTE SCRIPT passes to *script_block*, with defaults for those it leaves out.
|
|
836
|
+
|
|
837
|
+
``node.arguments`` must already be substituted. Raises :class:`ErrInfo`
|
|
838
|
+
when a required parameter has no argument.
|
|
700
839
|
"""
|
|
701
|
-
from execsql.script.variables import ScriptArgSubVarSet
|
|
702
840
|
from execsql.utils.strings import wo_quotes
|
|
703
841
|
|
|
704
|
-
# Parse arguments (replicates ScriptExecSpec logic)
|
|
705
|
-
# Expand the argument string in the *caller's* scope so that references
|
|
706
|
-
# like val=!!#parent_param!! or val=!!~parent_local!! resolve correctly
|
|
707
|
-
# before we push a new scope for this script.
|
|
708
|
-
paramvals: ScriptArgSubVarSet | None = None
|
|
709
842
|
if node.arguments is not None:
|
|
710
|
-
caller_locals = _stack_localvars(ctx)
|
|
711
|
-
expanded_args = substitute_vars(node.arguments, caller_locals, ctx=ctx)
|
|
712
843
|
args_rx = re.compile(
|
|
713
844
|
r'(?P<param>#?\w+)\s*=\s*(?P<arg>(?:(?:[^"\'\[][^,\)]*)|(?:"[^"]*")|(?:\'[^\']*\')|(?:\[[^\]]*\])))',
|
|
714
845
|
re.I,
|
|
715
846
|
)
|
|
716
|
-
all_args = re.findall(args_rx,
|
|
847
|
+
all_args = re.findall(args_rx, node.arguments)
|
|
717
848
|
all_cleaned_args = [(ae[0], wo_quotes(ae[1])) for ae in all_args]
|
|
718
849
|
all_prepared_args = [(ae[0] if ae[0][0] == "#" else "#" + ae[0], ae[1]) for ae in all_cleaned_args]
|
|
719
850
|
paramvals = ScriptArgSubVarSet()
|
|
720
851
|
for param, arg in all_prepared_args:
|
|
721
852
|
paramvals.add_substitution(param, arg)
|
|
722
|
-
|
|
723
|
-
# Validate parameter names match — with default parameter support
|
|
724
|
-
if script_block.param_defs is not None:
|
|
725
|
-
passed_names = [p[0].lstrip("#") for p in all_prepared_args]
|
|
726
|
-
required = [p.name for p in script_block.param_defs if p.required]
|
|
727
|
-
missing = [p for p in required if p not in passed_names]
|
|
728
|
-
if missing:
|
|
729
|
-
raise ErrInfo(
|
|
730
|
-
"error",
|
|
731
|
-
other_msg=(f"Missing required parameter(s) ({', '.join(missing)}) in call to {script_block.name}."),
|
|
732
|
-
)
|
|
733
|
-
# Inject defaults for optional params not provided
|
|
734
|
-
for pdef in script_block.param_defs:
|
|
735
|
-
if pdef.default is not None and pdef.name not in passed_names:
|
|
736
|
-
paramvals.add_substitution(f"#{pdef.name}", pdef.default)
|
|
853
|
+
passed_names = [p[0].lstrip("#") for p in all_prepared_args]
|
|
737
854
|
else:
|
|
738
|
-
if script_block.param_defs is
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
855
|
+
if script_block.param_defs is None:
|
|
856
|
+
return None
|
|
857
|
+
paramvals = ScriptArgSubVarSet()
|
|
858
|
+
passed_names = []
|
|
859
|
+
|
|
860
|
+
# Validate parameter names match — with default parameter support
|
|
861
|
+
if script_block.param_defs is not None:
|
|
862
|
+
missing = [p.name for p in script_block.param_defs if p.required and p.name not in passed_names]
|
|
863
|
+
if missing:
|
|
864
|
+
raise ErrInfo(
|
|
865
|
+
"error",
|
|
866
|
+
other_msg=f"Missing required parameter(s) ({', '.join(missing)}) in call to {script_block.name}.",
|
|
867
|
+
)
|
|
868
|
+
# Inject defaults for optional params not provided
|
|
869
|
+
for pdef in script_block.param_defs:
|
|
870
|
+
if pdef.default is not None and pdef.name not in passed_names:
|
|
871
|
+
paramvals.add_substitution(f"#{pdef.name}", pdef.default)
|
|
872
|
+
return paramvals
|
|
873
|
+
|
|
752
874
|
|
|
875
|
+
def _execute_script_native(
|
|
876
|
+
ctx: RuntimeContext,
|
|
877
|
+
node: IncludeDirective,
|
|
878
|
+
script_block: ScriptBlock,
|
|
879
|
+
paramvals: ScriptArgSubVarSet | None,
|
|
880
|
+
) -> None:
|
|
881
|
+
"""Execute a SCRIPT block natively through the AST executor.
|
|
882
|
+
|
|
883
|
+
*paramvals* are its arguments, from :func:`_script_args`.
|
|
884
|
+
|
|
885
|
+
Pushes a SCRIPT-kind :class:`~execsql.state.ExecFrame` onto
|
|
886
|
+
``ctx.ast_exec_stack`` so that metacommand handlers (``x_sub``,
|
|
887
|
+
``x_rm_sub``, ``xf_sub_defined``, prompt handlers, etc.) can access
|
|
888
|
+
``~`` local variables and ``#`` script arguments via
|
|
889
|
+
``ctx.current_localvars()`` / ``ctx.current_paramvals()``. The frame
|
|
890
|
+
is popped on exit (including on error) via ``try/finally``.
|
|
891
|
+
"""
|
|
753
892
|
# Push a SCRIPT-kind ExecFrame onto ast_exec_stack so that:
|
|
754
893
|
# - get_subvarset() can find ~local and +outer-scope variables
|
|
755
894
|
# - xf_sub_defined/xf_sub_empty can check ~local and #param variables
|
|
@@ -782,9 +921,8 @@ def _execute_script_native(
|
|
|
782
921
|
|
|
783
922
|
if node.loop_type == "WHILE":
|
|
784
923
|
while True:
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if not xcmd_test(expanded):
|
|
924
|
+
ctx.last_command = ExecutingStatement(node)
|
|
925
|
+
if not _condition_holds(ctx, node.command, node.span, condition):
|
|
788
926
|
break
|
|
789
927
|
frame.iteration += 1
|
|
790
928
|
try:
|
|
@@ -798,18 +936,14 @@ def _execute_script_native(
|
|
|
798
936
|
_run_body()
|
|
799
937
|
except _BreakLoop:
|
|
800
938
|
break
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
if xcmd_test(expanded):
|
|
939
|
+
ctx.last_command = ExecutingStatement(node)
|
|
940
|
+
if _condition_holds(ctx, node.command, node.span, condition):
|
|
804
941
|
break
|
|
805
942
|
else:
|
|
806
943
|
try:
|
|
807
944
|
_run_body()
|
|
808
|
-
except _BreakLoop
|
|
809
|
-
|
|
810
|
-
type="cmd",
|
|
811
|
-
other_msg=f"BREAK metacommand inside SCRIPT '{script_block.name}' but not inside a LOOP.",
|
|
812
|
-
) from exc
|
|
945
|
+
except _BreakLoop:
|
|
946
|
+
pass # BREAK outside a LOOP in the script returns from it
|
|
813
947
|
finally:
|
|
814
948
|
_pop_frame(ctx)
|
|
815
949
|
|
|
@@ -824,24 +958,68 @@ def _execute_include_native(
|
|
|
824
958
|
Handles tilde expansion, IF EXISTS, logging, and circular-include
|
|
825
959
|
detection via ``ctx.include_chain``.
|
|
826
960
|
"""
|
|
827
|
-
|
|
828
|
-
|
|
961
|
+
try:
|
|
962
|
+
loaded = _load_include(ctx, node)
|
|
963
|
+
except _OutsideIncludeRoot as refused:
|
|
964
|
+
# A security boundary, not a script error to carry on from: the run
|
|
965
|
+
# stops whatever METACOMMAND_ERROR_HALT says.
|
|
966
|
+
_metacommand_failed(ctx, node.command, refused.errinfo, node.span.file, node.span.start_line, always_halt=True)
|
|
967
|
+
return
|
|
968
|
+
except Exception as exc:
|
|
969
|
+
_directive_failed(ctx, node, _as_errinfo(exc))
|
|
970
|
+
return
|
|
971
|
+
if loaded is None:
|
|
972
|
+
return
|
|
973
|
+
resolved, included_tree = loaded
|
|
829
974
|
|
|
830
|
-
#
|
|
831
|
-
|
|
832
|
-
|
|
975
|
+
# Pre-register SCRIPT blocks in the included file so forward references work.
|
|
976
|
+
_pre_register_scripts(ctx, included_tree.body)
|
|
977
|
+
|
|
978
|
+
# Execute with include-chain tracking
|
|
979
|
+
from execsql.state import ExecFrame
|
|
980
|
+
|
|
981
|
+
ctx.include_chain.append(resolved)
|
|
982
|
+
_push_block_frame(
|
|
983
|
+
ctx,
|
|
984
|
+
ExecFrame(kind="include", label=Path(resolved).name, source=resolved, line=1),
|
|
985
|
+
)
|
|
986
|
+
try:
|
|
987
|
+
_execute_nodes(ctx, included_tree.body, included_tree.source, localvars)
|
|
988
|
+
except _BreakLoop:
|
|
989
|
+
pass # BREAK outside a LOOP in the file leaves it
|
|
990
|
+
finally:
|
|
991
|
+
ctx.ast_exec_stack.pop()
|
|
992
|
+
ctx.include_chain.pop()
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
class _OutsideIncludeRoot(Exception):
|
|
996
|
+
"""An INCLUDE's file is outside ``include_root``; *errinfo* says where it resolved."""
|
|
833
997
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
target = target[1:-1]
|
|
998
|
+
def __init__(self, errinfo: ErrInfo) -> None:
|
|
999
|
+
super().__init__(str(errinfo))
|
|
1000
|
+
self.errinfo = errinfo
|
|
838
1001
|
|
|
839
|
-
|
|
1002
|
+
|
|
1003
|
+
def _load_include(
|
|
1004
|
+
ctx: RuntimeContext,
|
|
1005
|
+
node: IncludeDirective,
|
|
1006
|
+
) -> tuple[str, Any] | None:
|
|
1007
|
+
"""Find and parse the file an INCLUDE names: ``(resolved path, tree)``, or ``None`` to skip it (IF EXISTS).
|
|
1008
|
+
|
|
1009
|
+
Raises :class:`_OutsideIncludeRoot` for a file outside ``include_root``,
|
|
1010
|
+
and :class:`ErrInfo` when the INCLUDE itself fails: no such file, a
|
|
1011
|
+
circular include, or a file that does not parse.
|
|
1012
|
+
"""
|
|
1013
|
+
from execsql.script.parser import parse_script
|
|
1014
|
+
from execsql.utils.errors import file_size_date
|
|
1015
|
+
|
|
1016
|
+
target = node.target
|
|
1017
|
+
# Tilde expansion (matches the monolith's x_include)
|
|
840
1018
|
if len(target) > 1 and target[0] == "~" and target[1] == os.sep:
|
|
841
1019
|
target = str(Path.home() / target[2:])
|
|
842
1020
|
|
|
843
|
-
# Optional containment: when conf.include_root is set, the
|
|
844
|
-
#
|
|
1021
|
+
# Optional containment: when conf.include_root is set, the file must
|
|
1022
|
+
# live under that root.
|
|
845
1023
|
include_root = getattr(ctx.conf, "include_root", None) if hasattr(ctx, "conf") else None
|
|
846
1024
|
if include_root is None:
|
|
847
1025
|
import execsql.state as _state
|
|
@@ -850,14 +1028,17 @@ def _execute_include_native(
|
|
|
850
1028
|
if include_root:
|
|
851
1029
|
from execsql.utils.fileio import safe_output_path
|
|
852
1030
|
|
|
853
|
-
|
|
1031
|
+
try:
|
|
1032
|
+
target = safe_output_path(target, include_root)
|
|
1033
|
+
except ErrInfo as e:
|
|
1034
|
+
raise _OutsideIncludeRoot(e) from e
|
|
854
1035
|
|
|
855
1036
|
target_path = Path(target)
|
|
856
1037
|
|
|
857
1038
|
# IF EXISTS handling
|
|
858
1039
|
if node.if_exists:
|
|
859
1040
|
if not target_path.is_file():
|
|
860
|
-
return
|
|
1041
|
+
return None
|
|
861
1042
|
else:
|
|
862
1043
|
if not target_path.is_file():
|
|
863
1044
|
raise ErrInfo(type="error", other_msg=f"File {target} does not exist.")
|
|
@@ -883,23 +1064,7 @@ def _execute_include_native(
|
|
|
883
1064
|
included_tree = parse_script(target, encoding=encoding)
|
|
884
1065
|
if (manifest := _manifest.current()) is not None:
|
|
885
1066
|
manifest.record_include(target, node.span.file, node.span.start_line)
|
|
886
|
-
|
|
887
|
-
# Pre-register SCRIPT blocks in the included file so forward references work.
|
|
888
|
-
_pre_register_scripts(ctx, included_tree.body)
|
|
889
|
-
|
|
890
|
-
# Execute with include-chain tracking
|
|
891
|
-
from execsql.state import ExecFrame
|
|
892
|
-
|
|
893
|
-
ctx.include_chain.append(resolved)
|
|
894
|
-
_push_block_frame(
|
|
895
|
-
ctx,
|
|
896
|
-
ExecFrame(kind="include", label=Path(resolved).name, source=resolved, line=1),
|
|
897
|
-
)
|
|
898
|
-
try:
|
|
899
|
-
_execute_nodes(ctx, included_tree.body, included_tree.source, localvars)
|
|
900
|
-
finally:
|
|
901
|
-
ctx.ast_exec_stack.pop()
|
|
902
|
-
ctx.include_chain.pop()
|
|
1067
|
+
return resolved, included_tree
|
|
903
1068
|
|
|
904
1069
|
|
|
905
1070
|
# ---------------------------------------------------------------------------
|
|
@@ -908,7 +1073,7 @@ def _execute_include_native(
|
|
|
908
1073
|
|
|
909
1074
|
|
|
910
1075
|
class _BreakLoop(BaseException):
|
|
911
|
-
"""Raised by BREAK
|
|
1076
|
+
"""Raised by BREAK to leave the innermost LOOP, SCRIPT or included file.
|
|
912
1077
|
|
|
913
1078
|
A ``BaseException``, like ``KeyboardInterrupt``, so the ``except
|
|
914
1079
|
Exception`` handlers it may pass on its way to the loop leave it alone:
|
|
@@ -917,13 +1082,23 @@ class _BreakLoop(BaseException):
|
|
|
917
1082
|
"""
|
|
918
1083
|
|
|
919
1084
|
|
|
920
|
-
def
|
|
921
|
-
"""Whether a BREAK at the current statement
|
|
1085
|
+
def _break_has_target(ctx: RuntimeContext) -> bool:
|
|
1086
|
+
"""Whether a BREAK at the current statement has something to leave.
|
|
1087
|
+
|
|
1088
|
+
BREAK leaves the innermost LOOP, SCRIPT or included file (a SCRIPT that
|
|
1089
|
+
``EXECUTE SCRIPT … WHILE`` / ``UNTIL`` repeats stops repeating); in the main
|
|
1090
|
+
script outside all of them there is nothing to leave.
|
|
1091
|
+
|
|
1092
|
+
Each kind listed here must have a runner that catches ``_BreakLoop``:
|
|
1093
|
+
``_execute_loop``, ``_execute_script_native`` and
|
|
1094
|
+
``_execute_include_native``. A ``_BreakLoop`` nobody catches would end
|
|
1095
|
+
the run as an uncaught BaseException.
|
|
1096
|
+
"""
|
|
922
1097
|
for frame in reversed(ctx.ast_exec_stack):
|
|
923
|
-
if frame.kind in ("loop_while", "loop_until"):
|
|
1098
|
+
if frame.kind in ("loop_while", "loop_until", "script", "include"):
|
|
924
1099
|
return True
|
|
925
|
-
if frame.kind
|
|
926
|
-
return
|
|
1100
|
+
if frame.kind == "main":
|
|
1101
|
+
return False
|
|
927
1102
|
return False
|
|
928
1103
|
|
|
929
1104
|
|
|
@@ -986,9 +1161,8 @@ class ExecutingStatement:
|
|
|
986
1161
|
node = self.node
|
|
987
1162
|
if isinstance(node, SqlStatement):
|
|
988
1163
|
return _StatementText(node.text)
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
return _StatementText("")
|
|
1164
|
+
command = getattr(node, "command", "")
|
|
1165
|
+
return _StatementText(command, "-- !x! " if command else "")
|
|
992
1166
|
|
|
993
1167
|
def __repr__(self) -> str:
|
|
994
1168
|
return f"ExecutingStatement({self.source}:{self.line_no}, {self.command_type})"
|
|
@@ -1006,23 +1180,7 @@ def _node_cmd_type(node: Node) -> str:
|
|
|
1006
1180
|
|
|
1007
1181
|
|
|
1008
1182
|
def _node_cmd_text(node: Node) -> str:
|
|
1009
|
-
|
|
1010
|
-
return node.text
|
|
1011
|
-
if isinstance(node, MetaCommandStatement):
|
|
1012
|
-
return "-- !x! " + node.command
|
|
1013
|
-
if isinstance(node, IfBlock):
|
|
1014
|
-
return f"-- !x! IF ({node.condition})"
|
|
1015
|
-
if isinstance(node, LoopBlock):
|
|
1016
|
-
return f"-- !x! LOOP {node.loop_type} ({node.condition})"
|
|
1017
|
-
if isinstance(node, BatchBlock):
|
|
1018
|
-
return "-- !x! BEGIN BATCH"
|
|
1019
|
-
if isinstance(node, ScriptBlock):
|
|
1020
|
-
return f"-- !x! BEGIN SCRIPT {node.name}"
|
|
1021
|
-
if isinstance(node, IncludeDirective):
|
|
1022
|
-
if node.is_execute_script:
|
|
1023
|
-
return f"-- !x! EXECUTE SCRIPT {node.target}"
|
|
1024
|
-
return f"-- !x! INCLUDE {node.target}"
|
|
1025
|
-
return repr(node)
|
|
1183
|
+
return ExecutingStatement(node).command.commandline() or repr(node)
|
|
1026
1184
|
|
|
1027
1185
|
|
|
1028
1186
|
# ---------------------------------------------------------------------------
|
|
@@ -1068,11 +1226,6 @@ def execute(script: Script, *, ctx: RuntimeContext | None = None) -> None:
|
|
|
1068
1226
|
_push_frame(ctx, "<main>", script.source, line_no=1, kind="main")
|
|
1069
1227
|
try:
|
|
1070
1228
|
_execute_nodes(ctx, script.body, script.source)
|
|
1071
|
-
except _BreakLoop as exc:
|
|
1072
|
-
raise ErrInfo(
|
|
1073
|
-
type="cmd",
|
|
1074
|
-
other_msg="BREAK metacommand outside of a LOOP block.",
|
|
1075
|
-
) from exc
|
|
1076
1229
|
finally:
|
|
1077
1230
|
_pop_frame(ctx)
|
|
1078
1231
|
|
|
@@ -1091,7 +1244,8 @@ def execute_input(
|
|
|
1091
1244
|
"""Run input typed at a BREAKPOINT prompt as the next lines of the paused script.
|
|
1092
1245
|
|
|
1093
1246
|
The input runs as if it were written where the script paused: in its
|
|
1094
|
-
variable scope, its batch and its loop, so a BREAK leaves that loop
|
|
1247
|
+
variable scope, its batch and its loop, so a BREAK leaves that loop
|
|
1248
|
+
(or the SCRIPT or included file it paused in).
|
|
1095
1249
|
Unlike :func:`execute`, nothing is reset.
|
|
1096
1250
|
|
|
1097
1251
|
*show* receives the result of each SQL statement run. An error that
|
|
@@ -1107,10 +1261,6 @@ def execute_input(
|
|
|
1107
1261
|
ctx.prompt_input = show
|
|
1108
1262
|
try:
|
|
1109
1263
|
_execute_nodes(ctx, script.body, script.source)
|
|
1110
|
-
except _BreakLoop as exc:
|
|
1111
|
-
if not _inside_loop(ctx):
|
|
1112
|
-
raise ErrInfo(type="cmd", other_msg="BREAK metacommand outside of a LOOP block.") from exc
|
|
1113
|
-
raise
|
|
1114
1264
|
finally:
|
|
1115
1265
|
ctx.prompt_input = prompt_input
|
|
1116
1266
|
ctx.last_command = last_command
|