execsql2 2.15.8__py3-none-any.whl → 2.16.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- execsql/__init__.py +8 -3
- execsql/api.py +580 -0
- execsql/cli/__init__.py +123 -0
- execsql/cli/lint_ast.py +439 -0
- execsql/cli/run.py +113 -102
- execsql/config.py +29 -4
- execsql/db/access.py +1 -0
- execsql/db/base.py +4 -1
- execsql/db/dsn.py +3 -2
- execsql/db/duckdb.py +1 -1
- execsql/db/factory.py +3 -0
- execsql/db/firebird.py +2 -1
- execsql/db/mysql.py +2 -1
- execsql/db/oracle.py +2 -1
- execsql/db/postgres.py +2 -1
- execsql/db/sqlite.py +1 -1
- execsql/db/sqlserver.py +3 -2
- execsql/debug/repl.py +27 -10
- execsql/exporters/base.py +6 -4
- execsql/exporters/delimited.py +11 -3
- execsql/exporters/pretty.py +9 -12
- execsql/gui/tui.py +59 -2
- execsql/metacommands/__init__.py +3 -0
- execsql/metacommands/conditions.py +20 -2
- execsql/metacommands/connect.py +1 -1
- execsql/metacommands/control.py +8 -14
- execsql/metacommands/debug.py +6 -4
- execsql/metacommands/io_export.py +117 -315
- execsql/metacommands/io_fileops.py +7 -13
- execsql/metacommands/io_write.py +1 -1
- execsql/metacommands/script_ext.py +8 -5
- execsql/metacommands/upsert.py +40 -0
- execsql/models.py +8 -12
- execsql/plugins.py +414 -0
- execsql/script/__init__.py +36 -12
- execsql/script/ast.py +562 -0
- execsql/script/engine.py +59 -368
- execsql/script/executor.py +833 -0
- execsql/script/parser.py +663 -0
- execsql/script/variables.py +11 -0
- execsql/state.py +55 -2
- execsql/utils/crypto.py +14 -10
- execsql/utils/errors.py +31 -8
- execsql/utils/gui.py +139 -17
- execsql/utils/mail.py +15 -12
- {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/METADATA +59 -1
- {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/RECORD +66 -60
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/WHEEL +0 -0
- {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/licenses/NOTICE +0 -0
execsql/script/engine.py
CHANGED
|
@@ -26,7 +26,6 @@ Functions:
|
|
|
26
26
|
- :func:`read_sqlstring` — parses an inline script string into a new :class:`CommandList`.
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
-
import copy
|
|
30
29
|
import datetime
|
|
31
30
|
import os
|
|
32
31
|
import re
|
|
@@ -36,9 +35,8 @@ from typing import Any
|
|
|
36
35
|
|
|
37
36
|
import execsql.state as _state
|
|
38
37
|
from execsql.exceptions import ErrInfo
|
|
39
|
-
from execsql.script.variables import LocalSubVarSet,
|
|
38
|
+
from execsql.script.variables import LocalSubVarSet, SubVarSet
|
|
40
39
|
from execsql.utils.errors import exception_desc
|
|
41
|
-
from execsql.utils.fileio import EncodedFile
|
|
42
40
|
|
|
43
41
|
__all__ = [
|
|
44
42
|
"MetaCommand",
|
|
@@ -47,16 +45,10 @@ __all__ = [
|
|
|
47
45
|
"MetacommandStmt",
|
|
48
46
|
"ScriptCmd",
|
|
49
47
|
"CommandList",
|
|
50
|
-
"CommandListWhileLoop",
|
|
51
|
-
"CommandListUntilLoop",
|
|
52
|
-
"ScriptFile",
|
|
53
48
|
"ScriptExecSpec",
|
|
54
49
|
"set_system_vars",
|
|
55
50
|
"substitute_vars",
|
|
56
|
-
"runscripts",
|
|
57
51
|
"current_script_line",
|
|
58
|
-
"read_sqlfile",
|
|
59
|
-
"read_sqlstring",
|
|
60
52
|
]
|
|
61
53
|
|
|
62
54
|
|
|
@@ -556,93 +548,6 @@ class CommandList:
|
|
|
556
548
|
return scriptcmd
|
|
557
549
|
|
|
558
550
|
|
|
559
|
-
class CommandListWhileLoop(CommandList):
|
|
560
|
-
"""A :class:`CommandList` that repeats its commands while a condition evaluates to true."""
|
|
561
|
-
|
|
562
|
-
# Subclass of CommandList that loops WHILE a condition is met.
|
|
563
|
-
def __init__(
|
|
564
|
-
self,
|
|
565
|
-
cmdlist: list[ScriptCmd],
|
|
566
|
-
listname: str,
|
|
567
|
-
paramnames: list[str] | None,
|
|
568
|
-
loopcondition: str,
|
|
569
|
-
) -> None:
|
|
570
|
-
super().__init__(cmdlist, listname, paramnames)
|
|
571
|
-
self.loopcondition = loopcondition
|
|
572
|
-
|
|
573
|
-
def run_next(self) -> None:
|
|
574
|
-
if self.cmdptr == 0:
|
|
575
|
-
self.init_if_level = len(_state.if_stack.if_levels)
|
|
576
|
-
from execsql.parser import CondParser
|
|
577
|
-
|
|
578
|
-
if not CondParser(substitute_vars(self.loopcondition)).parse().eval():
|
|
579
|
-
raise StopIteration
|
|
580
|
-
if self.cmdptr > len(self.cmdlist) - 1:
|
|
581
|
-
self.check_iflevels()
|
|
582
|
-
self.cmdptr = 0
|
|
583
|
-
else:
|
|
584
|
-
self.run_and_increment()
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
class CommandListUntilLoop(CommandList):
|
|
588
|
-
"""A :class:`CommandList` that repeats its commands until a condition evaluates to true."""
|
|
589
|
-
|
|
590
|
-
# Subclass of CommandList that loops UNTIL a condition is met.
|
|
591
|
-
def __init__(
|
|
592
|
-
self,
|
|
593
|
-
cmdlist: list[ScriptCmd],
|
|
594
|
-
listname: str,
|
|
595
|
-
paramnames: list[str] | None,
|
|
596
|
-
loopcondition: str,
|
|
597
|
-
) -> None:
|
|
598
|
-
super().__init__(cmdlist, listname, paramnames)
|
|
599
|
-
self.loopcondition = loopcondition
|
|
600
|
-
|
|
601
|
-
def run_next(self) -> None:
|
|
602
|
-
if self.cmdptr == 0:
|
|
603
|
-
self.init_if_level = len(_state.if_stack.if_levels)
|
|
604
|
-
if self.cmdptr > len(self.cmdlist) - 1:
|
|
605
|
-
self.check_iflevels()
|
|
606
|
-
from execsql.parser import CondParser
|
|
607
|
-
|
|
608
|
-
if CondParser(substitute_vars(self.loopcondition)).parse().eval():
|
|
609
|
-
raise StopIteration
|
|
610
|
-
self.cmdptr = 0
|
|
611
|
-
else:
|
|
612
|
-
self.run_and_increment()
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
# ---------------------------------------------------------------------------
|
|
616
|
-
# ScriptFile
|
|
617
|
-
# ---------------------------------------------------------------------------
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
class ScriptFile(EncodedFile):
|
|
621
|
-
"""An iterable file reader that tracks the current line number.
|
|
622
|
-
|
|
623
|
-
Wraps :class:`~execsql.utils.fileio.EncodedFile` and increments
|
|
624
|
-
:attr:`lno` on each ``next()`` call so that callers always know which
|
|
625
|
-
source line is being processed.
|
|
626
|
-
"""
|
|
627
|
-
|
|
628
|
-
# A file reader that returns lines and records the line number.
|
|
629
|
-
def __init__(self, scriptfname: str, file_encoding: str) -> None:
|
|
630
|
-
super().__init__(scriptfname, file_encoding)
|
|
631
|
-
self.lno = 0
|
|
632
|
-
self.f = self.open("r")
|
|
633
|
-
|
|
634
|
-
def __repr__(self) -> str:
|
|
635
|
-
return f"ScriptFile({super().filename!r}, {super().encoding!r})"
|
|
636
|
-
|
|
637
|
-
def __iter__(self) -> Any:
|
|
638
|
-
return self
|
|
639
|
-
|
|
640
|
-
def __next__(self) -> str:
|
|
641
|
-
line = next(self.f)
|
|
642
|
-
self.lno += 1
|
|
643
|
-
return line
|
|
644
|
-
|
|
645
|
-
|
|
646
551
|
# ---------------------------------------------------------------------------
|
|
647
552
|
# ScriptExecSpec
|
|
648
553
|
# ---------------------------------------------------------------------------
|
|
@@ -651,17 +556,12 @@ class ScriptFile(EncodedFile):
|
|
|
651
556
|
class ScriptExecSpec:
|
|
652
557
|
"""Deferred execution specification for a named SCRIPT block.
|
|
653
558
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
execution
|
|
559
|
+
Stores the script ID, argument expression, and loop-type flags at
|
|
560
|
+
construction time. Used by ON ERROR_HALT / ON CANCEL_HALT EXECUTE
|
|
561
|
+
SCRIPT handlers to capture the specification; actual execution is
|
|
562
|
+
handled by the AST executor via :func:`_run_deferred_script`.
|
|
657
563
|
"""
|
|
658
564
|
|
|
659
|
-
# Stores specifications for executing a SCRIPT, for later use.
|
|
660
|
-
args_rx = re.compile(
|
|
661
|
-
r'(?P<param>#?\w+)\s*=\s*(?P<arg>(?:(?:[^"\'\[][^,\)]*)|(?:"[^"]*")|(?:\'[^\']*\')|(?:\[[^\]]*\])))',
|
|
662
|
-
re.I,
|
|
663
|
-
)
|
|
664
|
-
|
|
665
565
|
def __init__(self, **kwargs: Any) -> None:
|
|
666
566
|
self.script_id = kwargs["script_id"].lower()
|
|
667
567
|
if self.script_id not in _state.savedscripts:
|
|
@@ -670,41 +570,13 @@ class ScriptExecSpec:
|
|
|
670
570
|
self.looptype = kwargs["looptype"].upper() if "looptype" in kwargs and kwargs["looptype"] is not None else None
|
|
671
571
|
self.loopcond = kwargs.get("loopcond")
|
|
672
572
|
|
|
673
|
-
def execute(self) -> None:
|
|
674
|
-
# Copy the saved script to avoid erasing saved script commands during execution.
|
|
675
|
-
cl = copy.deepcopy(_state.savedscripts[self.script_id])
|
|
676
|
-
# If looping is specified, redirect to appropriate CommandList subclass.
|
|
677
|
-
if self.looptype is not None:
|
|
678
|
-
if self.looptype == "WHILE":
|
|
679
|
-
cl = CommandListWhileLoop(cl.cmdlist, cl.listname, cl.paramnames, self.loopcond)
|
|
680
|
-
else:
|
|
681
|
-
cl = CommandListUntilLoop(cl.cmdlist, cl.listname, cl.paramnames, self.loopcond)
|
|
682
|
-
# If there are any argument expressions, parse the arguments.
|
|
683
|
-
if self.arg_exp is not None:
|
|
684
|
-
all_args = re.findall(self.args_rx, self.arg_exp)
|
|
685
|
-
from execsql.utils.strings import wo_quotes
|
|
686
|
-
|
|
687
|
-
all_cleaned_args = [(ae[0], wo_quotes(ae[1])) for ae in all_args]
|
|
688
|
-
all_prepared_args = [(ae[0] if ae[0][0] == "#" else "#" + ae[0], ae[1]) for ae in all_cleaned_args]
|
|
689
|
-
scriptvarset = ScriptArgSubVarSet()
|
|
690
|
-
for param, arg in all_prepared_args:
|
|
691
|
-
scriptvarset.add_substitution(param, arg)
|
|
692
|
-
cl.set_paramvals(scriptvarset)
|
|
693
|
-
else:
|
|
694
|
-
if cl.paramnames is not None:
|
|
695
|
-
raise ErrInfo(
|
|
696
|
-
"error",
|
|
697
|
-
other_msg=f"Missing expected parameters ({', '.join(cl.paramnames)}) in call to {cl.listname}.",
|
|
698
|
-
)
|
|
699
|
-
_state.commandliststack.append(cl)
|
|
700
|
-
|
|
701
573
|
|
|
702
574
|
# ---------------------------------------------------------------------------
|
|
703
575
|
# Module-level functions
|
|
704
576
|
# ---------------------------------------------------------------------------
|
|
705
577
|
|
|
706
578
|
|
|
707
|
-
def set_static_system_vars() -> None:
|
|
579
|
+
def set_static_system_vars(ctx: Any = None) -> None:
|
|
708
580
|
"""Set system substitution variables that only change on CONNECT or CHDIR.
|
|
709
581
|
|
|
710
582
|
Called once before the execution loop. These values are expensive to compute
|
|
@@ -712,85 +584,102 @@ def set_static_system_vars() -> None:
|
|
|
712
584
|
``CONNECT``, ``USE``, or ``CHDIR`` metacommands. The ``runscripts()`` loop
|
|
713
585
|
calls this once up front; metacommand handlers that change the connection or
|
|
714
586
|
working directory should call it again afterward.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
ctx: Optional :class:`RuntimeContext`. When ``None``, falls through
|
|
590
|
+
to the global ``_state`` module (legacy behavior).
|
|
715
591
|
"""
|
|
716
592
|
import random
|
|
717
593
|
|
|
594
|
+
_s = ctx if ctx is not None else _state
|
|
718
595
|
cwd = str(Path(".").resolve())
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
db =
|
|
723
|
-
|
|
724
|
-
|
|
596
|
+
_s.subvars.add_substitution("$CURRENT_DIR", cwd)
|
|
597
|
+
_s.subvars.add_substitution("$CURRENT_PATH", cwd + os.sep)
|
|
598
|
+
_s.subvars.add_substitution("$CURRENT_ALIAS", _s.dbs.current_alias())
|
|
599
|
+
db = _s.dbs.current()
|
|
600
|
+
_s.subvars.add_substitution("$DB_USER", db.user if db.user else "")
|
|
601
|
+
_s.subvars.add_substitution(
|
|
725
602
|
"$DB_SERVER",
|
|
726
603
|
db.server_name if db.server_name else "",
|
|
727
604
|
)
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
605
|
+
_s.subvars.add_substitution("$DB_NAME", db.db_name)
|
|
606
|
+
_s.subvars.add_substitution("$DB_NEED_PWD", "TRUE" if db.need_passwd else "FALSE")
|
|
607
|
+
_s.subvars.add_substitution("$CURRENT_DBMS", db.type.dbms_id)
|
|
608
|
+
_s.subvars.add_substitution("$CURRENT_DATABASE", db.name())
|
|
609
|
+
_s.subvars.add_substitution("$VERSION1", str(_state.primary_vno))
|
|
610
|
+
_s.subvars.add_substitution("$VERSION2", str(_state.secondary_vno))
|
|
611
|
+
_s.subvars.add_substitution("$VERSION3", str(_state.tertiary_vno))
|
|
735
612
|
# Register lazy providers for $RANDOM and $UUID — computed only when referenced.
|
|
736
|
-
|
|
737
|
-
|
|
613
|
+
_s.subvars.register_lazy("$random", lambda: str(random.random()))
|
|
614
|
+
_s.subvars.register_lazy("$uuid", lambda: str(uuid.uuid4()))
|
|
738
615
|
|
|
739
616
|
|
|
740
|
-
def set_dynamic_system_vars() -> None:
|
|
617
|
+
def set_dynamic_system_vars(ctx: Any = None) -> None:
|
|
741
618
|
"""Refresh system substitution variables that change every statement.
|
|
742
619
|
|
|
743
620
|
Called once per statement in the execution loop. Includes cheap boolean-to-string
|
|
744
621
|
conversions for halt states and autocommit (which can change on any CONFIG or
|
|
745
622
|
AUTOCOMMIT metacommand) plus ``$TIMER`` and lazy-variable cache reset.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
ctx: Optional :class:`RuntimeContext`. When ``None``, falls through
|
|
626
|
+
to the global ``_state`` module (legacy behavior).
|
|
746
627
|
"""
|
|
628
|
+
_s = ctx if ctx is not None else _state
|
|
747
629
|
# Halt/config state vars — cheap to set, can change on any CONFIG metacommand.
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
630
|
+
_s.subvars.add_substitution("$CANCEL_HALT_STATE", "ON" if _s.status.cancel_halt else "OFF")
|
|
631
|
+
_s.subvars.add_substitution("$ERROR_HALT_STATE", "ON" if _s.status.halt_on_err else "OFF")
|
|
632
|
+
_s.subvars.add_substitution(
|
|
751
633
|
"$METACOMMAND_ERROR_HALT_STATE",
|
|
752
|
-
"ON" if
|
|
634
|
+
"ON" if _s.status.halt_on_metacommand_err else "OFF",
|
|
753
635
|
)
|
|
754
|
-
|
|
636
|
+
_s.subvars.add_substitution(
|
|
755
637
|
"$CONSOLE_WAIT_WHEN_ERROR_HALT_STATE",
|
|
756
|
-
"ON" if
|
|
638
|
+
"ON" if _s.conf.gui_wait_on_error_halt else "OFF",
|
|
757
639
|
)
|
|
758
|
-
|
|
759
|
-
db =
|
|
760
|
-
|
|
640
|
+
_s.subvars.add_substitution("$CONSOLE_WAIT_WHEN_DONE_STATE", "ON" if _s.conf.gui_wait_on_exit else "OFF")
|
|
641
|
+
db = _s.dbs.current()
|
|
642
|
+
_s.subvars.add_substitution("$AUTOCOMMIT_STATE", "ON" if db.autocommit else "OFF")
|
|
761
643
|
# $CURRENT_TIME is set per-statement in run_and_increment() for accuracy.
|
|
762
|
-
|
|
763
|
-
|
|
644
|
+
_s.subvars.add_substitution("$TIMER", str(datetime.timedelta(seconds=_s.timer.elapsed())))
|
|
645
|
+
_s.subvars.clear_lazy_cache()
|
|
764
646
|
|
|
765
647
|
|
|
766
|
-
def set_system_vars() -> None:
|
|
648
|
+
def set_system_vars(ctx: Any = None) -> None:
|
|
767
649
|
"""Refresh all built-in system substitution variables.
|
|
768
650
|
|
|
769
651
|
Convenience wrapper that calls both :func:`set_static_system_vars` and
|
|
770
652
|
:func:`set_dynamic_system_vars`. Retained for backward compatibility with
|
|
771
653
|
tests and any external callers.
|
|
772
654
|
"""
|
|
773
|
-
set_static_system_vars()
|
|
774
|
-
set_dynamic_system_vars()
|
|
655
|
+
set_static_system_vars(ctx)
|
|
656
|
+
set_dynamic_system_vars(ctx)
|
|
775
657
|
|
|
776
658
|
|
|
777
659
|
_MAX_SUBSTITUTION_DEPTH = 100
|
|
778
660
|
|
|
779
661
|
|
|
780
|
-
def substitute_vars(command_str: str, localvars: SubVarSet | None = None) -> str:
|
|
781
|
-
"""Expand all ``!!$VAR!!`` tokens in *command_str*, merging *localvars* when provided.
|
|
782
|
-
|
|
662
|
+
def substitute_vars(command_str: str, localvars: SubVarSet | None = None, ctx: Any = None) -> str:
|
|
663
|
+
"""Expand all ``!!$VAR!!`` tokens in *command_str*, merging *localvars* when provided.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
command_str: The string containing ``!!VAR!!`` tokens to expand.
|
|
667
|
+
localvars: Optional local variable overlay to merge with globals.
|
|
668
|
+
ctx: Optional :class:`RuntimeContext`. When ``None``, falls through
|
|
669
|
+
to the global ``_state`` module (legacy behavior).
|
|
670
|
+
"""
|
|
671
|
+
_s = ctx if ctx is not None else _state
|
|
783
672
|
if localvars is not None:
|
|
784
|
-
subs =
|
|
673
|
+
subs = _s.subvars.merge(localvars)
|
|
785
674
|
else:
|
|
786
|
-
subs =
|
|
675
|
+
subs = _s.subvars
|
|
787
676
|
cmdstr = command_str
|
|
788
677
|
subs_made = True
|
|
789
678
|
iterations = 0
|
|
790
679
|
while subs_made:
|
|
791
680
|
subs_made = False
|
|
792
681
|
cmdstr, subs_made = subs.substitute_all(cmdstr)
|
|
793
|
-
cmdstr, any_subbed =
|
|
682
|
+
cmdstr, any_subbed = _s.counters.substitute_all(cmdstr)
|
|
794
683
|
subs_made = subs_made or any_subbed
|
|
795
684
|
iterations += 1
|
|
796
685
|
if iterations >= _MAX_SUBSTITUTION_DEPTH:
|
|
@@ -810,28 +699,6 @@ def substitute_vars(command_str: str, localvars: SubVarSet | None = None) -> str
|
|
|
810
699
|
return cmdstr
|
|
811
700
|
|
|
812
701
|
|
|
813
|
-
def runscripts() -> None:
|
|
814
|
-
"""Drive execution until the command-list stack is empty."""
|
|
815
|
-
# Set static vars once before the loop; they are refreshed by metacommand
|
|
816
|
-
# handlers (CONNECT, CONFIG, AUTOCOMMIT, CHDIR) when state changes.
|
|
817
|
-
set_static_system_vars()
|
|
818
|
-
while len(_state.commandliststack) > 0:
|
|
819
|
-
current_cmds = _state.commandliststack[-1]
|
|
820
|
-
set_dynamic_system_vars()
|
|
821
|
-
try:
|
|
822
|
-
current_cmds.run_next()
|
|
823
|
-
except StopIteration:
|
|
824
|
-
_state.commandliststack.pop()
|
|
825
|
-
continue
|
|
826
|
-
except SystemExit:
|
|
827
|
-
raise
|
|
828
|
-
except ErrInfo:
|
|
829
|
-
raise
|
|
830
|
-
except Exception as e:
|
|
831
|
-
raise ErrInfo(type="exception", exception_msg=exception_desc()) from e
|
|
832
|
-
_state.cmds_run += 1
|
|
833
|
-
|
|
834
|
-
|
|
835
702
|
def current_script_line() -> tuple:
|
|
836
703
|
"""Return ``(source_name, line_number)`` for the command currently executing."""
|
|
837
704
|
if len(_state.commandliststack) > 0:
|
|
@@ -842,179 +709,3 @@ def current_script_line() -> tuple:
|
|
|
842
709
|
return (f"script '{current_cmds.listname}'", len(current_cmds.cmdlist))
|
|
843
710
|
else:
|
|
844
711
|
return ("", 0)
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
def _parse_script_lines(lines_iter: Any, source_name: str) -> list:
|
|
848
|
-
# Parse an iterable of lines into a list of ScriptCmd objects.
|
|
849
|
-
from execsql.utils.errors import write_warning
|
|
850
|
-
|
|
851
|
-
beginscript = re.compile(
|
|
852
|
-
r"^\s*--\s*!x!\s*(?:BEGIN|CREATE)\s+SCRIPT\s+(?P<scriptname>\w+)(?:(?P<paramexpr>\s*\S+.*))?$",
|
|
853
|
-
re.I,
|
|
854
|
-
)
|
|
855
|
-
endscript = re.compile(r"^\s*--\s*!x!\s*END\s+SCRIPT(?:\s+(?P<scriptname>\w+))?\s*$", re.I)
|
|
856
|
-
beginsql = re.compile(r"^\s*--\s*!x!\s*BEGIN\s+SQL\s*$", re.I)
|
|
857
|
-
endsql = re.compile(r"^\s*--\s*!x!\s*END\s+SQL\s*$", re.I)
|
|
858
|
-
execline = re.compile(r"^\s*--\s*!x!\s*(?P<cmd>.+)$", re.I)
|
|
859
|
-
cmtline = re.compile(r"^\s*--")
|
|
860
|
-
in_block_cmt = False
|
|
861
|
-
in_block_sql = False
|
|
862
|
-
sqllist: list[ScriptCmd] = []
|
|
863
|
-
sqlline = 0
|
|
864
|
-
subscript_stack: list[CommandList] = []
|
|
865
|
-
currcmd = ""
|
|
866
|
-
scriptname = ""
|
|
867
|
-
for file_lineno, line in enumerate(lines_iter, 1):
|
|
868
|
-
# Remove trailing whitespace but not leading whitespace.
|
|
869
|
-
line = line.rstrip()
|
|
870
|
-
is_comment_line = False
|
|
871
|
-
comment_match = cmtline.match(line)
|
|
872
|
-
metacommand_match = execline.match(line)
|
|
873
|
-
if len(line) > 0:
|
|
874
|
-
if in_block_cmt:
|
|
875
|
-
is_comment_line = True
|
|
876
|
-
if len(line) > 1 and line[-2:] == "*/":
|
|
877
|
-
in_block_cmt = False
|
|
878
|
-
else:
|
|
879
|
-
# Not in block comment
|
|
880
|
-
if len(line.strip()) > 1 and line.strip()[0:2] == "/*":
|
|
881
|
-
in_block_cmt = True
|
|
882
|
-
is_comment_line = True
|
|
883
|
-
if line.strip()[-2:] == "*/":
|
|
884
|
-
in_block_cmt = False
|
|
885
|
-
else:
|
|
886
|
-
if comment_match:
|
|
887
|
-
is_comment_line = not metacommand_match
|
|
888
|
-
if not is_comment_line:
|
|
889
|
-
if metacommand_match:
|
|
890
|
-
if beginsql.match(line):
|
|
891
|
-
in_block_sql = True
|
|
892
|
-
if in_block_sql:
|
|
893
|
-
if endsql.match(line):
|
|
894
|
-
in_block_sql = False
|
|
895
|
-
if len(currcmd) > 0:
|
|
896
|
-
cmd = ScriptCmd(source_name, sqlline, "sql", SqlStmt(currcmd))
|
|
897
|
-
if len(subscript_stack) == 0:
|
|
898
|
-
sqllist.append(cmd)
|
|
899
|
-
else:
|
|
900
|
-
subscript_stack[-1].add(cmd)
|
|
901
|
-
currcmd = ""
|
|
902
|
-
else:
|
|
903
|
-
if len(currcmd) > 0:
|
|
904
|
-
write_warning(
|
|
905
|
-
f"Incomplete SQL statement starting on line {sqlline} at metacommand on line {file_lineno} of {source_name}.",
|
|
906
|
-
)
|
|
907
|
-
begs = beginscript.match(line)
|
|
908
|
-
if not begs:
|
|
909
|
-
ends = endscript.match(line)
|
|
910
|
-
if begs:
|
|
911
|
-
# This is a BEGIN SCRIPT metacommand.
|
|
912
|
-
scriptname = begs.group("scriptname").lower()
|
|
913
|
-
paramnames = None
|
|
914
|
-
paramexpr = begs.group("paramexpr")
|
|
915
|
-
if paramexpr:
|
|
916
|
-
withparams = re.compile(
|
|
917
|
-
r"(?:\s+WITH)?(?:\s+PARAM(?:ETER)?S)?\s*\(\s*(?P<params>\w+(?:\s*,\s*\w+)*)\s*\)\s*$",
|
|
918
|
-
re.I,
|
|
919
|
-
)
|
|
920
|
-
wp = withparams.match(paramexpr)
|
|
921
|
-
if not wp:
|
|
922
|
-
raise ErrInfo(
|
|
923
|
-
type="cmd",
|
|
924
|
-
command_text=line,
|
|
925
|
-
other_msg=f"Invalid BEGIN SCRIPT metacommand on line {file_lineno} of file {source_name}.",
|
|
926
|
-
)
|
|
927
|
-
else:
|
|
928
|
-
param_rx = re.compile(r"\w+", re.I)
|
|
929
|
-
paramnames = re.findall(param_rx, wp.group("params"))
|
|
930
|
-
subscript_stack.append(CommandList([], scriptname, paramnames))
|
|
931
|
-
elif ends:
|
|
932
|
-
# This is an END SCRIPT metacommand.
|
|
933
|
-
endscriptname = ends.group("scriptname")
|
|
934
|
-
if endscriptname is not None:
|
|
935
|
-
endscriptname = endscriptname.lower()
|
|
936
|
-
if len(subscript_stack) == 0:
|
|
937
|
-
raise ErrInfo(
|
|
938
|
-
type="cmd",
|
|
939
|
-
command_text=line,
|
|
940
|
-
other_msg=f"Unmatched END SCRIPT metacommand on line {file_lineno} of file {source_name}.",
|
|
941
|
-
)
|
|
942
|
-
if len(currcmd) > 0:
|
|
943
|
-
raise ErrInfo(
|
|
944
|
-
type="cmd",
|
|
945
|
-
command_text=line,
|
|
946
|
-
other_msg=f"Incomplete SQL statement\n ({currcmd})\nat END SCRIPT metacommand on line {file_lineno} of file {source_name}.",
|
|
947
|
-
)
|
|
948
|
-
if endscriptname is not None and endscriptname != scriptname:
|
|
949
|
-
raise ErrInfo(
|
|
950
|
-
type="cmd",
|
|
951
|
-
command_text=line,
|
|
952
|
-
other_msg=f"Mismatched script name in the END SCRIPT metacommand on line {file_lineno} of file {source_name}.",
|
|
953
|
-
)
|
|
954
|
-
sub_script = subscript_stack.pop()
|
|
955
|
-
_state.savedscripts[sub_script.listname] = sub_script
|
|
956
|
-
else:
|
|
957
|
-
# This is a non-IMMEDIATE metacommand.
|
|
958
|
-
cmd = ScriptCmd(
|
|
959
|
-
source_name,
|
|
960
|
-
file_lineno,
|
|
961
|
-
"cmd",
|
|
962
|
-
MetacommandStmt(metacommand_match.group("cmd").strip()),
|
|
963
|
-
)
|
|
964
|
-
if len(subscript_stack) == 0:
|
|
965
|
-
sqllist.append(cmd)
|
|
966
|
-
else:
|
|
967
|
-
subscript_stack[-1].add(cmd)
|
|
968
|
-
else:
|
|
969
|
-
# This line is not a comment and not a metacommand; part of a SQL statement.
|
|
970
|
-
cmd_end = line[-1] == ";"
|
|
971
|
-
if line[-1] == "\\":
|
|
972
|
-
line = line[:-1].strip()
|
|
973
|
-
if currcmd == "":
|
|
974
|
-
sqlline = file_lineno
|
|
975
|
-
currcmd = line
|
|
976
|
-
else:
|
|
977
|
-
currcmd = f"{currcmd} \n{line}"
|
|
978
|
-
if cmd_end and not in_block_sql:
|
|
979
|
-
cmd = ScriptCmd(source_name, sqlline, "sql", SqlStmt(currcmd.strip()))
|
|
980
|
-
if len(subscript_stack) == 0:
|
|
981
|
-
sqllist.append(cmd)
|
|
982
|
-
else:
|
|
983
|
-
subscript_stack[-1].add(cmd)
|
|
984
|
-
currcmd = ""
|
|
985
|
-
if len(subscript_stack) > 0:
|
|
986
|
-
raise ErrInfo(type="error", other_msg=f"Unmatched BEGIN SCRIPT metacommand at end of file {source_name}.")
|
|
987
|
-
if len(currcmd) > 0:
|
|
988
|
-
raise ErrInfo(
|
|
989
|
-
type="error",
|
|
990
|
-
other_msg=(
|
|
991
|
-
f"Incomplete SQL statement starting on line {sqlline} at end of file {source_name}."
|
|
992
|
-
+ (" Metacommands must be prefixed with '-- !x!'." if source_name == "<inline>" else "")
|
|
993
|
-
),
|
|
994
|
-
)
|
|
995
|
-
return sqllist
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
def read_sqlfile(sql_file_name: str) -> None:
|
|
999
|
-
"""Parse a ``.sql`` file and push the resulting :class:`CommandList` onto the execution stack."""
|
|
1000
|
-
# Read lines from the given script file, create a list of ScriptCmd objects,
|
|
1001
|
-
# and append the list to the top of the stack of script commands.
|
|
1002
|
-
from execsql.utils.errors import file_size_date
|
|
1003
|
-
|
|
1004
|
-
sz, dt = file_size_date(sql_file_name)
|
|
1005
|
-
_state.exec_log.log_status_info(f"Reading script file {sql_file_name} (size: {sz}; date: {dt})")
|
|
1006
|
-
scriptfile_obj = ScriptFile(sql_file_name, _state.conf.script_encoding)
|
|
1007
|
-
try:
|
|
1008
|
-
sqllist = _parse_script_lines(scriptfile_obj, sql_file_name)
|
|
1009
|
-
finally:
|
|
1010
|
-
scriptfile_obj.close()
|
|
1011
|
-
if sqllist:
|
|
1012
|
-
_state.commandliststack.append(CommandList(sqllist, Path(sql_file_name).name))
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
def read_sqlstring(content: str, source_name: str = "<inline>") -> None:
|
|
1016
|
-
"""Parse an inline script string and push it onto the command stack."""
|
|
1017
|
-
_state.exec_log.log_status_info(f"Reading inline script ({source_name})")
|
|
1018
|
-
sqllist = _parse_script_lines(content.splitlines(), source_name)
|
|
1019
|
-
if sqllist:
|
|
1020
|
-
_state.commandliststack.append(CommandList(sqllist, source_name))
|