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.
Files changed (66) hide show
  1. execsql/__init__.py +8 -3
  2. execsql/api.py +580 -0
  3. execsql/cli/__init__.py +123 -0
  4. execsql/cli/lint_ast.py +439 -0
  5. execsql/cli/run.py +113 -102
  6. execsql/config.py +29 -4
  7. execsql/db/access.py +1 -0
  8. execsql/db/base.py +4 -1
  9. execsql/db/dsn.py +3 -2
  10. execsql/db/duckdb.py +1 -1
  11. execsql/db/factory.py +3 -0
  12. execsql/db/firebird.py +2 -1
  13. execsql/db/mysql.py +2 -1
  14. execsql/db/oracle.py +2 -1
  15. execsql/db/postgres.py +2 -1
  16. execsql/db/sqlite.py +1 -1
  17. execsql/db/sqlserver.py +3 -2
  18. execsql/debug/repl.py +27 -10
  19. execsql/exporters/base.py +6 -4
  20. execsql/exporters/delimited.py +11 -3
  21. execsql/exporters/pretty.py +9 -12
  22. execsql/gui/tui.py +59 -2
  23. execsql/metacommands/__init__.py +3 -0
  24. execsql/metacommands/conditions.py +20 -2
  25. execsql/metacommands/connect.py +1 -1
  26. execsql/metacommands/control.py +8 -14
  27. execsql/metacommands/debug.py +6 -4
  28. execsql/metacommands/io_export.py +117 -315
  29. execsql/metacommands/io_fileops.py +7 -13
  30. execsql/metacommands/io_write.py +1 -1
  31. execsql/metacommands/script_ext.py +8 -5
  32. execsql/metacommands/upsert.py +40 -0
  33. execsql/models.py +8 -12
  34. execsql/plugins.py +414 -0
  35. execsql/script/__init__.py +36 -12
  36. execsql/script/ast.py +562 -0
  37. execsql/script/engine.py +59 -368
  38. execsql/script/executor.py +833 -0
  39. execsql/script/parser.py +663 -0
  40. execsql/script/variables.py +11 -0
  41. execsql/state.py +55 -2
  42. execsql/utils/crypto.py +14 -10
  43. execsql/utils/errors.py +31 -8
  44. execsql/utils/gui.py +139 -17
  45. execsql/utils/mail.py +15 -12
  46. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/METADATA +59 -1
  47. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/RECORD +66 -60
  48. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/README.md +0 -0
  49. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  50. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  51. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/execsql.conf +0 -0
  52. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  53. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  54. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  55. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  56. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  57. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  58. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  59. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/script_template.sql +0 -0
  60. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  61. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  62. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  63. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/WHEEL +0 -0
  64. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/entry_points.txt +0 -0
  65. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/licenses/LICENSE.txt +0 -0
  66. {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, ScriptArgSubVarSet, SubVarSet
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
- Parses argument expressions and loop-type flags at construction time;
655
- call :meth:`execute` to push the resolved :class:`CommandList` onto the
656
- execution stack.
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
- _state.subvars.add_substitution("$CURRENT_DIR", cwd)
720
- _state.subvars.add_substitution("$CURRENT_PATH", cwd + os.sep)
721
- _state.subvars.add_substitution("$CURRENT_ALIAS", _state.dbs.current_alias())
722
- db = _state.dbs.current()
723
- _state.subvars.add_substitution("$DB_USER", db.user if db.user else "")
724
- _state.subvars.add_substitution(
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
- _state.subvars.add_substitution("$DB_NAME", db.db_name)
729
- _state.subvars.add_substitution("$DB_NEED_PWD", "TRUE" if db.need_passwd else "FALSE")
730
- _state.subvars.add_substitution("$CURRENT_DBMS", db.type.dbms_id)
731
- _state.subvars.add_substitution("$CURRENT_DATABASE", db.name())
732
- _state.subvars.add_substitution("$VERSION1", str(_state.primary_vno))
733
- _state.subvars.add_substitution("$VERSION2", str(_state.secondary_vno))
734
- _state.subvars.add_substitution("$VERSION3", str(_state.tertiary_vno))
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
- _state.subvars.register_lazy("$random", lambda: str(random.random()))
737
- _state.subvars.register_lazy("$uuid", lambda: str(uuid.uuid4()))
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
- _state.subvars.add_substitution("$CANCEL_HALT_STATE", "ON" if _state.status.cancel_halt else "OFF")
749
- _state.subvars.add_substitution("$ERROR_HALT_STATE", "ON" if _state.status.halt_on_err else "OFF")
750
- _state.subvars.add_substitution(
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 _state.status.halt_on_metacommand_err else "OFF",
634
+ "ON" if _s.status.halt_on_metacommand_err else "OFF",
753
635
  )
754
- _state.subvars.add_substitution(
636
+ _s.subvars.add_substitution(
755
637
  "$CONSOLE_WAIT_WHEN_ERROR_HALT_STATE",
756
- "ON" if _state.conf.gui_wait_on_error_halt else "OFF",
638
+ "ON" if _s.conf.gui_wait_on_error_halt else "OFF",
757
639
  )
758
- _state.subvars.add_substitution("$CONSOLE_WAIT_WHEN_DONE_STATE", "ON" if _state.conf.gui_wait_on_exit else "OFF")
759
- db = _state.dbs.current()
760
- _state.subvars.add_substitution("$AUTOCOMMIT_STATE", "ON" if db.autocommit else "OFF")
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
- _state.subvars.add_substitution("$TIMER", str(datetime.timedelta(seconds=_state.timer.elapsed())))
763
- _state.subvars.clear_lazy_cache()
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
- # Substitutes global variables, global counters, and local variables.
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 = _state.subvars.merge(localvars)
673
+ subs = _s.subvars.merge(localvars)
785
674
  else:
786
- subs = _state.subvars
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 = _state.counters.substitute_all(cmdstr)
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))