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.
- execsql/__init__.py +6 -2
- execsql/api.py +25 -6
- execsql/cli/__init__.py +5 -3
- execsql/cli/lint.py +30 -34
- execsql/cli/run.py +10 -0
- execsql/config.py +145 -92
- execsql/db/access.py +54 -40
- execsql/db/base.py +33 -6
- execsql/db/firebird.py +3 -1
- execsql/db/mysql.py +4 -3
- execsql/db/oracle.py +36 -14
- execsql/db/postgres.py +8 -6
- execsql/db/sqlite.py +5 -2
- execsql/db/sqlserver.py +8 -6
- execsql/debug/repl.py +59 -21
- execsql/exceptions.py +19 -4
- execsql/exporters/base.py +3 -2
- execsql/exporters/delimited.py +2 -3
- execsql/exporters/feather.py +3 -3
- execsql/exporters/ods.py +1 -1
- execsql/exporters/xls.py +12 -4
- execsql/exporters/xlsx.py +1 -1
- execsql/gui/desktop.py +129 -15
- execsql/importers/__init__.py +1 -1
- execsql/importers/ods.py +1 -1
- execsql/importers/xls.py +1 -1
- execsql/metacommands/__init__.py +34 -5
- execsql/metacommands/conditions.py +26 -14
- execsql/metacommands/connect.py +21 -14
- execsql/metacommands/control.py +55 -68
- execsql/metacommands/data.py +25 -9
- execsql/metacommands/debug.py +132 -77
- execsql/metacommands/io_export.py +14 -2
- execsql/metacommands/io_import.py +11 -2
- execsql/metacommands/io_write.py +113 -11
- execsql/metacommands/prompt.py +46 -32
- execsql/metacommands/script_ext.py +63 -34
- execsql/metacommands/system.py +4 -3
- execsql/script/__init__.py +28 -37
- execsql/script/ast.py +7 -7
- execsql/script/control.py +4 -101
- execsql/script/engine.py +37 -251
- execsql/script/executor.py +181 -222
- execsql/script/parser.py +1 -3
- execsql/script/variables.py +8 -3
- execsql/state.py +125 -37
- execsql/utils/errors.py +0 -2
- execsql/utils/fileio.py +47 -3
- execsql/utils/mail.py +3 -2
- execsql/utils/strings.py +5 -5
- {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/METADATA +42 -36
- execsql2-2.17.2.dist-info/RECORD +124 -0
- execsql2-2.17.2.dist-info/licenses/NOTICE +11 -0
- execsql2-2.17.0.dist-info/RECORD +0 -124
- execsql2-2.17.0.dist-info/licenses/NOTICE +0 -10
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/WHEEL +0 -0
- {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/entry_points.txt +0 -0
- {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/licenses/LICENSE.txt +0 -0
execsql/metacommands/prompt.py
CHANGED
|
@@ -4,21 +4,35 @@ from execsql.exceptions import ErrInfo
|
|
|
4
4
|
"""
|
|
5
5
|
Interactive user prompt metacommand handlers for execsql.
|
|
6
6
|
|
|
7
|
-
Implements
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- ``
|
|
14
|
-
- ``
|
|
15
|
-
- ``
|
|
16
|
-
- ``
|
|
17
|
-
- ``
|
|
18
|
-
- ``
|
|
19
|
-
- ``
|
|
20
|
-
- ``
|
|
21
|
-
- ``
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
4
|
+
Script-block extension and dispatch handlers for execsql.
|
|
5
5
|
|
|
6
|
-
|
|
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
|
-
- ``
|
|
9
|
-
a
|
|
10
|
-
|
|
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
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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:
|
execsql/metacommands/system.py
CHANGED
|
@@ -3,9 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
"""
|
|
4
4
|
System and shell metacommand handlers for execsql.
|
|
5
5
|
|
|
6
|
-
Implements ``
|
|
7
|
-
and related system-interaction metacommands that allow
|
|
8
|
-
invoke external programs, set environment variables, or
|
|
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
|
execsql/script/__init__.py
CHANGED
|
@@ -1,55 +1,49 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
"""
|
|
4
|
-
Core script-execution
|
|
4
|
+
Core script-execution data types and helpers for execsql.
|
|
5
5
|
|
|
6
|
-
This
|
|
7
|
-
drive execution of execsql ``.sql`` script files.
|
|
8
|
-
|
|
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:`
|
|
15
|
-
- :class:`
|
|
16
|
-
|
|
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
|
-
|
|
24
|
-
- :class:`SqlStmt`
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
41
|
-
- :func:`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
:
|
|
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
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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,
|
|
9
|
-
|
|
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
|
|
18
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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]
|