execsql2 2.23.1__py3-none-any.whl → 2.25.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 +169 -160
- execsql/cli/__init__.py +19 -586
- execsql/cli/application.py +309 -0
- execsql/cli/commands/__init__.py +13 -0
- execsql/cli/commands/config.py +205 -0
- execsql/cli/commands/format.py +100 -0
- execsql/cli/commands/init.py +135 -0
- execsql/cli/commands/lint.py +225 -0
- execsql/cli/commands/listing.py +81 -0
- execsql/cli/commands/run.py +459 -0
- execsql/cli/dispatch.py +66 -0
- execsql/cli/dsn.py +11 -4
- execsql/cli/help.py +175 -24
- execsql/cli/lint.py +780 -100
- execsql/cli/options.py +108 -0
- execsql/cli/run.py +96 -32
- execsql/config.py +256 -173
- execsql/config_validation.py +282 -0
- execsql/data/execsql.conf.template +38 -5
- execsql/data/script_header.sql +23 -0
- execsql/db/access.py +29 -8
- execsql/db/base.py +40 -8
- execsql/db/dsn.py +13 -4
- execsql/db/duckdb.py +3 -1
- execsql/db/factory.py +13 -4
- execsql/db/firebird.py +21 -7
- execsql/db/mysql.py +37 -16
- execsql/db/oracle.py +49 -16
- execsql/db/postgres.py +30 -6
- execsql/db/sqlite.py +7 -9
- execsql/db/sqlserver.py +25 -8
- execsql/debug/repl.py +51 -193
- execsql/exceptions.py +9 -0
- execsql/exporters/delimited.py +87 -28
- execsql/exporters/xlsx.py +24 -9
- execsql/exporters/zip.py +1 -2
- execsql/format.py +115 -76
- execsql/gui/desktop.py +1 -3
- execsql/gui/tui.py +2 -2
- execsql/importers/base.py +35 -1
- execsql/importers/csv.py +3 -1
- execsql/interactive.py +232 -0
- execsql/manifest.py +200 -0
- execsql/metacommands/conditions.py +7 -8
- execsql/metacommands/control.py +2 -3
- execsql/metacommands/data.py +4 -1
- execsql/metacommands/debug.py +2 -4
- execsql/metacommands/effects.py +124 -0
- execsql/metacommands/io_fileops.py +62 -37
- execsql/metacommands/io_import.py +0 -1
- execsql/metacommands/prompt.py +2 -0
- execsql/metacommands/upsert.py +5 -2
- execsql/models.py +3 -2
- execsql/parser.py +14 -5
- execsql/plugins.py +24 -0
- execsql/script/__init__.py +0 -9
- execsql/script/ast.py +1 -1
- execsql/script/engine.py +46 -104
- execsql/script/executor.py +162 -39
- execsql/script/parser.py +167 -118
- execsql/state.py +34 -11
- execsql/state.pyi +11 -0
- execsql/types.py +22 -13
- execsql/utils/auth.py +169 -24
- execsql/utils/color.py +18 -0
- execsql/utils/datetime.py +55 -17
- execsql/utils/errors.py +82 -10
- execsql/utils/fileio.py +404 -117
- execsql/utils/gui.py +3 -0
- execsql/utils/mail.py +37 -12
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/README.md +1 -1
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/METADATA +122 -63
- execsql2-2.25.0.dist-info/RECORD +140 -0
- execsql2-2.25.0.dist-info/entry_points.txt +2 -0
- execsql2-2.23.1.dist-info/RECORD +0 -124
- execsql2-2.23.1.dist-info/entry_points.txt +0 -3
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/WHEEL +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/licenses/NOTICE +0 -0
execsql/script/parser.py
CHANGED
|
@@ -257,8 +257,15 @@ class _BlockFrame:
|
|
|
257
257
|
# ---------------------------------------------------------------------------
|
|
258
258
|
|
|
259
259
|
|
|
260
|
-
def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
261
|
-
"""Core parsing logic: convert an iterable of lines into a Script AST.
|
|
260
|
+
def _parse_lines(lines: Iterable[str], source_name: str, errors: list[ErrInfo] | None = None) -> Script:
|
|
261
|
+
"""Core parsing logic: convert an iterable of lines into a Script AST.
|
|
262
|
+
|
|
263
|
+
A block-structure error raises :class:`ErrInfo`, unless *errors* is a
|
|
264
|
+
list: then each error is appended to it and parsing recovers (a closing
|
|
265
|
+
keyword with nothing to close is skipped, one that skips past an open
|
|
266
|
+
block closes that block too, and blocks still open at the end are
|
|
267
|
+
closed), so the whole script is parsed.
|
|
268
|
+
"""
|
|
262
269
|
body: list[Node] = []
|
|
263
270
|
block_stack: list[_BlockFrame] = []
|
|
264
271
|
in_block_comment = False
|
|
@@ -288,6 +295,38 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
288
295
|
return node.body
|
|
289
296
|
return body # pragma: no cover — defensive fallback
|
|
290
297
|
|
|
298
|
+
def _structural_error(exc: ErrInfo) -> None:
|
|
299
|
+
"""Raise *exc*, or record it when the caller collects errors and lets parsing recover."""
|
|
300
|
+
if errors is None:
|
|
301
|
+
raise exc
|
|
302
|
+
errors.append(exc)
|
|
303
|
+
|
|
304
|
+
def _close_block(frame: _BlockFrame, line_no: int) -> None:
|
|
305
|
+
"""Give a popped block its end line and add it to the enclosing body."""
|
|
306
|
+
frame.node.span = SourceSpan(source_name, frame.start_line, line_no)
|
|
307
|
+
_current_body().append(frame.node)
|
|
308
|
+
|
|
309
|
+
def _pop_block(kind: str, line: str, line_no: int, no_match_msg: str) -> _BlockFrame | None:
|
|
310
|
+
"""Pop the open block of *kind* that the closing keyword on *line* ends.
|
|
311
|
+
|
|
312
|
+
``None`` (after reporting the error) when no such block is open. A
|
|
313
|
+
block of another kind opened inside it and left open is an error of
|
|
314
|
+
its own; when recovering, it is closed here too.
|
|
315
|
+
"""
|
|
316
|
+
if block_stack and block_stack[-1].kind == kind:
|
|
317
|
+
return block_stack.pop()
|
|
318
|
+
if errors is None:
|
|
319
|
+
msg = _unclosed_block_msg(block_stack[-1]) if block_stack else no_match_msg
|
|
320
|
+
raise ErrInfo(type="cmd", command_text=line, other_msg=msg)
|
|
321
|
+
if not any(frame.kind == kind for frame in block_stack):
|
|
322
|
+
errors.append(ErrInfo(type="cmd", command_text=line, other_msg=no_match_msg))
|
|
323
|
+
return None
|
|
324
|
+
while block_stack[-1].kind != kind:
|
|
325
|
+
frame = block_stack.pop()
|
|
326
|
+
errors.append(ErrInfo(type="cmd", command_text=line, other_msg=_unclosed_block_msg(frame)))
|
|
327
|
+
_close_block(frame, line_no)
|
|
328
|
+
return block_stack.pop()
|
|
329
|
+
|
|
291
330
|
def _flush_sql(line_no: int) -> None:
|
|
292
331
|
"""If there's accumulated SQL text, emit a SqlStatement node."""
|
|
293
332
|
nonlocal sql_accum, sql_start_line
|
|
@@ -322,6 +361,7 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
322
361
|
if doc_text and isinstance(frame.node, ScriptBlock):
|
|
323
362
|
frame.node.doc = doc_text
|
|
324
363
|
|
|
364
|
+
file_lineno = 0
|
|
325
365
|
for file_lineno, raw_line in enumerate(lines, 1):
|
|
326
366
|
line = raw_line.rstrip()
|
|
327
367
|
|
|
@@ -485,14 +525,15 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
485
525
|
_flush_sql(file_lineno)
|
|
486
526
|
in_sql_block = False
|
|
487
527
|
if not block_stack or block_stack[-1].kind != "sqlblock":
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
528
|
+
_structural_error(
|
|
529
|
+
ErrInfo(
|
|
530
|
+
type="cmd",
|
|
531
|
+
command_text=line,
|
|
532
|
+
other_msg=f"Unmatched END SQL on line {file_lineno} of {source_name}.",
|
|
533
|
+
),
|
|
492
534
|
)
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
_current_body().append(frame.node)
|
|
535
|
+
continue
|
|
536
|
+
_close_block(block_stack.pop(), file_lineno)
|
|
496
537
|
continue
|
|
497
538
|
|
|
498
539
|
# Inside a SQL block, non-END SQL metacommands are silently
|
|
@@ -517,12 +558,18 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
517
558
|
if paramexpr:
|
|
518
559
|
wp = _WITH_PARAMS_RX.match(paramexpr)
|
|
519
560
|
if not wp:
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
561
|
+
_structural_error(
|
|
562
|
+
ErrInfo(
|
|
563
|
+
type="cmd",
|
|
564
|
+
command_text=line,
|
|
565
|
+
other_msg=f"Invalid BEGIN SCRIPT metacommand on line {file_lineno} of file {source_name}.",
|
|
566
|
+
),
|
|
524
567
|
)
|
|
525
|
-
|
|
568
|
+
else:
|
|
569
|
+
try:
|
|
570
|
+
param_defs = _parse_param_defs(wp.group("params"), file_lineno, source_name)
|
|
571
|
+
except ErrInfo as exc:
|
|
572
|
+
_structural_error(exc)
|
|
526
573
|
block_stack.append(
|
|
527
574
|
_BlockFrame(
|
|
528
575
|
ScriptBlock(
|
|
@@ -543,38 +590,36 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
543
590
|
end_name = m.group("name")
|
|
544
591
|
if end_name is not None:
|
|
545
592
|
end_name = end_name.lower()
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
type="cmd",
|
|
563
|
-
command_text=line,
|
|
564
|
-
other_msg=f"Mismatched script name in the END SCRIPT metacommand on line {file_lineno} of file {source_name}.",
|
|
593
|
+
closed = _pop_block(
|
|
594
|
+
"script",
|
|
595
|
+
line,
|
|
596
|
+
file_lineno,
|
|
597
|
+
f"END SCRIPT on line {file_lineno} of {source_name} has no matching BEGIN SCRIPT.",
|
|
598
|
+
)
|
|
599
|
+
if closed is None:
|
|
600
|
+
continue
|
|
601
|
+
script_node = cast(ScriptBlock, closed.node)
|
|
602
|
+
if end_name is not None and end_name != script_node.name:
|
|
603
|
+
_structural_error(
|
|
604
|
+
ErrInfo(
|
|
605
|
+
type="cmd",
|
|
606
|
+
command_text=line,
|
|
607
|
+
other_msg=f"Mismatched script name in the END SCRIPT metacommand on line {file_lineno} of file {source_name}.",
|
|
608
|
+
),
|
|
565
609
|
)
|
|
566
610
|
if sql_accum:
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
611
|
+
_structural_error(
|
|
612
|
+
ErrInfo(
|
|
613
|
+
type="cmd",
|
|
614
|
+
command_text=line,
|
|
615
|
+
other_msg=(
|
|
616
|
+
f"Incomplete SQL statement\n ({sql_accum})\n"
|
|
617
|
+
f"at END SCRIPT metacommand on line {file_lineno} of file {source_name}."
|
|
618
|
+
),
|
|
573
619
|
),
|
|
574
620
|
)
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
_current_body().append(frame.node)
|
|
621
|
+
_flush_sql(file_lineno)
|
|
622
|
+
_close_block(closed, file_lineno)
|
|
578
623
|
continue
|
|
579
624
|
|
|
580
625
|
# -- IF (block form) --
|
|
@@ -612,11 +657,14 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
612
657
|
m = _ELSEIF_RX.match(cmd_text)
|
|
613
658
|
if m:
|
|
614
659
|
if not block_stack or block_stack[-1].kind != "if":
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
660
|
+
_structural_error(
|
|
661
|
+
ErrInfo(
|
|
662
|
+
type="cmd",
|
|
663
|
+
command_text=line,
|
|
664
|
+
other_msg=f"ELSEIF on line {file_lineno} of {source_name} has no matching IF.",
|
|
665
|
+
),
|
|
619
666
|
)
|
|
667
|
+
continue
|
|
620
668
|
frame = block_stack[-1]
|
|
621
669
|
frame._in_else = False
|
|
622
670
|
frame._in_elseif = True
|
|
@@ -633,11 +681,14 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
633
681
|
m = _ANDIF_RX.match(cmd_text)
|
|
634
682
|
if m:
|
|
635
683
|
if not block_stack or block_stack[-1].kind != "if":
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
684
|
+
_structural_error(
|
|
685
|
+
ErrInfo(
|
|
686
|
+
type="cmd",
|
|
687
|
+
command_text=line,
|
|
688
|
+
other_msg=f"ANDIF on line {file_lineno} of {source_name} has no matching IF.",
|
|
689
|
+
),
|
|
640
690
|
)
|
|
691
|
+
continue
|
|
641
692
|
modifier = ConditionModifier(
|
|
642
693
|
kind="AND",
|
|
643
694
|
condition=m.group("cond").strip(),
|
|
@@ -655,11 +706,14 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
655
706
|
m = _ORIF_RX.match(cmd_text)
|
|
656
707
|
if m:
|
|
657
708
|
if not block_stack or block_stack[-1].kind != "if":
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
709
|
+
_structural_error(
|
|
710
|
+
ErrInfo(
|
|
711
|
+
type="cmd",
|
|
712
|
+
command_text=line,
|
|
713
|
+
other_msg=f"ORIF on line {file_lineno} of {source_name} has no matching IF.",
|
|
714
|
+
),
|
|
662
715
|
)
|
|
716
|
+
continue
|
|
663
717
|
modifier = ConditionModifier(
|
|
664
718
|
kind="OR",
|
|
665
719
|
condition=m.group("cond").strip(),
|
|
@@ -677,11 +731,14 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
677
731
|
m = _ELSE_RX.match(cmd_text)
|
|
678
732
|
if m:
|
|
679
733
|
if not block_stack or block_stack[-1].kind != "if":
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
734
|
+
_structural_error(
|
|
735
|
+
ErrInfo(
|
|
736
|
+
type="cmd",
|
|
737
|
+
command_text=line,
|
|
738
|
+
other_msg=f"ELSE on line {file_lineno} of {source_name} has no matching IF.",
|
|
739
|
+
),
|
|
684
740
|
)
|
|
741
|
+
continue
|
|
685
742
|
frame = block_stack[-1]
|
|
686
743
|
frame._in_else = True
|
|
687
744
|
frame._in_elseif = False
|
|
@@ -691,21 +748,14 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
691
748
|
# -- ENDIF --
|
|
692
749
|
m = _ENDIF_RX.match(cmd_text)
|
|
693
750
|
if m:
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
if
|
|
701
|
-
|
|
702
|
-
type="cmd",
|
|
703
|
-
command_text=line,
|
|
704
|
-
other_msg=_unclosed_block_msg(block_stack[-1]),
|
|
705
|
-
)
|
|
706
|
-
frame = block_stack.pop()
|
|
707
|
-
frame.node.span = SourceSpan(source_name, frame.start_line, file_lineno)
|
|
708
|
-
_current_body().append(frame.node)
|
|
751
|
+
closed = _pop_block(
|
|
752
|
+
"if",
|
|
753
|
+
line,
|
|
754
|
+
file_lineno,
|
|
755
|
+
f"ENDIF on line {file_lineno} of {source_name} has no matching IF.",
|
|
756
|
+
)
|
|
757
|
+
if closed is not None:
|
|
758
|
+
_close_block(closed, file_lineno)
|
|
709
759
|
continue
|
|
710
760
|
|
|
711
761
|
# -- LOOP --
|
|
@@ -728,21 +778,14 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
728
778
|
# -- ENDLOOP --
|
|
729
779
|
m = _ENDLOOP_RX.match(cmd_text)
|
|
730
780
|
if m:
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
if
|
|
738
|
-
|
|
739
|
-
type="cmd",
|
|
740
|
-
command_text=line,
|
|
741
|
-
other_msg=_unclosed_block_msg(block_stack[-1]),
|
|
742
|
-
)
|
|
743
|
-
frame = block_stack.pop()
|
|
744
|
-
frame.node.span = SourceSpan(source_name, frame.start_line, file_lineno)
|
|
745
|
-
_current_body().append(frame.node)
|
|
781
|
+
closed = _pop_block(
|
|
782
|
+
"loop",
|
|
783
|
+
line,
|
|
784
|
+
file_lineno,
|
|
785
|
+
f"ENDLOOP on line {file_lineno} of {source_name} has no matching LOOP.",
|
|
786
|
+
)
|
|
787
|
+
if closed is not None:
|
|
788
|
+
_close_block(closed, file_lineno)
|
|
746
789
|
continue
|
|
747
790
|
|
|
748
791
|
# -- BEGIN BATCH --
|
|
@@ -761,21 +804,14 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
761
804
|
# -- END BATCH --
|
|
762
805
|
m = _END_BATCH_RX.match(cmd_text)
|
|
763
806
|
if m:
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
if
|
|
771
|
-
|
|
772
|
-
type="cmd",
|
|
773
|
-
command_text=line,
|
|
774
|
-
other_msg=_unclosed_block_msg(block_stack[-1]),
|
|
775
|
-
)
|
|
776
|
-
frame = block_stack.pop()
|
|
777
|
-
frame.node.span = SourceSpan(source_name, frame.start_line, file_lineno)
|
|
778
|
-
_current_body().append(frame.node)
|
|
807
|
+
closed = _pop_block(
|
|
808
|
+
"batch",
|
|
809
|
+
line,
|
|
810
|
+
file_lineno,
|
|
811
|
+
f"END BATCH on line {file_lineno} of {source_name} has no matching BEGIN BATCH.",
|
|
812
|
+
)
|
|
813
|
+
if closed is not None:
|
|
814
|
+
_close_block(closed, file_lineno)
|
|
779
815
|
continue
|
|
780
816
|
|
|
781
817
|
# -- INCLUDE --
|
|
@@ -846,24 +882,33 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
846
882
|
# Flush any trailing consecutive -- comments
|
|
847
883
|
_flush_line_comments()
|
|
848
884
|
|
|
849
|
-
# Unclosed blocks
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
885
|
+
# Unclosed blocks, innermost first
|
|
886
|
+
for frame in reversed(block_stack):
|
|
887
|
+
_structural_error(
|
|
888
|
+
ErrInfo(
|
|
889
|
+
type="error",
|
|
890
|
+
other_msg=f"Unmatched {frame.kind.upper()} block starting on line {frame.start_line} at end of file {source_name}.",
|
|
891
|
+
),
|
|
855
892
|
)
|
|
856
893
|
|
|
857
894
|
# Trailing SQL without semicolon
|
|
858
895
|
if sql_accum:
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
896
|
+
_structural_error(
|
|
897
|
+
ErrInfo(
|
|
898
|
+
type="error",
|
|
899
|
+
other_msg=(
|
|
900
|
+
f"Incomplete SQL statement starting on line {sql_start_line} at end of file {source_name}."
|
|
901
|
+
+ (" Metacommands must be prefixed with '-- !x!'." if source_name == "<inline>" else "")
|
|
902
|
+
),
|
|
864
903
|
),
|
|
865
904
|
)
|
|
866
905
|
|
|
906
|
+
# When recovering, keep the trailing SQL and close what is still open;
|
|
907
|
+
# otherwise both are already empty here.
|
|
908
|
+
_flush_sql(file_lineno)
|
|
909
|
+
while block_stack:
|
|
910
|
+
_close_block(block_stack.pop(), file_lineno)
|
|
911
|
+
|
|
867
912
|
return Script(source=source_name, body=body)
|
|
868
913
|
|
|
869
914
|
|
|
@@ -872,7 +917,7 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
872
917
|
# ---------------------------------------------------------------------------
|
|
873
918
|
|
|
874
919
|
|
|
875
|
-
def parse_script(filename: str, encoding: str = "utf-8") -> Script:
|
|
920
|
+
def parse_script(filename: str, encoding: str = "utf-8", *, errors: list[ErrInfo] | None = None) -> Script:
|
|
876
921
|
"""Parse a ``.sql`` file and return a :class:`Script` AST.
|
|
877
922
|
|
|
878
923
|
Reads the file directly (no dependency on runtime state) so it can be
|
|
@@ -881,22 +926,26 @@ def parse_script(filename: str, encoding: str = "utf-8") -> Script:
|
|
|
881
926
|
Args:
|
|
882
927
|
filename: Path to the SQL script file.
|
|
883
928
|
encoding: File encoding (default ``utf-8``).
|
|
929
|
+
errors: When a list, block-structure errors are appended to it
|
|
930
|
+
and parsing recovers instead of raising (see ``_parse_lines``).
|
|
884
931
|
|
|
885
932
|
Returns:
|
|
886
933
|
A :class:`Script` tree representing the parsed file.
|
|
887
934
|
"""
|
|
888
935
|
text = Path(filename).read_text(encoding=encoding)
|
|
889
|
-
return _parse_lines(text.splitlines(), filename)
|
|
936
|
+
return _parse_lines(text.splitlines(), filename, errors)
|
|
890
937
|
|
|
891
938
|
|
|
892
|
-
def parse_string(content: str, source_name: str = "<inline>") -> Script:
|
|
939
|
+
def parse_string(content: str, source_name: str = "<inline>", *, errors: list[ErrInfo] | None = None) -> Script:
|
|
893
940
|
"""Parse an inline script string and return a :class:`Script` AST.
|
|
894
941
|
|
|
895
942
|
Args:
|
|
896
943
|
content: The script content as a string.
|
|
897
944
|
source_name: Name to use in source spans (default ``"<inline>"``).
|
|
945
|
+
errors: When a list, block-structure errors are appended to it
|
|
946
|
+
and parsing recovers instead of raising (see ``_parse_lines``).
|
|
898
947
|
|
|
899
948
|
Returns:
|
|
900
949
|
A :class:`Script` tree representing the parsed content.
|
|
901
950
|
"""
|
|
902
|
-
return _parse_lines(content.splitlines(), source_name)
|
|
951
|
+
return _parse_lines(content.splitlines(), source_name, errors)
|
execsql/state.py
CHANGED
|
@@ -33,6 +33,7 @@ import threading
|
|
|
33
33
|
import types
|
|
34
34
|
from dataclasses import dataclass
|
|
35
35
|
from typing import TYPE_CHECKING, Any
|
|
36
|
+
from collections.abc import Callable
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
@dataclass
|
|
@@ -96,10 +97,12 @@ if TYPE_CHECKING:
|
|
|
96
97
|
from execsql.script import (
|
|
97
98
|
CounterVars,
|
|
98
99
|
MetaCommandList,
|
|
99
|
-
ScriptCmd,
|
|
100
100
|
ScriptExecSpec,
|
|
101
101
|
SubVarSet,
|
|
102
102
|
)
|
|
103
|
+
from execsql.script.executor import ExecutingStatement
|
|
104
|
+
from execsql.exceptions import ErrInfo
|
|
105
|
+
from execsql.manifest import RunManifest
|
|
103
106
|
from execsql.utils.fileio import FileWriter, Logger, TempFileMgr
|
|
104
107
|
from execsql.utils.mail import MailSpec
|
|
105
108
|
from execsql.utils.timer import Timer
|
|
@@ -143,6 +146,7 @@ __all__ = [
|
|
|
143
146
|
"profile_data",
|
|
144
147
|
# Debug REPL
|
|
145
148
|
"step_mode",
|
|
149
|
+
"prompt_input",
|
|
146
150
|
# Version
|
|
147
151
|
"primary_vno",
|
|
148
152
|
"secondary_vno",
|
|
@@ -223,6 +227,9 @@ _CONTEXT_ATTRS: frozenset[str] = frozenset(
|
|
|
223
227
|
"output",
|
|
224
228
|
"filewriter",
|
|
225
229
|
"tier_notices_shown",
|
|
230
|
+
"manifest",
|
|
231
|
+
"halt_error",
|
|
232
|
+
"halt_output_error",
|
|
226
233
|
# Lazy singletons
|
|
227
234
|
"counters",
|
|
228
235
|
"timer",
|
|
@@ -239,6 +246,7 @@ _CONTEXT_ATTRS: frozenset[str] = frozenset(
|
|
|
239
246
|
"profile_data",
|
|
240
247
|
# Debug REPL
|
|
241
248
|
"step_mode",
|
|
249
|
+
"prompt_input",
|
|
242
250
|
# AST executor
|
|
243
251
|
"ast_scripts",
|
|
244
252
|
"include_chain",
|
|
@@ -277,6 +285,9 @@ class RuntimeContext:
|
|
|
277
285
|
"output",
|
|
278
286
|
"filewriter",
|
|
279
287
|
"tier_notices_shown",
|
|
288
|
+
"manifest",
|
|
289
|
+
"halt_error",
|
|
290
|
+
"halt_output_error",
|
|
280
291
|
# Lazy singletons
|
|
281
292
|
"counters",
|
|
282
293
|
"timer",
|
|
@@ -293,6 +304,7 @@ class RuntimeContext:
|
|
|
293
304
|
"profile_data",
|
|
294
305
|
# Debug REPL
|
|
295
306
|
"step_mode",
|
|
307
|
+
"prompt_input",
|
|
296
308
|
# AST executor
|
|
297
309
|
"ast_scripts",
|
|
298
310
|
"include_chain",
|
|
@@ -305,7 +317,7 @@ class RuntimeContext:
|
|
|
305
317
|
self.logfile_encoding: str = "utf8"
|
|
306
318
|
|
|
307
319
|
# Runtime flags
|
|
308
|
-
self.last_command:
|
|
320
|
+
self.last_command: ExecutingStatement | None = None
|
|
309
321
|
self.upass: str | None = None
|
|
310
322
|
self.err_halt_writespec: WriteSpec | None = None
|
|
311
323
|
self.err_halt_email: MailSpec | None = None
|
|
@@ -326,6 +338,12 @@ class RuntimeContext:
|
|
|
326
338
|
# DBMS names whose best-effort support notice has already been shown,
|
|
327
339
|
# so repeated connections to the same backend announce it only once.
|
|
328
340
|
self.tier_notices_shown: set[str] = set()
|
|
341
|
+
# ``--manifest`` recorder for this run, or None.
|
|
342
|
+
self.manifest: RunManifest | None = None
|
|
343
|
+
# The error that halted this run (set by ``exit_now``), or None.
|
|
344
|
+
self.halt_error: ErrInfo | None = None
|
|
345
|
+
# File output lost when the run halted (set by ``exit_now``), or None.
|
|
346
|
+
self.halt_output_error: ErrInfo | None = None
|
|
329
347
|
|
|
330
348
|
# Lazy singletons
|
|
331
349
|
self.counters: CounterVars | None = None
|
|
@@ -347,6 +365,11 @@ class RuntimeContext:
|
|
|
347
365
|
|
|
348
366
|
# Debug REPL — True after a ``next`` command; engine re-enters REPL after next statement.
|
|
349
367
|
self.step_mode: bool = False
|
|
368
|
+
# Set while input typed at a BREAKPOINT prompt runs:
|
|
369
|
+
# called with each SQL statement's result, as ``(column_names, rows)``
|
|
370
|
+
# or ``None``. An error that would halt the run instead returns to the
|
|
371
|
+
# prompt (see ``exit_now``).
|
|
372
|
+
self.prompt_input: Callable[[tuple[list[str], list] | None], None] | None = None
|
|
350
373
|
|
|
351
374
|
# AST executor — script block registry and include-chain tracking.
|
|
352
375
|
self.ast_scripts: dict = {}
|
|
@@ -355,9 +378,7 @@ class RuntimeContext:
|
|
|
355
378
|
# nesting construct: top-level script, EXECUTE SCRIPT calls, INCLUDE'd
|
|
356
379
|
# files, IF/ELSEIF/ELSE branches, LOOP iterations, BATCH blocks. See
|
|
357
380
|
# :class:`ExecFrame` for frame structure. Read by the debug REPL's
|
|
358
|
-
# ``.stack`` command and ``DEBUG WRITE COMMANDLISTSTACK
|
|
359
|
-
# execution context — the legacy ``commandliststack`` only records
|
|
360
|
-
# SCRIPT call frames and is therefore insufficient for the debugger.
|
|
381
|
+
# ``.stack`` command and ``DEBUG WRITE COMMANDLISTSTACK``.
|
|
361
382
|
self.ast_exec_stack: list[ExecFrame] = []
|
|
362
383
|
|
|
363
384
|
# -----------------------------------------------------------------
|
|
@@ -543,7 +564,7 @@ def reset() -> None:
|
|
|
543
564
|
"""Reset all mutable state to initial values.
|
|
544
565
|
|
|
545
566
|
Intended for use in tests. Creates a fresh :class:`RuntimeContext`,
|
|
546
|
-
preserving only the ``filewriter``
|
|
567
|
+
preserving only the ``filewriter`` thread (which is ``atexit``-managed
|
|
547
568
|
and must not be discarded while alive).
|
|
548
569
|
"""
|
|
549
570
|
ctx = _get_ctx()
|
|
@@ -585,8 +606,9 @@ def initialize(
|
|
|
585
606
|
Note:
|
|
586
607
|
``subvars``, ``status``, ``output``, ``filewriter``, and ``exec_log``
|
|
587
608
|
are **not** set here because they require CLI-specific arguments
|
|
588
|
-
(script path,
|
|
589
|
-
assigned directly in ``_run()``
|
|
609
|
+
(script path, output settings, local class definitions). Those are
|
|
610
|
+
assigned directly in ``_run()`` (or :func:`execsql.api.run`) before
|
|
611
|
+
and after this call.
|
|
590
612
|
"""
|
|
591
613
|
import execsql.db.base as _db_base
|
|
592
614
|
import execsql.exporters.base as _exporters_base
|
|
@@ -605,10 +627,11 @@ def initialize(
|
|
|
605
627
|
ctx.conditionallist = conditional_table
|
|
606
628
|
|
|
607
629
|
# Discover and register metacommand plugins via entry points.
|
|
608
|
-
# Runs here (not at import time) to avoid I/O side effects during import
|
|
609
|
-
|
|
630
|
+
# Runs here (not at import time) to avoid I/O side effects during import,
|
|
631
|
+
# and only once per dispatch table: every run shares the table.
|
|
632
|
+
from execsql.plugins import register_metacommand_plugins_once
|
|
610
633
|
|
|
611
|
-
|
|
634
|
+
register_metacommand_plugins_once(dispatch_table)
|
|
612
635
|
|
|
613
636
|
|
|
614
637
|
# ---------------------------------------------------------------------------
|
execsql/state.pyi
CHANGED
|
@@ -5,11 +5,14 @@ import threading
|
|
|
5
5
|
from contextlib import AbstractContextManager
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from typing import Any
|
|
8
|
+
from collections.abc import Callable
|
|
8
9
|
|
|
9
10
|
from execsql.config import ConfigData, StatObj
|
|
10
11
|
from execsql.db.base import DatabasePool
|
|
11
12
|
from execsql.exporters.base import ExportMetadata, WriteSpec
|
|
12
13
|
from execsql.script import CounterVars, MetaCommandList, ScriptExecSpec, SubVarSet
|
|
14
|
+
from execsql.exceptions import ErrInfo
|
|
15
|
+
from execsql.manifest import RunManifest
|
|
13
16
|
from execsql.utils.fileio import FileWriter, Logger, TempFileMgr
|
|
14
17
|
from execsql.utils.mail import MailSpec
|
|
15
18
|
from execsql.utils.timer import Timer
|
|
@@ -53,6 +56,9 @@ status: StatObj
|
|
|
53
56
|
output: Any
|
|
54
57
|
tier_notices_shown: set[str]
|
|
55
58
|
filewriter: FileWriter | None
|
|
59
|
+
manifest: RunManifest | None
|
|
60
|
+
halt_error: ErrInfo | None
|
|
61
|
+
halt_output_error: ErrInfo | None
|
|
56
62
|
counters: CounterVars
|
|
57
63
|
timer: Timer
|
|
58
64
|
dbs: DatabasePool
|
|
@@ -65,6 +71,7 @@ gui_manager_queue: Any
|
|
|
65
71
|
gui_manager_thread: threading.Thread | None
|
|
66
72
|
profile_data: list[tuple[Any, ...]] | None
|
|
67
73
|
step_mode: bool
|
|
74
|
+
prompt_input: Callable[[tuple[list[str], list[Any]] | None], None] | None
|
|
68
75
|
ast_scripts: dict[str, Any]
|
|
69
76
|
include_chain: list[str]
|
|
70
77
|
ast_exec_stack: list[ExecFrame]
|
|
@@ -87,6 +94,9 @@ class RuntimeContext:
|
|
|
87
94
|
output: Any
|
|
88
95
|
tier_notices_shown: set[str]
|
|
89
96
|
filewriter: FileWriter | None
|
|
97
|
+
manifest: RunManifest | None
|
|
98
|
+
halt_error: ErrInfo | None
|
|
99
|
+
halt_output_error: ErrInfo | None
|
|
90
100
|
counters: CounterVars | None
|
|
91
101
|
timer: Timer | None
|
|
92
102
|
dbs: DatabasePool | None
|
|
@@ -99,6 +109,7 @@ class RuntimeContext:
|
|
|
99
109
|
gui_manager_thread: threading.Thread | None
|
|
100
110
|
profile_data: list[tuple[Any, ...]] | None
|
|
101
111
|
step_mode: bool
|
|
112
|
+
prompt_input: Callable[[tuple[list[str], list[Any]] | None], None] | None
|
|
102
113
|
ast_scripts: dict[str, Any]
|
|
103
114
|
include_chain: list[str]
|
|
104
115
|
ast_exec_stack: list[ExecFrame]
|