execsql2 2.17.0__py3-none-any.whl → 2.17.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. execsql/__init__.py +6 -2
  2. execsql/api.py +25 -6
  3. execsql/cli/__init__.py +5 -3
  4. execsql/cli/lint.py +30 -34
  5. execsql/cli/run.py +10 -0
  6. execsql/config.py +145 -92
  7. execsql/db/access.py +54 -40
  8. execsql/db/base.py +33 -6
  9. execsql/db/firebird.py +3 -1
  10. execsql/db/mysql.py +4 -3
  11. execsql/db/oracle.py +36 -14
  12. execsql/db/postgres.py +8 -6
  13. execsql/db/sqlite.py +5 -2
  14. execsql/db/sqlserver.py +8 -6
  15. execsql/debug/repl.py +59 -21
  16. execsql/exceptions.py +19 -4
  17. execsql/exporters/base.py +3 -2
  18. execsql/exporters/delimited.py +2 -3
  19. execsql/exporters/feather.py +3 -3
  20. execsql/exporters/ods.py +1 -1
  21. execsql/exporters/xls.py +12 -4
  22. execsql/exporters/xlsx.py +1 -1
  23. execsql/gui/desktop.py +129 -15
  24. execsql/importers/__init__.py +1 -1
  25. execsql/importers/ods.py +1 -1
  26. execsql/importers/xls.py +1 -1
  27. execsql/metacommands/__init__.py +34 -5
  28. execsql/metacommands/conditions.py +26 -14
  29. execsql/metacommands/connect.py +21 -14
  30. execsql/metacommands/control.py +55 -68
  31. execsql/metacommands/data.py +25 -9
  32. execsql/metacommands/debug.py +132 -77
  33. execsql/metacommands/io_export.py +14 -2
  34. execsql/metacommands/io_import.py +11 -2
  35. execsql/metacommands/io_write.py +113 -11
  36. execsql/metacommands/prompt.py +46 -32
  37. execsql/metacommands/script_ext.py +63 -34
  38. execsql/metacommands/system.py +4 -3
  39. execsql/script/__init__.py +28 -37
  40. execsql/script/ast.py +7 -7
  41. execsql/script/control.py +4 -101
  42. execsql/script/engine.py +37 -251
  43. execsql/script/executor.py +181 -222
  44. execsql/script/parser.py +1 -3
  45. execsql/script/variables.py +8 -3
  46. execsql/state.py +125 -37
  47. execsql/utils/errors.py +0 -2
  48. execsql/utils/fileio.py +47 -3
  49. execsql/utils/mail.py +3 -2
  50. execsql/utils/strings.py +5 -5
  51. {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/METADATA +42 -36
  52. execsql2-2.17.2.dist-info/RECORD +124 -0
  53. execsql2-2.17.2.dist-info/licenses/NOTICE +11 -0
  54. execsql2-2.17.0.dist-info/RECORD +0 -124
  55. execsql2-2.17.0.dist-info/licenses/NOTICE +0 -10
  56. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/README.md +0 -0
  57. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  58. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  59. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/execsql.conf +0 -0
  60. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
  61. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_compare.sql +0 -0
  62. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
  63. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
  64. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
  65. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  66. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  67. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/script_template.sql +0 -0
  68. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
  69. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  70. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  71. {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/WHEEL +0 -0
  72. {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/entry_points.txt +0 -0
  73. {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/licenses/LICENSE.txt +0 -0
@@ -4,21 +4,35 @@ from execsql.exceptions import ErrInfo
4
4
  """
5
5
  Interactive user prompt metacommand handlers for execsql.
6
6
 
7
- Implements all ``x_*`` handler functions that display GUI dialogs or
8
- console prompts to the user:
9
-
10
- - ``x_prompt_action`` — ACTION prompt (choose from a list of actions)
11
- - ``x_prompt_message`` — MESSAGE dialog
12
- - ``x_prompt_display`` — DISPLAY (show a query result)
13
- - ``x_prompt_entry`` — ENTRY FORM (fill in substitution variables)
14
- - ``x_prompt_compare`` — COMPARE dialog
15
- - ``x_prompt_selectrows`` — SELECT ROWS dialog
16
- - ``x_prompt_map`` — MAP display
17
- - ``x_open_file`` — OPEN FILE browser
18
- - ``x_save_file`` — SAVE FILE browser
19
- - ``x_get_directory`` — GET DIRECTORY browser
20
- - ``x_credentials`` — CREDENTIALS dialog
21
- - ``x_gui_console`` — GUI console on/off control
7
+ Implements handlers that display GUI dialogs or console prompts.
8
+ The PROMPT family routes through the active GUI backend (Tkinter,
9
+ Textual, or Console) via :mod:`execsql.utils.gui`.
10
+
11
+ PROMPT variants:
12
+
13
+ - ``x_prompt`` — PROMPT DISPLAY (show a query result in a dialog)
14
+ - ``x_prompt_action`` — PROMPT ACTION (choose from a list of actions)
15
+ - ``x_prompt_ask`` — PROMPT ASK (yes/no, optionally with DISPLAY/COMPARE)
16
+ - ``x_prompt_ask_compare`` — PROMPT ASK … COMPARE (yes/no over two tables)
17
+ - ``x_prompt_compare`` — PROMPT COMPARE (side-by-side or stacked table diff)
18
+ - ``x_prompt_connect`` — PROMPT CONNECT (interactive DB connection dialog)
19
+ - ``x_prompt_credentials`` — PROMPT CREDENTIALS (username/password dialog)
20
+ - ``x_prompt_directory`` — PROMPT DIRECTORY (folder picker)
21
+ - ``x_prompt_enter`` — PROMPT ENTER_SUB (single text-entry dialog)
22
+ - ``x_prompt_entryform`` — PROMPT ENTRY_FORM (multi-field form)
23
+ - ``x_prompt_map`` — PROMPT MAP (markers on an interactive map)
24
+ - ``x_prompt_openfile`` — PROMPT OPENFILE (file picker)
25
+ - ``x_prompt_pause`` — PROMPT PAUSE (modal pause dialog with timer)
26
+ - ``x_prompt_savefile`` — PROMPT SAVEFILE (file save dialog)
27
+ - ``x_prompt_select_rows`` — PROMPT SELECT_ROWS (multi-row selection dialog)
28
+
29
+ Console / non-PROMPT siblings:
30
+
31
+ - ``x_ask`` — ASK (terminal-only yes/no, no GUI)
32
+ - ``x_pause`` — PAUSE (terminal-only "press a key to continue")
33
+ - ``x_msg`` — PROMPT MESSAGE (single-button text dialog)
34
+ - ``x_reset_dialog_canceled`` — RESET DIALOG_CANCELED (clear the
35
+ dialog-cancellation flag set by a PROMPT cancel).
22
36
  """
23
37
 
24
38
  import os
@@ -126,7 +140,7 @@ def x_prompt_enter(**kwargs: Any) -> None:
126
140
  _state.exec_log.log_exit_halt(*current_script_line(), msg="Quit from prompt to enter a SUB value.")
127
141
  exit_now(2, None)
128
142
  else:
129
- subvarset = _state.subvars if sub_var[0] != "~" else _state.commandliststack[-1].localvars
143
+ subvarset = _state.subvars if sub_var[0] != "~" else _state.current_localvars()
130
144
  subvarset.add_substitution(sub_var, txtval)
131
145
  script_name, lno = current_script_line()
132
146
  if as_pw:
@@ -252,7 +266,7 @@ def x_prompt_entryform(**kwargs: Any) -> None:
252
266
  ) from e
253
267
  if entry_col < 1:
254
268
  entry_col = 1
255
- subvarset = _state.subvars if subvar[0] != "~" else _state.commandliststack[-1].localvars
269
+ subvarset = _state.subvars if subvar[0] != "~" else _state.current_localvars()
256
270
  subvarset.remove_substitution(subvar)
257
271
  entry_list.append(
258
272
  EntrySpec(
@@ -293,7 +307,7 @@ def x_prompt_entryform(**kwargs: Any) -> None:
293
307
  for e in entries:
294
308
  if e.value:
295
309
  value = str(e.value)
296
- subvarset = _state.subvars if subvar[0] != "~" else _state.commandliststack[-1].localvars
310
+ subvarset = _state.subvars if subvar[0] != "~" else _state.current_localvars()
297
311
  subvarset.add_substitution(e.name, value)
298
312
  _state.exec_log.log_status_info(
299
313
  f"Substitution variable {e.name} set to {{{value}}} on line {line_no} of {script}",
@@ -425,7 +439,7 @@ def x_prompt_ask_compare(**kwargs: Any) -> None:
425
439
  exit_now(2, None)
426
440
  else:
427
441
  respstr = "Yes" if btn == 1 else "No"
428
- subvarset = _state.subvars if subvar[0] != "~" else _state.commandliststack[-1].localvars
442
+ subvarset = _state.subvars if subvar[0] != "~" else _state.current_localvars()
429
443
  subvarset.add_substitution(subvar, respstr)
430
444
  _state.exec_log.log_status_info(f"Question {{{kwargs['msg']}}} on line {lno} answered {respstr}")
431
445
 
@@ -462,7 +476,7 @@ def x_prompt_ask(**kwargs: Any) -> None:
462
476
  exit_now(2, None)
463
477
  else:
464
478
  respstr = "Yes" if btn == 1 else "No"
465
- subvarset = _state.subvars if subvar[0] != "~" else _state.commandliststack[-1].localvars
479
+ subvarset = _state.subvars if subvar[0] != "~" else _state.current_localvars()
466
480
  subvarset.add_substitution(subvar, respstr)
467
481
  _state.exec_log.log_status_info(
468
482
  f"Question {{{kwargs['question']}}} answered {respstr} on line {lno} of script {script}",
@@ -605,7 +619,7 @@ def x_prompt_savefile(**kwargs: Any) -> None:
605
619
  sub_name5 = kwargs["fnbase_match"]
606
620
  startdir = kwargs["startdir"]
607
621
  try:
608
- subvarset = _state.subvars if sub_name[0] != "~" else _state.commandliststack[-1].localvars
622
+ subvarset = _state.subvars if sub_name[0] != "~" else _state.current_localvars()
609
623
  subvarset.remove_substitution(sub_name)
610
624
  script, lno = current_script_line()
611
625
  working_dir = startdir if startdir is not None else str(Path(script).resolve().parent)
@@ -628,7 +642,7 @@ def x_prompt_savefile(**kwargs: Any) -> None:
628
642
  f"Substitution variable {sub_name} set to path and filename {fn} at line {lno} of {script}",
629
643
  )
630
644
  if sub_name2 is not None:
631
- subvarset2 = _state.subvars if sub_name2[0] != "~" else _state.commandliststack[-1].localvars
645
+ subvarset2 = _state.subvars if sub_name2[0] != "~" else _state.current_localvars()
632
646
  subvarset2.remove_substitution(sub_name2)
633
647
  basefn = Path(fn).name
634
648
  subvarset2.add_substitution(sub_name2, basefn)
@@ -636,7 +650,7 @@ def x_prompt_savefile(**kwargs: Any) -> None:
636
650
  f"Substitution variable {sub_name2} set to filename {basefn} at line {lno} of {script}",
637
651
  )
638
652
  if sub_name3 is not None:
639
- subvarset3 = _state.subvars if sub_name3[0] != "~" else _state.commandliststack[-1].localvars
653
+ subvarset3 = _state.subvars if sub_name3[0] != "~" else _state.current_localvars()
640
654
  subvarset3.remove_substitution(sub_name3)
641
655
  dirname = str(Path(fn).parent)
642
656
  if os.name != "posix":
@@ -646,7 +660,7 @@ def x_prompt_savefile(**kwargs: Any) -> None:
646
660
  f"Substitution variable {sub_name3} set to path {dirname} at line {lno} of {script}",
647
661
  )
648
662
  if sub_name4 is not None:
649
- subvarset4 = _state.subvars if sub_name4[0] != "~" else _state.commandliststack[-1].localvars
663
+ subvarset4 = _state.subvars if sub_name4[0] != "~" else _state.current_localvars()
650
664
  subvarset4.remove_substitution(sub_name4)
651
665
  ext = Path(fn).suffix
652
666
  if ext is None:
@@ -655,7 +669,7 @@ def x_prompt_savefile(**kwargs: Any) -> None:
655
669
  ext = ext[1:]
656
670
  subvarset4.add_substitution(sub_name4, ext)
657
671
  if sub_name5 is not None:
658
- subvarset5 = _state.subvars if sub_name5[0] != "~" else _state.commandliststack[-1].localvars
672
+ subvarset5 = _state.subvars if sub_name5[0] != "~" else _state.current_localvars()
659
673
  subvarset5.remove_substitution(sub_name5)
660
674
  subvarset5.add_substitution(sub_name5, Path(fn).stem)
661
675
  except (ErrInfo, SystemExit):
@@ -678,7 +692,7 @@ def x_prompt_openfile(**kwargs: Any) -> None:
678
692
  other_msg="Different values can't be assigned to the same substitution variable.",
679
693
  )
680
694
  try:
681
- subvarset = _state.subvars if sub_name[0] != "~" else _state.commandliststack[-1].localvars
695
+ subvarset = _state.subvars if sub_name[0] != "~" else _state.current_localvars()
682
696
  subvarset.remove_substitution(sub_name)
683
697
  script, lno = current_script_line()
684
698
  working_dir = startdir if startdir is not None else str(Path(script).resolve().parent)
@@ -701,19 +715,19 @@ def x_prompt_openfile(**kwargs: Any) -> None:
701
715
  f"Substitution variable {sub_name} set to path and filename {fn} at line {lno} of {script}",
702
716
  )
703
717
  if sub_name2 is not None:
704
- subvarset2 = _state.subvars if sub_name2[0] != "~" else _state.commandliststack[-1].localvars
718
+ subvarset2 = _state.subvars if sub_name2[0] != "~" else _state.current_localvars()
705
719
  subvarset2.remove_substitution(sub_name2)
706
720
  basefn = Path(fn).name
707
721
  subvarset2.add_substitution(sub_name2, basefn)
708
722
  if sub_name3 is not None:
709
- subvarset3 = _state.subvars if sub_name3[0] != "~" else _state.commandliststack[-1].localvars
723
+ subvarset3 = _state.subvars if sub_name3[0] != "~" else _state.current_localvars()
710
724
  subvarset3.remove_substitution(sub_name3)
711
725
  dirname = str(Path(fn).parent)
712
726
  if os.name != "posix":
713
727
  dirname = dirname.replace("/", "\\")
714
728
  subvarset3.add_substitution(sub_name3, dirname)
715
729
  if sub_name4 is not None:
716
- subvarset4 = _state.subvars if sub_name4[0] != "~" else _state.commandliststack[-1].localvars
730
+ subvarset4 = _state.subvars if sub_name4[0] != "~" else _state.current_localvars()
717
731
  subvarset4.remove_substitution(sub_name4)
718
732
  ext = Path(fn).suffix
719
733
  if ext is None:
@@ -722,7 +736,7 @@ def x_prompt_openfile(**kwargs: Any) -> None:
722
736
  ext = ext[1:]
723
737
  subvarset4.add_substitution(sub_name4, ext)
724
738
  if sub_name5 is not None:
725
- subvarset5 = _state.subvars if sub_name5[0] != "~" else _state.commandliststack[-1].localvars
739
+ subvarset5 = _state.subvars if sub_name5[0] != "~" else _state.current_localvars()
726
740
  subvarset5.remove_substitution(sub_name5)
727
741
  subvarset5.add_substitution(sub_name5, Path(fn).stem)
728
742
  except (ErrInfo, SystemExit):
@@ -737,7 +751,7 @@ def x_prompt_directory(**kwargs: Any) -> None:
737
751
  fullpath = kwargs["fullpath"]
738
752
  startdir = kwargs["startdir"]
739
753
  try:
740
- subvarset = _state.subvars if sub_name[0] != "~" else _state.commandliststack[-1].localvars
754
+ subvarset = _state.subvars if sub_name[0] != "~" else _state.current_localvars()
741
755
  subvarset.remove_substitution(sub_name)
742
756
  script, lno = current_script_line()
743
757
  working_dir = startdir if startdir is not None else str(Path(script).resolve().parent)
@@ -878,7 +892,7 @@ def x_ask(**kwargs: Any) -> None:
878
892
  exit_now(2, None)
879
893
  else:
880
894
  respstr = "Yes" if resp == "y" else "No"
881
- subvarset = _state.subvars if subvar[0] != "~" else _state.commandliststack[-1].localvars
895
+ subvarset = _state.subvars if subvar[0] != "~" else _state.current_localvars()
882
896
  subvarset.add_substitution(subvar, respstr)
883
897
  _state.exec_log.log_status_info(
884
898
  f"Question {{{message}}} answered {respstr} on line {lno} of script {script}",
@@ -1,59 +1,88 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  """
4
- Script extension metacommand handlers for execsql.
4
+ Script-block extension and dispatch handlers for execsql.
5
5
 
6
- Implements metacommands that extend or augment a running script:
6
+ Handlers for the named-script invocation and dynamic-extension
7
+ metacommands. Used by both the AST executor and legacy command paths:
7
8
 
8
- - ``x_extendscript`` — EXTEND SCRIPT (append additional commands from
9
- a file into the current script stream)
10
- - Other substitution-variable and script-modification helpers.
9
+ - ``x_executescript`` — ``EXECUTE SCRIPT <name>`` / ``RUN SCRIPT <name>``
10
+ (look up a previously-registered ``BEGIN SCRIPT`` block and run it,
11
+ optionally with parameter bindings and a WHILE / UNTIL loop).
12
+ - ``x_extendscript`` — ``EXTEND SCRIPT <name> WITH SCRIPT|FILE …``
13
+ (append additional commands to an existing named script block from
14
+ an inline source).
15
+ - ``x_extendscript_metacommand`` — ``EXTEND SCRIPT … WITH METACOMMAND …``.
16
+ - ``x_extendscript_sql`` — ``EXTEND SCRIPT … WITH SQL …``.
17
+
18
+ Registration of ``BEGIN SCRIPT … END SCRIPT`` blocks themselves is
19
+ handled by the AST parser (block boundaries) and executor (registering
20
+ the block on ``ctx.ast_scripts``); this module is only the call-site /
21
+ extension handlers.
11
22
  """
12
23
 
24
+ import copy
25
+ from dataclasses import replace
13
26
  from typing import Any
14
27
 
15
28
  import execsql.state as _state
16
29
  from execsql.exceptions import ErrInfo
17
- from execsql.script import MetacommandStmt, ScriptCmd, SqlStmt, current_script_line
30
+ from execsql.script import current_script_line
31
+
32
+
33
+ def _get_ast_script(name: str):
34
+ """Return the AST :class:`ScriptBlock` for ``name`` or raise ErrInfo."""
35
+ block = _state.ast_scripts.get(name.lower())
36
+ if block is None:
37
+ raise ErrInfo("cmd", other_msg=f"There is no SCRIPT named {name}.")
38
+ return block
39
+
40
+
41
+ def _new_span(source: str, line_no: int):
42
+ """Construct a SourceSpan for a synthetic AST node created at runtime."""
43
+ from execsql.script.ast import SourceSpan
44
+
45
+ return SourceSpan(file=source, start_line=line_no, end_line=line_no)
18
46
 
19
47
 
20
48
  def x_extendscript(**kwargs: Any) -> None:
21
- script1 = kwargs["script1"].lower()
22
- if script1 not in _state.savedscripts:
23
- raise ErrInfo("cmd", other_msg=f"There is no SCRIPT named {script1}.")
24
- script2 = kwargs["script2"].lower()
25
- if script2 not in _state.savedscripts:
26
- raise ErrInfo("cmd", other_msg=f"There is no SCRIPT named {script2}.")
27
- s1 = _state.savedscripts[script1]
28
- s2 = _state.savedscripts[script2]
29
- for cmd in s1.cmdlist:
30
- s2.add(cmd)
31
- if s1.paramnames is not None:
32
- if s2.paramnames is None:
33
- s2.paramnames = []
34
- for param in s1.paramnames:
35
- if param not in s2.paramnames:
36
- s2.paramnames.append(param)
49
+ """Append the body of one SCRIPT to another, merging parameter names."""
50
+ target = _get_ast_script(kwargs["script2"])
51
+ source = _get_ast_script(kwargs["script1"])
52
+
53
+ # Append a deep copy of the source body so future mutations don't bleed.
54
+ target.body.extend(copy.deepcopy(source.body))
55
+
56
+ # Merge parameter definitions, preserving the target's existing order and
57
+ # adding any new params from the source.
58
+ if source.param_defs:
59
+ existing = list(target.param_defs or [])
60
+ existing_names = {p.name for p in existing}
61
+ for pdef in source.param_defs:
62
+ if pdef.name not in existing_names:
63
+ existing.append(replace(pdef))
64
+ existing_names.add(pdef.name)
65
+ target.param_defs = existing
37
66
 
38
67
 
39
68
  def x_extendscript_metacommand(**kwargs: Any) -> None:
40
- script = kwargs["script"].lower()
41
- if script not in _state.savedscripts:
42
- raise ErrInfo("cmd", other_msg=f"There is no SCRIPT named {script}.")
69
+ """Append a single metacommand line to an existing SCRIPT body."""
70
+ from execsql.script.ast import MetaCommandStatement
71
+
72
+ block = _get_ast_script(kwargs["script"])
43
73
  script_file, script_line_no = current_script_line()
44
- _state.savedscripts[script].add(
45
- ScriptCmd(script_file, script_line_no, "cmd", MetacommandStmt(kwargs["cmd"])),
46
- )
74
+ span = _new_span(script_file, script_line_no or 0)
75
+ block.body.append(MetaCommandStatement(span=span, command=kwargs["cmd"]))
47
76
 
48
77
 
49
78
  def x_extendscript_sql(**kwargs: Any) -> None:
50
- script = kwargs["script"].lower()
51
- if script not in _state.savedscripts:
52
- raise ErrInfo("cmd", other_msg=f"There is no SCRIPT named {script}.")
79
+ """Append a single SQL statement to an existing SCRIPT body."""
80
+ from execsql.script.ast import SqlStatement
81
+
82
+ block = _get_ast_script(kwargs["script"])
53
83
  script_file, script_line_no = current_script_line()
54
- _state.savedscripts[script].add(
55
- ScriptCmd(script_file, script_line_no, "sql", SqlStmt(kwargs["sql"])),
56
- )
84
+ span = _new_span(script_file, script_line_no or 0)
85
+ block.body.append(SqlStatement(span=span, text=kwargs["sql"]))
57
86
 
58
87
 
59
88
  def x_executescript(**kwargs: Any) -> None:
@@ -3,9 +3,10 @@ from __future__ import annotations
3
3
  """
4
4
  System and shell metacommand handlers for execsql.
5
5
 
6
- Implements ``x_shell`` (SHELL — execute an OS command via ``subprocess``)
7
- and related system-interaction metacommands that allow SQL scripts to
8
- invoke external programs, set environment variables, or query the OS.
6
+ Implements ``x_system_cmd`` (SYSTEM_CMD — execute an OS command via
7
+ ``subprocess``) and related system-interaction metacommands that allow
8
+ SQL scripts to invoke external programs, set environment variables, or
9
+ query the OS.
9
10
  """
10
11
 
11
12
  import os
@@ -1,55 +1,49 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  """
4
- Core script-execution engine for execsql.
4
+ Core script-execution data types and helpers for execsql.
5
5
 
6
- This module contains the data structures and functions that load, parse, and
7
- drive execution of execsql ``.sql`` script files. It is the heart of the
8
- runtime.
6
+ This package re-exports the data structures, dispatch primitives, and AST
7
+ machinery that drive execution of execsql ``.sql`` script files. Script
8
+ parsing lives in :mod:`execsql.script.parser`; tree-walking execution
9
+ lives in :mod:`execsql.script.executor`.
9
10
 
10
11
  Key classes:
11
12
 
12
13
  - :class:`BatchLevels` — tracks which databases are used in nested BEGIN/END
13
14
  BATCH blocks for commit/rollback handling.
14
- - :class:`IfItem` / :class:`IfLevels` stack-based IF/ELSE/ENDIF nesting.
15
- - :class:`CounterVars` — named integer counters (``@NAME``).
16
- - :class:`SubVarSet` global ``!!$VAR!!`` substitution-variable store, plus
17
- ``&ENV``, ``@COUNTER``, ``~LOCAL``, and ``#ARG`` prefixes.
15
+ - :class:`CounterVars` named integer counters (``$COUNTER_N``).
16
+ - :class:`SubVarSet` — substitution-variable store covering all sigils
17
+ (no prefix, ``$``, ``&``, ``@``).
18
18
  - :class:`LocalSubVarSet` / :class:`ScriptArgSubVarSet` — per-script-scope
19
- variable overlays.
19
+ variable overlays for ``~`` local and ``#`` argument variables. Stored
20
+ on the active :class:`~execsql.state.ExecFrame` and retrieved via
21
+ ``ctx.current_localvars()`` / ``ctx.current_paramvals()``.
20
22
  - :class:`MetaCommand` — one entry in the metacommand dispatch table (regex +
21
23
  handler function + flags).
22
- - :class:`MetaCommandList` — ordered list of :class:`MetaCommand` entries;
23
- ``get_match()`` finds the first matching entry for a given line.
24
- - :class:`SqlStmt` wraps a single SQL string; ``run()`` executes it via the
25
- active database connection.
26
- - :class:`MetacommandStmt` — wraps a metacommand line; ``run()`` dispatches
27
- through :attr:`execsql.state.metacommandlist`.
28
- - :class:`ScriptCmd` pairs a statement with its source-file location.
29
- - :class:`CommandList` — ordered list of :class:`ScriptCmd` objects plus an
30
- execution cursor; ``run_next()`` drives one step of execution.
31
- - :class:`CommandListWhileLoop` / :class:`CommandListUntilLoop` — loop
32
- variants of :class:`CommandList` that re-evaluate a condition each pass.
33
- - :class:`ScriptFile` — reads and tokenises a ``.sql`` file into
34
- :class:`ScriptCmd` objects.
35
- - :class:`ScriptExecSpec` — specification for deferred script execution.
24
+ - :class:`MetaCommandList` — ordered list of :class:`MetaCommand` entries
25
+ with a keyword index for fast dispatch.
26
+ - :class:`SqlStmt` / :class:`MetacommandStmt` / :class:`ScriptCmd`
27
+ statement wrappers carried in the AST and used by ``ctx.last_command``
28
+ for source-location tracking.
29
+ - :class:`ScriptExecSpec` — specification for deferred script execution
30
+ (used by ``ON ERROR_HALT`` / ``ON CANCEL_HALT EXECUTE SCRIPT``).
36
31
 
37
32
  Key functions:
38
33
 
39
- - :func:`set_system_vars` — populates built-in ``$VARNAME`` system variables.
40
- - :func:`substitute_vars` performs ``!!$VAR!!`` and ``!{$var}!`` expansion.
41
- - :func:`runscripts` — central execution loop; pops the top
42
- :class:`CommandList` from ``_state.commandliststack`` and drives
43
- ``run_next()`` until the stack is empty.
44
- - :func:`current_script_line` — returns the source location of the currently
45
- executing command.
46
- - :func:`read_sqlfile` parses a SQL script file into a new
47
- :class:`CommandList` and pushes it onto ``_state.commandliststack``.
34
+ - :func:`set_system_vars` — populates built-in ``$VARNAME`` system variables
35
+ (calls the static + dynamic helpers).
36
+ - :func:`substitute_vars` — performs ``!!$VAR!!`` / ``!'!var!'!`` /
37
+ ``!"!var!"!`` / ``!{$var}!`` expansion.
38
+ - :func:`current_script_line` — returns the ``(file, line_no)`` of the
39
+ currently executing command.
40
+ - :func:`parse_script` / :func:`parse_string` — produce a
41
+ :class:`~execsql.script.ast.Script` AST tree from a file or string,
42
+ consumed by :func:`execsql.script.executor.execute`.
48
43
  """
49
44
 
50
- from execsql.script.control import BatchLevels, IfItem, IfLevels
45
+ from execsql.script.control import BatchLevels
51
46
  from execsql.script.engine import (
52
- CommandList,
53
47
  MetaCommand,
54
48
  MetaCommandList,
55
49
  MetacommandStmt,
@@ -84,8 +78,6 @@ from execsql.script.variables import CounterVars, LocalSubVarSet, ScriptArgSubVa
84
78
 
85
79
  __all__ = [
86
80
  "BatchLevels",
87
- "IfItem",
88
- "IfLevels",
89
81
  "CounterVars",
90
82
  "SubVarSet",
91
83
  "LocalSubVarSet",
@@ -95,7 +87,6 @@ __all__ = [
95
87
  "SqlStmt",
96
88
  "MetacommandStmt",
97
89
  "ScriptCmd",
98
- "CommandList",
99
90
  "ScriptExecSpec",
100
91
  "set_dynamic_system_vars",
101
92
  "set_static_system_vars",
execsql/script/ast.py CHANGED
@@ -1,12 +1,12 @@
1
1
  """Abstract Syntax Tree node definitions for execsql scripts.
2
2
 
3
- This module defines the node types that make up the execsql AST. A parser
4
- (to be added in a later phase) will convert raw ``.sql`` script text into a
5
- tree of these nodes; an executor will walk the tree to run the script.
3
+ Defines the node types that make up the execsql AST.
4
+ :func:`execsql.script.parser.parse_script` produces trees of these nodes;
5
+ :func:`execsql.script.executor.execute` walks them to run the script.
6
6
 
7
7
  Design principles:
8
- - Every node carries a :class:`SourceSpan` so that error messages, the
9
- LSP, and ``--lint`` can report precise source locations.
8
+ - Every node carries a :class:`SourceSpan` so that error messages,
9
+ ``--lint``, and the formatter can report precise source locations.
10
10
  - Block structures (IF, LOOP, BATCH, SCRIPT) are represented as nodes
11
11
  whose ``body`` (and optional ``else_body``, ``elseif_clauses``) contain
12
12
  child nodes, forming the tree structure.
@@ -14,8 +14,8 @@ Design principles:
14
14
  :class:`Comment`) have no children.
15
15
  - All nodes inherit from :class:`Node`, which provides a uniform
16
16
  ``children()`` iterator for tree traversal.
17
- - The tree is meant to be *walked* for execution nodes are data, not
18
- behavior. Execution logic will live in a separate executor module.
17
+ - The tree is data, not behavior execution logic lives in
18
+ :mod:`execsql.script.executor`.
19
19
 
20
20
  Node hierarchy::
21
21
 
execsql/script/control.py CHANGED
@@ -4,15 +4,14 @@ from __future__ import annotations
4
4
 
5
5
  Classes:
6
6
  - :class:`BatchLevels` — tracks which databases are used in nested BEGIN/END BATCH blocks.
7
- - :class:`IfItem` — one level of a nested IF/ELSE/ENDIF condition.
8
- - :class:`IfLevels` stack of boolean IF-level states.
7
+
8
+ IF/ELSE/ENDIF condition handling is structural under the AST executor;
9
+ the legacy ``IfItem`` / ``IfLevels`` classes were removed.
9
10
  """
10
11
 
11
12
  from typing import Any
12
13
 
13
- from execsql.exceptions import ErrInfo
14
-
15
- __all__ = ["BatchLevels", "IfItem", "IfLevels"]
14
+ __all__ = ["BatchLevels"]
16
15
 
17
16
 
18
17
  # ---------------------------------------------------------------------------
@@ -66,99 +65,3 @@ class BatchLevels:
66
65
  b = self.batchlevels.pop()
67
66
  for db in b.dbs_used:
68
67
  db.commit()
69
-
70
-
71
- # ---------------------------------------------------------------------------
72
- # IfItem / IfLevels
73
- # ---------------------------------------------------------------------------
74
-
75
-
76
- class IfItem:
77
- """One level of a nested IF/ELSE/ENDIF condition, paired with its source location."""
78
-
79
- # An object representing an 'if' level, with context data.
80
- def __init__(self, tf_value: bool) -> None:
81
- self.tf_value = tf_value
82
- # Import from the package (not engine directly) so that test patches on
83
- # execsql.script.current_script_line are effective.
84
- from execsql.script import current_script_line
85
-
86
- self.scriptname, self.scriptline = current_script_line()
87
-
88
- def value(self) -> bool:
89
- return self.tf_value
90
-
91
- def invert(self) -> None:
92
- self.tf_value = not self.tf_value
93
-
94
- def change_to(self, tf_value: bool) -> None:
95
- self.tf_value = tf_value
96
-
97
- def script_line(self) -> tuple:
98
- return (self.scriptname, self.scriptline)
99
-
100
-
101
- class IfLevels:
102
- """Stack of boolean IF-level states for nested conditional execution.
103
-
104
- Each :meth:`nest` call corresponds to an IF statement; each
105
- :meth:`unnest` call corresponds to an ENDIF. :meth:`all_true` drives
106
- the execution gate — commands are skipped unless every level is ``True``.
107
- """
108
-
109
- # A stack of True/False values corresponding to a nested set of conditionals,
110
- # with methods to manipulate and query the set of conditional states.
111
- def __init__(self) -> None:
112
- self.if_levels: list[IfItem] = []
113
-
114
- def nest(self, tf_value: bool) -> None:
115
- """Push a new IF level onto the stack with the given boolean value."""
116
- self.if_levels.append(IfItem(tf_value))
117
-
118
- def unnest(self) -> None:
119
- """Pop the innermost IF level; raise ErrInfo if the stack is empty."""
120
- if len(self.if_levels) == 0:
121
- raise ErrInfo(type="error", other_msg="Can't exit an IF block; no IF block is active.")
122
- else:
123
- self.if_levels.pop()
124
-
125
- def invert(self) -> None:
126
- if len(self.if_levels) == 0:
127
- raise ErrInfo(type="error", other_msg="Can't change the IF state; no IF block is active.")
128
- else:
129
- self.if_levels[-1].invert()
130
-
131
- def replace(self, tf_value: bool) -> None:
132
- if len(self.if_levels) == 0:
133
- raise ErrInfo(type="error", other_msg="Can't change the IF state; no IF block is active.")
134
- else:
135
- self.if_levels[-1].change_to(tf_value)
136
-
137
- def current(self) -> bool:
138
- if len(self.if_levels) == 0:
139
- raise ErrInfo(type="error", other_msg="No IF block is active.")
140
- else:
141
- return self.if_levels[-1].value()
142
-
143
- def all_true(self) -> bool:
144
- """Return True if every active IF level is true (or the stack is empty)."""
145
- if self.if_levels == []:
146
- return True
147
- return all(tf.value() for tf in self.if_levels)
148
-
149
- def only_current_false(self) -> bool:
150
- # Returns True if the current if level is false and all higher levels are True.
151
- if len(self.if_levels) == 0:
152
- return False
153
- elif len(self.if_levels) == 1:
154
- return not self.if_levels[-1].value()
155
- else:
156
- return not self.if_levels[-1].value() and all(tf.value() for tf in self.if_levels[:-1])
157
-
158
- def script_lines(self, top_n: int) -> list[tuple]:
159
- # Returns a list of tuples containing the script name and line number
160
- # for the topmost 'top_n' if levels, in bottom-up order.
161
- if len(self.if_levels) < top_n:
162
- raise ErrInfo(type="error", other_msg="Invalid IF stack depth reference.")
163
- levels = self.if_levels[len(self.if_levels) - top_n :]
164
- return [lvl.script_line() for lvl in levels]