execsql2 2.24.0__py3-none-any.whl → 2.25.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 +169 -160
- execsql/cli/__init__.py +2 -0
- execsql/cli/commands/lint.py +2 -10
- execsql/cli/commands/run.py +35 -27
- execsql/cli/dsn.py +11 -4
- execsql/cli/lint.py +351 -48
- execsql/cli/options.py +1 -1
- execsql/cli/run.py +56 -19
- execsql/config.py +5 -19
- execsql/data/execsql.conf.template +14 -4
- execsql/db/access.py +29 -8
- execsql/db/base.py +40 -8
- execsql/db/dsn.py +13 -4
- execsql/db/duckdb.py +3 -1
- execsql/db/factory.py +11 -2
- execsql/db/firebird.py +21 -7
- execsql/db/mysql.py +37 -16
- execsql/db/oracle.py +49 -16
- execsql/db/postgres.py +30 -6
- execsql/db/sqlite.py +7 -9
- execsql/db/sqlserver.py +25 -8
- execsql/exporters/delimited.py +87 -28
- execsql/exporters/xlsx.py +24 -9
- execsql/exporters/zip.py +1 -2
- execsql/format.py +29 -2
- execsql/gui/desktop.py +1 -3
- execsql/gui/tui.py +2 -2
- execsql/importers/base.py +35 -1
- execsql/importers/csv.py +3 -1
- execsql/manifest.py +33 -22
- execsql/metacommands/conditions.py +6 -7
- execsql/metacommands/data.py +4 -1
- execsql/metacommands/debug.py +2 -4
- execsql/metacommands/io_fileops.py +62 -37
- execsql/metacommands/io_import.py +0 -1
- execsql/metacommands/prompt.py +2 -0
- execsql/metacommands/upsert.py +5 -2
- execsql/models.py +3 -2
- execsql/parser.py +14 -5
- execsql/plugins.py +24 -0
- execsql/script/engine.py +40 -11
- execsql/script/executor.py +9 -5
- execsql/script/parser.py +167 -118
- execsql/state.py +23 -9
- execsql/state.pyi +8 -0
- execsql/types.py +22 -13
- execsql/utils/auth.py +169 -24
- execsql/utils/datetime.py +55 -17
- execsql/utils/errors.py +71 -9
- execsql/utils/fileio.py +404 -117
- execsql/utils/gui.py +3 -0
- execsql/utils/mail.py +37 -12
- {execsql2-2.24.0.dist-info → execsql2-2.25.1.dist-info}/METADATA +18 -12
- {execsql2-2.24.0.dist-info → execsql2-2.25.1.dist-info}/RECORD +72 -72
- {execsql2-2.24.0.data → execsql2-2.25.1.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.24.0.data → execsql2-2.25.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.24.0.data → execsql2-2.25.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.24.0.data → execsql2-2.25.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.24.0.data → execsql2-2.25.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.24.0.data → execsql2-2.25.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.24.0.data → execsql2-2.25.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.24.0.data → execsql2-2.25.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.24.0.data → execsql2-2.25.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.24.0.data → execsql2-2.25.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.24.0.data → execsql2-2.25.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.24.0.data → execsql2-2.25.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.24.0.data → execsql2-2.25.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.24.0.data → execsql2-2.25.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.24.0.dist-info → execsql2-2.25.1.dist-info}/WHEEL +0 -0
- {execsql2-2.24.0.dist-info → execsql2-2.25.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.24.0.dist-info → execsql2-2.25.1.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.24.0.dist-info → execsql2-2.25.1.dist-info}/licenses/NOTICE +0 -0
execsql/metacommands/upsert.py
CHANGED
|
@@ -4,7 +4,7 @@ Integrates pg-upsert (https://pg-upsert.readthedocs.io/) as an optional
|
|
|
4
4
|
dependency, providing QA-checked, FK-dependency-ordered upserts from a
|
|
5
5
|
staging schema to a base schema on PostgreSQL.
|
|
6
6
|
|
|
7
|
-
Requires
|
|
7
|
+
Requires the ``upsert`` extra (``execsql2[upsert]``).
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
@@ -213,7 +213,10 @@ def _require_pg_upsert() -> None:
|
|
|
213
213
|
except ImportError as exc:
|
|
214
214
|
raise ErrInfo(
|
|
215
215
|
"exception",
|
|
216
|
-
other_msg=(
|
|
216
|
+
other_msg=(
|
|
217
|
+
'PG_UPSERT requires the pg-upsert package. Install the upsert extra: uv tool install "execsql2[upsert]"'
|
|
218
|
+
' (or pip install "execsql2[upsert]").'
|
|
219
|
+
),
|
|
217
220
|
) from exc
|
|
218
221
|
|
|
219
222
|
|
execsql/models.py
CHANGED
|
@@ -268,7 +268,6 @@ class DataTable:
|
|
|
268
268
|
|
|
269
269
|
self.inputrows = 0 # Total number of rows in the row source.
|
|
270
270
|
self.datarows = 0 # Number of non-empty rows (with data values).
|
|
271
|
-
self.shortrows = 0 # Number of rows without as many data values as column names.
|
|
272
271
|
self.cols: list = [] # List of Column objects.
|
|
273
272
|
for n in column_names:
|
|
274
273
|
self.cols.append(Column(n, infer_strings=infer_strings))
|
|
@@ -281,7 +280,6 @@ class DataTable:
|
|
|
281
280
|
self.datarows += 1
|
|
282
281
|
chkcols = len(self.cols)
|
|
283
282
|
if dataitems < chkcols:
|
|
284
|
-
self.shortrows += 1
|
|
285
283
|
chkcols = len(datarow)
|
|
286
284
|
else:
|
|
287
285
|
if dataitems > chkcols:
|
|
@@ -307,6 +305,9 @@ class DataTable:
|
|
|
307
305
|
raise DataTableError(errmsg)
|
|
308
306
|
for i in range(chkcols):
|
|
309
307
|
self.cols[i].eval_types(datarow[i])
|
|
308
|
+
# Trailing values left off a short row are imported as NULL.
|
|
309
|
+
for i in range(chkcols, len(self.cols)):
|
|
310
|
+
self.cols[i].eval_types(None)
|
|
310
311
|
for col in self.cols:
|
|
311
312
|
col.column_type()
|
|
312
313
|
|
execsql/parser.py
CHANGED
|
@@ -215,10 +215,17 @@ class CondParser(CondTokens):
|
|
|
215
215
|
"""Recursive-descent parser for boolean conditional expressions."""
|
|
216
216
|
|
|
217
217
|
# Takes a conditional expression string.
|
|
218
|
-
def __init__(self, condexpr: str) -> None:
|
|
219
|
-
"""Initialise the parser with the conditional expression string.
|
|
218
|
+
def __init__(self, condexpr: str, conditionallist: Any = None) -> None:
|
|
219
|
+
"""Initialise the parser with the conditional expression string.
|
|
220
|
+
|
|
221
|
+
*conditionallist* is the table of conditional tests to match against;
|
|
222
|
+
``None`` means the current run's ``conditionallist``. Passing one lets
|
|
223
|
+
a caller with no run in progress, such as ``execsql lint``, parse
|
|
224
|
+
(without evaluating) a condition.
|
|
225
|
+
"""
|
|
220
226
|
self.condexpr = condexpr
|
|
221
227
|
self.cond_expr = SourceString(condexpr)
|
|
228
|
+
self.conditionallist = conditionallist
|
|
222
229
|
|
|
223
230
|
def match_not(self) -> int | None:
|
|
224
231
|
"""Match a NOT operator and return its token type, or None."""
|
|
@@ -251,10 +258,12 @@ class CondParser(CondTokens):
|
|
|
251
258
|
m1 = self.factor()
|
|
252
259
|
return CondAstNode(self.NOT, m1, None)
|
|
253
260
|
# Find the matching metacommand -- get a tuple consisting of (metacommand, groupdict)
|
|
254
|
-
|
|
255
|
-
|
|
261
|
+
conditionallist = self.conditionallist
|
|
262
|
+
if conditionallist is None:
|
|
263
|
+
import execsql.state as _state
|
|
256
264
|
|
|
257
|
-
|
|
265
|
+
conditionallist = _state.conditionallist
|
|
266
|
+
matched_metacommand = self.cond_expr.match_metacommand(conditionallist)
|
|
258
267
|
if matched_metacommand is not None:
|
|
259
268
|
matched_metacommand[1]["metacommandline"] = self.condexpr
|
|
260
269
|
return CondAstNode(self.CONDITIONAL, matched_metacommand, None)
|
execsql/plugins.py
CHANGED
|
@@ -61,6 +61,8 @@ adds entries::
|
|
|
61
61
|
from __future__ import annotations
|
|
62
62
|
|
|
63
63
|
import logging
|
|
64
|
+
import threading
|
|
65
|
+
import weakref
|
|
64
66
|
from collections.abc import Callable
|
|
65
67
|
from importlib.metadata import entry_points
|
|
66
68
|
from typing import Any
|
|
@@ -71,6 +73,7 @@ __all__ = [
|
|
|
71
73
|
"ImporterEntry",
|
|
72
74
|
"ImporterRegistry",
|
|
73
75
|
"discover_metacommand_plugins",
|
|
76
|
+
"register_metacommand_plugins_once",
|
|
74
77
|
"discover_exporter_plugins",
|
|
75
78
|
"discover_importer_plugins",
|
|
76
79
|
"discover_all_plugins",
|
|
@@ -344,6 +347,27 @@ def discover_metacommand_plugins(mcl: Any) -> int:
|
|
|
344
347
|
return loaded
|
|
345
348
|
|
|
346
349
|
|
|
350
|
+
_registered_tables: weakref.WeakSet[Any] = weakref.WeakSet()
|
|
351
|
+
_registration_lock = threading.Lock()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def register_metacommand_plugins_once(mcl: Any) -> int:
|
|
355
|
+
"""Register metacommand plugins into *mcl* unless that was already done.
|
|
356
|
+
|
|
357
|
+
Every run in a process shares one dispatch table, so registering on each
|
|
358
|
+
run would prepend another copy of every plugin command each time, while
|
|
359
|
+
other threads may be dispatching from the table.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Number of plugins loaded by this call (0 if *mcl* already had them).
|
|
363
|
+
"""
|
|
364
|
+
with _registration_lock:
|
|
365
|
+
if mcl in _registered_tables:
|
|
366
|
+
return 0
|
|
367
|
+
_registered_tables.add(mcl)
|
|
368
|
+
return discover_metacommand_plugins(mcl)
|
|
369
|
+
|
|
370
|
+
|
|
347
371
|
def discover_exporter_plugins(registry: ExporterRegistry | None = None) -> int:
|
|
348
372
|
"""Discover and register exporter plugins.
|
|
349
373
|
|
execsql/script/engine.py
CHANGED
|
@@ -50,12 +50,16 @@ __all__ = [
|
|
|
50
50
|
class MetaCommand:
|
|
51
51
|
"""A single entry in the metacommand dispatch table.
|
|
52
52
|
|
|
53
|
-
Holds a
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
Holds a regex, a handler function, and execution-control flags. Call
|
|
54
|
+
:meth:`run` with a raw command string to attempt a match and invoke the
|
|
55
|
+
handler.
|
|
56
|
+
|
|
57
|
+
*rx* is a compiled pattern, or a pattern string that is compiled
|
|
58
|
+
(case-insensitively) the first time :attr:`rx` is read. The dispatch
|
|
59
|
+
table registers some 1,700 patterns and a script uses a few dozen, so
|
|
60
|
+
compiling them all up front cost most of a second on every run.
|
|
56
61
|
"""
|
|
57
62
|
|
|
58
|
-
# A compiled metacommand that can be run if it matches a metacommand command string.
|
|
59
63
|
def __init__(
|
|
60
64
|
self,
|
|
61
65
|
rx: Any,
|
|
@@ -64,6 +68,8 @@ class MetaCommand:
|
|
|
64
68
|
set_error_flag: bool = True,
|
|
65
69
|
category: str | None = None,
|
|
66
70
|
) -> None:
|
|
71
|
+
self._pattern: str
|
|
72
|
+
self._rx: Any = None
|
|
67
73
|
self.rx = rx
|
|
68
74
|
self.exec_fn = exec_func
|
|
69
75
|
self.description = description
|
|
@@ -71,8 +77,32 @@ class MetaCommand:
|
|
|
71
77
|
self.category = category
|
|
72
78
|
self.hitcount = 0
|
|
73
79
|
|
|
80
|
+
@property
|
|
81
|
+
def rx(self) -> Any:
|
|
82
|
+
"""The compiled pattern, compiled on first use."""
|
|
83
|
+
if self._rx is None:
|
|
84
|
+
self._rx = re.compile(self._pattern, re.I)
|
|
85
|
+
return self._rx
|
|
86
|
+
|
|
87
|
+
@rx.setter
|
|
88
|
+
def rx(self, value: Any) -> None:
|
|
89
|
+
if isinstance(value, str):
|
|
90
|
+
self._pattern, self._rx = value, None
|
|
91
|
+
else:
|
|
92
|
+
self._pattern, self._rx = value.pattern, value
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def pattern(self) -> str:
|
|
96
|
+
"""The pattern string, without compiling it."""
|
|
97
|
+
return self._pattern
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def compiled(self) -> bool:
|
|
101
|
+
"""Whether :attr:`rx` has been compiled yet."""
|
|
102
|
+
return self._rx is not None
|
|
103
|
+
|
|
74
104
|
def __repr__(self) -> str:
|
|
75
|
-
return f"MetaCommand({self.
|
|
105
|
+
return f"MetaCommand({self.pattern!r}, {self.exec_fn!r}, {self.description!r})"
|
|
76
106
|
|
|
77
107
|
def run(self, cmd_str: str) -> tuple:
|
|
78
108
|
"""Match *cmd_str* against this entry's regex and, if it matches, invoke the handler.
|
|
@@ -165,18 +195,17 @@ class MetaCommandList:
|
|
|
165
195
|
"""Register one or more regex patterns as a new :class:`MetaCommand` entry.
|
|
166
196
|
|
|
167
197
|
*matching_regexes* may be a single pattern string or a list/tuple of
|
|
168
|
-
patterns; each
|
|
169
|
-
the dispatch list so that later registrations take priority.
|
|
198
|
+
patterns; each becomes a separate :class:`MetaCommand` prepended to
|
|
199
|
+
the dispatch list so that later registrations take priority. The
|
|
200
|
+
patterns are compiled when first matched, not here.
|
|
170
201
|
"""
|
|
171
202
|
if isinstance(matching_regexes, (tuple, list)):
|
|
172
203
|
raw_patterns = list(matching_regexes)
|
|
173
|
-
regexes = [re.compile(rx, re.I) for rx in raw_patterns]
|
|
174
204
|
else:
|
|
175
205
|
raw_patterns = [matching_regexes]
|
|
176
|
-
|
|
177
|
-
for rx, raw in zip(regexes, raw_patterns):
|
|
206
|
+
for raw in raw_patterns:
|
|
178
207
|
mc = MetaCommand(
|
|
179
|
-
|
|
208
|
+
raw,
|
|
180
209
|
exec_func,
|
|
181
210
|
description,
|
|
182
211
|
set_error_flag,
|
execsql/script/executor.py
CHANGED
|
@@ -69,7 +69,7 @@ from execsql import manifest as _manifest
|
|
|
69
69
|
from execsql.script.engine import set_dynamic_system_vars, set_static_system_vars, substitute_vars
|
|
70
70
|
from execsql.script.variables import SubVarSet
|
|
71
71
|
from execsql.state import ExecFrame, RuntimeContext, active_context, get_context, xcmd_test
|
|
72
|
-
from execsql.utils.errors import exception_desc, exit_now, stamp_errinfo
|
|
72
|
+
from execsql.utils.errors import exception_desc, exit_now, logged_error, stamp_errinfo
|
|
73
73
|
|
|
74
74
|
__all__ = ["execute", "execute_input"]
|
|
75
75
|
|
|
@@ -254,8 +254,10 @@ def _exec_sql(
|
|
|
254
254
|
effective_locals = _stack_localvars(ctx) or localvars
|
|
255
255
|
cmd = substitute_vars(text, effective_locals, ctx=ctx)
|
|
256
256
|
if _VARLIKE.search(cmd):
|
|
257
|
+
from execsql.utils.fileio import redact
|
|
258
|
+
|
|
257
259
|
output.write(
|
|
258
|
-
f"Warning: There is a potential un-substituted variable in the command\n {cmd}\n",
|
|
260
|
+
f"Warning: There is a potential un-substituted variable in the command\n {redact(cmd)}\n",
|
|
259
261
|
)
|
|
260
262
|
e = None
|
|
261
263
|
result = None
|
|
@@ -282,7 +284,7 @@ def _exec_sql(
|
|
|
282
284
|
subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
|
|
283
285
|
status.sql_error = True
|
|
284
286
|
if ctx.exec_log is not None:
|
|
285
|
-
ctx.exec_log.log_status_info(f"SQL error: {e
|
|
287
|
+
ctx.exec_log.log_status_info(f"SQL error: {logged_error(e)}")
|
|
286
288
|
if status.halt_on_err:
|
|
287
289
|
exit_now(1, e)
|
|
288
290
|
status.error_history.append((source, line_no, cmd, e.errmsg()))
|
|
@@ -321,8 +323,10 @@ def _exec_metacommand(
|
|
|
321
323
|
assert subvars is not None
|
|
322
324
|
|
|
323
325
|
if _VARLIKE.search(cmd):
|
|
326
|
+
from execsql.utils.fileio import redact
|
|
327
|
+
|
|
324
328
|
output.write(
|
|
325
|
-
f"Warning: There is a potential un-substituted variable in the command\n {cmd}\n",
|
|
329
|
+
f"Warning: There is a potential un-substituted variable in the command\n {redact(cmd)}\n",
|
|
326
330
|
)
|
|
327
331
|
e = None
|
|
328
332
|
try:
|
|
@@ -344,7 +348,7 @@ def _exec_metacommand(
|
|
|
344
348
|
subvars.add_substitution("$LAST_ERROR", cmd)
|
|
345
349
|
subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
|
|
346
350
|
if ctx.exec_log is not None:
|
|
347
|
-
ctx.exec_log.log_status_info(f"Metacommand error: {e
|
|
351
|
+
ctx.exec_log.log_status_info(f"Metacommand error: {logged_error(e)}")
|
|
348
352
|
if status.halt_on_metacommand_err:
|
|
349
353
|
raise e
|
|
350
354
|
status.error_history.append((source, line_no, cmd, e.errmsg()))
|