execsql2 2.21.1__py3-none-any.whl → 2.22.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- execsql/api.py +21 -4
- execsql/cli/run.py +37 -17
- execsql/config.py +37 -22
- execsql/data/execsql.conf.template +6 -0
- execsql/db/access.py +6 -5
- execsql/db/base.py +23 -5
- execsql/db/dsn.py +2 -0
- execsql/db/duckdb.py +1 -1
- execsql/db/mysql.py +6 -8
- execsql/db/sqlite.py +1 -1
- execsql/exceptions.py +14 -11
- execsql/exporters/base.py +1 -1
- execsql/exporters/delimited.py +22 -16
- execsql/exporters/duckdb.py +6 -1
- execsql/exporters/feather.py +2 -2
- execsql/exporters/json.py +5 -3
- execsql/exporters/latex.py +2 -2
- execsql/exporters/markdown.py +1 -0
- execsql/exporters/ods.py +7 -3
- execsql/exporters/parquet.py +1 -1
- execsql/exporters/raw.py +8 -8
- execsql/exporters/sqlite.py +11 -1
- execsql/exporters/templates.py +2 -1
- execsql/exporters/xls.py +10 -10
- execsql/exporters/xml.py +1 -0
- execsql/exporters/yaml.py +1 -0
- execsql/exporters/zip.py +9 -7
- execsql/format.py +6 -6
- execsql/gui/console.py +3 -3
- execsql/gui/desktop.py +28 -10
- execsql/gui/tui.py +8 -6
- execsql/importers/base.py +14 -8
- execsql/importers/csv.py +8 -3
- execsql/importers/xls.py +1 -0
- execsql/metacommands/conditions.py +14 -14
- execsql/metacommands/connect.py +2 -2
- execsql/metacommands/control.py +1 -1
- execsql/metacommands/data.py +2 -1
- execsql/metacommands/io_export.py +4 -0
- execsql/metacommands/io_fileops.py +85 -39
- execsql/metacommands/io_import.py +1 -0
- execsql/metacommands/io_write.py +1 -1
- execsql/metacommands/prompt.py +24 -22
- execsql/metacommands/system.py +14 -1
- execsql/models.py +56 -17
- execsql/parser.py +8 -8
- execsql/plugins.py +1 -1
- execsql/script/engine.py +4 -4
- execsql/script/executor.py +67 -28
- execsql/script/parser.py +13 -12
- execsql/script/variables.py +4 -2
- execsql/state.pyi +122 -0
- execsql/types.py +31 -26
- execsql/utils/auth.py +6 -2
- execsql/utils/datetime.py +3 -3
- execsql/utils/errors.py +7 -5
- execsql/utils/fileio.py +77 -31
- execsql/utils/gui.py +15 -11
- execsql/utils/mail.py +9 -2
- execsql/utils/numeric.py +1 -1
- execsql/utils/regex.py +9 -9
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/METADATA +3 -3
- execsql2-2.22.0.dist-info/RECORD +123 -0
- execsql2-2.21.1.dist-info/RECORD +0 -122
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.21.1.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/WHEEL +0 -0
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.21.1.dist-info → execsql2-2.22.0.dist-info}/licenses/NOTICE +0 -0
execsql/gui/desktop.py
CHANGED
|
@@ -12,7 +12,8 @@ from __future__ import annotations
|
|
|
12
12
|
import os
|
|
13
13
|
import threading
|
|
14
14
|
import time
|
|
15
|
-
from typing import Any
|
|
15
|
+
from typing import Any, cast
|
|
16
|
+
from collections.abc import Callable
|
|
16
17
|
|
|
17
18
|
from execsql.gui.base import GuiBackend
|
|
18
19
|
|
|
@@ -53,7 +54,7 @@ def _center_window(win: tk.Tk | tk.Toplevel, width: int = 600, height: int = 400
|
|
|
53
54
|
from execsql.gui.base import DIFF_MARKER, compare_stats as _compare_stats, compute_row_diffs
|
|
54
55
|
|
|
55
56
|
|
|
56
|
-
def _add_help_button(frame:
|
|
57
|
+
def _add_help_button(frame: Any, url: str | None) -> None:
|
|
57
58
|
"""Add a Help button to the top-right of *frame* that opens *url* in the system browser.
|
|
58
59
|
|
|
59
60
|
Uses ``place()`` so the button overlays the top-right corner without
|
|
@@ -82,19 +83,31 @@ def _populate_treeview(tree: ttk.Treeview, headers: list, rows: list) -> None:
|
|
|
82
83
|
tree.insert("", "end", values=[str(c) if c is not None else "" for c in row])
|
|
83
84
|
|
|
84
85
|
|
|
85
|
-
def _add_buttons(frame:
|
|
86
|
+
def _add_buttons(frame: Any, button_list: list, callback: Callable[[Any], Any]) -> None:
|
|
86
87
|
"""Add buttons from button_list = [(label, value, key?), ...] to frame."""
|
|
87
88
|
for _i, btn in enumerate(button_list):
|
|
88
89
|
label = btn[0]
|
|
89
90
|
value = btn[1]
|
|
90
|
-
|
|
91
|
+
|
|
92
|
+
def _command(v: Any = value) -> None:
|
|
93
|
+
callback(v)
|
|
94
|
+
|
|
95
|
+
b = tk.Button(frame, text=label, command=_command, padx=8)
|
|
91
96
|
b.pack(side=tk.LEFT, padx=4)
|
|
92
97
|
if len(btn) > 2 and btn[2]:
|
|
93
98
|
key_seq = btn[2]
|
|
94
99
|
if key_seq == "<Return>":
|
|
95
|
-
|
|
100
|
+
|
|
101
|
+
def _return_handler(_event: Any, v: Any = value) -> None:
|
|
102
|
+
callback(v)
|
|
103
|
+
|
|
104
|
+
frame.winfo_toplevel().bind("<Return>", _return_handler)
|
|
96
105
|
elif len(key_seq) == 1:
|
|
97
|
-
|
|
106
|
+
|
|
107
|
+
def _key_handler(_event: Any, v: Any = value) -> None:
|
|
108
|
+
callback(v)
|
|
109
|
+
|
|
110
|
+
frame.winfo_toplevel().bind(key_seq, _key_handler)
|
|
98
111
|
|
|
99
112
|
|
|
100
113
|
# ---------------------------------------------------------------------------
|
|
@@ -318,6 +331,7 @@ class EntryFormDialog:
|
|
|
318
331
|
|
|
319
332
|
for i, spec in enumerate(specs):
|
|
320
333
|
etype = (spec.entry_type or "text").lower()
|
|
334
|
+
var: Any
|
|
321
335
|
# For radiobuttons, the label is semicolon-delimited: first part is the label
|
|
322
336
|
if etype == "radiobuttons":
|
|
323
337
|
parts = (spec.label or "").split(";")
|
|
@@ -369,7 +383,7 @@ class EntryFormDialog:
|
|
|
369
383
|
var = tk.StringVar(value=spec.initial_value or "")
|
|
370
384
|
ttk.Entry(file_frame, textvariable=var, width=30).pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
371
385
|
|
|
372
|
-
def _browse(sv=var, mode=etype):
|
|
386
|
+
def _browse(sv: Any = var, mode: str = etype) -> None:
|
|
373
387
|
from tkinter import filedialog
|
|
374
388
|
|
|
375
389
|
if mode == "inputfile":
|
|
@@ -597,7 +611,7 @@ class CompareDialog:
|
|
|
597
611
|
tree.item(iid, tags=("diff_match",))
|
|
598
612
|
elif state == "changed":
|
|
599
613
|
tree.item(iid, tags=("diff_changed",))
|
|
600
|
-
originals[iid] = tree.item(iid, "values")
|
|
614
|
+
originals[iid] = cast(tuple[Any, ...], tree.item(iid, "values"))
|
|
601
615
|
vals = list(tree.item(iid, "values"))
|
|
602
616
|
diff_set = changed_cols[ridx]
|
|
603
617
|
for ci, col_name in enumerate(headers_str):
|
|
@@ -828,10 +842,14 @@ class ActionDialog:
|
|
|
828
842
|
button_specs = args.get("button_specs", [])
|
|
829
843
|
for i, spec in enumerate(button_specs):
|
|
830
844
|
btn_text = f"{spec.label}\n{spec.prompt}"
|
|
845
|
+
|
|
846
|
+
def _button_command(v: int = i + 1) -> None:
|
|
847
|
+
self._close(win, v)
|
|
848
|
+
|
|
831
849
|
ttk.Button(
|
|
832
850
|
frame,
|
|
833
851
|
text=btn_text,
|
|
834
|
-
command=
|
|
852
|
+
command=_button_command,
|
|
835
853
|
).pack(fill=tk.X, pady=2)
|
|
836
854
|
|
|
837
855
|
include_continue = args.get("include_continue_button")
|
|
@@ -1380,7 +1398,7 @@ class TkinterBackend(GuiBackend):
|
|
|
1380
1398
|
root = self._root_or_raise()
|
|
1381
1399
|
dlg = dialog_class(root, args)
|
|
1382
1400
|
root.update()
|
|
1383
|
-
return dlg.result
|
|
1401
|
+
return cast(dict, dlg.result)
|
|
1384
1402
|
|
|
1385
1403
|
def show_halt(self, args: dict) -> dict:
|
|
1386
1404
|
return self._run_dialog(MsgDialog, args)
|
execsql/gui/tui.py
CHANGED
|
@@ -21,7 +21,7 @@ import ctypes
|
|
|
21
21
|
import os
|
|
22
22
|
import queue as _stdlib_queue
|
|
23
23
|
import threading
|
|
24
|
-
from typing import Any
|
|
24
|
+
from typing import Any, Literal, cast
|
|
25
25
|
|
|
26
26
|
from execsql.gui.base import GuiBackend
|
|
27
27
|
|
|
@@ -61,7 +61,7 @@ from textual.widgets import (
|
|
|
61
61
|
|
|
62
62
|
__all__ = ["TextualBackend"]
|
|
63
63
|
|
|
64
|
-
from execsql.gui.base import DIFF_MARKER, compare_stats as _compare_stats, compute_row_diffs
|
|
64
|
+
from execsql.gui.base import DIFF_MARKER, DiffResult, compare_stats as _compare_stats, compute_row_diffs
|
|
65
65
|
|
|
66
66
|
# ---------------------------------------------------------------------------
|
|
67
67
|
# Helpers
|
|
@@ -105,7 +105,7 @@ def _button_row(button_list: list) -> list[Button]:
|
|
|
105
105
|
if not value and value != 1: # falsy but not accidentally 1
|
|
106
106
|
cancel_label = label
|
|
107
107
|
else:
|
|
108
|
-
variant = "primary" if i == 0 else "default"
|
|
108
|
+
variant: Literal["primary", "default"] = "primary" if i == 0 else "default"
|
|
109
109
|
primary_buttons.append(Button(label, id=f"btn_{i}", variant=variant))
|
|
110
110
|
return [Button(cancel_label, id="btn_cancel_exit", variant="warning")] + primary_buttons
|
|
111
111
|
|
|
@@ -695,7 +695,7 @@ class CompareScreen(_BaseDialog):
|
|
|
695
695
|
self._col_keys1: list = []
|
|
696
696
|
self._col_keys2: list = []
|
|
697
697
|
self._diff_on = False
|
|
698
|
-
self._diff_result = None
|
|
698
|
+
self._diff_result: DiffResult | None = None
|
|
699
699
|
self._original_cells1: dict = {}
|
|
700
700
|
self._original_cells2: dict = {}
|
|
701
701
|
|
|
@@ -1407,7 +1407,9 @@ class _TextualSyncQueue:
|
|
|
1407
1407
|
return
|
|
1408
1408
|
if self._screen_map is None:
|
|
1409
1409
|
self.__class__._screen_map = _build_screen_map()
|
|
1410
|
-
|
|
1410
|
+
screen_map = self._screen_map
|
|
1411
|
+
assert screen_map is not None
|
|
1412
|
+
screen_class = screen_map.get(spec.gui_type)
|
|
1411
1413
|
if screen_class is None:
|
|
1412
1414
|
spec.return_queue.put({"error": f"Unknown GUI type: {spec.gui_type}", "button": None})
|
|
1413
1415
|
return
|
|
@@ -1578,7 +1580,7 @@ class ConsoleApp(App):
|
|
|
1578
1580
|
else:
|
|
1579
1581
|
spec.return_queue.put(result if result is not None else {})
|
|
1580
1582
|
|
|
1581
|
-
self.run_worker(_push, exclusive=False)
|
|
1583
|
+
self.run_worker(cast(Any, _push), exclusive=False)
|
|
1582
1584
|
|
|
1583
1585
|
def write_console(self, text: str) -> None:
|
|
1584
1586
|
"""Thread-safe method to append text to the RichLog widget."""
|
execsql/importers/base.py
CHANGED
|
@@ -63,23 +63,29 @@ def import_data_table(
|
|
|
63
63
|
|
|
64
64
|
hdrs = dedup_words(hdrs)
|
|
65
65
|
|
|
66
|
+
tablespec_cache: Any = None
|
|
67
|
+
|
|
66
68
|
def get_ts():
|
|
67
|
-
|
|
69
|
+
nonlocal tablespec_cache
|
|
70
|
+
if tablespec_cache is None:
|
|
68
71
|
from execsql.models import DataTable
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
get_ts.tablespec = None
|
|
73
|
+
tablespec_cache = DataTable(hdrs, data)
|
|
74
|
+
return tablespec_cache
|
|
74
75
|
|
|
75
|
-
exec_log = _state.exec_log
|
|
76
76
|
if is_new:
|
|
77
77
|
if is_new == 2:
|
|
78
78
|
tblspec = db.schema_qualified_table_name(schemaname, tablename)
|
|
79
79
|
try:
|
|
80
80
|
db.drop_table(tblspec)
|
|
81
|
-
except
|
|
82
|
-
|
|
81
|
+
except ErrInfo:
|
|
82
|
+
raise
|
|
83
|
+
except Exception as e:
|
|
84
|
+
raise ErrInfo(
|
|
85
|
+
type="db",
|
|
86
|
+
other_msg=f"Could not drop existing table ({tblspec}) for IMPORT metacommand",
|
|
87
|
+
exception_msg=exception_desc(),
|
|
88
|
+
) from e
|
|
83
89
|
sql = get_ts().create_table(db.type, schemaname, tablename)
|
|
84
90
|
try:
|
|
85
91
|
db.execute(sql)
|
execsql/importers/csv.py
CHANGED
|
@@ -54,9 +54,14 @@ def importtable(
|
|
|
54
54
|
if is_new == 2:
|
|
55
55
|
try:
|
|
56
56
|
db.drop_table(db.schema_qualified_table_name(schemaname, tablename))
|
|
57
|
-
except
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
except ErrInfo:
|
|
58
|
+
raise
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise ErrInfo(
|
|
61
|
+
type="db",
|
|
62
|
+
other_msg=f"Could not drop existing table ({tablename}) for IMPORT metacommand",
|
|
63
|
+
exception_msg=exception_desc(),
|
|
64
|
+
) from e
|
|
60
65
|
try:
|
|
61
66
|
db.execute(sql)
|
|
62
67
|
# Don't commit table creation here; the commit will be done after data import
|
execsql/importers/xls.py
CHANGED
|
@@ -33,6 +33,7 @@ def xls_data(
|
|
|
33
33
|
if len(filename) < 4:
|
|
34
34
|
raise ErrInfo(type="cmd", other_msg=f"{filename} is not a recognizable Excel spreadsheet name.")
|
|
35
35
|
ext3 = filename[-3:].lower()
|
|
36
|
+
wbk: Any
|
|
36
37
|
if ext3 == "xls":
|
|
37
38
|
# xlrd imported lazily
|
|
38
39
|
from execsql.exporters.xls import XlsFile
|
|
@@ -61,8 +61,8 @@ def _quote_table_name(name: str) -> str:
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def xf_contains(**kwargs: Any) -> bool:
|
|
64
|
-
s1 = kwargs["string1"]
|
|
65
|
-
s2 = kwargs["string2"]
|
|
64
|
+
s1 = str(kwargs["string1"])
|
|
65
|
+
s2 = str(kwargs["string2"])
|
|
66
66
|
if kwargs["ignorecase"] and kwargs["ignorecase"].lower() == "i":
|
|
67
67
|
s1 = s1.lower()
|
|
68
68
|
s2 = s2.lower()
|
|
@@ -70,8 +70,8 @@ def xf_contains(**kwargs: Any) -> bool:
|
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
def xf_startswith(**kwargs: Any) -> bool:
|
|
73
|
-
s1 = kwargs["string1"]
|
|
74
|
-
s2 = kwargs["string2"]
|
|
73
|
+
s1 = str(kwargs["string1"])
|
|
74
|
+
s2 = str(kwargs["string2"])
|
|
75
75
|
if kwargs["ignorecase"] and kwargs["ignorecase"].lower() == "i":
|
|
76
76
|
s1 = s1.lower()
|
|
77
77
|
s2 = s2.lower()
|
|
@@ -79,8 +79,8 @@ def xf_startswith(**kwargs: Any) -> bool:
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
def xf_endswith(**kwargs: Any) -> bool:
|
|
82
|
-
s1 = kwargs["string1"]
|
|
83
|
-
s2 = kwargs["string2"]
|
|
82
|
+
s1 = str(kwargs["string1"])
|
|
83
|
+
s2 = str(kwargs["string2"])
|
|
84
84
|
if kwargs["ignorecase"] and kwargs["ignorecase"].lower() == "i":
|
|
85
85
|
s1 = s1.lower()
|
|
86
86
|
s2 = s2.lower()
|
|
@@ -97,7 +97,7 @@ def xf_hasrows(**kwargs: Any) -> bool:
|
|
|
97
97
|
except Exception as e:
|
|
98
98
|
raise ErrInfo("db", sql, exception_msg=exception_desc()) from e
|
|
99
99
|
nrows = rec[0][0]
|
|
100
|
-
return nrows > 0
|
|
100
|
+
return bool(nrows > 0)
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
def _row_count(queryname: str, sql_context: str, metacommandline: str) -> int:
|
|
@@ -313,13 +313,13 @@ def xf_equals(**kwargs: Any) -> bool:
|
|
|
313
313
|
|
|
314
314
|
|
|
315
315
|
def xf_identical(**kwargs: Any) -> bool:
|
|
316
|
-
s1 = kwargs["string1"].strip('"')
|
|
317
|
-
s2 = kwargs["string2"].strip('"')
|
|
316
|
+
s1 = str(kwargs["string1"]).strip('"')
|
|
317
|
+
s2 = str(kwargs["string2"]).strip('"')
|
|
318
318
|
return s1 == s2
|
|
319
319
|
|
|
320
320
|
|
|
321
321
|
def xf_isnull(**kwargs: Any) -> bool:
|
|
322
|
-
item = kwargs["item"].strip().strip('"')
|
|
322
|
+
item = str(kwargs["item"]).strip().strip('"')
|
|
323
323
|
return item == ""
|
|
324
324
|
|
|
325
325
|
|
|
@@ -379,12 +379,12 @@ def xf_isfalse(**kwargs: Any) -> bool:
|
|
|
379
379
|
|
|
380
380
|
|
|
381
381
|
def xf_dbms(**kwargs: Any) -> bool:
|
|
382
|
-
dbms = kwargs["dbms"]
|
|
383
|
-
return _state.dbs.current().type.dbms_id.lower() == dbms.strip().lower()
|
|
382
|
+
dbms = str(kwargs["dbms"])
|
|
383
|
+
return bool(_state.dbs.current().type.dbms_id.lower() == dbms.strip().lower())
|
|
384
384
|
|
|
385
385
|
|
|
386
386
|
def xf_dbname(**kwargs: Any) -> bool:
|
|
387
|
-
dbname = kwargs["dbname"]
|
|
387
|
+
dbname = str(kwargs["dbname"])
|
|
388
388
|
return _state.dbs.current().name().lower() == dbname.strip().lower()
|
|
389
389
|
|
|
390
390
|
|
|
@@ -875,6 +875,6 @@ CONDITIONAL_TABLE = build_conditional_table()
|
|
|
875
875
|
def xcmd_test(teststr: str) -> bool:
|
|
876
876
|
result = CondParser(teststr).parse().eval()
|
|
877
877
|
if result is not None:
|
|
878
|
-
return result
|
|
878
|
+
return bool(result)
|
|
879
879
|
else:
|
|
880
880
|
raise ErrInfo(type="cmd", command_text=teststr, other_msg="Unrecognized conditional")
|
execsql/metacommands/connect.py
CHANGED
|
@@ -27,7 +27,7 @@ Plus the connection-management handlers:
|
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
29
|
from pathlib import Path
|
|
30
|
-
from typing import Any
|
|
30
|
+
from typing import Any, cast
|
|
31
31
|
|
|
32
32
|
import execsql.state as _state
|
|
33
33
|
from execsql.db.access import AccessDatabase # noqa: F401 — used in x_connect_access; module-level for test patchability
|
|
@@ -498,7 +498,7 @@ def x_pg_vacuum(**kwargs: Any) -> None:
|
|
|
498
498
|
db = _state.dbs.current()
|
|
499
499
|
if db.type == dbt_postgres:
|
|
500
500
|
args = kwargs["vacuum_args"]
|
|
501
|
-
db.vacuum(args)
|
|
501
|
+
cast(Any, db).vacuum(args)
|
|
502
502
|
|
|
503
503
|
|
|
504
504
|
def x_daoflushdelay(**kwargs: Any) -> None:
|
execsql/metacommands/control.py
CHANGED
execsql/metacommands/data.py
CHANGED
|
@@ -227,7 +227,7 @@ def x_prompt_selectsub(**kwargs: Any) -> None:
|
|
|
227
227
|
if cont:
|
|
228
228
|
btns.append(("Continue", 2, "<Return>"))
|
|
229
229
|
enable_gui()
|
|
230
|
-
return_queue = _queue.Queue()
|
|
230
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
231
231
|
gui_args = {
|
|
232
232
|
"title": "Select data",
|
|
233
233
|
"message": msg,
|
|
@@ -254,6 +254,7 @@ def x_prompt_selectsub(**kwargs: Any) -> None:
|
|
|
254
254
|
exit_now(2, None)
|
|
255
255
|
else:
|
|
256
256
|
if btn_val == 1:
|
|
257
|
+
assert selected_row is not None
|
|
257
258
|
for i, item in enumerate(selected_row):
|
|
258
259
|
if item is None:
|
|
259
260
|
item = ""
|
|
@@ -113,10 +113,13 @@ def _dispatch_format(
|
|
|
113
113
|
elif filefmt == "xlsx":
|
|
114
114
|
write_query_to_xlsx(select_stmt, db, outfile, append, sheetname=sheetname, desc=description)
|
|
115
115
|
elif filefmt == "duckdb":
|
|
116
|
+
assert tablename is not None
|
|
116
117
|
write_query_to_duckdb(select_stmt, db, outfile, append, tablename=tablename)
|
|
117
118
|
elif filefmt == "sqlite":
|
|
119
|
+
assert tablename is not None
|
|
118
120
|
write_query_to_sqlite(select_stmt, db, outfile, append, tablename=tablename)
|
|
119
121
|
elif filefmt == "xml":
|
|
122
|
+
assert xml_table is not None
|
|
120
123
|
write_query_to_xml(select_stmt, xml_table, db, outfile, append, desc=description, zipfile=zipfilename)
|
|
121
124
|
elif filefmt == "json":
|
|
122
125
|
write_query_to_json(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
|
|
@@ -131,6 +134,7 @@ def _dispatch_format(
|
|
|
131
134
|
elif filefmt == "latex":
|
|
132
135
|
write_query_to_latex(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
|
|
133
136
|
elif filefmt == "hdf5":
|
|
137
|
+
assert hdf5_table is not None
|
|
134
138
|
write_query_to_hdf5(hdf5_table, select_stmt, db, outfile, append, desc=description)
|
|
135
139
|
elif filefmt == "yaml":
|
|
136
140
|
write_query_to_yaml(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
|
|
@@ -9,9 +9,10 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
11
|
import sys
|
|
12
|
+
from itertools import tee
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from shutil import copyfileobj
|
|
14
|
-
from typing import Any
|
|
15
|
+
from typing import Any, Literal, cast
|
|
15
16
|
|
|
16
17
|
import execsql.state as _state
|
|
17
18
|
from execsql.exceptions import ErrInfo
|
|
@@ -22,6 +23,12 @@ from execsql.utils.fileio import filewriter_close
|
|
|
22
23
|
from execsql.utils.strings import unquoted
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
def _close_rowsource(rows: Any) -> None:
|
|
27
|
+
close = getattr(rows, "close", None)
|
|
28
|
+
if close:
|
|
29
|
+
close()
|
|
30
|
+
|
|
31
|
+
|
|
25
32
|
def x_include(**kwargs: Any) -> None:
|
|
26
33
|
# INCLUDE is now handled natively by the AST executor (_execute_include_native).
|
|
27
34
|
# This handler exists only for dispatch table registration compatibility.
|
|
@@ -77,36 +84,59 @@ def x_copy(**kwargs: Any) -> None:
|
|
|
77
84
|
pass # Best-effort check; some adapters lack information_schema.
|
|
78
85
|
select_stmt = f"select * from {tbl1};"
|
|
79
86
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
get_ts.tablespec = DataTable(hdrs, rows)
|
|
84
|
-
return get_ts.tablespec
|
|
85
|
-
|
|
86
|
-
get_ts.tablespec = None
|
|
87
|
+
get_ts_tablespec = None
|
|
88
|
+
rows_to_close = None
|
|
89
|
+
rows_for_insert: Any
|
|
87
90
|
|
|
88
91
|
if new_tbl2:
|
|
89
|
-
|
|
92
|
+
try:
|
|
93
|
+
hdrs, rows = db1.select_rowsource(select_stmt)
|
|
94
|
+
except ErrInfo:
|
|
95
|
+
raise
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
98
|
+
rows_to_close = rows
|
|
99
|
+
rows = cast(Any, rows)
|
|
100
|
+
rows_for_schema, rows_for_insert = tee(rows)
|
|
101
|
+
get_ts_tablespec = DataTable(hdrs, rows_for_schema)
|
|
102
|
+
tbl_desc = get_ts_tablespec
|
|
90
103
|
create_tbl = tbl_desc.create_table(db2.type, schema2, table2)
|
|
91
104
|
if new_tbl2 == "replacement":
|
|
92
105
|
try:
|
|
93
106
|
db2.drop_table(tbl2)
|
|
94
|
-
except
|
|
95
|
-
|
|
107
|
+
except ErrInfo:
|
|
108
|
+
raise
|
|
109
|
+
except Exception as e:
|
|
110
|
+
raise ErrInfo(
|
|
111
|
+
type="db",
|
|
112
|
+
other_msg=f"Could not drop existing table ({tbl2}) for COPY metacommand",
|
|
113
|
+
exception_msg=exception_desc(),
|
|
114
|
+
) from e
|
|
96
115
|
db2.execute(create_tbl)
|
|
97
116
|
if db2.needs_explicit_commit_after_ddl():
|
|
98
117
|
db2.execute("COMMIT;")
|
|
118
|
+
else:
|
|
119
|
+
try:
|
|
120
|
+
hdrs, rows = db1.select_rowsource(select_stmt)
|
|
121
|
+
except ErrInfo:
|
|
122
|
+
raise
|
|
123
|
+
except Exception as e:
|
|
124
|
+
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
125
|
+
rows_to_close = rows
|
|
126
|
+
rows_for_insert = rows
|
|
127
|
+
|
|
128
|
+
def get_ts() -> DataTable:
|
|
129
|
+
nonlocal get_ts_tablespec
|
|
130
|
+
if get_ts_tablespec is None:
|
|
131
|
+
ts_hdrs, ts_rows = db1.select_rowsource(select_stmt)
|
|
132
|
+
get_ts_tablespec = DataTable(ts_hdrs, ts_rows)
|
|
133
|
+
return get_ts_tablespec
|
|
134
|
+
|
|
99
135
|
try:
|
|
100
|
-
|
|
101
|
-
except ErrInfo:
|
|
102
|
-
raise
|
|
103
|
-
except Exception as e:
|
|
104
|
-
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
105
|
-
try:
|
|
106
|
-
db2.populate_table(schema2, table2, rows, hdrs, get_ts)
|
|
136
|
+
db2.populate_table(schema2, table2, rows_for_insert, hdrs, get_ts)
|
|
107
137
|
db2.commit()
|
|
108
138
|
except BaseException:
|
|
109
|
-
|
|
139
|
+
_close_rowsource(rows_to_close)
|
|
110
140
|
raise
|
|
111
141
|
|
|
112
142
|
|
|
@@ -144,13 +174,9 @@ def x_copy_query(**kwargs: Any) -> None:
|
|
|
144
174
|
except Exception:
|
|
145
175
|
pass # Best-effort check; some adapters lack information_schema.
|
|
146
176
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
get_ts.tablespec = DataTable(hdrs, rows)
|
|
151
|
-
return get_ts.tablespec
|
|
152
|
-
|
|
153
|
-
get_ts.tablespec = None
|
|
177
|
+
get_ts_tablespec = None
|
|
178
|
+
rows_to_close = None
|
|
179
|
+
rows_for_insert: Any
|
|
154
180
|
|
|
155
181
|
if new_tbl2:
|
|
156
182
|
try:
|
|
@@ -159,28 +185,48 @@ def x_copy_query(**kwargs: Any) -> None:
|
|
|
159
185
|
raise
|
|
160
186
|
except Exception as e:
|
|
161
187
|
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
162
|
-
|
|
163
|
-
|
|
188
|
+
rows_to_close = rows
|
|
189
|
+
rows = cast(Any, rows)
|
|
190
|
+
rows_for_schema, rows_for_insert = tee(rows)
|
|
191
|
+
get_ts_tablespec = DataTable(hdrs, rows_for_schema)
|
|
192
|
+
tbl_desc = get_ts_tablespec
|
|
164
193
|
create_tbl = tbl_desc.create_table(db2.type, schema2, table2)
|
|
165
194
|
if new_tbl2 == "replacement":
|
|
166
195
|
try:
|
|
167
196
|
db2.drop_table(tbl2)
|
|
168
|
-
except
|
|
169
|
-
|
|
197
|
+
except ErrInfo:
|
|
198
|
+
raise
|
|
199
|
+
except Exception as e:
|
|
200
|
+
raise ErrInfo(
|
|
201
|
+
type="db",
|
|
202
|
+
other_msg=f"Could not drop existing table ({tbl2}) for COPY metacommand",
|
|
203
|
+
exception_msg=exception_desc(),
|
|
204
|
+
) from e
|
|
170
205
|
db2.execute(create_tbl)
|
|
171
206
|
if db2.needs_explicit_commit_after_ddl():
|
|
172
207
|
db2.execute("COMMIT;")
|
|
208
|
+
else:
|
|
209
|
+
try:
|
|
210
|
+
hdrs, rows = db1.select_rowsource(select_stmt)
|
|
211
|
+
except ErrInfo:
|
|
212
|
+
raise
|
|
213
|
+
except Exception as e:
|
|
214
|
+
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
215
|
+
rows_to_close = rows
|
|
216
|
+
rows_for_insert = rows
|
|
217
|
+
|
|
218
|
+
def get_ts() -> DataTable:
|
|
219
|
+
nonlocal get_ts_tablespec
|
|
220
|
+
if get_ts_tablespec is None:
|
|
221
|
+
ts_hdrs, ts_rows = db1.select_rowsource(select_stmt)
|
|
222
|
+
get_ts_tablespec = DataTable(ts_hdrs, ts_rows)
|
|
223
|
+
return get_ts_tablespec
|
|
224
|
+
|
|
173
225
|
try:
|
|
174
|
-
|
|
175
|
-
except ErrInfo:
|
|
176
|
-
raise
|
|
177
|
-
except Exception as e:
|
|
178
|
-
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
179
|
-
try:
|
|
180
|
-
db2.populate_table(schema2, table2, rows, hdrs, get_ts)
|
|
226
|
+
db2.populate_table(schema2, table2, rows_for_insert, hdrs, get_ts)
|
|
181
227
|
db2.commit()
|
|
182
228
|
except BaseException:
|
|
183
|
-
|
|
229
|
+
_close_rowsource(rows_to_close)
|
|
184
230
|
raise
|
|
185
231
|
|
|
186
232
|
|
|
@@ -191,7 +237,7 @@ def x_zip(**kwargs: Any) -> None:
|
|
|
191
237
|
files = kwargs["filename"].strip(' "')
|
|
192
238
|
zipfile_name = kwargs["zipfilename"].strip(' "')
|
|
193
239
|
append = kwargs["append"]
|
|
194
|
-
zmode = "a" if append is not None else "w"
|
|
240
|
+
zmode: Literal["a", "w"] = "a" if append is not None else "w"
|
|
195
241
|
zf = _zipfile.ZipFile(zipfile_name, mode=zmode, compression=_zipfile.ZIP_BZIP2, compresslevel=9)
|
|
196
242
|
fnlist = _glob.glob(files)
|
|
197
243
|
for f in fnlist:
|
|
@@ -296,6 +296,7 @@ def x_import_xls_pattern(**kwargs: Any) -> None:
|
|
|
296
296
|
if len(filename) < 4:
|
|
297
297
|
raise ErrInfo(type="cmd", other_msg=f"{filename} is not a recognizable Excel spreadsheet name.")
|
|
298
298
|
ext3 = filename[-3:].lower()
|
|
299
|
+
wbk: Any
|
|
299
300
|
if ext3 == "xls":
|
|
300
301
|
wbk = XlsFile()
|
|
301
302
|
elif ext3 == "lsx":
|
execsql/metacommands/io_write.py
CHANGED
|
@@ -51,7 +51,7 @@ def x_write(**kwargs: Any) -> None:
|
|
|
51
51
|
) from e
|
|
52
52
|
except ConsoleUIError as e:
|
|
53
53
|
_state.output.reset()
|
|
54
|
-
_state.exec_log.log_status_info(f"Console UI write failed (message {{{e
|
|
54
|
+
_state.exec_log.log_status_info(f"Console UI write failed (message {{{e}}}); output reset to stdout.")
|
|
55
55
|
_state.output.write(msg)
|
|
56
56
|
if _state.conf.tee_write_log:
|
|
57
57
|
_state.exec_log.log_user_msg(msg)
|