execsql2 2.2.1__py3-none-any.whl → 2.4.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/config.py +52 -0
- execsql/db/access.py +11 -3
- execsql/db/base.py +180 -135
- execsql/db/dsn.py +4 -0
- execsql/db/duckdb.py +4 -0
- execsql/db/factory.py +21 -0
- execsql/db/firebird.py +4 -0
- execsql/db/mysql.py +4 -0
- execsql/db/oracle.py +4 -0
- execsql/db/postgres.py +3 -0
- execsql/db/sqlite.py +3 -0
- execsql/db/sqlserver.py +11 -2
- execsql/exceptions.py +18 -0
- execsql/exporters/base.py +6 -0
- execsql/exporters/delimited.py +36 -0
- execsql/exporters/duckdb.py +4 -0
- execsql/exporters/feather.py +4 -0
- execsql/exporters/html.py +6 -0
- execsql/exporters/json.py +5 -6
- execsql/exporters/latex.py +4 -0
- execsql/exporters/ods.py +28 -7
- execsql/exporters/parquet.py +3 -0
- execsql/exporters/pretty.py +5 -0
- execsql/exporters/raw.py +5 -3
- execsql/exporters/sqlite.py +4 -0
- execsql/exporters/templates.py +16 -6
- execsql/exporters/values.py +4 -0
- execsql/exporters/xls.py +26 -7
- execsql/exporters/xml.py +3 -0
- execsql/exporters/zip.py +15 -0
- execsql/importers/base.py +2 -0
- execsql/importers/csv.py +2 -0
- execsql/importers/feather.py +2 -0
- execsql/importers/ods.py +2 -0
- execsql/importers/xls.py +2 -0
- execsql/metacommands/__init__.py +177 -1968
- execsql/metacommands/dispatch.py +2011 -0
- execsql/models.py +7 -0
- execsql/parser.py +10 -0
- execsql/script/__init__.py +95 -0
- execsql/script/control.py +162 -0
- execsql/{script.py → script/engine.py} +144 -406
- execsql/script/variables.py +281 -0
- execsql/types.py +29 -0
- execsql/utils/auth.py +2 -0
- execsql/utils/crypto.py +4 -6
- execsql/utils/datetime.py +1 -0
- execsql/utils/errors.py +11 -0
- execsql/utils/fileio.py +18 -0
- execsql/utils/gui.py +46 -0
- execsql/utils/mail.py +7 -17
- execsql/utils/numeric.py +2 -0
- execsql/utils/regex.py +9 -0
- execsql/utils/strings.py +16 -0
- execsql/utils/timer.py +2 -0
- execsql2-2.4.0.data/data/execsql2_extras/README.md +65 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/METADATA +8 -1
- execsql2-2.4.0.dist-info/RECORD +108 -0
- execsql2-2.2.1.data/data/execsql2_extras/READ_ME.rst +0 -127
- execsql2-2.2.1.dist-info/RECORD +0 -104
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,50 +1,29 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- :class:`
|
|
13
|
-
BATCH blocks for commit/rollback handling.
|
|
14
|
-
- :class:`IfItem` / :class:`IfLevels` — stack-based IF/ELSE/ENDIF nesting.
|
|
15
|
-
- :class:`CounterVars` — named integer counters (``@NAME``).
|
|
16
|
-
- :class:`SubVarSet` — global ``!!$VAR!!`` substitution-variable store, plus
|
|
17
|
-
``&ENV``, ``@COUNTER``, ``~LOCAL``, and ``#ARG`` prefixes.
|
|
18
|
-
- :class:`LocalSubVarSet` / :class:`ScriptArgSubVarSet` — per-script-scope
|
|
19
|
-
variable overlays.
|
|
20
|
-
- :class:`MetaCommand` — one entry in the metacommand dispatch table (regex +
|
|
21
|
-
handler function + flags).
|
|
22
|
-
- :class:`MetaCommandList` — ordered list of :class:`MetaCommand` entries;
|
|
23
|
-
``get_match()`` finds the first matching entry for a given line.
|
|
24
|
-
- :class:`SqlStmt` — wraps a single SQL string; ``run()`` executes it via the
|
|
25
|
-
active database connection.
|
|
26
|
-
- :class:`MetacommandStmt` — wraps a metacommand line; ``run()`` dispatches
|
|
27
|
-
through :attr:`execsql.state.metacommandlist`.
|
|
3
|
+
"""Script execution engine for execsql.
|
|
4
|
+
|
|
5
|
+
Classes and functions that load, parse, and drive execution of execsql
|
|
6
|
+
``.sql`` script files.
|
|
7
|
+
|
|
8
|
+
Classes:
|
|
9
|
+
- :class:`MetaCommand` — one entry in the metacommand dispatch table.
|
|
10
|
+
- :class:`MetaCommandList` — ordered list of :class:`MetaCommand` entries.
|
|
11
|
+
- :class:`SqlStmt` — wraps a single SQL string for execution.
|
|
12
|
+
- :class:`MetacommandStmt` — wraps a metacommand line for dispatch.
|
|
28
13
|
- :class:`ScriptCmd` — pairs a statement with its source-file location.
|
|
29
|
-
- :class:`CommandList` — ordered list of :class:`ScriptCmd` objects
|
|
30
|
-
|
|
31
|
-
- :class:`
|
|
32
|
-
|
|
33
|
-
- :class:`ScriptFile` — reads and tokenises a ``.sql`` file into
|
|
34
|
-
:class:`ScriptCmd` objects.
|
|
14
|
+
- :class:`CommandList` — ordered list of :class:`ScriptCmd` objects.
|
|
15
|
+
- :class:`CommandListWhileLoop` — loop variant that repeats while a condition is true.
|
|
16
|
+
- :class:`CommandListUntilLoop` — loop variant that repeats until a condition is true.
|
|
17
|
+
- :class:`ScriptFile` — reads and tokenises a ``.sql`` file.
|
|
35
18
|
- :class:`ScriptExecSpec` — specification for deferred script execution.
|
|
36
19
|
|
|
37
|
-
|
|
38
|
-
|
|
20
|
+
Functions:
|
|
39
21
|
- :func:`set_system_vars` — populates built-in ``$VARNAME`` system variables.
|
|
40
22
|
- :func:`substitute_vars` — performs ``!!$VAR!!`` and ``!{$var}!`` expansion.
|
|
41
|
-
- :func:`runscripts` — central execution loop
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- :func:`
|
|
45
|
-
executing command.
|
|
46
|
-
- :func:`read_sqlfile` — parses a SQL script file into a new
|
|
47
|
-
:class:`CommandList` and pushes it onto ``_state.commandliststack``.
|
|
23
|
+
- :func:`runscripts` — central execution loop.
|
|
24
|
+
- :func:`current_script_line` — returns the source location of the currently executing command.
|
|
25
|
+
- :func:`read_sqlfile` — parses a SQL script file into a new :class:`CommandList`.
|
|
26
|
+
- :func:`read_sqlstring` — parses an inline script string into a new :class:`CommandList`.
|
|
48
27
|
"""
|
|
49
28
|
|
|
50
29
|
import copy
|
|
@@ -57,359 +36,24 @@ from typing import Any
|
|
|
57
36
|
|
|
58
37
|
import execsql.state as _state
|
|
59
38
|
from execsql.exceptions import ErrInfo
|
|
39
|
+
from execsql.script.variables import LocalSubVarSet, ScriptArgSubVarSet, SubVarSet
|
|
60
40
|
from execsql.utils.errors import exception_desc
|
|
61
41
|
from execsql.utils.fileio import EncodedFile
|
|
62
42
|
|
|
63
43
|
|
|
64
|
-
# ---------------------------------------------------------------------------
|
|
65
|
-
# BatchLevels
|
|
66
|
-
# ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class BatchLevels:
|
|
70
|
-
# A stack to keep a record of the databases used in nested batches.
|
|
71
|
-
class Batch:
|
|
72
|
-
def __init__(self) -> None:
|
|
73
|
-
self.dbs_used: list[Any] = []
|
|
74
|
-
|
|
75
|
-
def __init__(self) -> None:
|
|
76
|
-
self.batchlevels: list[BatchLevels.Batch] = []
|
|
77
|
-
|
|
78
|
-
def in_batch(self) -> bool:
|
|
79
|
-
return len(self.batchlevels) > 0
|
|
80
|
-
|
|
81
|
-
def new_batch(self) -> None:
|
|
82
|
-
self.batchlevels.append(self.Batch())
|
|
83
|
-
|
|
84
|
-
def using_db(self, db: Any) -> None:
|
|
85
|
-
if len(self.batchlevels) > 0 and db not in self.batchlevels[-1].dbs_used:
|
|
86
|
-
self.batchlevels[-1].dbs_used.append(db)
|
|
87
|
-
|
|
88
|
-
def uses_db(self, db: Any) -> bool:
|
|
89
|
-
if len(self.batchlevels) == 0:
|
|
90
|
-
return False
|
|
91
|
-
return any(db in batch.dbs_used for batch in self.batchlevels)
|
|
92
|
-
|
|
93
|
-
def rollback_batch(self) -> None:
|
|
94
|
-
if len(self.batchlevels) > 0:
|
|
95
|
-
b = self.batchlevels[-1]
|
|
96
|
-
for db in b.dbs_used:
|
|
97
|
-
db.rollback()
|
|
98
|
-
|
|
99
|
-
def end_batch(self) -> None:
|
|
100
|
-
b = self.batchlevels.pop()
|
|
101
|
-
for db in b.dbs_used:
|
|
102
|
-
db.commit()
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# ---------------------------------------------------------------------------
|
|
106
|
-
# IfItem / IfLevels
|
|
107
|
-
# ---------------------------------------------------------------------------
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
class IfItem:
|
|
111
|
-
# An object representing an 'if' level, with context data.
|
|
112
|
-
def __init__(self, tf_value: bool) -> None:
|
|
113
|
-
self.tf_value = tf_value
|
|
114
|
-
self.scriptname, self.scriptline = current_script_line()
|
|
115
|
-
|
|
116
|
-
def value(self) -> bool:
|
|
117
|
-
return self.tf_value
|
|
118
|
-
|
|
119
|
-
def invert(self) -> None:
|
|
120
|
-
self.tf_value = not self.tf_value
|
|
121
|
-
|
|
122
|
-
def change_to(self, tf_value: bool) -> None:
|
|
123
|
-
self.tf_value = tf_value
|
|
124
|
-
|
|
125
|
-
def script_line(self) -> tuple:
|
|
126
|
-
return (self.scriptname, self.scriptline)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
class IfLevels:
|
|
130
|
-
# A stack of True/False values corresponding to a nested set of conditionals,
|
|
131
|
-
# with methods to manipulate and query the set of conditional states.
|
|
132
|
-
def __init__(self) -> None:
|
|
133
|
-
self.if_levels: list[IfItem] = []
|
|
134
|
-
|
|
135
|
-
def nest(self, tf_value: bool) -> None:
|
|
136
|
-
self.if_levels.append(IfItem(tf_value))
|
|
137
|
-
|
|
138
|
-
def unnest(self) -> None:
|
|
139
|
-
if len(self.if_levels) == 0:
|
|
140
|
-
raise ErrInfo(type="error", other_msg="Can't exit an IF block; no IF block is active.")
|
|
141
|
-
else:
|
|
142
|
-
self.if_levels.pop()
|
|
143
|
-
|
|
144
|
-
def invert(self) -> None:
|
|
145
|
-
if len(self.if_levels) == 0:
|
|
146
|
-
raise ErrInfo(type="error", other_msg="Can't change the IF state; no IF block is active.")
|
|
147
|
-
else:
|
|
148
|
-
self.if_levels[-1].invert()
|
|
149
|
-
|
|
150
|
-
def replace(self, tf_value: bool) -> None:
|
|
151
|
-
if len(self.if_levels) == 0:
|
|
152
|
-
raise ErrInfo(type="error", other_msg="Can't change the IF state; no IF block is active.")
|
|
153
|
-
else:
|
|
154
|
-
self.if_levels[-1].change_to(tf_value)
|
|
155
|
-
|
|
156
|
-
def current(self) -> bool:
|
|
157
|
-
if len(self.if_levels) == 0:
|
|
158
|
-
raise ErrInfo(type="error", other_msg="No IF block is active.")
|
|
159
|
-
else:
|
|
160
|
-
return self.if_levels[-1].value()
|
|
161
|
-
|
|
162
|
-
def all_true(self) -> bool:
|
|
163
|
-
if self.if_levels == []:
|
|
164
|
-
return True
|
|
165
|
-
return all(tf.value() for tf in self.if_levels)
|
|
166
|
-
|
|
167
|
-
def only_current_false(self) -> bool:
|
|
168
|
-
# Returns True if the current if level is false and all higher levels are True.
|
|
169
|
-
if len(self.if_levels) == 0:
|
|
170
|
-
return False
|
|
171
|
-
elif len(self.if_levels) == 1:
|
|
172
|
-
return not self.if_levels[-1].value()
|
|
173
|
-
else:
|
|
174
|
-
return not self.if_levels[-1].value() and all(tf.value() for tf in self.if_levels[:-1])
|
|
175
|
-
|
|
176
|
-
def script_lines(self, top_n: int) -> list[tuple]:
|
|
177
|
-
# Returns a list of tuples containing the script name and line number
|
|
178
|
-
# for the topmost 'top_n' if levels, in bottom-up order.
|
|
179
|
-
if len(self.if_levels) < top_n:
|
|
180
|
-
raise ErrInfo(type="error", other_msg="Invalid IF stack depth reference.")
|
|
181
|
-
levels = self.if_levels[len(self.if_levels) - top_n :]
|
|
182
|
-
return [lvl.script_line() for lvl in levels]
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
# ---------------------------------------------------------------------------
|
|
186
|
-
# CounterVars
|
|
187
|
-
# ---------------------------------------------------------------------------
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
class CounterVars:
|
|
191
|
-
# A dictionary of dynamically created named counter variables.
|
|
192
|
-
_COUNTER_RX = re.compile(r"!!\$(COUNTER_\d+)!!", re.I)
|
|
193
|
-
|
|
194
|
-
def __init__(self) -> None:
|
|
195
|
-
self.counters: dict[str, int] = {}
|
|
196
|
-
|
|
197
|
-
def _ctrid(self, ctr_no: int) -> str:
|
|
198
|
-
return f"counter_{ctr_no}"
|
|
199
|
-
|
|
200
|
-
def set_counter(self, ctr_no: int, ctr_val: int) -> None:
|
|
201
|
-
self.counters[self._ctrid(ctr_no)] = ctr_val
|
|
202
|
-
|
|
203
|
-
def remove_counter(self, ctr_no: int) -> None:
|
|
204
|
-
ctr_id = self._ctrid(ctr_no)
|
|
205
|
-
if ctr_id in self.counters:
|
|
206
|
-
del self.counters[ctr_id]
|
|
207
|
-
|
|
208
|
-
def remove_all_counters(self) -> None:
|
|
209
|
-
self.counters = {}
|
|
210
|
-
|
|
211
|
-
def substitute(self, command_str: str) -> tuple:
|
|
212
|
-
# Substitutes any counter variable references with the counter value and
|
|
213
|
-
# returns the modified command string and a flag indicating replacements.
|
|
214
|
-
m = self._COUNTER_RX.search(command_str, re.I)
|
|
215
|
-
if m:
|
|
216
|
-
ctr_id = m.group(1).lower()
|
|
217
|
-
if ctr_id not in self.counters:
|
|
218
|
-
self.counters[ctr_id] = 0
|
|
219
|
-
new_count = self.counters[ctr_id] + 1
|
|
220
|
-
self.counters[ctr_id] = new_count
|
|
221
|
-
return command_str.replace("!!$" + m.group(1) + "!!", str(new_count)), True
|
|
222
|
-
return command_str, False
|
|
223
|
-
|
|
224
|
-
def substitute_all(self, any_text: str) -> tuple:
|
|
225
|
-
subbed = True
|
|
226
|
-
any_subbed = False
|
|
227
|
-
while subbed:
|
|
228
|
-
any_text, subbed = self.substitute(any_text)
|
|
229
|
-
if subbed:
|
|
230
|
-
any_subbed = True
|
|
231
|
-
return any_text, any_subbed
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
# ---------------------------------------------------------------------------
|
|
235
|
-
# SubVarSet / LocalSubVarSet / ScriptArgSubVarSet
|
|
236
|
-
# ---------------------------------------------------------------------------
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
class SubVarSet:
|
|
240
|
-
# A pool of substitution variables. Each variable consists of a name and
|
|
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.
|
|
245
|
-
def __init__(self) -> None:
|
|
246
|
-
self._subs_dict: dict[str, Any] = {}
|
|
247
|
-
self._compiled_patterns: dict[str, tuple] = {}
|
|
248
|
-
self.prefix_list: list[str] = ["$", "&", "@"]
|
|
249
|
-
# Don't construct/compile on init because deepcopy() can't handle compiled regexes.
|
|
250
|
-
self.var_rx = None
|
|
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
|
-
|
|
280
|
-
def compile_var_rx(self) -> None:
|
|
281
|
-
self.var_rx_str = r"^[" + "".join(self.prefix_list) + r"]?\w+$"
|
|
282
|
-
self.var_rx = re.compile(self.var_rx_str, re.I)
|
|
283
|
-
|
|
284
|
-
def var_name_ok(self, varname: str) -> bool:
|
|
285
|
-
if self.var_rx is None:
|
|
286
|
-
self.compile_var_rx()
|
|
287
|
-
return self.var_rx.match(varname) is not None
|
|
288
|
-
|
|
289
|
-
def check_var_name(self, varname: str) -> None:
|
|
290
|
-
if not self.var_name_ok(varname.lower()):
|
|
291
|
-
raise ErrInfo("error", other_msg=f"Invalid variable name ({varname}) in this context.")
|
|
292
|
-
|
|
293
|
-
def remove_substitution(self, template_str: str) -> None:
|
|
294
|
-
self.check_var_name(template_str)
|
|
295
|
-
old_sub = template_str.lower()
|
|
296
|
-
self._subs_dict.pop(old_sub, None)
|
|
297
|
-
self._compiled_patterns.pop(old_sub, None)
|
|
298
|
-
|
|
299
|
-
def add_substitution(self, varname: str, repl_str: Any) -> None:
|
|
300
|
-
self.check_var_name(varname)
|
|
301
|
-
varname = varname.lower()
|
|
302
|
-
self._subs_dict[varname] = repl_str
|
|
303
|
-
self._compiled_patterns[varname] = self._compile_patterns_for(varname)
|
|
304
|
-
|
|
305
|
-
def append_substitution(self, varname: str, repl_str: str) -> None:
|
|
306
|
-
self.check_var_name(varname)
|
|
307
|
-
varname = varname.lower()
|
|
308
|
-
if varname in self._subs_dict:
|
|
309
|
-
self.add_substitution(varname, f"{self._subs_dict[varname]}\n{repl_str}")
|
|
310
|
-
else:
|
|
311
|
-
self.add_substitution(varname, repl_str)
|
|
312
|
-
|
|
313
|
-
def varvalue(self, varname: str) -> str | None:
|
|
314
|
-
self.check_var_name(varname)
|
|
315
|
-
return self._subs_dict.get(varname.lower())
|
|
316
|
-
|
|
317
|
-
def increment_by(self, varname: str, numeric_increment: Any) -> None:
|
|
318
|
-
self.check_var_name(varname)
|
|
319
|
-
varvalue = self.varvalue(varname)
|
|
320
|
-
if varvalue is None:
|
|
321
|
-
varvalue = "0"
|
|
322
|
-
self.add_substitution(varname, varvalue)
|
|
323
|
-
# Import as_numeric lazily to avoid circular dependency
|
|
324
|
-
from execsql.utils.numeric import as_numeric
|
|
325
|
-
|
|
326
|
-
numvalue = as_numeric(varvalue)
|
|
327
|
-
numinc = as_numeric(numeric_increment)
|
|
328
|
-
if numvalue is None or numinc is None:
|
|
329
|
-
newval = f"{varvalue}+{numeric_increment}"
|
|
330
|
-
else:
|
|
331
|
-
newval = str(numvalue + numinc)
|
|
332
|
-
self.add_substitution(varname, newval)
|
|
333
|
-
|
|
334
|
-
def sub_exists(self, template_str: str) -> bool:
|
|
335
|
-
self.check_var_name(template_str)
|
|
336
|
-
return template_str.lower() in self._subs_dict
|
|
337
|
-
|
|
338
|
-
def merge(self, other_subvars: SubVarSet | None) -> SubVarSet:
|
|
339
|
-
# Return a new SubVarSet with this object's variables merged with other_subvars.
|
|
340
|
-
if other_subvars is not None:
|
|
341
|
-
newsubs = SubVarSet()
|
|
342
|
-
newsubs._subs_dict = dict(self._subs_dict)
|
|
343
|
-
newsubs._compiled_patterns = dict(self._compiled_patterns)
|
|
344
|
-
newsubs.prefix_list = list(set(self.prefix_list + other_subvars.prefix_list))
|
|
345
|
-
newsubs.compile_var_rx()
|
|
346
|
-
for varname, value in other_subvars._subs_dict.items():
|
|
347
|
-
newsubs.add_substitution(varname, value)
|
|
348
|
-
return newsubs
|
|
349
|
-
return self
|
|
350
|
-
|
|
351
|
-
def substitute(self, command_str: str) -> tuple:
|
|
352
|
-
# Replace any substitution variables in the command string.
|
|
353
|
-
if isinstance(command_str, str):
|
|
354
|
-
for varname, sub in self._subs_dict.items():
|
|
355
|
-
if sub is None:
|
|
356
|
-
sub = ""
|
|
357
|
-
sub = str(sub)
|
|
358
|
-
if os.name != "posix":
|
|
359
|
-
sub = sub.replace("\\", "\\\\")
|
|
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):
|
|
364
|
-
sub = sub.replace("'", "''")
|
|
365
|
-
return patq_rx.sub(sub, command_str), True
|
|
366
|
-
if patdq_rx.search(command_str):
|
|
367
|
-
sub = '"' + sub + '"'
|
|
368
|
-
return patdq_rx.sub(sub, command_str), True
|
|
369
|
-
return command_str, False
|
|
370
|
-
|
|
371
|
-
def substitute_all(self, any_text: str) -> tuple:
|
|
372
|
-
subbed = True
|
|
373
|
-
any_subbed = False
|
|
374
|
-
while subbed:
|
|
375
|
-
any_text, subbed = self.substitute(any_text)
|
|
376
|
-
if subbed:
|
|
377
|
-
any_subbed = True
|
|
378
|
-
return any_text, any_subbed
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
class LocalSubVarSet(SubVarSet):
|
|
382
|
-
# A pool of local substitution variables.
|
|
383
|
-
# Only '~' is allowed as a prefix and MUST be present.
|
|
384
|
-
def __init__(self) -> None:
|
|
385
|
-
SubVarSet.__init__(self)
|
|
386
|
-
self.prefix_list = ["~"]
|
|
387
|
-
|
|
388
|
-
def compile_var_rx(self) -> None:
|
|
389
|
-
# Prefix is required, not optional.
|
|
390
|
-
self.var_rx_str = r"^[" + "".join(self.prefix_list) + r"]\w+$"
|
|
391
|
-
self.var_rx = re.compile(self.var_rx_str, re.I)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
class ScriptArgSubVarSet(SubVarSet):
|
|
395
|
-
# A pool of script argument names.
|
|
396
|
-
# Only '#' is allowed as a prefix and MUST be present.
|
|
397
|
-
def __init__(self) -> None:
|
|
398
|
-
SubVarSet.__init__(self)
|
|
399
|
-
self.prefix_list = ["#"]
|
|
400
|
-
|
|
401
|
-
def compile_var_rx(self) -> None:
|
|
402
|
-
# Prefix is required, not optional.
|
|
403
|
-
self.var_rx_str = r"^[" + "".join(self.prefix_list) + r"]\w+$"
|
|
404
|
-
self.var_rx = re.compile(self.var_rx_str, re.I)
|
|
405
|
-
|
|
406
|
-
|
|
407
44
|
# ---------------------------------------------------------------------------
|
|
408
45
|
# MetaCommand / MetaCommandList
|
|
409
46
|
# ---------------------------------------------------------------------------
|
|
410
47
|
|
|
411
48
|
|
|
412
49
|
class MetaCommand:
|
|
50
|
+
"""A single entry in the metacommand dispatch table.
|
|
51
|
+
|
|
52
|
+
Holds a compiled regex, a handler function, and execution-control flags.
|
|
53
|
+
Call :meth:`run` with a raw command string to attempt a match and invoke
|
|
54
|
+
the handler.
|
|
55
|
+
"""
|
|
56
|
+
|
|
413
57
|
# A compiled metacommand that can be run if it matches a metacommand command string.
|
|
414
58
|
def __init__(
|
|
415
59
|
self,
|
|
@@ -437,6 +81,10 @@ class MetaCommand:
|
|
|
437
81
|
)
|
|
438
82
|
|
|
439
83
|
def run(self, cmd_str: str) -> tuple:
|
|
84
|
+
"""Match *cmd_str* against this entry's regex and, if it matches, invoke the handler.
|
|
85
|
+
|
|
86
|
+
Returns ``(True, return_value)`` on a match, ``(False, None)`` otherwise.
|
|
87
|
+
"""
|
|
440
88
|
# Runs the metacommand if the command string matches the regex.
|
|
441
89
|
m = self.rx.match(cmd_str.strip())
|
|
442
90
|
if m:
|
|
@@ -468,20 +116,49 @@ class MetaCommand:
|
|
|
468
116
|
|
|
469
117
|
|
|
470
118
|
class MetaCommandList:
|
|
471
|
-
"""Ordered list of :class:`MetaCommand` entries.
|
|
119
|
+
"""Ordered list of :class:`MetaCommand` entries with keyword-indexed dispatch.
|
|
472
120
|
|
|
473
121
|
Commands are stored with the most-recently-added entry first, matching
|
|
474
|
-
the original linked-list prepend semantics.
|
|
475
|
-
|
|
476
|
-
|
|
122
|
+
the original linked-list prepend semantics. A keyword index
|
|
123
|
+
(``_by_keyword``) groups entries by their leading keyword so that
|
|
124
|
+
``eval()`` and ``get_match()`` test only the small subset of regexes
|
|
125
|
+
that could possibly match, reducing dispatch from O(N) to O(K) where
|
|
126
|
+
K is the number of patterns sharing the same leading keyword (typically
|
|
127
|
+
1–5 vs. 205 total).
|
|
477
128
|
"""
|
|
478
129
|
|
|
130
|
+
# Regex to extract the leading keyword from a metacommand regex pattern.
|
|
131
|
+
# Handles ^\s*KEYWORD, ^KEYWORD, and ^\s*(?:PREFIX\s+)?KEYWORD.
|
|
132
|
+
_KEYWORD_RX = re.compile(
|
|
133
|
+
r"^\^"
|
|
134
|
+
r"(?:\\s\*)?(?:\(\?:[^)]+\))?(?:\\s\+)?"
|
|
135
|
+
r"(?:\\s\*)?"
|
|
136
|
+
r"([A-Z_]+)",
|
|
137
|
+
)
|
|
138
|
+
|
|
479
139
|
def __init__(self) -> None:
|
|
480
140
|
self._commands: list[MetaCommand] = []
|
|
141
|
+
self._by_keyword: dict[str, list[MetaCommand]] = {}
|
|
142
|
+
self._unkeyed: list[MetaCommand] = []
|
|
481
143
|
|
|
482
144
|
def __iter__(self) -> Any:
|
|
483
145
|
return iter(self._commands)
|
|
484
146
|
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _extract_keyword(cmd_str: str) -> str | None:
|
|
149
|
+
"""Extract the leading keyword from a metacommand string."""
|
|
150
|
+
word = cmd_str.strip().split(None, 1)
|
|
151
|
+
return word[0].upper() if word else None
|
|
152
|
+
|
|
153
|
+
def _index_command(self, mc: MetaCommand, rx_pattern: str) -> None:
|
|
154
|
+
"""Add *mc* to the keyword index based on its regex pattern."""
|
|
155
|
+
m = self._KEYWORD_RX.match(rx_pattern)
|
|
156
|
+
if m:
|
|
157
|
+
kw = m.group(1)
|
|
158
|
+
self._by_keyword.setdefault(kw, []).insert(0, mc)
|
|
159
|
+
else:
|
|
160
|
+
self._unkeyed.insert(0, mc)
|
|
161
|
+
|
|
485
162
|
def add(
|
|
486
163
|
self,
|
|
487
164
|
matching_regexes: Any,
|
|
@@ -492,24 +169,42 @@ class MetaCommandList:
|
|
|
492
169
|
set_error_flag: bool = True,
|
|
493
170
|
category: str | None = None,
|
|
494
171
|
) -> None:
|
|
172
|
+
"""Register one or more regex patterns as a new :class:`MetaCommand` entry.
|
|
173
|
+
|
|
174
|
+
*matching_regexes* may be a single pattern string or a list/tuple of
|
|
175
|
+
patterns; each compiles into a separate :class:`MetaCommand` prepended to
|
|
176
|
+
the dispatch list so that later registrations take priority.
|
|
177
|
+
"""
|
|
495
178
|
if type(matching_regexes) in (tuple, list):
|
|
496
|
-
|
|
179
|
+
raw_patterns = list(matching_regexes)
|
|
180
|
+
regexes = [re.compile(rx, re.I) for rx in raw_patterns]
|
|
497
181
|
else:
|
|
182
|
+
raw_patterns = [matching_regexes]
|
|
498
183
|
regexes = [re.compile(matching_regexes, re.I)]
|
|
499
|
-
for rx in regexes:
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
run_when_false,
|
|
509
|
-
set_error_flag,
|
|
510
|
-
category,
|
|
511
|
-
),
|
|
184
|
+
for rx, raw in zip(regexes, raw_patterns):
|
|
185
|
+
mc = MetaCommand(
|
|
186
|
+
rx,
|
|
187
|
+
exec_func,
|
|
188
|
+
description,
|
|
189
|
+
run_in_batch,
|
|
190
|
+
run_when_false,
|
|
191
|
+
set_error_flag,
|
|
192
|
+
category,
|
|
512
193
|
)
|
|
194
|
+
# Prepend to preserve "last registered, first checked" ordering.
|
|
195
|
+
self._commands.insert(0, mc)
|
|
196
|
+
self._index_command(mc, raw)
|
|
197
|
+
|
|
198
|
+
def _candidates(self, cmd_str: str) -> list[MetaCommand]:
|
|
199
|
+
"""Return the subset of commands whose keyword matches *cmd_str*.
|
|
200
|
+
|
|
201
|
+
Falls back to the full command list if no keyword match is found.
|
|
202
|
+
"""
|
|
203
|
+
kw = self._extract_keyword(cmd_str)
|
|
204
|
+
if kw and kw in self._by_keyword:
|
|
205
|
+
# Keyword-matched entries plus any unkeyed entries that could match anything.
|
|
206
|
+
return self._by_keyword[kw] + self._unkeyed
|
|
207
|
+
return self._commands
|
|
513
208
|
|
|
514
209
|
def keywords_by_category(self) -> dict[str, list[str]]:
|
|
515
210
|
"""Return ``{category: [keyword, ...]}`` from entries that have both.
|
|
@@ -530,7 +225,7 @@ class MetaCommandList:
|
|
|
530
225
|
Returns ``(True, return_value)`` if a matching command was found and
|
|
531
226
|
run, ``(False, None)`` if no command matched.
|
|
532
227
|
"""
|
|
533
|
-
for cmd in self.
|
|
228
|
+
for cmd in self._candidates(cmd_str):
|
|
534
229
|
if _state.if_stack.all_true() or cmd.run_when_false:
|
|
535
230
|
success, value = cmd.run(cmd_str)
|
|
536
231
|
if success:
|
|
@@ -541,8 +236,9 @@ class MetaCommandList:
|
|
|
541
236
|
"""Return ``(MetaCommand, re.Match)`` for the first entry matching *cmd*,
|
|
542
237
|
or ``None`` if no entry matches.
|
|
543
238
|
"""
|
|
544
|
-
|
|
545
|
-
|
|
239
|
+
stripped = cmd.strip()
|
|
240
|
+
for node in self._candidates(stripped):
|
|
241
|
+
m = node.rx.match(stripped)
|
|
546
242
|
if m is not None:
|
|
547
243
|
return (node, m)
|
|
548
244
|
return None
|
|
@@ -554,6 +250,8 @@ class MetaCommandList:
|
|
|
554
250
|
|
|
555
251
|
|
|
556
252
|
class SqlStmt:
|
|
253
|
+
"""A single SQL statement ready to be executed against the active database."""
|
|
254
|
+
|
|
557
255
|
# A SQL statement to be passed to a database to execute.
|
|
558
256
|
def __init__(self, sql_statement: str) -> None:
|
|
559
257
|
self.statement = re.sub(r"\s*;(\s*;\s*)+$", ";", sql_statement)
|
|
@@ -562,6 +260,7 @@ class SqlStmt:
|
|
|
562
260
|
return f"SqlStmt({self.statement})"
|
|
563
261
|
|
|
564
262
|
def run(self, localvars: SubVarSet | None = None, commit: bool = True) -> None:
|
|
263
|
+
"""Execute the statement on the current database, committing unless in a batch."""
|
|
565
264
|
# Run the SQL statement on the current database.
|
|
566
265
|
if _state.if_stack.all_true():
|
|
567
266
|
e = None
|
|
@@ -597,10 +296,13 @@ class SqlStmt:
|
|
|
597
296
|
_state.subvars.add_substitution("$LAST_SQL", cmd)
|
|
598
297
|
|
|
599
298
|
def commandline(self) -> str:
|
|
299
|
+
"""Return the raw SQL statement text."""
|
|
600
300
|
return self.statement
|
|
601
301
|
|
|
602
302
|
|
|
603
303
|
class MetacommandStmt:
|
|
304
|
+
"""A single execsql metacommand line ready to be dispatched."""
|
|
305
|
+
|
|
604
306
|
# A metacommand to be handled by execsql.
|
|
605
307
|
def __init__(self, metacommand_statement: str) -> None:
|
|
606
308
|
self.statement = metacommand_statement
|
|
@@ -609,6 +311,7 @@ class MetacommandStmt:
|
|
|
609
311
|
return f"MetacommandStmt({self.statement})"
|
|
610
312
|
|
|
611
313
|
def run(self, localvars: SubVarSet | None = None, commit: bool = False) -> Any:
|
|
314
|
+
"""Expand substitution variables then dispatch through the metacommand table."""
|
|
612
315
|
# Tries all metacommands in the dispatch table until one runs.
|
|
613
316
|
errmsg = "Unknown metacommand"
|
|
614
317
|
cmd = substitute_vars(self.statement, localvars)
|
|
@@ -637,6 +340,7 @@ class MetacommandStmt:
|
|
|
637
340
|
return None
|
|
638
341
|
|
|
639
342
|
def commandline(self) -> str:
|
|
343
|
+
"""Return the metacommand line in its canonical ``-- !x! ...`` form."""
|
|
640
344
|
return "-- !x! " + self.statement
|
|
641
345
|
|
|
642
346
|
|
|
@@ -646,6 +350,8 @@ class MetacommandStmt:
|
|
|
646
350
|
|
|
647
351
|
|
|
648
352
|
class ScriptCmd:
|
|
353
|
+
"""A parsed script item: either a :class:`SqlStmt` or a :class:`MetacommandStmt`, with source location."""
|
|
354
|
+
|
|
649
355
|
# A SQL script object that is either a SQL statement or a metacommand.
|
|
650
356
|
def __init__(
|
|
651
357
|
self,
|
|
@@ -675,6 +381,12 @@ class ScriptCmd:
|
|
|
675
381
|
|
|
676
382
|
|
|
677
383
|
class CommandList:
|
|
384
|
+
"""Ordered sequence of :class:`ScriptCmd` objects with a forward-only execution cursor.
|
|
385
|
+
|
|
386
|
+
Push onto ``_state.commandliststack`` and call :meth:`run_next` in a loop
|
|
387
|
+
(or let :func:`runscripts` drive it) to execute each command in turn.
|
|
388
|
+
"""
|
|
389
|
+
|
|
678
390
|
# A list of ScriptCmd objects including execution state.
|
|
679
391
|
def __init__(
|
|
680
392
|
self,
|
|
@@ -693,6 +405,7 @@ class CommandList:
|
|
|
693
405
|
self.init_if_level: int | None = None
|
|
694
406
|
|
|
695
407
|
def add(self, script_command: ScriptCmd) -> None:
|
|
408
|
+
"""Append *script_command* to the end of this command list."""
|
|
696
409
|
self.cmdlist.append(script_command)
|
|
697
410
|
|
|
698
411
|
def set_paramvals(self, paramvals: SubVarSet) -> None:
|
|
@@ -706,11 +419,13 @@ class CommandList:
|
|
|
706
419
|
)
|
|
707
420
|
|
|
708
421
|
def current_command(self) -> ScriptCmd | None:
|
|
422
|
+
"""Return the :class:`ScriptCmd` at the current cursor position, or ``None`` if exhausted."""
|
|
709
423
|
if self.cmdptr > len(self.cmdlist) - 1:
|
|
710
424
|
return None
|
|
711
425
|
return self.cmdlist[self.cmdptr]
|
|
712
426
|
|
|
713
427
|
def check_iflevels(self) -> None:
|
|
428
|
+
"""Warn if the IF-stack depth changed during execution of this command list."""
|
|
714
429
|
if_excess = len(_state.if_stack.if_levels) - self.init_if_level
|
|
715
430
|
if if_excess > 0:
|
|
716
431
|
sources = _state.if_stack.script_lines(if_excess)
|
|
@@ -759,6 +474,7 @@ class CommandList:
|
|
|
759
474
|
self.cmdptr += 1
|
|
760
475
|
|
|
761
476
|
def run_next(self) -> None:
|
|
477
|
+
"""Execute the command at the current cursor and advance; raise StopIteration when done."""
|
|
762
478
|
if self.cmdptr == 0:
|
|
763
479
|
self.init_if_level = len(_state.if_stack.if_levels)
|
|
764
480
|
if self.cmdptr > len(self.cmdlist) - 1:
|
|
@@ -778,6 +494,8 @@ class CommandList:
|
|
|
778
494
|
|
|
779
495
|
|
|
780
496
|
class CommandListWhileLoop(CommandList):
|
|
497
|
+
"""A :class:`CommandList` that repeats its commands while a condition evaluates to true."""
|
|
498
|
+
|
|
781
499
|
# Subclass of CommandList that loops WHILE a condition is met.
|
|
782
500
|
def __init__(
|
|
783
501
|
self,
|
|
@@ -804,6 +522,8 @@ class CommandListWhileLoop(CommandList):
|
|
|
804
522
|
|
|
805
523
|
|
|
806
524
|
class CommandListUntilLoop(CommandList):
|
|
525
|
+
"""A :class:`CommandList` that repeats its commands until a condition evaluates to true."""
|
|
526
|
+
|
|
807
527
|
# Subclass of CommandList that loops UNTIL a condition is met.
|
|
808
528
|
def __init__(
|
|
809
529
|
self,
|
|
@@ -835,6 +555,13 @@ class CommandListUntilLoop(CommandList):
|
|
|
835
555
|
|
|
836
556
|
|
|
837
557
|
class ScriptFile(EncodedFile):
|
|
558
|
+
"""An iterable file reader that tracks the current line number.
|
|
559
|
+
|
|
560
|
+
Wraps :class:`~execsql.utils.fileio.EncodedFile` and increments
|
|
561
|
+
:attr:`lno` on each ``next()`` call so that callers always know which
|
|
562
|
+
source line is being processed.
|
|
563
|
+
"""
|
|
564
|
+
|
|
838
565
|
# A file reader that returns lines and records the line number.
|
|
839
566
|
def __init__(self, scriptfname: str, file_encoding: str) -> None:
|
|
840
567
|
super().__init__(scriptfname, file_encoding)
|
|
@@ -859,6 +586,13 @@ class ScriptFile(EncodedFile):
|
|
|
859
586
|
|
|
860
587
|
|
|
861
588
|
class ScriptExecSpec:
|
|
589
|
+
"""Deferred execution specification for a named SCRIPT block.
|
|
590
|
+
|
|
591
|
+
Parses argument expressions and loop-type flags at construction time;
|
|
592
|
+
call :meth:`execute` to push the resolved :class:`CommandList` onto the
|
|
593
|
+
execution stack.
|
|
594
|
+
"""
|
|
595
|
+
|
|
862
596
|
# Stores specifications for executing a SCRIPT, for later use.
|
|
863
597
|
args_rx = re.compile(
|
|
864
598
|
r'(?P<param>#?\w+)\s*=\s*(?P<arg>(?:(?:[^"\'\[][^,\)]*)|(?:"[^"]*")|(?:\'[^\']*\')|(?:\[[^\]]*\])))',
|
|
@@ -908,6 +642,7 @@ class ScriptExecSpec:
|
|
|
908
642
|
|
|
909
643
|
|
|
910
644
|
def set_system_vars() -> None:
|
|
645
|
+
"""Refresh all built-in system substitution variables (``$CURRENT_TIME``, ``$DB_NAME``, etc.)."""
|
|
911
646
|
# (Re)define the system substitution variables that are not script-specific.
|
|
912
647
|
_state.subvars.add_substitution("$CANCEL_HALT_STATE", "ON" if _state.status.cancel_halt else "OFF")
|
|
913
648
|
_state.subvars.add_substitution("$ERROR_HALT_STATE", "ON" if _state.status.halt_on_err else "OFF")
|
|
@@ -947,6 +682,7 @@ _MAX_SUBSTITUTION_DEPTH = 100
|
|
|
947
682
|
|
|
948
683
|
|
|
949
684
|
def substitute_vars(command_str: str, localvars: SubVarSet | None = None) -> str:
|
|
685
|
+
"""Expand all ``!!$VAR!!`` tokens in *command_str*, merging *localvars* when provided."""
|
|
950
686
|
# Substitutes global variables, global counters, and local variables.
|
|
951
687
|
if localvars is not None:
|
|
952
688
|
subs = _state.subvars.merge(localvars)
|
|
@@ -979,7 +715,7 @@ def substitute_vars(command_str: str, localvars: SubVarSet | None = None) -> str
|
|
|
979
715
|
|
|
980
716
|
|
|
981
717
|
def runscripts() -> None:
|
|
982
|
-
|
|
718
|
+
"""Drive execution until the command-list stack is empty."""
|
|
983
719
|
# Repeatedly run the next statement from the script at the top of the
|
|
984
720
|
# command list stack until there are no more statements.
|
|
985
721
|
while len(_state.commandliststack) > 0:
|
|
@@ -999,6 +735,7 @@ def runscripts() -> None:
|
|
|
999
735
|
|
|
1000
736
|
|
|
1001
737
|
def current_script_line() -> tuple:
|
|
738
|
+
"""Return ``(source_name, line_number)`` for the command currently executing."""
|
|
1002
739
|
if len(_state.commandliststack) > 0:
|
|
1003
740
|
current_cmds = _state.commandliststack[-1]
|
|
1004
741
|
if current_cmds.current_command() is not None:
|
|
@@ -1009,7 +746,7 @@ def current_script_line() -> tuple:
|
|
|
1009
746
|
return ("", 0)
|
|
1010
747
|
|
|
1011
748
|
|
|
1012
|
-
def _parse_script_lines(lines_iter, source_name: str) -> list:
|
|
749
|
+
def _parse_script_lines(lines_iter: Any, source_name: str) -> list:
|
|
1013
750
|
# Parse an iterable of lines into a list of ScriptCmd objects.
|
|
1014
751
|
from execsql.utils.errors import write_warning
|
|
1015
752
|
|
|
@@ -1161,6 +898,7 @@ def _parse_script_lines(lines_iter, source_name: str) -> list:
|
|
|
1161
898
|
|
|
1162
899
|
|
|
1163
900
|
def read_sqlfile(sql_file_name: str) -> None:
|
|
901
|
+
"""Parse a ``.sql`` file and push the resulting :class:`CommandList` onto the execution stack."""
|
|
1164
902
|
# Read lines from the given script file, create a list of ScriptCmd objects,
|
|
1165
903
|
# and append the list to the top of the stack of script commands.
|
|
1166
904
|
from execsql.utils.errors import file_size_date
|