execsql2 2.0.1__py3-none-any.whl → 2.1.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/cli.py +322 -108
- execsql/config.py +134 -114
- execsql/db/access.py +89 -65
- execsql/db/base.py +97 -68
- execsql/db/dsn.py +45 -29
- execsql/db/duckdb.py +4 -5
- execsql/db/factory.py +27 -27
- execsql/db/firebird.py +30 -18
- execsql/db/mysql.py +38 -14
- execsql/db/oracle.py +58 -33
- execsql/db/postgres.py +68 -28
- execsql/db/sqlite.py +36 -27
- execsql/db/sqlserver.py +45 -30
- execsql/exceptions.py +68 -64
- execsql/exporters/__init__.py +1 -1
- execsql/exporters/base.py +42 -17
- execsql/exporters/delimited.py +60 -59
- execsql/exporters/duckdb.py +8 -12
- execsql/exporters/feather.py +32 -24
- execsql/exporters/html.py +33 -30
- execsql/exporters/json.py +18 -17
- execsql/exporters/latex.py +11 -13
- execsql/exporters/ods.py +50 -46
- execsql/exporters/parquet.py +32 -0
- execsql/exporters/pretty.py +16 -15
- execsql/exporters/raw.py +9 -11
- execsql/exporters/sqlite.py +38 -38
- execsql/exporters/templates.py +15 -72
- execsql/exporters/values.py +13 -12
- execsql/exporters/xls.py +26 -26
- execsql/exporters/xml.py +12 -12
- execsql/exporters/zip.py +0 -3
- execsql/gui/__init__.py +2 -2
- execsql/gui/console.py +0 -1
- execsql/gui/desktop.py +6 -7
- execsql/gui/tui.py +8 -14
- execsql/importers/base.py +6 -9
- execsql/importers/csv.py +10 -17
- execsql/importers/feather.py +16 -22
- execsql/importers/ods.py +3 -4
- execsql/importers/xls.py +5 -6
- execsql/metacommands/__init__.py +8 -8
- execsql/metacommands/conditions.py +41 -33
- execsql/metacommands/connect.py +113 -99
- execsql/metacommands/control.py +38 -26
- execsql/metacommands/data.py +35 -33
- execsql/metacommands/debug.py +13 -9
- execsql/metacommands/io.py +288 -229
- execsql/metacommands/prompt.py +179 -157
- execsql/metacommands/script_ext.py +11 -9
- execsql/metacommands/system.py +44 -25
- execsql/models.py +9 -16
- execsql/parser.py +10 -10
- execsql/script.py +183 -157
- execsql/state.py +170 -208
- execsql/types.py +46 -81
- execsql/utils/auth.py +114 -14
- execsql/utils/crypto.py +31 -4
- execsql/utils/datetime.py +7 -7
- execsql/utils/errors.py +34 -29
- execsql/utils/fileio.py +90 -55
- execsql/utils/gui.py +22 -23
- execsql/utils/mail.py +15 -17
- execsql/utils/numeric.py +2 -3
- execsql/utils/regex.py +9 -12
- execsql/utils/strings.py +10 -12
- execsql/utils/timer.py +0 -2
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/execsql.conf +1 -1
- execsql2-2.1.2.dist-info/METADATA +300 -0
- execsql2-2.1.2.dist-info/RECORD +96 -0
- execsql2-2.0.1.dist-info/METADATA +0 -406
- execsql2-2.0.1.dist-info/RECORD +0 -95
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/READ_ME.rst +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/WHEEL +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/entry_points.txt +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/NOTICE +0 -0
execsql/script.py
CHANGED
|
@@ -49,14 +49,11 @@ Key functions:
|
|
|
49
49
|
|
|
50
50
|
import copy
|
|
51
51
|
import datetime
|
|
52
|
-
import glob
|
|
53
|
-
import io
|
|
54
52
|
import os
|
|
55
|
-
import os.path
|
|
56
53
|
import re
|
|
57
|
-
import sys
|
|
58
54
|
import uuid
|
|
59
|
-
from
|
|
55
|
+
from pathlib import Path
|
|
56
|
+
from typing import Any
|
|
60
57
|
|
|
61
58
|
import execsql.state as _state
|
|
62
59
|
from execsql.exceptions import ErrInfo
|
|
@@ -73,10 +70,10 @@ class BatchLevels:
|
|
|
73
70
|
# A stack to keep a record of the databases used in nested batches.
|
|
74
71
|
class Batch:
|
|
75
72
|
def __init__(self) -> None:
|
|
76
|
-
self.dbs_used:
|
|
73
|
+
self.dbs_used: list[Any] = []
|
|
77
74
|
|
|
78
75
|
def __init__(self) -> None:
|
|
79
|
-
self.batchlevels:
|
|
76
|
+
self.batchlevels: list[BatchLevels.Batch] = []
|
|
80
77
|
|
|
81
78
|
def in_batch(self) -> bool:
|
|
82
79
|
return len(self.batchlevels) > 0
|
|
@@ -91,10 +88,7 @@ class BatchLevels:
|
|
|
91
88
|
def uses_db(self, db: Any) -> bool:
|
|
92
89
|
if len(self.batchlevels) == 0:
|
|
93
90
|
return False
|
|
94
|
-
for batch in self.batchlevels
|
|
95
|
-
if db in batch.dbs_used:
|
|
96
|
-
return True
|
|
97
|
-
return False
|
|
91
|
+
return any(db in batch.dbs_used for batch in self.batchlevels)
|
|
98
92
|
|
|
99
93
|
def rollback_batch(self) -> None:
|
|
100
94
|
if len(self.batchlevels) > 0:
|
|
@@ -136,7 +130,7 @@ class IfLevels:
|
|
|
136
130
|
# A stack of True/False values corresponding to a nested set of conditionals,
|
|
137
131
|
# with methods to manipulate and query the set of conditional states.
|
|
138
132
|
def __init__(self) -> None:
|
|
139
|
-
self.if_levels:
|
|
133
|
+
self.if_levels: list[IfItem] = []
|
|
140
134
|
|
|
141
135
|
def nest(self, tf_value: bool) -> None:
|
|
142
136
|
self.if_levels.append(IfItem(tf_value))
|
|
@@ -168,7 +162,7 @@ class IfLevels:
|
|
|
168
162
|
def all_true(self) -> bool:
|
|
169
163
|
if self.if_levels == []:
|
|
170
164
|
return True
|
|
171
|
-
return all(
|
|
165
|
+
return all(tf.value() for tf in self.if_levels)
|
|
172
166
|
|
|
173
167
|
def only_current_false(self) -> bool:
|
|
174
168
|
# Returns True if the current if level is false and all higher levels are True.
|
|
@@ -177,9 +171,9 @@ class IfLevels:
|
|
|
177
171
|
elif len(self.if_levels) == 1:
|
|
178
172
|
return not self.if_levels[-1].value()
|
|
179
173
|
else:
|
|
180
|
-
return not self.if_levels[-1].value() and all(
|
|
174
|
+
return not self.if_levels[-1].value() and all(tf.value() for tf in self.if_levels[:-1])
|
|
181
175
|
|
|
182
|
-
def script_lines(self, top_n: int) ->
|
|
176
|
+
def script_lines(self, top_n: int) -> list[tuple]:
|
|
183
177
|
# Returns a list of tuples containing the script name and line number
|
|
184
178
|
# for the topmost 'top_n' if levels, in bottom-up order.
|
|
185
179
|
if len(self.if_levels) < top_n:
|
|
@@ -198,7 +192,7 @@ class CounterVars:
|
|
|
198
192
|
_COUNTER_RX = re.compile(r"!!\$(COUNTER_\d+)!!", re.I)
|
|
199
193
|
|
|
200
194
|
def __init__(self) -> None:
|
|
201
|
-
self.counters:
|
|
195
|
+
self.counters: dict[str, int] = {}
|
|
202
196
|
|
|
203
197
|
def _ctrid(self, ctr_no: int) -> str:
|
|
204
198
|
return f"counter_{ctr_no}"
|
|
@@ -217,7 +211,6 @@ class CounterVars:
|
|
|
217
211
|
def substitute(self, command_str: str) -> tuple:
|
|
218
212
|
# Substitutes any counter variable references with the counter value and
|
|
219
213
|
# returns the modified command string and a flag indicating replacements.
|
|
220
|
-
match_found = False
|
|
221
214
|
m = self._COUNTER_RX.search(command_str, re.I)
|
|
222
215
|
if m:
|
|
223
216
|
ctr_id = m.group(1).lower()
|
|
@@ -246,12 +239,44 @@ class CounterVars:
|
|
|
246
239
|
class SubVarSet:
|
|
247
240
|
# A pool of substitution variables. Each variable consists of a name and
|
|
248
241
|
# a (string) value. All variable names are stored as lowercase text.
|
|
242
|
+
# Internally uses a dict for O(1) lookups; the ``substitutions`` property
|
|
243
|
+
# exposes the data as a list of ``(name, value)`` tuples for backward
|
|
244
|
+
# compatibility with external code.
|
|
249
245
|
def __init__(self) -> None:
|
|
250
|
-
self.
|
|
251
|
-
self.
|
|
246
|
+
self._subs_dict: dict[str, Any] = {}
|
|
247
|
+
self._compiled_patterns: dict[str, tuple] = {}
|
|
248
|
+
self.prefix_list: list[str] = ["$", "&", "@"]
|
|
252
249
|
# Don't construct/compile on init because deepcopy() can't handle compiled regexes.
|
|
253
250
|
self.var_rx = None
|
|
254
251
|
|
|
252
|
+
@property
|
|
253
|
+
def substitutions(self) -> list[tuple]:
|
|
254
|
+
"""Backward-compatible view of substitutions as a list of (name, value) tuples."""
|
|
255
|
+
return list(self._subs_dict.items())
|
|
256
|
+
|
|
257
|
+
@substitutions.setter
|
|
258
|
+
def substitutions(self, value: Any) -> None:
|
|
259
|
+
"""Accept a list of (name, value) tuples or a dict and populate the internal dict."""
|
|
260
|
+
if isinstance(value, dict):
|
|
261
|
+
self._subs_dict = dict(value)
|
|
262
|
+
else:
|
|
263
|
+
self._subs_dict = dict(value)
|
|
264
|
+
self._rebuild_all_patterns()
|
|
265
|
+
|
|
266
|
+
def _compile_patterns_for(self, varname: str) -> tuple:
|
|
267
|
+
"""Compile and return the three regex patterns (plain, single-quoted, double-quoted) for *varname*."""
|
|
268
|
+
match_escaped = "\\" + varname if varname[0] == "$" else varname
|
|
269
|
+
pat = re.compile(f"!!{match_escaped}!!", re.I)
|
|
270
|
+
patq = re.compile(f"!'!{match_escaped}!'!", re.I)
|
|
271
|
+
patdq = re.compile(f'!"!{match_escaped}!"!', re.I)
|
|
272
|
+
return (pat, patq, patdq)
|
|
273
|
+
|
|
274
|
+
def _rebuild_all_patterns(self) -> None:
|
|
275
|
+
"""Rebuild compiled patterns for every variable currently stored."""
|
|
276
|
+
self._compiled_patterns = {}
|
|
277
|
+
for varname in self._subs_dict:
|
|
278
|
+
self._compiled_patterns[varname] = self._compile_patterns_for(varname)
|
|
279
|
+
|
|
255
280
|
def compile_var_rx(self) -> None:
|
|
256
281
|
self.var_rx_str = r"^[" + "".join(self.prefix_list) + r"]?\w+$"
|
|
257
282
|
self.var_rx = re.compile(self.var_rx_str, re.I)
|
|
@@ -268,30 +293,26 @@ class SubVarSet:
|
|
|
268
293
|
def remove_substitution(self, template_str: str) -> None:
|
|
269
294
|
self.check_var_name(template_str)
|
|
270
295
|
old_sub = template_str.lower()
|
|
271
|
-
self.
|
|
296
|
+
self._subs_dict.pop(old_sub, None)
|
|
297
|
+
self._compiled_patterns.pop(old_sub, None)
|
|
272
298
|
|
|
273
299
|
def add_substitution(self, varname: str, repl_str: Any) -> None:
|
|
274
300
|
self.check_var_name(varname)
|
|
275
301
|
varname = varname.lower()
|
|
276
|
-
self.
|
|
277
|
-
self.
|
|
302
|
+
self._subs_dict[varname] = repl_str
|
|
303
|
+
self._compiled_patterns[varname] = self._compile_patterns_for(varname)
|
|
278
304
|
|
|
279
305
|
def append_substitution(self, varname: str, repl_str: str) -> None:
|
|
280
306
|
self.check_var_name(varname)
|
|
281
307
|
varname = varname.lower()
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
self.add_substitution(varname, repl_str)
|
|
308
|
+
if varname in self._subs_dict:
|
|
309
|
+
self.add_substitution(varname, f"{self._subs_dict[varname]}\n{repl_str}")
|
|
285
310
|
else:
|
|
286
|
-
self.add_substitution(varname,
|
|
311
|
+
self.add_substitution(varname, repl_str)
|
|
287
312
|
|
|
288
|
-
def varvalue(self, varname: str) ->
|
|
313
|
+
def varvalue(self, varname: str) -> str | None:
|
|
289
314
|
self.check_var_name(varname)
|
|
290
|
-
|
|
291
|
-
for vardef in self.substitutions:
|
|
292
|
-
if vardef[0] == vname:
|
|
293
|
-
return vardef[1]
|
|
294
|
-
return None
|
|
315
|
+
return self._subs_dict.get(varname.lower())
|
|
295
316
|
|
|
296
317
|
def increment_by(self, varname: str, numeric_increment: Any) -> None:
|
|
297
318
|
self.check_var_name(varname)
|
|
@@ -312,44 +333,39 @@ class SubVarSet:
|
|
|
312
333
|
|
|
313
334
|
def sub_exists(self, template_str: str) -> bool:
|
|
314
335
|
self.check_var_name(template_str)
|
|
315
|
-
|
|
316
|
-
return test_str in [s[0] for s in self.substitutions]
|
|
336
|
+
return template_str.lower() in self._subs_dict
|
|
317
337
|
|
|
318
|
-
def merge(self, other_subvars:
|
|
338
|
+
def merge(self, other_subvars: SubVarSet | None) -> SubVarSet:
|
|
319
339
|
# Return a new SubVarSet with this object's variables merged with other_subvars.
|
|
320
340
|
if other_subvars is not None:
|
|
321
341
|
newsubs = SubVarSet()
|
|
322
|
-
newsubs.
|
|
342
|
+
newsubs._subs_dict = dict(self._subs_dict)
|
|
343
|
+
newsubs._compiled_patterns = dict(self._compiled_patterns)
|
|
323
344
|
newsubs.prefix_list = list(set(self.prefix_list + other_subvars.prefix_list))
|
|
324
345
|
newsubs.compile_var_rx()
|
|
325
|
-
for
|
|
326
|
-
newsubs.add_substitution(
|
|
346
|
+
for varname, value in other_subvars._subs_dict.items():
|
|
347
|
+
newsubs.add_substitution(varname, value)
|
|
327
348
|
return newsubs
|
|
328
349
|
return self
|
|
329
350
|
|
|
330
351
|
def substitute(self, command_str: str) -> tuple:
|
|
331
352
|
# Replace any substitution variables in the command string.
|
|
332
|
-
match_found = False
|
|
333
353
|
if isinstance(command_str, str):
|
|
334
|
-
for
|
|
354
|
+
for varname, sub in self._subs_dict.items():
|
|
335
355
|
if sub is None:
|
|
336
356
|
sub = ""
|
|
337
357
|
sub = str(sub)
|
|
338
|
-
if match[0] == "$":
|
|
339
|
-
match = "\\" + match
|
|
340
358
|
if os.name != "posix":
|
|
341
359
|
sub = sub.replace("\\", "\\\\")
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if
|
|
346
|
-
return re.sub(pat, sub, command_str, flags=re.I), True
|
|
347
|
-
if re.search(patq, command_str, re.I):
|
|
360
|
+
pat_rx, patq_rx, patdq_rx = self._compiled_patterns[varname]
|
|
361
|
+
if pat_rx.search(command_str):
|
|
362
|
+
return pat_rx.sub(sub, command_str), True
|
|
363
|
+
if patq_rx.search(command_str):
|
|
348
364
|
sub = sub.replace("'", "''")
|
|
349
|
-
return
|
|
350
|
-
if
|
|
365
|
+
return patq_rx.sub(sub, command_str), True
|
|
366
|
+
if patdq_rx.search(command_str):
|
|
351
367
|
sub = '"' + sub + '"'
|
|
352
|
-
return
|
|
368
|
+
return patdq_rx.sub(sub, command_str), True
|
|
353
369
|
return command_str, False
|
|
354
370
|
|
|
355
371
|
def substitute_all(self, any_text: str) -> tuple:
|
|
@@ -399,12 +415,11 @@ class MetaCommand:
|
|
|
399
415
|
self,
|
|
400
416
|
rx: Any,
|
|
401
417
|
exec_func: Any,
|
|
402
|
-
description:
|
|
418
|
+
description: str | None = None,
|
|
403
419
|
run_in_batch: bool = False,
|
|
404
420
|
run_when_false: bool = False,
|
|
405
421
|
set_error_flag: bool = True,
|
|
406
422
|
) -> None:
|
|
407
|
-
self.next_node = None
|
|
408
423
|
self.rx = rx
|
|
409
424
|
self.exec_fn = exec_func
|
|
410
425
|
self.description = description
|
|
@@ -432,7 +447,7 @@ class MetaCommand:
|
|
|
432
447
|
raise
|
|
433
448
|
except ErrInfo as errinf:
|
|
434
449
|
er = errinf
|
|
435
|
-
except:
|
|
450
|
+
except Exception:
|
|
436
451
|
er = ErrInfo("cmd", command_text=cmd_str, exception_msg=exception_desc())
|
|
437
452
|
if er:
|
|
438
453
|
if _state.status.halt_on_metacommand_err:
|
|
@@ -451,63 +466,61 @@ class MetaCommand:
|
|
|
451
466
|
|
|
452
467
|
|
|
453
468
|
class MetaCommandList:
|
|
454
|
-
|
|
469
|
+
"""Ordered list of :class:`MetaCommand` entries.
|
|
470
|
+
|
|
471
|
+
Commands are stored with the most-recently-added entry first, matching
|
|
472
|
+
the original linked-list prepend semantics. ``eval()`` and ``get_match()``
|
|
473
|
+
scan the list linearly; the move-to-front heuristic from the original
|
|
474
|
+
implementation has been removed for predictable, stable ordering.
|
|
475
|
+
"""
|
|
476
|
+
|
|
455
477
|
def __init__(self) -> None:
|
|
456
|
-
self.
|
|
478
|
+
self._commands: list[MetaCommand] = []
|
|
457
479
|
|
|
458
480
|
def __iter__(self) -> Any:
|
|
459
|
-
|
|
460
|
-
while n1 is not None:
|
|
461
|
-
yield n1
|
|
462
|
-
n1 = n1.next_node
|
|
463
|
-
|
|
464
|
-
def insert_node(self, new_node: MetaCommand) -> None:
|
|
465
|
-
new_node.next_node = self.next_node
|
|
466
|
-
self.next_node = new_node
|
|
481
|
+
return iter(self._commands)
|
|
467
482
|
|
|
468
483
|
def add(
|
|
469
484
|
self,
|
|
470
485
|
matching_regexes: Any,
|
|
471
486
|
exec_func: Any,
|
|
472
|
-
description:
|
|
487
|
+
description: str | None = None,
|
|
473
488
|
run_in_batch: bool = False,
|
|
474
489
|
run_when_false: bool = False,
|
|
475
490
|
set_error_flag: bool = True,
|
|
476
491
|
) -> None:
|
|
477
492
|
if type(matching_regexes) in (tuple, list):
|
|
478
|
-
|
|
493
|
+
regexes = [re.compile(rx, re.I) for rx in tuple(matching_regexes)]
|
|
479
494
|
else:
|
|
480
|
-
|
|
481
|
-
for rx in
|
|
482
|
-
|
|
495
|
+
regexes = [re.compile(matching_regexes, re.I)]
|
|
496
|
+
for rx in regexes:
|
|
497
|
+
# Prepend to preserve "last registered, first checked" ordering.
|
|
498
|
+
self._commands.insert(
|
|
499
|
+
0,
|
|
500
|
+
MetaCommand(rx, exec_func, description, run_in_batch, run_when_false, set_error_flag),
|
|
501
|
+
)
|
|
483
502
|
|
|
484
503
|
def eval(self, cmd_str: str) -> tuple:
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
# Move n2 to the head of the list.
|
|
496
|
-
n1.next_node = n2.next_node
|
|
497
|
-
n2.next_node = self.next_node
|
|
498
|
-
self.next_node = n2
|
|
499
|
-
return True, value
|
|
500
|
-
n1 = n2
|
|
504
|
+
"""Evaluate *cmd_str* against the registered metacommands.
|
|
505
|
+
|
|
506
|
+
Returns ``(True, return_value)`` if a matching command was found and
|
|
507
|
+
run, ``(False, None)`` if no command matched.
|
|
508
|
+
"""
|
|
509
|
+
for cmd in self._commands:
|
|
510
|
+
if _state.if_stack.all_true() or cmd.run_when_false:
|
|
511
|
+
success, value = cmd.run(cmd_str)
|
|
512
|
+
if success:
|
|
513
|
+
return True, value
|
|
501
514
|
return False, None
|
|
502
515
|
|
|
503
|
-
def get_match(self, cmd: str) ->
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
516
|
+
def get_match(self, cmd: str) -> tuple | None:
|
|
517
|
+
"""Return ``(MetaCommand, re.Match)`` for the first entry matching *cmd*,
|
|
518
|
+
or ``None`` if no entry matches.
|
|
519
|
+
"""
|
|
520
|
+
for node in self._commands:
|
|
521
|
+
m = node.rx.match(cmd.strip())
|
|
508
522
|
if m is not None:
|
|
509
|
-
return (
|
|
510
|
-
n1 = n1.next_node
|
|
523
|
+
return (node, m)
|
|
511
524
|
return None
|
|
512
525
|
|
|
513
526
|
|
|
@@ -524,7 +537,7 @@ class SqlStmt:
|
|
|
524
537
|
def __repr__(self) -> str:
|
|
525
538
|
return f"SqlStmt({self.statement})"
|
|
526
539
|
|
|
527
|
-
def run(self, localvars:
|
|
540
|
+
def run(self, localvars: SubVarSet | None = None, commit: bool = True) -> None:
|
|
528
541
|
# Run the SQL statement on the current database.
|
|
529
542
|
if _state.if_stack.all_true():
|
|
530
543
|
e = None
|
|
@@ -543,7 +556,7 @@ class SqlStmt:
|
|
|
543
556
|
e = errinfo
|
|
544
557
|
except SystemExit:
|
|
545
558
|
raise
|
|
546
|
-
except:
|
|
559
|
+
except Exception:
|
|
547
560
|
e = ErrInfo(type="exception", exception_msg=exception_desc())
|
|
548
561
|
if e:
|
|
549
562
|
_state.subvars.add_substitution("$LAST_ERROR", cmd)
|
|
@@ -567,7 +580,7 @@ class MetacommandStmt:
|
|
|
567
580
|
def __repr__(self) -> str:
|
|
568
581
|
return f"MetacommandStmt({self.statement})"
|
|
569
582
|
|
|
570
|
-
def run(self, localvars:
|
|
583
|
+
def run(self, localvars: SubVarSet | None = None, commit: bool = False) -> Any:
|
|
571
584
|
# Tries all metacommands in the dispatch table until one runs.
|
|
572
585
|
errmsg = "Unknown metacommand"
|
|
573
586
|
cmd = substitute_vars(self.statement, localvars)
|
|
@@ -582,7 +595,7 @@ class MetacommandStmt:
|
|
|
582
595
|
e = errinfo
|
|
583
596
|
except SystemExit:
|
|
584
597
|
raise
|
|
585
|
-
except:
|
|
598
|
+
except Exception:
|
|
586
599
|
e = ErrInfo(type="exception", exception_msg=exception_desc())
|
|
587
600
|
if e:
|
|
588
601
|
_state.status.metacommand_error = True
|
|
@@ -637,9 +650,9 @@ class CommandList:
|
|
|
637
650
|
# A list of ScriptCmd objects including execution state.
|
|
638
651
|
def __init__(
|
|
639
652
|
self,
|
|
640
|
-
cmdlist:
|
|
653
|
+
cmdlist: list[ScriptCmd],
|
|
641
654
|
listname: str,
|
|
642
|
-
paramnames:
|
|
655
|
+
paramnames: list[str] | None = None,
|
|
643
656
|
) -> None:
|
|
644
657
|
if cmdlist is None:
|
|
645
658
|
raise ErrInfo("error", other_msg="Initiating a command list without any commands.")
|
|
@@ -647,9 +660,9 @@ class CommandList:
|
|
|
647
660
|
self.cmdlist = cmdlist
|
|
648
661
|
self.cmdptr = 0
|
|
649
662
|
self.paramnames = paramnames
|
|
650
|
-
self.paramvals:
|
|
663
|
+
self.paramvals: SubVarSet | None = None
|
|
651
664
|
self.localvars = LocalSubVarSet()
|
|
652
|
-
self.init_if_level:
|
|
665
|
+
self.init_if_level: int | None = None
|
|
653
666
|
|
|
654
667
|
def add(self, script_command: ScriptCmd) -> None:
|
|
655
668
|
self.cmdlist.append(script_command)
|
|
@@ -658,13 +671,13 @@ class CommandList:
|
|
|
658
671
|
self.paramvals = paramvals
|
|
659
672
|
if self.paramnames is not None:
|
|
660
673
|
passed_paramnames = [p[0][1:] if p[0][0] == "#" else p[0][1:] for p in paramvals.substitutions]
|
|
661
|
-
if not all(
|
|
674
|
+
if not all(p in passed_paramnames for p in self.paramnames):
|
|
662
675
|
raise ErrInfo(
|
|
663
676
|
"error",
|
|
664
677
|
other_msg=f"Formal and actual parameter name mismatch in call to {self.listname}.",
|
|
665
678
|
)
|
|
666
679
|
|
|
667
|
-
def current_command(self) ->
|
|
680
|
+
def current_command(self) -> ScriptCmd | None:
|
|
668
681
|
if self.cmdptr > len(self.cmdlist) - 1:
|
|
669
682
|
return None
|
|
670
683
|
return self.cmdlist[self.cmdptr]
|
|
@@ -704,17 +717,14 @@ class CommandList:
|
|
|
704
717
|
if cmditem.command_type == "sql" and _state.status.batch.in_batch():
|
|
705
718
|
_state.status.batch.using_db(_state.dbs.current())
|
|
706
719
|
_state.subvars.add_substitution("$CURRENT_TIME", datetime.datetime.now().strftime("%Y-%m-%d %H:%M"))
|
|
707
|
-
|
|
708
|
-
utcnow = datetime.datetime.utcnow()
|
|
709
|
-
else:
|
|
710
|
-
utcnow = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
720
|
+
utcnow = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
711
721
|
_state.subvars.add_substitution("$CURRENT_TIME_UTC", utcnow.strftime("%Y-%m-%d %H:%M"))
|
|
712
722
|
_state.subvars.add_substitution("$CURRENT_SCRIPT", cmditem.source)
|
|
713
723
|
_state.subvars.add_substitution(
|
|
714
724
|
"$CURRENT_SCRIPT_PATH",
|
|
715
|
-
|
|
725
|
+
str(Path(cmditem.source).resolve().parent) + os.sep,
|
|
716
726
|
)
|
|
717
|
-
_state.subvars.add_substitution("$CURRENT_SCRIPT_NAME",
|
|
727
|
+
_state.subvars.add_substitution("$CURRENT_SCRIPT_NAME", Path(cmditem.source).name)
|
|
718
728
|
_state.subvars.add_substitution("$CURRENT_SCRIPT_LINE", str(cmditem.line_no))
|
|
719
729
|
_state.subvars.add_substitution("$SCRIPT_LINE", str(cmditem.line_no))
|
|
720
730
|
cmditem.command.run(self.localvars.merge(self.paramvals), not _state.status.batch.in_batch())
|
|
@@ -743,12 +753,12 @@ class CommandListWhileLoop(CommandList):
|
|
|
743
753
|
# Subclass of CommandList that loops WHILE a condition is met.
|
|
744
754
|
def __init__(
|
|
745
755
|
self,
|
|
746
|
-
cmdlist:
|
|
756
|
+
cmdlist: list[ScriptCmd],
|
|
747
757
|
listname: str,
|
|
748
|
-
paramnames:
|
|
758
|
+
paramnames: list[str] | None,
|
|
749
759
|
loopcondition: str,
|
|
750
760
|
) -> None:
|
|
751
|
-
super(
|
|
761
|
+
super().__init__(cmdlist, listname, paramnames)
|
|
752
762
|
self.loopcondition = loopcondition
|
|
753
763
|
|
|
754
764
|
def run_next(self) -> None:
|
|
@@ -769,12 +779,12 @@ class CommandListUntilLoop(CommandList):
|
|
|
769
779
|
# Subclass of CommandList that loops UNTIL a condition is met.
|
|
770
780
|
def __init__(
|
|
771
781
|
self,
|
|
772
|
-
cmdlist:
|
|
782
|
+
cmdlist: list[ScriptCmd],
|
|
773
783
|
listname: str,
|
|
774
|
-
paramnames:
|
|
784
|
+
paramnames: list[str] | None,
|
|
775
785
|
loopcondition: str,
|
|
776
786
|
) -> None:
|
|
777
|
-
super(
|
|
787
|
+
super().__init__(cmdlist, listname, paramnames)
|
|
778
788
|
self.loopcondition = loopcondition
|
|
779
789
|
|
|
780
790
|
def run_next(self) -> None:
|
|
@@ -799,20 +809,20 @@ class CommandListUntilLoop(CommandList):
|
|
|
799
809
|
class ScriptFile(EncodedFile):
|
|
800
810
|
# A file reader that returns lines and records the line number.
|
|
801
811
|
def __init__(self, scriptfname: str, file_encoding: str) -> None:
|
|
802
|
-
super(
|
|
812
|
+
super().__init__(scriptfname, file_encoding)
|
|
803
813
|
self.lno = 0
|
|
804
814
|
self.f = self.open("r")
|
|
805
815
|
|
|
806
816
|
def __repr__(self) -> str:
|
|
807
|
-
return f"ScriptFile({super(
|
|
817
|
+
return f"ScriptFile({super().filename!r}, {super().encoding!r})"
|
|
808
818
|
|
|
809
819
|
def __iter__(self) -> Any:
|
|
810
820
|
return self
|
|
811
821
|
|
|
812
822
|
def __next__(self) -> str:
|
|
813
|
-
|
|
823
|
+
line = next(self.f)
|
|
814
824
|
self.lno += 1
|
|
815
|
-
return
|
|
825
|
+
return line
|
|
816
826
|
|
|
817
827
|
|
|
818
828
|
# ---------------------------------------------------------------------------
|
|
@@ -829,11 +839,11 @@ class ScriptExecSpec:
|
|
|
829
839
|
|
|
830
840
|
def __init__(self, **kwargs: Any) -> None:
|
|
831
841
|
self.script_id = kwargs["script_id"].lower()
|
|
832
|
-
if self.script_id not in _state.savedscripts
|
|
842
|
+
if self.script_id not in _state.savedscripts:
|
|
833
843
|
raise ErrInfo("cmd", other_msg=f"There is no SCRIPT named {self.script_id}.")
|
|
834
844
|
self.arg_exp = kwargs["argexp"]
|
|
835
845
|
self.looptype = kwargs["looptype"].upper() if "looptype" in kwargs and kwargs["looptype"] is not None else None
|
|
836
|
-
self.loopcond = kwargs
|
|
846
|
+
self.loopcond = kwargs.get("loopcond")
|
|
837
847
|
|
|
838
848
|
def execute(self) -> None:
|
|
839
849
|
# Copy the saved script to avoid erasing saved script commands during execution.
|
|
@@ -883,18 +893,19 @@ def set_system_vars() -> None:
|
|
|
883
893
|
)
|
|
884
894
|
_state.subvars.add_substitution("$CONSOLE_WAIT_WHEN_DONE_STATE", "ON" if _state.conf.gui_wait_on_exit else "OFF")
|
|
885
895
|
_state.subvars.add_substitution("$CURRENT_TIME", datetime.datetime.now().strftime("%Y-%m-%d %H:%M"))
|
|
886
|
-
_state.subvars.add_substitution("$CURRENT_DIR",
|
|
887
|
-
_state.subvars.add_substitution("$CURRENT_PATH",
|
|
896
|
+
_state.subvars.add_substitution("$CURRENT_DIR", str(Path(".").resolve()))
|
|
897
|
+
_state.subvars.add_substitution("$CURRENT_PATH", str(Path(".").resolve()) + os.sep)
|
|
888
898
|
_state.subvars.add_substitution("$CURRENT_ALIAS", _state.dbs.current_alias())
|
|
889
|
-
|
|
899
|
+
db = _state.dbs.current()
|
|
900
|
+
_state.subvars.add_substitution("$AUTOCOMMIT_STATE", "ON" if db.autocommit else "OFF")
|
|
890
901
|
_state.subvars.add_substitution("$TIMER", str(datetime.timedelta(seconds=_state.timer.elapsed())))
|
|
891
|
-
_state.subvars.add_substitution("$DB_USER",
|
|
902
|
+
_state.subvars.add_substitution("$DB_USER", db.user if db.user else "")
|
|
892
903
|
_state.subvars.add_substitution(
|
|
893
904
|
"$DB_SERVER",
|
|
894
|
-
|
|
905
|
+
db.server_name if db.server_name else "",
|
|
895
906
|
)
|
|
896
|
-
_state.subvars.add_substitution("$DB_NAME",
|
|
897
|
-
_state.subvars.add_substitution("$DB_NEED_PWD", "TRUE" if
|
|
907
|
+
_state.subvars.add_substitution("$DB_NAME", db.db_name)
|
|
908
|
+
_state.subvars.add_substitution("$DB_NEED_PWD", "TRUE" if db.need_passwd else "FALSE")
|
|
898
909
|
import random
|
|
899
910
|
|
|
900
911
|
_state.subvars.add_substitution("$RANDOM", str(random.random()))
|
|
@@ -904,7 +915,7 @@ def set_system_vars() -> None:
|
|
|
904
915
|
_state.subvars.add_substitution("$VERSION3", str(_state.tertiary_vno))
|
|
905
916
|
|
|
906
917
|
|
|
907
|
-
def substitute_vars(command_str: str, localvars:
|
|
918
|
+
def substitute_vars(command_str: str, localvars: SubVarSet | None = None) -> str:
|
|
908
919
|
# Substitutes global variables, global counters, and local variables.
|
|
909
920
|
if localvars is not None:
|
|
910
921
|
subs = _state.subvars.merge(localvars)
|
|
@@ -927,7 +938,6 @@ def substitute_vars(command_str: str, localvars: Optional[SubVarSet] = None) ->
|
|
|
927
938
|
|
|
928
939
|
|
|
929
940
|
def runscripts() -> None:
|
|
930
|
-
from execsql.metacommands import DISPATCH_TABLE # deferred — avoids circular import
|
|
931
941
|
|
|
932
942
|
# Repeatedly run the next statement from the script at the top of the
|
|
933
943
|
# command list stack until there are no more statements.
|
|
@@ -942,7 +952,7 @@ def runscripts() -> None:
|
|
|
942
952
|
raise
|
|
943
953
|
except ErrInfo:
|
|
944
954
|
raise
|
|
945
|
-
except:
|
|
955
|
+
except Exception:
|
|
946
956
|
raise ErrInfo(type="exception", exception_msg=exception_desc())
|
|
947
957
|
_state.cmds_run += 1
|
|
948
958
|
|
|
@@ -958,10 +968,9 @@ def current_script_line() -> tuple:
|
|
|
958
968
|
return ("", 0)
|
|
959
969
|
|
|
960
970
|
|
|
961
|
-
def
|
|
962
|
-
#
|
|
963
|
-
|
|
964
|
-
from execsql.utils.errors import file_size_date, write_warning
|
|
971
|
+
def _parse_script_lines(lines_iter, source_name: str) -> list:
|
|
972
|
+
# Parse an iterable of lines into a list of ScriptCmd objects.
|
|
973
|
+
from execsql.utils.errors import write_warning
|
|
965
974
|
|
|
966
975
|
beginscript = re.compile(
|
|
967
976
|
r"^\s*--\s*!x!\s*(?:BEGIN|CREATE)\s+SCRIPT\s+(?P<scriptname>\w+)(?:(?P<paramexpr>\s*\S+.*))?$",
|
|
@@ -974,17 +983,12 @@ def read_sqlfile(sql_file_name: str) -> None:
|
|
|
974
983
|
cmtline = re.compile(r"^\s*--")
|
|
975
984
|
in_block_cmt = False
|
|
976
985
|
in_block_sql = False
|
|
977
|
-
|
|
978
|
-
_state.exec_log.log_status_info(f"Reading script file {sql_file_name} (size: {sz}; date: {dt})")
|
|
979
|
-
scriptfilename = os.path.basename(sql_file_name)
|
|
980
|
-
scriptfile_obj = ScriptFile(sql_file_name, _state.conf.script_encoding).open("r")
|
|
981
|
-
sqllist: List[ScriptCmd] = []
|
|
986
|
+
sqllist: list[ScriptCmd] = []
|
|
982
987
|
sqlline = 0
|
|
983
|
-
subscript_stack:
|
|
984
|
-
file_lineno = 0
|
|
988
|
+
subscript_stack: list[CommandList] = []
|
|
985
989
|
currcmd = ""
|
|
986
|
-
|
|
987
|
-
|
|
990
|
+
scriptname = ""
|
|
991
|
+
for file_lineno, line in enumerate(lines_iter, 1):
|
|
988
992
|
# Remove trailing whitespace but not leading whitespace.
|
|
989
993
|
line = line.rstrip()
|
|
990
994
|
is_comment_line = False
|
|
@@ -1013,7 +1017,7 @@ def read_sqlfile(sql_file_name: str) -> None:
|
|
|
1013
1017
|
if endsql.match(line):
|
|
1014
1018
|
in_block_sql = False
|
|
1015
1019
|
if len(currcmd) > 0:
|
|
1016
|
-
cmd = ScriptCmd(
|
|
1020
|
+
cmd = ScriptCmd(source_name, sqlline, "sql", SqlStmt(currcmd))
|
|
1017
1021
|
if len(subscript_stack) == 0:
|
|
1018
1022
|
sqllist.append(cmd)
|
|
1019
1023
|
else:
|
|
@@ -1022,7 +1026,7 @@ def read_sqlfile(sql_file_name: str) -> None:
|
|
|
1022
1026
|
else:
|
|
1023
1027
|
if len(currcmd) > 0:
|
|
1024
1028
|
write_warning(
|
|
1025
|
-
f"Incomplete SQL statement starting on line {sqlline} at metacommand on line {file_lineno} of {
|
|
1029
|
+
f"Incomplete SQL statement starting on line {sqlline} at metacommand on line {file_lineno} of {source_name}.",
|
|
1026
1030
|
)
|
|
1027
1031
|
begs = beginscript.match(line)
|
|
1028
1032
|
if not begs:
|
|
@@ -1042,7 +1046,7 @@ def read_sqlfile(sql_file_name: str) -> None:
|
|
|
1042
1046
|
raise ErrInfo(
|
|
1043
1047
|
type="cmd",
|
|
1044
1048
|
command_text=line,
|
|
1045
|
-
other_msg=f"Invalid BEGIN SCRIPT metacommand on line {file_lineno} of file {
|
|
1049
|
+
other_msg=f"Invalid BEGIN SCRIPT metacommand on line {file_lineno} of file {source_name}.",
|
|
1046
1050
|
)
|
|
1047
1051
|
else:
|
|
1048
1052
|
param_rx = re.compile(r"\w+", re.I)
|
|
@@ -1057,26 +1061,26 @@ def read_sqlfile(sql_file_name: str) -> None:
|
|
|
1057
1061
|
raise ErrInfo(
|
|
1058
1062
|
type="cmd",
|
|
1059
1063
|
command_text=line,
|
|
1060
|
-
other_msg=f"Unmatched END SCRIPT metacommand on line {file_lineno} of file {
|
|
1064
|
+
other_msg=f"Unmatched END SCRIPT metacommand on line {file_lineno} of file {source_name}.",
|
|
1061
1065
|
)
|
|
1062
1066
|
if len(currcmd) > 0:
|
|
1063
1067
|
raise ErrInfo(
|
|
1064
1068
|
type="cmd",
|
|
1065
1069
|
command_text=line,
|
|
1066
|
-
other_msg=f"Incomplete SQL statement\n ({currcmd})\nat END SCRIPT metacommand on line {file_lineno} of file {
|
|
1070
|
+
other_msg=f"Incomplete SQL statement\n ({currcmd})\nat END SCRIPT metacommand on line {file_lineno} of file {source_name}.",
|
|
1067
1071
|
)
|
|
1068
1072
|
if endscriptname is not None and endscriptname != scriptname:
|
|
1069
1073
|
raise ErrInfo(
|
|
1070
1074
|
type="cmd",
|
|
1071
1075
|
command_text=line,
|
|
1072
|
-
other_msg=f"Mismatched script name in the END SCRIPT metacommand on line {file_lineno} of file {
|
|
1076
|
+
other_msg=f"Mismatched script name in the END SCRIPT metacommand on line {file_lineno} of file {source_name}.",
|
|
1073
1077
|
)
|
|
1074
1078
|
sub_script = subscript_stack.pop()
|
|
1075
1079
|
_state.savedscripts[sub_script.listname] = sub_script
|
|
1076
1080
|
else:
|
|
1077
1081
|
# This is a non-IMMEDIATE metacommand.
|
|
1078
1082
|
cmd = ScriptCmd(
|
|
1079
|
-
|
|
1083
|
+
source_name,
|
|
1080
1084
|
file_lineno,
|
|
1081
1085
|
"cmd",
|
|
1082
1086
|
MetacommandStmt(metacommand_match.group("cmd").strip()),
|
|
@@ -1087,7 +1091,7 @@ def read_sqlfile(sql_file_name: str) -> None:
|
|
|
1087
1091
|
subscript_stack[-1].add(cmd)
|
|
1088
1092
|
else:
|
|
1089
1093
|
# This line is not a comment and not a metacommand; part of a SQL statement.
|
|
1090
|
-
cmd_end =
|
|
1094
|
+
cmd_end = line[-1] == ";"
|
|
1091
1095
|
if line[-1] == "\\":
|
|
1092
1096
|
line = line[:-1].strip()
|
|
1093
1097
|
if currcmd == "":
|
|
@@ -1096,19 +1100,41 @@ def read_sqlfile(sql_file_name: str) -> None:
|
|
|
1096
1100
|
else:
|
|
1097
1101
|
currcmd = f"{currcmd} \n{line}"
|
|
1098
1102
|
if cmd_end and not in_block_sql:
|
|
1099
|
-
cmd = ScriptCmd(
|
|
1103
|
+
cmd = ScriptCmd(source_name, sqlline, "sql", SqlStmt(currcmd.strip()))
|
|
1100
1104
|
if len(subscript_stack) == 0:
|
|
1101
1105
|
sqllist.append(cmd)
|
|
1102
1106
|
else:
|
|
1103
1107
|
subscript_stack[-1].add(cmd)
|
|
1104
1108
|
currcmd = ""
|
|
1105
1109
|
if len(subscript_stack) > 0:
|
|
1106
|
-
raise ErrInfo(type="error", other_msg=f"Unmatched BEGIN SCRIPT metacommand at end of file {
|
|
1110
|
+
raise ErrInfo(type="error", other_msg=f"Unmatched BEGIN SCRIPT metacommand at end of file {source_name}.")
|
|
1107
1111
|
if len(currcmd) > 0:
|
|
1108
1112
|
raise ErrInfo(
|
|
1109
1113
|
type="error",
|
|
1110
|
-
other_msg=
|
|
1114
|
+
other_msg=(
|
|
1115
|
+
f"Incomplete SQL statement starting on line {sqlline} at end of file {source_name}."
|
|
1116
|
+
+ (" Metacommands must be prefixed with '-- !x!'." if source_name == "<inline>" else "")
|
|
1117
|
+
),
|
|
1111
1118
|
)
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1119
|
+
return sqllist
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def read_sqlfile(sql_file_name: str) -> None:
|
|
1123
|
+
# Read lines from the given script file, create a list of ScriptCmd objects,
|
|
1124
|
+
# and append the list to the top of the stack of script commands.
|
|
1125
|
+
from execsql.utils.errors import file_size_date
|
|
1126
|
+
|
|
1127
|
+
sz, dt = file_size_date(sql_file_name)
|
|
1128
|
+
_state.exec_log.log_status_info(f"Reading script file {sql_file_name} (size: {sz}; date: {dt})")
|
|
1129
|
+
scriptfile_obj = ScriptFile(sql_file_name, _state.conf.script_encoding).open("r")
|
|
1130
|
+
sqllist = _parse_script_lines(scriptfile_obj, sql_file_name)
|
|
1131
|
+
if sqllist:
|
|
1132
|
+
_state.commandliststack.append(CommandList(sqllist, Path(sql_file_name).name))
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
def read_sqlstring(content: str, source_name: str = "<inline>") -> None:
|
|
1136
|
+
"""Parse an inline script string and push it onto the command stack."""
|
|
1137
|
+
_state.exec_log.log_status_info(f"Reading inline script ({source_name})")
|
|
1138
|
+
sqllist = _parse_script_lines(content.splitlines(), source_name)
|
|
1139
|
+
if sqllist:
|
|
1140
|
+
_state.commandliststack.append(CommandList(sqllist, source_name))
|