execsql2 2.16.18__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/metacommands/upsert.py +0 -29
- 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 +193 -230
- 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.16.18.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.16.18.dist-info/RECORD +0 -124
- execsql2-2.16.18.dist-info/licenses/NOTICE +0 -10
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.16.18.dist-info → execsql2-2.17.2.dist-info}/WHEEL +0 -0
- {execsql2-2.16.18.dist-info → execsql2-2.17.2.dist-info}/entry_points.txt +0 -0
- {execsql2-2.16.18.dist-info → execsql2-2.17.2.dist-info}/licenses/LICENSE.txt +0 -0
execsql/gui/desktop.py
CHANGED
|
@@ -849,7 +849,7 @@ class ActionDialog:
|
|
|
849
849
|
|
|
850
850
|
|
|
851
851
|
# ---------------------------------------------------------------------------
|
|
852
|
-
# MapDialog —
|
|
852
|
+
# MapDialog — interactive map via tkintermapview (tabular fallback if missing)
|
|
853
853
|
# ---------------------------------------------------------------------------
|
|
854
854
|
|
|
855
855
|
|
|
@@ -870,22 +870,31 @@ class MapDialog:
|
|
|
870
870
|
anchor="w",
|
|
871
871
|
pady=(0, 4),
|
|
872
872
|
)
|
|
873
|
-
ttk.Label(frame, text="(Interactive map requires tkintermapview; showing tabular data)").pack(
|
|
874
|
-
anchor="w",
|
|
875
|
-
pady=(0, 8),
|
|
876
|
-
)
|
|
877
873
|
|
|
878
874
|
headers = args.get("headers", [])
|
|
879
875
|
rows = args.get("rows", [])
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
876
|
+
rendered_map = self._try_render_map(frame, args, headers, rows)
|
|
877
|
+
if not rendered_map:
|
|
878
|
+
ttk.Label(
|
|
879
|
+
frame,
|
|
880
|
+
text=(
|
|
881
|
+
"(Interactive map requires the tkintermapview package — "
|
|
882
|
+
"`pip install execsql2[map]` — showing tabular data)"
|
|
883
|
+
),
|
|
884
|
+
wraplength=580,
|
|
885
|
+
justify="left",
|
|
886
|
+
).pack(anchor="w", pady=(0, 8))
|
|
887
|
+
if headers and rows:
|
|
888
|
+
tree_frame = ttk.Frame(frame)
|
|
889
|
+
tree_frame.pack(fill=tk.BOTH, expand=True)
|
|
890
|
+
tree = ttk.Treeview(tree_frame, height=min(12, len(rows)))
|
|
891
|
+
_populate_treeview(tree, headers, rows)
|
|
892
|
+
vsb = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=tree.yview)
|
|
893
|
+
tree.configure(yscrollcommand=vsb.set)
|
|
894
|
+
tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
895
|
+
vsb.pack(side=tk.RIGHT, fill=tk.Y)
|
|
896
|
+
|
|
897
|
+
if rows:
|
|
889
898
|
ttk.Label(frame, text=_row_count_text(len(rows))).pack(anchor="w")
|
|
890
899
|
|
|
891
900
|
button_list = args.get("button_list", [("Continue", 1, "<Return>")])
|
|
@@ -893,15 +902,120 @@ class MapDialog:
|
|
|
893
902
|
btn_frame.pack(pady=8, anchor="e")
|
|
894
903
|
_add_buttons(btn_frame, button_list, lambda v: self._close(win, v))
|
|
895
904
|
|
|
896
|
-
_center_window(win,
|
|
905
|
+
_center_window(win, 750, 600)
|
|
897
906
|
root.wait_window(win)
|
|
898
907
|
|
|
908
|
+
def _try_render_map(self, frame: ttk.Frame, args: dict, headers: list, rows: list) -> bool:
|
|
909
|
+
import warnings
|
|
910
|
+
|
|
911
|
+
# tkintermapview pulls in `geocoder`, which has a Python 3.12+ SyntaxWarning
|
|
912
|
+
# from an unescaped \d in a regex string literal. It's harmless but noisy.
|
|
913
|
+
with warnings.catch_warnings():
|
|
914
|
+
warnings.filterwarnings("ignore", category=SyntaxWarning, module=r"geocoder.*")
|
|
915
|
+
try:
|
|
916
|
+
from tkintermapview import TkinterMapView
|
|
917
|
+
except ImportError:
|
|
918
|
+
return False
|
|
919
|
+
|
|
920
|
+
lat_col = args.get("lat_col")
|
|
921
|
+
lon_col = args.get("lon_col")
|
|
922
|
+
if not (lat_col and lon_col and headers and rows):
|
|
923
|
+
return False
|
|
924
|
+
|
|
925
|
+
hdrs_lower = [str(h).lower() for h in headers]
|
|
926
|
+
|
|
927
|
+
def _idx(col: str | None) -> int | None:
|
|
928
|
+
if not col:
|
|
929
|
+
return None
|
|
930
|
+
try:
|
|
931
|
+
return hdrs_lower.index(col.lower())
|
|
932
|
+
except ValueError:
|
|
933
|
+
return None
|
|
934
|
+
|
|
935
|
+
lat_idx = _idx(lat_col)
|
|
936
|
+
lon_idx = _idx(lon_col)
|
|
937
|
+
if lat_idx is None or lon_idx is None:
|
|
938
|
+
return False
|
|
939
|
+
|
|
940
|
+
label_idx = _idx(args.get("label_col"))
|
|
941
|
+
color_idx = _idx(args.get("color_col"))
|
|
942
|
+
|
|
943
|
+
markers: list[tuple[float, float, str | None, str | None]] = []
|
|
944
|
+
for row in rows:
|
|
945
|
+
try:
|
|
946
|
+
lat = float(row[lat_idx])
|
|
947
|
+
lon = float(row[lon_idx])
|
|
948
|
+
except (TypeError, ValueError, IndexError):
|
|
949
|
+
continue
|
|
950
|
+
label = str(row[label_idx]) if label_idx is not None and row[label_idx] is not None else None
|
|
951
|
+
color = str(row[color_idx]) if color_idx is not None and row[color_idx] is not None else None
|
|
952
|
+
markers.append((lat, lon, label, color))
|
|
953
|
+
|
|
954
|
+
if not markers:
|
|
955
|
+
return False
|
|
956
|
+
|
|
957
|
+
try:
|
|
958
|
+
map_widget = TkinterMapView(frame, width=700, height=480, corner_radius=0)
|
|
959
|
+
except Exception:
|
|
960
|
+
# Headless display, no Tk root, or any other map-widget failure —
|
|
961
|
+
# fall back to tabular rather than crashing the dialog.
|
|
962
|
+
return False
|
|
963
|
+
map_widget.pack(fill=tk.BOTH, expand=True, pady=(0, 8))
|
|
964
|
+
# Force the geometry manager to assign the real packed size to the widget
|
|
965
|
+
# so its tile-box math uses the actual on-screen dimensions, not the
|
|
966
|
+
# constructor values. Without this the first paint can fetch tiles for
|
|
967
|
+
# the wrong viewport and the basemap appears blank.
|
|
968
|
+
map_widget.update_idletasks()
|
|
969
|
+
|
|
970
|
+
avg_lat = sum(m[0] for m in markers) / len(markers)
|
|
971
|
+
avg_lon = sum(m[1] for m in markers) / len(markers)
|
|
972
|
+
zoom = _pick_zoom(markers)
|
|
973
|
+
|
|
974
|
+
def _populate() -> None:
|
|
975
|
+
map_widget.set_position(avg_lat, avg_lon)
|
|
976
|
+
map_widget.set_zoom(zoom)
|
|
977
|
+
for lat, lon, label, color in markers:
|
|
978
|
+
kwargs: dict = {}
|
|
979
|
+
if label:
|
|
980
|
+
kwargs["text"] = label
|
|
981
|
+
# Setting only `marker_color_circle` (not `marker_color_outside`)
|
|
982
|
+
# renders a small colored dot instead of the chunky full drop-pin.
|
|
983
|
+
if color:
|
|
984
|
+
kwargs["marker_color_circle"] = color
|
|
985
|
+
map_widget.set_marker(lat, lon, **kwargs)
|
|
986
|
+
|
|
987
|
+
# Defer marker / position setup until after the widget's first <Configure>
|
|
988
|
+
# event so tile loading happens against the realized dimensions.
|
|
989
|
+
map_widget.after_idle(_populate)
|
|
990
|
+
return True
|
|
991
|
+
|
|
899
992
|
def _close(self, win: tk.Toplevel, value: int | None) -> None:
|
|
900
993
|
self.result = {"button": value}
|
|
901
994
|
if win.winfo_exists():
|
|
902
995
|
win.destroy()
|
|
903
996
|
|
|
904
997
|
|
|
998
|
+
def _pick_zoom(markers: list[tuple[float, float, str | None, str | None]]) -> int:
|
|
999
|
+
if len(markers) <= 1:
|
|
1000
|
+
return 10
|
|
1001
|
+
lat_span = max(m[0] for m in markers) - min(m[0] for m in markers)
|
|
1002
|
+
lon_span = max(m[1] for m in markers) - min(m[1] for m in markers)
|
|
1003
|
+
span = max(lat_span, lon_span)
|
|
1004
|
+
if span > 60:
|
|
1005
|
+
return 2
|
|
1006
|
+
if span > 30:
|
|
1007
|
+
return 3
|
|
1008
|
+
if span > 15:
|
|
1009
|
+
return 4
|
|
1010
|
+
if span > 5:
|
|
1011
|
+
return 5
|
|
1012
|
+
if span > 2:
|
|
1013
|
+
return 7
|
|
1014
|
+
if span > 0.5:
|
|
1015
|
+
return 9
|
|
1016
|
+
return 11
|
|
1017
|
+
|
|
1018
|
+
|
|
905
1019
|
# ---------------------------------------------------------------------------
|
|
906
1020
|
# CredentialsDialog
|
|
907
1021
|
# ---------------------------------------------------------------------------
|
execsql/importers/__init__.py
CHANGED
|
@@ -5,5 +5,5 @@ Importer package for execsql.
|
|
|
5
5
|
|
|
6
6
|
Each sub-module reads a specific file format and loads data into a target
|
|
7
7
|
database table via the active :class:`~execsql.db.base.Database` connection.
|
|
8
|
-
Sub-modules: ``base``, ``csv``, ``ods``, ``xls``, ``feather``.
|
|
8
|
+
Sub-modules: ``base``, ``csv``, ``json``, ``ods``, ``xls``, ``feather``.
|
|
9
9
|
"""
|
execsql/importers/ods.py
CHANGED
|
@@ -6,7 +6,7 @@ ODS spreadsheet import for execsql.
|
|
|
6
6
|
Provides :func:`ods_data` (row iterator over an ODS sheet) and
|
|
7
7
|
:func:`importods` (imports an ODS sheet into a database table), used by
|
|
8
8
|
the ``IMPORT … FORMAT ods`` metacommand. Requires ``odfpy``
|
|
9
|
-
(``execsql2[
|
|
9
|
+
(``execsql2[formats]``).
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from typing import Any
|
execsql/importers/xls.py
CHANGED
|
@@ -6,7 +6,7 @@ XLS and XLSX spreadsheet import for execsql.
|
|
|
6
6
|
Provides :func:`xls_data` / :func:`importxls` (``xlrd``-based ``.xls``
|
|
7
7
|
reader) and the XLSX equivalent using ``openpyxl``. Used by
|
|
8
8
|
``IMPORT … FORMAT xls`` and ``FORMAT xlsx``. Requires
|
|
9
|
-
``execsql2[
|
|
9
|
+
``execsql2[formats]``.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from typing import Any
|
execsql/metacommands/__init__.py
CHANGED
|
@@ -1,11 +1,40 @@
|
|
|
1
1
|
"""execsql metacommand dispatch table.
|
|
2
2
|
|
|
3
|
-
Importing this module populates a MetaCommandList (``DISPATCH_TABLE``)
|
|
4
|
-
every metacommand regex and its handler function.
|
|
5
|
-
consumed by script.MetacommandStmt.run() via
|
|
3
|
+
Importing this module populates a ``MetaCommandList`` (``DISPATCH_TABLE``)
|
|
4
|
+
with every metacommand regex and its handler function. The dispatch
|
|
5
|
+
table is consumed by ``script.MetacommandStmt.run()`` via
|
|
6
|
+
``_state.metacommandlist``.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
The table itself is built by ``build_dispatch_table()`` in
|
|
9
|
+
:mod:`execsql.metacommands.dispatch`. Handler functions are organized
|
|
10
|
+
into sibling modules by topic:
|
|
11
|
+
|
|
12
|
+
- :mod:`~execsql.metacommands.connect` — CONNECT / USE / DISCONNECT
|
|
13
|
+
/ AUTOCOMMIT / PG_VACUUM.
|
|
14
|
+
- :mod:`~execsql.metacommands.conditions` — ``xf_*`` predicates used
|
|
15
|
+
by IF / ELSEIF / ASSERT.
|
|
16
|
+
- :mod:`~execsql.metacommands.control` — IF / ELSEIF / ELSE / ENDIF /
|
|
17
|
+
ANDIF / ORIF, ASSERT, LOOP, BATCH, HALT, BREAK, WAIT_UNTIL,
|
|
18
|
+
error-halt directives.
|
|
19
|
+
- :mod:`~execsql.metacommands.data` — SUB family, counters, IMPORT
|
|
20
|
+
config metacommands.
|
|
21
|
+
- :mod:`~execsql.metacommands.debug` — DEBUG WRITE/LOG variants and
|
|
22
|
+
SHOW SCRIPTS.
|
|
23
|
+
- :mod:`~execsql.metacommands.io_export` — EXPORT and EXPORT QUERY.
|
|
24
|
+
- :mod:`~execsql.metacommands.io_import` — IMPORT and IMPORT_FILE.
|
|
25
|
+
- :mod:`~execsql.metacommands.io_write` — WRITE, WRITE SCRIPT,
|
|
26
|
+
WRITE CREATE_TABLE.
|
|
27
|
+
- :mod:`~execsql.metacommands.io_fileops` — INCLUDE, COPY, ZIP,
|
|
28
|
+
RM_FILE, CD, SERVE.
|
|
29
|
+
- :mod:`~execsql.metacommands.io` — re-export façade for the four
|
|
30
|
+
``io_*`` modules above (kept for backward-compatible import paths).
|
|
31
|
+
- :mod:`~execsql.metacommands.prompt` — PROMPT family, ASK, PAUSE.
|
|
32
|
+
- :mod:`~execsql.metacommands.script_ext` — EXECUTE SCRIPT, RUN
|
|
33
|
+
SCRIPT, EXTEND SCRIPT.
|
|
34
|
+
- :mod:`~execsql.metacommands.system` — SYSTEM_CMD, LOG, EMAIL,
|
|
35
|
+
CONSOLE, ON ERROR_HALT / ON CANCEL_HALT directives, TIMER.
|
|
36
|
+
- :mod:`~execsql.metacommands.upsert` — PG_UPSERT (optional, requires
|
|
37
|
+
``execsql2[upsert]``).
|
|
9
38
|
"""
|
|
10
39
|
|
|
11
40
|
from __future__ import annotations
|
|
@@ -3,14 +3,26 @@ from __future__ import annotations
|
|
|
3
3
|
"""
|
|
4
4
|
Conditional test handler functions for execsql.
|
|
5
5
|
|
|
6
|
-
Implements
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
Implements the ``xf_*`` predicates used by IF / ELSEIF / ANDIF / ORIF
|
|
7
|
+
/ ASSERT / WAIT_UNTIL expressions. The IF-nesting *imperative*
|
|
8
|
+
handlers (``x_if``, ``x_if_elseif``, ``x_if_else``, ``x_if_end``,
|
|
9
|
+
etc.) live in :mod:`execsql.metacommands.control`.
|
|
10
|
+
|
|
11
|
+
Predicates registered here (with regex variants generated at
|
|
12
|
+
registration time for quoted, unquoted, and bracketed arguments):
|
|
13
|
+
|
|
14
|
+
- Object existence: ``xf_fileexists``, ``xf_direxists``,
|
|
15
|
+
``xf_schemaexists``, ``xf_tableexists``, ``xf_roleexists``,
|
|
16
|
+
``xf_script_exists``, ``xf_sub_defined``.
|
|
17
|
+
- Value tests: ``xf_equals``, ``xf_identical``, ``xf_contains``,
|
|
18
|
+
``xf_startswith``, ``xf_endswith``, ``xf_isnull``, ``xf_iszero``,
|
|
19
|
+
``xf_istrue``, ``xf_boolliteral``, ``xf_sub_empty``.
|
|
20
|
+
- Numeric comparison: ``xf_isgt``, ``xf_isgte``.
|
|
21
|
+
- Row counts: ``xf_hasrows``, ``xf_row_count_gt``, ``xf_row_count_gte``,
|
|
22
|
+
``xf_row_count_eq``, ``xf_row_count_lt``.
|
|
23
|
+
- Runtime state: ``xf_sqlerror``, ``xf_dialogcanceled``, ``xf_dbms``.
|
|
24
|
+
|
|
25
|
+
The full list is also surfaced by ``execsql --dump-keywords``.
|
|
14
26
|
"""
|
|
15
27
|
|
|
16
28
|
import os
|
|
@@ -245,9 +257,9 @@ def xf_sub_defined(**kwargs: Any) -> bool:
|
|
|
245
257
|
if varname[0] not in ("~", "#"):
|
|
246
258
|
subvarset = _state.subvars
|
|
247
259
|
elif varname[0] == "~":
|
|
248
|
-
subvarset = _state.
|
|
260
|
+
subvarset = _state.current_localvars()
|
|
249
261
|
else:
|
|
250
|
-
subvarset = _state.
|
|
262
|
+
subvarset = _state.current_paramvals()
|
|
251
263
|
return subvarset.sub_exists(varname) if subvarset else False
|
|
252
264
|
|
|
253
265
|
|
|
@@ -256,10 +268,10 @@ def xf_sub_empty(**kwargs: Any) -> bool:
|
|
|
256
268
|
if varname[0] not in ("~", "#"):
|
|
257
269
|
subvarset = _state.subvars
|
|
258
270
|
elif varname[0] == "~":
|
|
259
|
-
subvarset = _state.
|
|
271
|
+
subvarset = _state.current_localvars()
|
|
260
272
|
else:
|
|
261
|
-
subvarset = _state.
|
|
262
|
-
if not subvarset.sub_exists(varname):
|
|
273
|
+
subvarset = _state.current_paramvals()
|
|
274
|
+
if subvarset is None or not subvarset.sub_exists(varname):
|
|
263
275
|
raise ErrInfo(
|
|
264
276
|
type="cmd",
|
|
265
277
|
command_text=kwargs["metacommandline"],
|
|
@@ -270,7 +282,7 @@ def xf_sub_empty(**kwargs: Any) -> bool:
|
|
|
270
282
|
|
|
271
283
|
def xf_script_exists(**kwargs: Any) -> bool:
|
|
272
284
|
script_id = kwargs["script_id"].lower()
|
|
273
|
-
return script_id in _state.
|
|
285
|
+
return script_id in _state.ast_scripts
|
|
274
286
|
|
|
275
287
|
|
|
276
288
|
def xf_equals(**kwargs: Any) -> bool:
|
execsql/metacommands/connect.py
CHANGED
|
@@ -3,20 +3,27 @@ from __future__ import annotations
|
|
|
3
3
|
"""
|
|
4
4
|
Database connection metacommand handlers for execsql.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- ``
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- ``
|
|
14
|
-
- ``
|
|
15
|
-
-
|
|
16
|
-
- ``
|
|
17
|
-
- ``
|
|
18
|
-
-
|
|
19
|
-
|
|
6
|
+
Per-DBMS ``CONNECT`` handlers. Each DBMS has two variants — the bare
|
|
7
|
+
form (credentials taken from regex captures / config) and the
|
|
8
|
+
``_user_`` form (interactive password prompt):
|
|
9
|
+
|
|
10
|
+
- PostgreSQL: ``x_connect_pg``, ``x_connect_user_pg``
|
|
11
|
+
- SQL Server: ``x_connect_ssvr``, ``x_connect_user_ssvr``
|
|
12
|
+
- MySQL / MariaDB: ``x_connect_mysql``, ``x_connect_user_mysql``
|
|
13
|
+
- Oracle: ``x_connect_ora``, ``x_connect_user_ora``
|
|
14
|
+
- Firebird: ``x_connect_fb``, ``x_connect_user_fb``
|
|
15
|
+
- MS Access: ``x_connect_access``
|
|
16
|
+
- DuckDB: ``x_connect_duckdb``
|
|
17
|
+
- SQLite: ``x_connect_sqlite``
|
|
18
|
+
- ODBC DSN: ``x_connect_dsn``
|
|
19
|
+
|
|
20
|
+
Plus the connection-management handlers:
|
|
21
|
+
|
|
22
|
+
- ``x_use`` — ``USE <alias>`` (switch the active database).
|
|
23
|
+
- ``x_disconnect`` — ``DISCONNECT [<alias>]`` (close a registered connection).
|
|
24
|
+
- ``x_autocommit_on`` / ``x_autocommit_off`` — ``AUTOCOMMIT ON|OFF``.
|
|
25
|
+
- ``x_pg_vacuum`` — ``PG_VACUUM`` (run VACUUM against the current Postgres connection).
|
|
26
|
+
- ``x_daoflushdelay`` — ``CONFIG DAO_FLUSH_DELAY_SECS`` (MS Access only).
|
|
20
27
|
"""
|
|
21
28
|
|
|
22
29
|
from pathlib import Path
|
execsql/metacommands/control.py
CHANGED
|
@@ -3,17 +3,28 @@ from __future__ import annotations
|
|
|
3
3
|
"""
|
|
4
4
|
Control-flow metacommand handlers for execsql.
|
|
5
5
|
|
|
6
|
-
Implements the
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
|
|
6
|
+
Implements the ``x_*`` functions for script flow control:
|
|
7
|
+
|
|
8
|
+
- Conditional execution: ``x_if`` and its companions ``x_if_andif``,
|
|
9
|
+
``x_if_orif``, ``x_if_elseif``, ``x_if_else``, ``x_if_end``,
|
|
10
|
+
``x_if_block`` (compound IF / ELSEIF / ELSE / ENDIF handling).
|
|
11
|
+
- Assertion: ``x_assert`` (ASSERT condition with optional message).
|
|
12
|
+
- Loop management: ``x_loop`` (LOOP … END LOOP, including WHILE/UNTIL
|
|
13
|
+
variants matched by the same regex).
|
|
14
|
+
- Batch control: ``x_begin_batch``, ``x_end_batch``, ``x_rollback``.
|
|
15
|
+
- Error/halt control: ``x_halt``, ``x_halt_msg``, ``x_error_halt``,
|
|
16
|
+
``x_metacommand_error_halt``.
|
|
17
|
+
- Flow modifiers: ``x_break`` (exit a LOOP), ``x_wait_until``.
|
|
18
|
+
|
|
19
|
+
Handlers for related areas live in sibling modules:
|
|
20
|
+
|
|
21
|
+
- INCLUDE / EXECUTE SCRIPT / RUN / named-script registration —
|
|
22
|
+
:mod:`execsql.metacommands.script_ext` (and the AST executor).
|
|
23
|
+
- Counter operations (RESET COUNTER, SET COUNTER) and substitution
|
|
24
|
+
variable assignment (SUB, SUB_LOCAL, etc.) —
|
|
25
|
+
:mod:`execsql.metacommands.data`.
|
|
26
|
+
- ON ERROR_HALT / ON CANCEL_HALT directives —
|
|
27
|
+
:mod:`execsql.metacommands.system`.
|
|
17
28
|
"""
|
|
18
29
|
|
|
19
30
|
import time
|
|
@@ -22,13 +33,8 @@ from execsql.exceptions import ErrInfo
|
|
|
22
33
|
from typing import Any
|
|
23
34
|
|
|
24
35
|
import execsql.state as _state
|
|
25
|
-
from execsql.script import
|
|
26
|
-
|
|
27
|
-
MetacommandStmt,
|
|
28
|
-
ScriptCmd,
|
|
29
|
-
current_script_line,
|
|
30
|
-
)
|
|
31
|
-
from execsql.utils.errors import exit_now, write_warning
|
|
36
|
+
from execsql.script import current_script_line
|
|
37
|
+
from execsql.utils.errors import exit_now
|
|
32
38
|
from execsql.utils.fileio import EncodedFile, check_dir
|
|
33
39
|
from execsql.utils.gui import GUI_HALT, GuiSpec, enable_gui, gui_console_isrunning
|
|
34
40
|
|
|
@@ -67,67 +73,55 @@ def x_assert(**kwargs: Any) -> None:
|
|
|
67
73
|
raise ErrInfo(type="assert", other_msg=message)
|
|
68
74
|
|
|
69
75
|
|
|
76
|
+
def _ast_only_stub(name: str):
|
|
77
|
+
"""Return an ErrInfo for a metacommand the AST executor handles structurally.
|
|
78
|
+
|
|
79
|
+
These handlers stay registered in the dispatch table so ``--dump-keywords``,
|
|
80
|
+
the VS Code grammar generator, and ``--list-keywords`` still see the
|
|
81
|
+
keyword. The AST parser converts the source form (``IF`` / ``ENDIF`` /
|
|
82
|
+
``LOOP`` / ``BEGIN BATCH`` / ``BREAK`` / ``ELSE`` / ``ELSEIF`` / ``ANDIF``
|
|
83
|
+
/ ``ORIF``) into structural AST nodes that the executor walks directly —
|
|
84
|
+
none of these dispatch handlers fire for parsed scripts. Reaching one
|
|
85
|
+
means the AST parser failed to recognise the keyword, which is a bug.
|
|
86
|
+
"""
|
|
87
|
+
from execsql.exceptions import ErrInfo
|
|
88
|
+
|
|
89
|
+
return ErrInfo(
|
|
90
|
+
type="cmd",
|
|
91
|
+
other_msg=f"{name} should be handled by the AST executor, not the dispatch table.",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
70
95
|
def x_if(**kwargs: Any) -> None:
|
|
71
|
-
|
|
72
|
-
if tf_value:
|
|
73
|
-
src, line_no = current_script_line()
|
|
74
|
-
metacmd = MetacommandStmt(kwargs["condcmd"])
|
|
75
|
-
script_cmd = ScriptCmd(src, line_no, "cmd", metacmd)
|
|
76
|
-
cmdlist = CommandList([script_cmd], f"{src}_{line_no}")
|
|
77
|
-
_state.commandliststack.append(cmdlist)
|
|
78
|
-
return None
|
|
96
|
+
raise _ast_only_stub("IF")
|
|
79
97
|
|
|
80
98
|
|
|
81
99
|
def x_if_orif(**kwargs: Any) -> None:
|
|
82
|
-
|
|
83
|
-
return None # Short-circuit evaluation
|
|
84
|
-
if _state.if_stack.only_current_false():
|
|
85
|
-
_state.if_stack.replace(_state.xcmd_test(kwargs["condtest"]))
|
|
86
|
-
return None
|
|
100
|
+
raise _ast_only_stub("ORIF")
|
|
87
101
|
|
|
88
102
|
|
|
89
103
|
def x_if_andif(**kwargs: Any) -> None:
|
|
90
|
-
|
|
91
|
-
_state.if_stack.replace(_state.if_stack.current() and _state.xcmd_test(kwargs["condtest"]))
|
|
92
|
-
return None
|
|
104
|
+
raise _ast_only_stub("ANDIF")
|
|
93
105
|
|
|
94
106
|
|
|
95
107
|
def x_if_elseif(**kwargs: Any) -> None:
|
|
96
|
-
|
|
97
|
-
_state.if_stack.replace(_state.xcmd_test(kwargs["condtest"]))
|
|
98
|
-
else:
|
|
99
|
-
_state.if_stack.replace(False)
|
|
100
|
-
return None
|
|
108
|
+
raise _ast_only_stub("ELSEIF")
|
|
101
109
|
|
|
102
110
|
|
|
103
111
|
def x_if_else(**kwargs: Any) -> None:
|
|
104
|
-
|
|
105
|
-
_state.if_stack.invert()
|
|
106
|
-
return None
|
|
112
|
+
raise _ast_only_stub("ELSE")
|
|
107
113
|
|
|
108
114
|
|
|
109
115
|
def x_if_block(**kwargs: Any) -> None:
|
|
110
|
-
|
|
111
|
-
_state.if_stack.nest(_state.xcmd_test(kwargs["condtest"]))
|
|
112
|
-
else:
|
|
113
|
-
_state.if_stack.nest(False)
|
|
114
|
-
return None
|
|
116
|
+
raise _ast_only_stub("IF")
|
|
115
117
|
|
|
116
118
|
|
|
117
119
|
def x_if_end(**kwargs: Any) -> None:
|
|
118
|
-
|
|
119
|
-
return None
|
|
120
|
+
raise _ast_only_stub("ENDIF")
|
|
120
121
|
|
|
121
122
|
|
|
122
123
|
def x_loop(**kwargs: Any) -> None:
|
|
123
|
-
|
|
124
|
-
# This handler exists only for dispatch table registration compatibility.
|
|
125
|
-
from execsql.exceptions import ErrInfo
|
|
126
|
-
|
|
127
|
-
raise ErrInfo(
|
|
128
|
-
type="cmd",
|
|
129
|
-
other_msg="LOOP should be handled by the AST executor, not the dispatch table.",
|
|
130
|
-
)
|
|
124
|
+
raise _ast_only_stub("LOOP")
|
|
131
125
|
|
|
132
126
|
|
|
133
127
|
def x_halt(**kwargs: Any) -> None:
|
|
@@ -186,27 +180,20 @@ def x_metacommand_error_halt(**kwargs: Any) -> None:
|
|
|
186
180
|
|
|
187
181
|
|
|
188
182
|
def x_begin_batch(**kwargs: Any) -> None:
|
|
189
|
-
|
|
190
|
-
return None
|
|
183
|
+
raise _ast_only_stub("BEGIN BATCH")
|
|
191
184
|
|
|
192
185
|
|
|
193
186
|
def x_end_batch(**kwargs: Any) -> None:
|
|
194
|
-
|
|
195
|
-
return None
|
|
187
|
+
raise _ast_only_stub("END BATCH")
|
|
196
188
|
|
|
197
189
|
|
|
198
190
|
def x_rollback(**kwargs: Any) -> None:
|
|
191
|
+
"""Roll back all DBs registered in the innermost batch level."""
|
|
199
192
|
_state.status.batch.rollback_batch()
|
|
200
193
|
|
|
201
194
|
|
|
202
195
|
def x_break(**kwargs: Any) -> None:
|
|
203
|
-
|
|
204
|
-
src, line_no = current_script_line()
|
|
205
|
-
write_warning(f"BREAK metacommand with no command nesting on line {line_no} of {src}")
|
|
206
|
-
else:
|
|
207
|
-
_state.if_stack.if_levels = _state.if_stack.if_levels[: _state.commandliststack[-1].init_if_level]
|
|
208
|
-
_state.commandliststack.pop()
|
|
209
|
-
return None
|
|
196
|
+
raise _ast_only_stub("BREAK")
|
|
210
197
|
|
|
211
198
|
|
|
212
199
|
def x_wait_until(**kwargs: Any) -> None:
|
execsql/metacommands/data.py
CHANGED
|
@@ -2,13 +2,29 @@ from __future__ import annotations
|
|
|
2
2
|
from execsql.exceptions import ErrInfo
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
IMPORT
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
Substitution-variable, counter, and IMPORT/EXPORT configuration handlers.
|
|
6
|
+
|
|
7
|
+
Despite the module name, this file does **not** implement the EXPORT or
|
|
8
|
+
IMPORT metacommands themselves — those live in
|
|
9
|
+
:mod:`execsql.metacommands.io_export` and :mod:`execsql.metacommands.io_import`.
|
|
10
|
+
|
|
11
|
+
What's actually here:
|
|
12
|
+
|
|
13
|
+
- Substitution-variable manipulation: ``x_sub``, ``x_sub_local``,
|
|
14
|
+
``x_sub_add``, ``x_sub_append``, ``x_sub_empty``, ``x_rm_sub``,
|
|
15
|
+
``x_sub_tempfile``, ``x_sub_ini``, ``x_sub_querystring``,
|
|
16
|
+
``x_sub_encrypt``, ``x_sub_decrypt``, ``x_subdata``.
|
|
17
|
+
- Data-variable assignment from queries / prompts: ``x_selectsub``,
|
|
18
|
+
``x_prompt_selectsub``.
|
|
19
|
+
- Counter operations: ``x_reset_counter``, ``x_reset_counters``,
|
|
20
|
+
``x_set_counter``.
|
|
21
|
+
- IMPORT / scanning configuration metacommands (the ``CONFIG …`` form):
|
|
22
|
+
``x_empty_strings``, ``x_trim_strings``, ``x_replace_newlines``,
|
|
23
|
+
``x_empty_rows``, ``x_only_strings``, ``x_boolean_int``,
|
|
24
|
+
``x_boolean_words``, ``x_fold_col_hdrs``, ``x_trim_col_hdrs``,
|
|
25
|
+
``x_clean_col_hdrs``, ``x_del_empty_cols``, ``x_create_col_hdrs``,
|
|
26
|
+
``x_dedup_col_hdrs``, ``x_import_common_cols_only``, ``x_max_int``.
|
|
27
|
+
- EXPORT configuration: ``x_quote_all_text``.
|
|
12
28
|
"""
|
|
13
29
|
|
|
14
30
|
import math
|
|
@@ -54,7 +70,7 @@ def x_sub_empty(**kwargs: Any) -> None:
|
|
|
54
70
|
|
|
55
71
|
def x_rm_sub(**kwargs: Any) -> None:
|
|
56
72
|
varname = kwargs["match"]
|
|
57
|
-
subvarset = _state.subvars if varname[0] != "~" else _state.
|
|
73
|
+
subvarset = _state.subvars if varname[0] != "~" else _state.current_localvars()
|
|
58
74
|
subvarset.remove_substitution(varname)
|
|
59
75
|
return None
|
|
60
76
|
|
|
@@ -63,7 +79,7 @@ def x_sub_local(**kwargs: Any) -> None:
|
|
|
63
79
|
varname = kwargs["match"]
|
|
64
80
|
if varname[0] != "~":
|
|
65
81
|
varname = "~" + varname
|
|
66
|
-
_state.
|
|
82
|
+
_state.current_localvars().add_substitution(varname, kwargs["repl"])
|
|
67
83
|
return None
|
|
68
84
|
|
|
69
85
|
|