execsql2 2.21.2__py3-none-any.whl → 2.22.1__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 +29 -4
- execsql/cli/__init__.py +1 -1
- execsql/cli/run.py +37 -17
- execsql/config.py +40 -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 +1 -1
- 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/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 +69 -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 +80 -32
- 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.2.dist-info → execsql2-2.22.1.dist-info}/METADATA +114 -132
- execsql2-2.22.1.dist-info/RECORD +123 -0
- execsql2-2.21.2.dist-info/RECORD +0 -122
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.21.2.data → execsql2-2.22.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.21.2.dist-info → execsql2-2.22.1.dist-info}/WHEEL +0 -0
- {execsql2-2.21.2.dist-info → execsql2-2.22.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.21.2.dist-info → execsql2-2.22.1.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.21.2.dist-info → execsql2-2.22.1.dist-info}/licenses/NOTICE +0 -0
execsql/metacommands/prompt.py
CHANGED
|
@@ -81,7 +81,7 @@ def x_prompt(**kwargs: Any) -> None:
|
|
|
81
81
|
cmd = f"select * from {sq_name};"
|
|
82
82
|
colnames, rows = db.select_data(cmd)
|
|
83
83
|
enable_gui()
|
|
84
|
-
return_queue = _queue.Queue()
|
|
84
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
85
85
|
gui_args = {
|
|
86
86
|
"title": table,
|
|
87
87
|
"message": message,
|
|
@@ -117,7 +117,7 @@ def x_prompt_enter(**kwargs: Any) -> None:
|
|
|
117
117
|
else:
|
|
118
118
|
hdrs, rows = None, None
|
|
119
119
|
enable_gui()
|
|
120
|
-
return_queue = _queue.Queue()
|
|
120
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
121
121
|
gui_args = {
|
|
122
122
|
"title": "Enter a value",
|
|
123
123
|
"message": message,
|
|
@@ -289,7 +289,7 @@ def x_prompt_entryform(**kwargs: Any) -> None:
|
|
|
289
289
|
sq_name = db.schema_qualified_table_name(display_schema, display_table)
|
|
290
290
|
colnames, rows = db.select_data(f"select * from {sq_name};")
|
|
291
291
|
enable_gui()
|
|
292
|
-
return_queue = _queue.Queue()
|
|
292
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
293
293
|
gui_args = {
|
|
294
294
|
"title": "Entry",
|
|
295
295
|
"message": message,
|
|
@@ -304,18 +304,20 @@ def x_prompt_entryform(**kwargs: Any) -> None:
|
|
|
304
304
|
entries = user_response["return_value"]
|
|
305
305
|
script, line_no = current_script_line()
|
|
306
306
|
if btn:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
307
|
+
assert entries is not None
|
|
308
|
+
assert subvar is not None
|
|
309
|
+
for entry in entries:
|
|
310
|
+
if entry.value:
|
|
311
|
+
value = str(entry.value)
|
|
310
312
|
subvarset = _state.subvars if subvar[0] != "~" else _state.current_localvars()
|
|
311
|
-
subvarset.add_substitution(
|
|
313
|
+
subvarset.add_substitution(entry.name, value)
|
|
312
314
|
_state.exec_log.log_status_info(
|
|
313
|
-
f"Substitution variable {
|
|
315
|
+
f"Substitution variable {entry.name} set to {{{value}}} on line {line_no} of {script}",
|
|
314
316
|
)
|
|
315
317
|
else:
|
|
316
|
-
if _state.subvars.sub_exists(
|
|
318
|
+
if _state.subvars.sub_exists(entry.name):
|
|
317
319
|
_state.exec_log.log_status_info(
|
|
318
|
-
f"Substitution variable {
|
|
320
|
+
f"Substitution variable {entry.name} removed on line {line_no} of {script}",
|
|
319
321
|
)
|
|
320
322
|
else:
|
|
321
323
|
if _state.status.cancel_halt:
|
|
@@ -340,7 +342,7 @@ def x_prompt_pause(**kwargs: Any) -> None:
|
|
|
340
342
|
if timeunit and timeunit.lower() == "minutes":
|
|
341
343
|
maxtime_secs = maxtime_secs * 60
|
|
342
344
|
enable_gui()
|
|
343
|
-
return_queue = _queue.Queue()
|
|
345
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
344
346
|
gui_args = {
|
|
345
347
|
"title": f"Script {current_script_line()[0]}",
|
|
346
348
|
"message": msg,
|
|
@@ -401,7 +403,7 @@ def prompt_compare(button_list: list, **kwargs: Any) -> Any:
|
|
|
401
403
|
other_msg=f"Specified primary key columns do not exist in PROMPT COMPARE metacommand on line {line_no} of {script}.",
|
|
402
404
|
)
|
|
403
405
|
enable_gui()
|
|
404
|
-
return_queue = _queue.Queue()
|
|
406
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
405
407
|
gui_args = {
|
|
406
408
|
"title": "Compare data",
|
|
407
409
|
"message": msg,
|
|
@@ -458,7 +460,7 @@ def x_prompt_ask(**kwargs: Any) -> None:
|
|
|
458
460
|
else:
|
|
459
461
|
colnames, rows = None, None
|
|
460
462
|
enable_gui()
|
|
461
|
-
return_queue = _queue.Queue()
|
|
463
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
462
464
|
gui_args = {
|
|
463
465
|
"title": script,
|
|
464
466
|
"message": kwargs["question"],
|
|
@@ -499,7 +501,7 @@ def x_prompt_map(**kwargs: Any) -> None:
|
|
|
499
501
|
cmd = f"select * from {sq_name};"
|
|
500
502
|
colnames, rows = db.select_data(cmd)
|
|
501
503
|
enable_gui()
|
|
502
|
-
return_queue = _queue.Queue()
|
|
504
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
503
505
|
gui_args = {
|
|
504
506
|
"title": table,
|
|
505
507
|
"message": message,
|
|
@@ -590,7 +592,7 @@ def x_prompt_action(**kwargs: Any) -> None:
|
|
|
590
592
|
sq_name = db.schema_qualified_table_name(display_schema, display_table)
|
|
591
593
|
colnames, rows = db.select_data(f"select * from {sq_name};")
|
|
592
594
|
enable_gui()
|
|
593
|
-
return_queue = _queue.Queue()
|
|
595
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
594
596
|
gui_args = {
|
|
595
597
|
"title": "Actions",
|
|
596
598
|
"message": message,
|
|
@@ -624,7 +626,7 @@ def x_prompt_savefile(**kwargs: Any) -> None:
|
|
|
624
626
|
script, lno = current_script_line()
|
|
625
627
|
working_dir = startdir if startdir is not None else str(Path(script).resolve().parent)
|
|
626
628
|
enable_gui()
|
|
627
|
-
return_queue = _queue.Queue()
|
|
629
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
628
630
|
gui_args = {"working_dir": working_dir, "script": script}
|
|
629
631
|
_state.gui_manager_queue.put(GuiSpec(GUI_SAVEFILE, gui_args, return_queue))
|
|
630
632
|
user_response = return_queue.get(block=True)
|
|
@@ -697,7 +699,7 @@ def x_prompt_openfile(**kwargs: Any) -> None:
|
|
|
697
699
|
script, lno = current_script_line()
|
|
698
700
|
working_dir = startdir if startdir is not None else str(Path(script).resolve().parent)
|
|
699
701
|
enable_gui()
|
|
700
|
-
return_queue = _queue.Queue()
|
|
702
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
701
703
|
gui_args = {"working_dir": working_dir, "script": script}
|
|
702
704
|
_state.gui_manager_queue.put(GuiSpec(GUI_OPENFILE, gui_args, return_queue))
|
|
703
705
|
user_response = return_queue.get(block=True)
|
|
@@ -756,7 +758,7 @@ def x_prompt_directory(**kwargs: Any) -> None:
|
|
|
756
758
|
script, lno = current_script_line()
|
|
757
759
|
working_dir = startdir if startdir is not None else str(Path(script).resolve().parent)
|
|
758
760
|
enable_gui()
|
|
759
|
-
return_queue = _queue.Queue()
|
|
761
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
760
762
|
gui_args = {"working_dir": working_dir, "script": script}
|
|
761
763
|
_state.gui_manager_queue.put(GuiSpec(GUI_DIRECTORY, gui_args, return_queue))
|
|
762
764
|
user_response = return_queue.get(block=True)
|
|
@@ -818,7 +820,7 @@ def prompt_select_rows(button_list: list, **kwargs: Any) -> Any:
|
|
|
818
820
|
if len(missing_hdrs) > 0:
|
|
819
821
|
raise ErrInfo("error", other_msg=f"Columns [{', '.join(missing_hdrs)}] are missing from {sq_name2}.")
|
|
820
822
|
enable_gui()
|
|
821
|
-
return_queue = _queue.Queue()
|
|
823
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
822
824
|
gui_args = {
|
|
823
825
|
"title": "Select rows",
|
|
824
826
|
"message": msg,
|
|
@@ -867,7 +869,7 @@ def x_ask(**kwargs: Any) -> None:
|
|
|
867
869
|
subvar = kwargs["match"]
|
|
868
870
|
script, lno = current_script_line()
|
|
869
871
|
if _state.gui_console:
|
|
870
|
-
return_queue = _queue.Queue()
|
|
872
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
871
873
|
gui_args = {
|
|
872
874
|
"title": script,
|
|
873
875
|
"message": kwargs["question"],
|
|
@@ -919,7 +921,7 @@ def x_pause(**kwargs: Any) -> None:
|
|
|
919
921
|
maxtime_secs = maxtime_secs * 60
|
|
920
922
|
use_gui = False
|
|
921
923
|
if _state.gui_manager_thread:
|
|
922
|
-
return_queue = _queue.Queue()
|
|
924
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
923
925
|
_state.gui_manager_queue.put(GuiSpec(QUERY_CONSOLE, {}, return_queue))
|
|
924
926
|
user_response = return_queue.get(block=True)
|
|
925
927
|
use_gui = user_response["console_running"]
|
|
@@ -953,7 +955,7 @@ def x_msg(**kwargs: Any) -> None:
|
|
|
953
955
|
message = kwargs["message"]
|
|
954
956
|
current_script_line()
|
|
955
957
|
enable_gui()
|
|
956
|
-
return_queue = _queue.Queue()
|
|
958
|
+
return_queue: _queue.Queue[Any] = _queue.Queue()
|
|
957
959
|
gui_args = {"title": "Message", "message": message}
|
|
958
960
|
_state.gui_manager_queue.put(GuiSpec(GUI_MSG, gui_args, return_queue))
|
|
959
961
|
return_queue.get(block=True)
|
execsql/metacommands/system.py
CHANGED
|
@@ -49,7 +49,20 @@ def x_system_cmd(**kwargs: Any) -> None:
|
|
|
49
49
|
filewriter_close_all_after_write()
|
|
50
50
|
cmdlist = shlex.split(syscmd, posix=(os.name == "posix"))
|
|
51
51
|
if cont is None:
|
|
52
|
-
|
|
52
|
+
timeout = getattr(_state.conf, "system_cmd_timeout", 0.0)
|
|
53
|
+
try:
|
|
54
|
+
if timeout and timeout > 0:
|
|
55
|
+
result = subprocess.run(cmdlist, timeout=timeout)
|
|
56
|
+
else:
|
|
57
|
+
result = subprocess.run(cmdlist)
|
|
58
|
+
except subprocess.TimeoutExpired as e:
|
|
59
|
+
_state.subvars.add_substitution("$SYSTEM_CMD_EXIT_STATUS", "124")
|
|
60
|
+
raise ErrInfo(
|
|
61
|
+
type="cmd",
|
|
62
|
+
command_text=kwargs.get("metacommandline", "SYSTEM_CMD"),
|
|
63
|
+
other_msg=f"SYSTEM_CMD timed out after {timeout:g} seconds.",
|
|
64
|
+
exception_msg=exception_desc(),
|
|
65
|
+
) from e
|
|
53
66
|
_state.subvars.add_substitution("$SYSTEM_CMD_EXIT_STATUS", str(result.returncode))
|
|
54
67
|
else:
|
|
55
68
|
proc = subprocess.Popen(cmdlist)
|
execsql/models.py
CHANGED
|
@@ -47,6 +47,8 @@ __all__ = [
|
|
|
47
47
|
"to_json_type",
|
|
48
48
|
]
|
|
49
49
|
|
|
50
|
+
ColumnType = tuple[str, type[DataType], int | None, bool, int | None, int | None]
|
|
51
|
+
|
|
50
52
|
|
|
51
53
|
class Column:
|
|
52
54
|
"""Compile data-type match statistics for a single column of imported data."""
|
|
@@ -66,9 +68,11 @@ class Column:
|
|
|
66
68
|
self.count = 0
|
|
67
69
|
self.maxlen = 0
|
|
68
70
|
self.varlen = False
|
|
69
|
-
self.maxprecision = None
|
|
70
|
-
self.scale = None
|
|
71
|
+
self.maxprecision: int | None = None
|
|
72
|
+
self.scale: int | None = None
|
|
71
73
|
self.varscale = False
|
|
74
|
+
self.maxscale: int | None = None
|
|
75
|
+
self.max_int_digits: int | None = None
|
|
72
76
|
|
|
73
77
|
def __repr__(self) -> str:
|
|
74
78
|
return (
|
|
@@ -97,21 +101,35 @@ class Column:
|
|
|
97
101
|
if vlen > self.maxlen:
|
|
98
102
|
self.maxlen = vlen
|
|
99
103
|
if self.dt.precision is not None and self.dt.scale is not None:
|
|
104
|
+
int_digits = self.dt.precision - self.dt.scale
|
|
100
105
|
if self.maxprecision is None:
|
|
101
106
|
self.maxprecision = self.dt.precision
|
|
102
|
-
else:
|
|
103
|
-
self.maxprecision = max(self.dt.precision, self.maxprecision)
|
|
104
|
-
if self.scale is None:
|
|
105
107
|
self.scale = self.dt.scale
|
|
108
|
+
self.maxscale = self.dt.scale
|
|
109
|
+
self.max_int_digits = int_digits
|
|
106
110
|
else:
|
|
111
|
+
assert self.maxscale is not None
|
|
112
|
+
assert self.max_int_digits is not None
|
|
113
|
+
self.maxprecision = max(self.dt.precision, self.maxprecision)
|
|
107
114
|
if self.dt.scale != self.scale:
|
|
108
115
|
self.varscale = True
|
|
109
|
-
|
|
116
|
+
self.maxscale = max(self.dt.scale, self.maxscale)
|
|
117
|
+
self.max_int_digits = max(int_digits, self.max_int_digits)
|
|
110
118
|
else:
|
|
111
119
|
self.failed = True
|
|
112
120
|
|
|
113
|
-
def __init__(self, colname: str) -> None:
|
|
114
|
-
"""Create a column characteriser for the named column.
|
|
121
|
+
def __init__(self, colname: str, infer_strings: bool = True) -> None:
|
|
122
|
+
"""Create a column characteriser for the named column.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
colname: The name of the column to characterise.
|
|
126
|
+
infer_strings: When ``False``, string values are only tested
|
|
127
|
+
against string-typed accumulators (Character, Varchar, Text),
|
|
128
|
+
skipping numeric/date/boolean checks. Set to ``False`` when
|
|
129
|
+
the row source already contains typed values (e.g. exported
|
|
130
|
+
from a DBMS cursor) so that a Python ``str`` that happens to
|
|
131
|
+
look like a date is not misclassified.
|
|
132
|
+
"""
|
|
115
133
|
from execsql.exceptions import ErrInfo
|
|
116
134
|
import execsql.state as _state
|
|
117
135
|
|
|
@@ -126,6 +144,7 @@ class Column:
|
|
|
126
144
|
self.nullrows = 0
|
|
127
145
|
# The list of accumulators must be in order from most specific to least specific data type.
|
|
128
146
|
conf = _state.conf
|
|
147
|
+
self.accums: tuple[Column.Accum, ...]
|
|
129
148
|
if conf.only_strings:
|
|
130
149
|
self.accums = (
|
|
131
150
|
self.Accum(DT_Character()),
|
|
@@ -148,10 +167,12 @@ class Column:
|
|
|
148
167
|
self.Accum(DT_Text()),
|
|
149
168
|
self.Accum(DT_Binary()),
|
|
150
169
|
)
|
|
170
|
+
self.infer_strings = infer_strings
|
|
171
|
+
self.str_accums = tuple(ac for ac in self.accums if isinstance(ac.dt, (DT_Character, DT_Varchar, DT_Text)))
|
|
151
172
|
self.dt_eval = False
|
|
152
173
|
# self.dt is a tuple of: 0: column name; 1: data type class; 2: max length or None;
|
|
153
174
|
# 3: bool indicating any null values; 4: precision or None; 5: scale or None.
|
|
154
|
-
self.dt
|
|
175
|
+
self.dt: ColumnType | None = None
|
|
155
176
|
|
|
156
177
|
def __repr__(self) -> str:
|
|
157
178
|
return f"Column({self.name!r})"
|
|
@@ -174,15 +195,20 @@ class Column:
|
|
|
174
195
|
):
|
|
175
196
|
self.nullrows += 1
|
|
176
197
|
return
|
|
177
|
-
|
|
178
|
-
dt.
|
|
198
|
+
if not self.infer_strings and isinstance(column_value, str):
|
|
199
|
+
for dt in self.str_accums:
|
|
200
|
+
dt.check(column_value)
|
|
201
|
+
else:
|
|
202
|
+
for dt in self.accums:
|
|
203
|
+
dt.check(column_value)
|
|
179
204
|
|
|
180
|
-
def column_type(self) ->
|
|
205
|
+
def column_type(self) -> ColumnType:
|
|
181
206
|
"""Return the inferred type of this column as a 6-tuple."""
|
|
182
207
|
# Return the type of this column as a tuple of:
|
|
183
208
|
# column name, data type class, max length or None, bool for null values,
|
|
184
209
|
# precision or None, scale or None.
|
|
185
210
|
if self.dt_eval:
|
|
211
|
+
assert self.dt is not None
|
|
186
212
|
return self.dt
|
|
187
213
|
sel_type = None # Will be set to an Accum instance.
|
|
188
214
|
if self.nullrows == self.rowcount:
|
|
@@ -213,8 +239,12 @@ class Column:
|
|
|
213
239
|
sel_type.dt.__class__,
|
|
214
240
|
None if not sel_type.dt.lenspec else sel_type.maxlen,
|
|
215
241
|
self.nullrows > 0,
|
|
216
|
-
|
|
217
|
-
|
|
242
|
+
(
|
|
243
|
+
sel_type.max_int_digits + sel_type.maxscale
|
|
244
|
+
if sel_type.max_int_digits is not None and sel_type.maxscale is not None
|
|
245
|
+
else sel_type.maxprecision
|
|
246
|
+
),
|
|
247
|
+
sel_type.maxscale if sel_type.maxscale is not None else sel_type.scale,
|
|
218
248
|
)
|
|
219
249
|
self.dt_eval = True
|
|
220
250
|
return self.dt
|
|
@@ -223,8 +253,17 @@ class Column:
|
|
|
223
253
|
class DataTable:
|
|
224
254
|
"""Scan a row source and infer column types for CREATE TABLE generation."""
|
|
225
255
|
|
|
226
|
-
def __init__(self, column_names: list[str], rowsource: Any) -> None:
|
|
227
|
-
"""Scan all rows from the source and infer a column type for each column.
|
|
256
|
+
def __init__(self, column_names: list[str], rowsource: Any, infer_strings: bool = True) -> None:
|
|
257
|
+
"""Scan all rows from the source and infer a column type for each column.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
column_names: Ordered list of column names for the table.
|
|
261
|
+
rowsource: Iterable of row tuples (or lists) to scan.
|
|
262
|
+
infer_strings: Passed through to each :class:`Column`. Set to
|
|
263
|
+
``False`` when the row source originates from a DBMS cursor so
|
|
264
|
+
that Python ``str`` values that resemble dates or numbers are
|
|
265
|
+
not misclassified.
|
|
266
|
+
"""
|
|
228
267
|
import execsql.state as _state
|
|
229
268
|
|
|
230
269
|
self.inputrows = 0 # Total number of rows in the row source.
|
|
@@ -232,7 +271,7 @@ class DataTable:
|
|
|
232
271
|
self.shortrows = 0 # Number of rows without as many data values as column names.
|
|
233
272
|
self.cols: list = [] # List of Column objects.
|
|
234
273
|
for n in column_names:
|
|
235
|
-
self.cols.append(Column(n))
|
|
274
|
+
self.cols.append(Column(n, infer_strings=infer_strings))
|
|
236
275
|
conf = _state.conf
|
|
237
276
|
# Read and evaluate columns in the rowsource until done (or until an error).
|
|
238
277
|
for datarow in rowsource:
|
execsql/parser.py
CHANGED
|
@@ -23,7 +23,7 @@ Classes:
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
import re
|
|
26
|
-
from typing import Any
|
|
26
|
+
from typing import Any, cast
|
|
27
27
|
|
|
28
28
|
from execsql.exceptions import CondParserError, NumericParserError
|
|
29
29
|
|
|
@@ -154,18 +154,18 @@ class CondAstNode(CondTokens):
|
|
|
154
154
|
if self.type == self.CONDITIONAL:
|
|
155
155
|
exec_fn = self.left[0].exec_fn
|
|
156
156
|
cmdargs = self.left[1]
|
|
157
|
-
return exec_fn(**cmdargs)
|
|
157
|
+
return bool(exec_fn(**cmdargs))
|
|
158
158
|
if self.type == self.NOT:
|
|
159
159
|
return not self.left.eval()
|
|
160
160
|
lcond = self.left.eval()
|
|
161
161
|
if self.type == self.AND:
|
|
162
162
|
if not lcond:
|
|
163
163
|
return False
|
|
164
|
-
return self.right.eval()
|
|
164
|
+
return cast(bool, self.right.eval())
|
|
165
165
|
if self.type == self.OR:
|
|
166
166
|
if lcond:
|
|
167
167
|
return True
|
|
168
|
-
return self.right.eval()
|
|
168
|
+
return cast(bool, self.right.eval())
|
|
169
169
|
raise CondParserError(f"Unknown conditional node type: {self.type}")
|
|
170
170
|
|
|
171
171
|
|
|
@@ -254,10 +254,10 @@ class CondParser(CondTokens):
|
|
|
254
254
|
# conditionallist is a module-level global in the main execsql module
|
|
255
255
|
import execsql.state as _state
|
|
256
256
|
|
|
257
|
-
|
|
258
|
-
if
|
|
259
|
-
|
|
260
|
-
return CondAstNode(self.CONDITIONAL,
|
|
257
|
+
matched_metacommand = self.cond_expr.match_metacommand(_state.conditionallist)
|
|
258
|
+
if matched_metacommand is not None:
|
|
259
|
+
matched_metacommand[1]["metacommandline"] = self.condexpr
|
|
260
|
+
return CondAstNode(self.CONDITIONAL, matched_metacommand, None)
|
|
261
261
|
else:
|
|
262
262
|
if self.cond_expr.match_str("(") is not None:
|
|
263
263
|
m1 = self.expression()
|
execsql/plugins.py
CHANGED
|
@@ -298,7 +298,7 @@ def _load_entry_points(group: str) -> list[tuple[str, Any]]:
|
|
|
298
298
|
Errors during loading are logged and skipped — a broken plugin should
|
|
299
299
|
not prevent execsql from starting.
|
|
300
300
|
"""
|
|
301
|
-
results = []
|
|
301
|
+
results: list[tuple[str, Any]] = []
|
|
302
302
|
try:
|
|
303
303
|
eps = entry_points(group=group)
|
|
304
304
|
except Exception:
|
execsql/script/engine.py
CHANGED
|
@@ -28,7 +28,7 @@ import os
|
|
|
28
28
|
import re
|
|
29
29
|
import uuid
|
|
30
30
|
from pathlib import Path
|
|
31
|
-
from typing import Any
|
|
31
|
+
from typing import Any, cast
|
|
32
32
|
|
|
33
33
|
import execsql.state as _state
|
|
34
34
|
from execsql.exceptions import ErrInfo
|
|
@@ -315,11 +315,11 @@ class ScriptCmd:
|
|
|
315
315
|
def __repr__(self) -> str:
|
|
316
316
|
return f"ScriptCmd({self.source!r}, {self.line_no!r}, {self.command_type!r}, {repr(self.command)!r})"
|
|
317
317
|
|
|
318
|
-
def current_script_line(self) -> tuple:
|
|
318
|
+
def current_script_line(self) -> tuple[str, int]:
|
|
319
319
|
return (self.source, self.line_no)
|
|
320
320
|
|
|
321
321
|
def commandline(self) -> str:
|
|
322
|
-
return self.command.statement if self.command_type == "sql" else "-- !x! " + self.command.statement
|
|
322
|
+
return cast(str, self.command.statement if self.command_type == "sql" else "-- !x! " + self.command.statement)
|
|
323
323
|
|
|
324
324
|
|
|
325
325
|
# ---------------------------------------------------------------------------
|
|
@@ -505,7 +505,7 @@ def substitute_vars(command_str: str, localvars: SubVarSet | None = None, ctx: A
|
|
|
505
505
|
return cmdstr
|
|
506
506
|
|
|
507
507
|
|
|
508
|
-
def current_script_line() -> tuple:
|
|
508
|
+
def current_script_line() -> tuple[str, int]:
|
|
509
509
|
"""Return ``(source_name, line_number)`` for the command currently executing.
|
|
510
510
|
|
|
511
511
|
Reads from ``_state.last_command``, which the AST executor updates on
|
execsql/script/executor.py
CHANGED
|
@@ -47,7 +47,7 @@ import os
|
|
|
47
47
|
import re
|
|
48
48
|
import time as _time
|
|
49
49
|
from pathlib import Path
|
|
50
|
-
from typing import Any
|
|
50
|
+
from typing import Any, cast
|
|
51
51
|
|
|
52
52
|
from execsql.exceptions import ErrInfo
|
|
53
53
|
from execsql.script.ast import (
|
|
@@ -98,7 +98,17 @@ def _stack_localvars(ctx: RuntimeContext) -> SubVarSet | None:
|
|
|
98
98
|
localvars = scope.localvars
|
|
99
99
|
if localvars is None:
|
|
100
100
|
return None
|
|
101
|
-
return localvars.merge(scope.paramvals)
|
|
101
|
+
return cast(SubVarSet, localvars.merge(scope.paramvals))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _assert_command_runtime(ctx: RuntimeContext) -> None:
|
|
105
|
+
"""Assert runtime singletons required by command execution are initialised."""
|
|
106
|
+
assert ctx.conf is not None
|
|
107
|
+
assert ctx.dbs is not None
|
|
108
|
+
assert ctx.metacommandlist is not None
|
|
109
|
+
assert ctx.output is not None
|
|
110
|
+
assert ctx.status is not None
|
|
111
|
+
assert ctx.subvars is not None
|
|
102
112
|
|
|
103
113
|
|
|
104
114
|
def _push_frame(
|
|
@@ -192,6 +202,7 @@ def _eval_condition(
|
|
|
192
202
|
|
|
193
203
|
def _set_command_vars(ctx: RuntimeContext, source: str, line_no: int) -> None:
|
|
194
204
|
"""Set per-command system variables (current script, line, time)."""
|
|
205
|
+
assert ctx.subvars is not None
|
|
195
206
|
now = datetime.datetime.now()
|
|
196
207
|
ctx.subvars.add_substitution("$CURRENT_TIME", now.strftime("%Y-%m-%d %H:%M"))
|
|
197
208
|
ctx.subvars.add_substitution("$CURRENT_DATE", now.strftime("%Y-%m-%d"))
|
|
@@ -219,9 +230,21 @@ def _exec_sql(
|
|
|
219
230
|
commit: bool = True,
|
|
220
231
|
) -> None:
|
|
221
232
|
"""Execute a SQL statement against the current database."""
|
|
222
|
-
ctx
|
|
223
|
-
|
|
224
|
-
|
|
233
|
+
_assert_command_runtime(ctx)
|
|
234
|
+
conf = ctx.conf
|
|
235
|
+
dbs = ctx.dbs
|
|
236
|
+
output = ctx.output
|
|
237
|
+
status = ctx.status
|
|
238
|
+
subvars = ctx.subvars
|
|
239
|
+
assert conf is not None
|
|
240
|
+
assert dbs is not None
|
|
241
|
+
assert output is not None
|
|
242
|
+
assert status is not None
|
|
243
|
+
assert subvars is not None
|
|
244
|
+
|
|
245
|
+
status.sql_error = False
|
|
246
|
+
if status.batch.in_batch():
|
|
247
|
+
status.batch.using_db(dbs.current())
|
|
225
248
|
# Build localvars from the command-list stack frame so that ~ and # vars
|
|
226
249
|
# written by x_sub/get_subvarset are visible. The stack frame is the
|
|
227
250
|
# canonical source; the `localvars` parameter is kept for backward compat
|
|
@@ -229,13 +252,13 @@ def _exec_sql(
|
|
|
229
252
|
effective_locals = _stack_localvars(ctx) or localvars
|
|
230
253
|
cmd = substitute_vars(text, effective_locals, ctx=ctx)
|
|
231
254
|
if _VARLIKE.search(cmd):
|
|
232
|
-
|
|
255
|
+
output.write(
|
|
233
256
|
f"Warning: There is a potential un-substituted variable in the command\n {cmd}\n",
|
|
234
257
|
)
|
|
235
258
|
e = None
|
|
236
259
|
try:
|
|
237
|
-
db =
|
|
238
|
-
if
|
|
260
|
+
db = dbs.current()
|
|
261
|
+
if conf.log_sql and ctx.exec_log:
|
|
239
262
|
ctx.exec_log.log_sql_query(cmd, db.name(), line_no)
|
|
240
263
|
db.execute(cmd)
|
|
241
264
|
if commit:
|
|
@@ -248,15 +271,16 @@ def _exec_sql(
|
|
|
248
271
|
e = ErrInfo(type="exception", exception_msg=exception_desc())
|
|
249
272
|
if e:
|
|
250
273
|
stamp_errinfo(e)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
274
|
+
subvars.add_substitution("$LAST_ERROR", cmd)
|
|
275
|
+
subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
|
|
276
|
+
status.sql_error = True
|
|
254
277
|
if ctx.exec_log is not None:
|
|
255
278
|
ctx.exec_log.log_status_info(f"SQL error: {e.errmsg()}")
|
|
256
|
-
if
|
|
279
|
+
if status.halt_on_err:
|
|
257
280
|
exit_now(1, e)
|
|
281
|
+
status.error_history.append((source, line_no, cmd, e.errmsg()))
|
|
258
282
|
return
|
|
259
|
-
|
|
283
|
+
subvars.add_substitution("$LAST_SQL", cmd)
|
|
260
284
|
|
|
261
285
|
|
|
262
286
|
# ---------------------------------------------------------------------------
|
|
@@ -277,13 +301,23 @@ def _exec_metacommand(
|
|
|
277
301
|
increments, ``$RANDOM``, ``$UUID``) are evaluated exactly once per
|
|
278
302
|
metacommand reference.
|
|
279
303
|
"""
|
|
304
|
+
_assert_command_runtime(ctx)
|
|
305
|
+
metacommandlist = ctx.metacommandlist
|
|
306
|
+
output = ctx.output
|
|
307
|
+
status = ctx.status
|
|
308
|
+
subvars = ctx.subvars
|
|
309
|
+
assert metacommandlist is not None
|
|
310
|
+
assert output is not None
|
|
311
|
+
assert status is not None
|
|
312
|
+
assert subvars is not None
|
|
313
|
+
|
|
280
314
|
if _VARLIKE.search(cmd):
|
|
281
|
-
|
|
315
|
+
output.write(
|
|
282
316
|
f"Warning: There is a potential un-substituted variable in the command\n {cmd}\n",
|
|
283
317
|
)
|
|
284
318
|
e = None
|
|
285
319
|
try:
|
|
286
|
-
applies, result =
|
|
320
|
+
applies, result = metacommandlist.eval(cmd)
|
|
287
321
|
if applies:
|
|
288
322
|
return result
|
|
289
323
|
except ErrInfo as errinfo:
|
|
@@ -294,16 +328,17 @@ def _exec_metacommand(
|
|
|
294
328
|
e = ErrInfo(type="exception", exception_msg=exception_desc())
|
|
295
329
|
if e:
|
|
296
330
|
stamp_errinfo(e)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
331
|
+
status.metacommand_error = True
|
|
332
|
+
subvars.add_substitution("$LAST_ERROR", cmd)
|
|
333
|
+
subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
|
|
300
334
|
if ctx.exec_log is not None:
|
|
301
335
|
ctx.exec_log.log_status_info(f"Metacommand error: {e.errmsg()}")
|
|
302
|
-
if
|
|
336
|
+
if status.halt_on_metacommand_err:
|
|
303
337
|
raise e
|
|
338
|
+
status.error_history.append((source, line_no, cmd, e.errmsg()))
|
|
304
339
|
return None
|
|
305
340
|
# No handler matched — truly unknown metacommand
|
|
306
|
-
|
|
341
|
+
status.metacommand_error = True
|
|
307
342
|
raise ErrInfo(type="cmd", command_text=cmd, other_msg="Unknown metacommand")
|
|
308
343
|
|
|
309
344
|
|
|
@@ -345,7 +380,9 @@ def _execute_nodes(
|
|
|
345
380
|
elapsed = _time.perf_counter() - t0
|
|
346
381
|
cmd_type = _node_cmd_type(node)
|
|
347
382
|
cmd_text = _node_cmd_text(node)[:100]
|
|
348
|
-
ctx.profile_data
|
|
383
|
+
profile_data = ctx.profile_data
|
|
384
|
+
assert profile_data is not None
|
|
385
|
+
profile_data.append(
|
|
349
386
|
(node.span.file, node.span.start_line, cmd_type, elapsed, cmd_text),
|
|
350
387
|
)
|
|
351
388
|
|
|
@@ -360,6 +397,7 @@ def _execute_node(
|
|
|
360
397
|
in_loop: bool = False,
|
|
361
398
|
) -> None:
|
|
362
399
|
"""Execute a single AST node."""
|
|
400
|
+
assert ctx.status is not None
|
|
363
401
|
if isinstance(node, SqlStatement):
|
|
364
402
|
text = node.text
|
|
365
403
|
if in_loop:
|
|
@@ -528,7 +566,9 @@ def _execute_batch(
|
|
|
528
566
|
"""Execute a BEGIN BATCH / END BATCH block."""
|
|
529
567
|
from execsql.state import ExecFrame
|
|
530
568
|
|
|
531
|
-
ctx.status
|
|
569
|
+
status = ctx.status
|
|
570
|
+
assert status is not None
|
|
571
|
+
status.batch.new_batch()
|
|
532
572
|
_push_block_frame(
|
|
533
573
|
ctx,
|
|
534
574
|
ExecFrame(kind="batch", label="", source=node.span.file, line=node.span.start_line),
|
|
@@ -537,8 +577,8 @@ def _execute_batch(
|
|
|
537
577
|
_execute_nodes(ctx, node.body, node.span.file, localvars, in_loop=in_loop)
|
|
538
578
|
finally:
|
|
539
579
|
ctx.ast_exec_stack.pop()
|
|
540
|
-
if
|
|
541
|
-
|
|
580
|
+
if status.batch.in_batch():
|
|
581
|
+
status.batch.end_batch()
|
|
542
582
|
|
|
543
583
|
|
|
544
584
|
def _pre_register_scripts(ctx: RuntimeContext, nodes: list[Node]) -> None:
|
|
@@ -716,7 +756,8 @@ def _execute_script_native(
|
|
|
716
756
|
|
|
717
757
|
# Handle WHILE/UNTIL loops
|
|
718
758
|
# Convert deferred vars once — node.loop_condition is immutable after parsing
|
|
719
|
-
|
|
759
|
+
condition = ""
|
|
760
|
+
if node.loop_type is not None and node.loop_condition is not None:
|
|
720
761
|
condition = _convert_deferred_vars(node.loop_condition)
|
|
721
762
|
|
|
722
763
|
if node.loop_type == "WHILE":
|
|
@@ -818,7 +859,7 @@ def _execute_include_native(
|
|
|
818
859
|
ctx.exec_log.log_status_info(f"Reading script file {target} (size: {sz}; date: {dt})")
|
|
819
860
|
|
|
820
861
|
# Parse with AST parser
|
|
821
|
-
encoding = ctx.conf.script_encoding if ctx.conf else "utf-8"
|
|
862
|
+
encoding = ctx.conf.script_encoding if ctx.conf is not None else "utf-8"
|
|
822
863
|
included_tree = parse_script(target, encoding=encoding)
|
|
823
864
|
|
|
824
865
|
# Pre-register SCRIPT blocks in the included file so forward references work.
|
|
@@ -879,11 +920,11 @@ class _FakeScriptCmd:
|
|
|
879
920
|
else:
|
|
880
921
|
self.command = type("_cmd", (), {"statement": "", "commandline": lambda self: ""})()
|
|
881
922
|
|
|
882
|
-
def current_script_line(self) -> tuple:
|
|
923
|
+
def current_script_line(self) -> tuple[str, int]:
|
|
883
924
|
return (self.source, self.line_no)
|
|
884
925
|
|
|
885
926
|
def commandline(self) -> str:
|
|
886
|
-
return self.command.commandline()
|
|
927
|
+
return cast(str, self.command.commandline())
|
|
887
928
|
|
|
888
929
|
|
|
889
930
|
# ---------------------------------------------------------------------------
|