execsql2 2.13.0__py3-none-any.whl → 2.13.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/lint.py +277 -92
- {execsql2-2.13.0.dist-info → execsql2-2.13.2.dist-info}/METADATA +3 -3
- {execsql2-2.13.0.dist-info → execsql2-2.13.2.dist-info}/RECORD +22 -22
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.13.0.dist-info → execsql2-2.13.2.dist-info}/WHEEL +0 -0
- {execsql2-2.13.0.dist-info → execsql2-2.13.2.dist-info}/entry_points.txt +0 -0
- {execsql2-2.13.0.dist-info → execsql2-2.13.2.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.13.0.dist-info → execsql2-2.13.2.dist-info}/licenses/NOTICE +0 -0
execsql/cli/lint.py
CHANGED
|
@@ -10,11 +10,18 @@ Checks performed
|
|
|
10
10
|
2. **Unmatched LOOP / END LOOP** — mismatched nesting depth (error).
|
|
11
11
|
3. **Unmatched BEGIN BATCH / END BATCH** — mismatched nesting depth (error).
|
|
12
12
|
4. **Potentially undefined variables** — ``!!$VAR!!`` tokens not preceded by a
|
|
13
|
-
``SUB``
|
|
14
|
-
built-in
|
|
15
|
-
|
|
13
|
+
``SUB`` (or ``SUB_EMPTY``, ``SUB_ADD``, ``SUB_APPEND``, ``SUBDATA``)
|
|
14
|
+
metacommand in the same parsed command list and not in the set of built-in
|
|
15
|
+
variables (warning). Note: ``SUB_INI`` and ``SELECT_SUB`` define variables
|
|
16
|
+
whose names are not statically knowable — those may produce false-positive
|
|
17
|
+
warnings.
|
|
18
|
+
5. **EXECUTE SCRIPT flow analysis** — when an ``EXECUTE SCRIPT <name>``
|
|
19
|
+
metacommand is encountered, the linter descends into the named script
|
|
20
|
+
block (if found in ``_state.savedscripts``) and merges any variables it
|
|
21
|
+
defines back into the caller's scope.
|
|
22
|
+
6. **Missing INCLUDE files** — INCLUDE target does not exist on disk relative
|
|
16
23
|
to the script directory (warning).
|
|
17
|
-
|
|
24
|
+
7. **Empty script** — no commands found (warning).
|
|
18
25
|
|
|
19
26
|
The function walks ``CommandList.cmdlist`` and also descends into any
|
|
20
27
|
``CommandList`` objects stored in ``_state.savedscripts`` (i.e. named scripts
|
|
@@ -67,6 +74,31 @@ _RX_END_BATCH = re.compile(r"^\s*END\s+BATCH\s*$", re.I)
|
|
|
67
74
|
# SUB <varname> <value> — defines a substitution variable
|
|
68
75
|
_RX_SUB = re.compile(r"^\s*SUB\s+(?P<name>[+~]?\w+)\s+", re.I)
|
|
69
76
|
|
|
77
|
+
# SUB_EMPTY <varname> — defines a variable with empty string
|
|
78
|
+
_RX_SUB_EMPTY = re.compile(r"^\s*SUB_EMPTY\s+(?P<name>[+~]?\w+)\s*$", re.I)
|
|
79
|
+
|
|
80
|
+
# SUB_ADD <varname> <expr> — increments a variable (implies it exists)
|
|
81
|
+
_RX_SUB_ADD = re.compile(r"^\s*SUB_ADD\s+(?P<name>[+~]?\w+)\s+", re.I)
|
|
82
|
+
|
|
83
|
+
# SUB_APPEND <varname> <text> — appends to a variable (implies it exists)
|
|
84
|
+
_RX_SUB_APPEND = re.compile(r"^\s*SUB_APPEND\s+(?P<name>[+~]?\w+)\s", re.I)
|
|
85
|
+
|
|
86
|
+
# SUBDATA <varname> <datasource> — defines a variable from a query result
|
|
87
|
+
_RX_SUBDATA = re.compile(r"^\s*SUBDATA\s+(?P<name>[+~]?\w+)\s+", re.I)
|
|
88
|
+
|
|
89
|
+
# SUB_INI [FILE] <filename> [SECTION] <section> — bulk-defines variables from INI file
|
|
90
|
+
_RX_SUB_INI = re.compile(
|
|
91
|
+
r'^\s*SUB_INI\s+(?:FILE\s+)?(?:"(?P<qfile>[^"]+)"|(?P<file>\S+))'
|
|
92
|
+
r"(?:\s+SECTION)?\s+(?P<section>\w+)\s*$",
|
|
93
|
+
re.I,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# EXECUTE SCRIPT / EXEC SCRIPT / RUN SCRIPT
|
|
97
|
+
_RX_EXEC_SCRIPT = re.compile(
|
|
98
|
+
r"^\s*(?:EXEC(?:UTE)?|RUN)\s+SCRIPT(?:\s+IF\s+EXISTS)?\s+(?P<script_id>\w+)",
|
|
99
|
+
re.I,
|
|
100
|
+
)
|
|
101
|
+
|
|
70
102
|
# INCLUDE <file>
|
|
71
103
|
_RX_INCLUDE = re.compile(
|
|
72
104
|
r"^\s*INCLUDE(?:\s+IF\s+EXISTS?)?\s+(?P<path>\S+.*?)\s*$",
|
|
@@ -76,67 +108,41 @@ _RX_INCLUDE = re.compile(
|
|
|
76
108
|
# Variable reference — !!name!! where name may start with $, @, &, ~, #, +
|
|
77
109
|
_RX_VAR_REF = re.compile(r"!!([$@&~#+]?\w+)!!", re.I)
|
|
78
110
|
|
|
79
|
-
# Built-in system variables
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"CURRENT_ALIAS",
|
|
115
|
-
"AUTOCOMMIT_STATE",
|
|
116
|
-
"TIMER",
|
|
117
|
-
"DB_USER",
|
|
118
|
-
"DB_SERVER",
|
|
119
|
-
"DB_NAME",
|
|
120
|
-
"DB_NEED_PWD",
|
|
121
|
-
"RANDOM",
|
|
122
|
-
"UUID",
|
|
123
|
-
"VERSION1",
|
|
124
|
-
"VERSION2",
|
|
125
|
-
"VERSION3",
|
|
126
|
-
"CANCEL_HALT_STATE",
|
|
127
|
-
"ERROR_HALT_STATE",
|
|
128
|
-
"METACOMMAND_ERROR_HALT_STATE",
|
|
129
|
-
"CONSOLE_WAIT_WHEN_ERROR_HALT_STATE",
|
|
130
|
-
"CONSOLE_WAIT_WHEN_DONE_STATE",
|
|
131
|
-
"CURRENT_DBMS",
|
|
132
|
-
"CURRENT_DATABASE",
|
|
133
|
-
"SYSTEM_CMD_EXIT_STATUS",
|
|
134
|
-
# Connection-populated
|
|
135
|
-
"DB_FILE",
|
|
136
|
-
"DB_PORT",
|
|
137
|
-
# Counter variables (@@name) are always valid — skip validation
|
|
138
|
-
},
|
|
139
|
-
)
|
|
111
|
+
# Built-in system variables — extracted automatically from the installed
|
|
112
|
+
# ``execsql`` source by scanning for ``add_substitution("$NAME", ...)`` and
|
|
113
|
+
# ``register_lazy("$NAME", ...)`` calls. This avoids maintaining a hand-
|
|
114
|
+
# curated list that drifts out of sync when new system variables are added.
|
|
115
|
+
# Variable names are stored upper-case without the leading ``$``.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _discover_builtin_vars() -> frozenset[str]:
|
|
119
|
+
"""Scan the execsql package source for ``$VARNAME`` system variables."""
|
|
120
|
+
import importlib.util
|
|
121
|
+
|
|
122
|
+
_rx_add_sub = re.compile(r'(?:(?<!\w)add_substitution|(?<!\w)sv)\s*\(\s*["\'](\$\w+)["\']')
|
|
123
|
+
_rx_lazy = re.compile(r'register_lazy\s*\(\s*["\'](\$\w+)["\']')
|
|
124
|
+
|
|
125
|
+
names: set[str] = set()
|
|
126
|
+
|
|
127
|
+
spec = importlib.util.find_spec("execsql")
|
|
128
|
+
if spec is None or spec.submodule_search_locations is None:
|
|
129
|
+
return frozenset(names)
|
|
130
|
+
|
|
131
|
+
pkg_dir = Path(spec.submodule_search_locations[0])
|
|
132
|
+
for src_file in pkg_dir.rglob("*.py"):
|
|
133
|
+
try:
|
|
134
|
+
text = src_file.read_text(encoding="utf-8")
|
|
135
|
+
except OSError:
|
|
136
|
+
continue
|
|
137
|
+
for m in _rx_add_sub.finditer(text):
|
|
138
|
+
names.add(m.group(1).lstrip("$").upper())
|
|
139
|
+
for m in _rx_lazy.finditer(text):
|
|
140
|
+
names.add(m.group(1).lstrip("$").upper())
|
|
141
|
+
|
|
142
|
+
return frozenset(names)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
_BUILTIN_VARS: frozenset[str] = _discover_builtin_vars()
|
|
140
146
|
|
|
141
147
|
|
|
142
148
|
# ---------------------------------------------------------------------------
|
|
@@ -159,23 +165,90 @@ def _warning(source: str, line_no: int, message: str) -> _Issue:
|
|
|
159
165
|
# ---------------------------------------------------------------------------
|
|
160
166
|
|
|
161
167
|
|
|
168
|
+
def _collect_defined_vars(
|
|
169
|
+
cmdlist: CommandList,
|
|
170
|
+
script_dir: Path | None,
|
|
171
|
+
defined_vars: set[str],
|
|
172
|
+
*,
|
|
173
|
+
_savedscripts: dict | None = None,
|
|
174
|
+
_visited_scripts: set[str] | None = None,
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Pass 1: walk *cmdlist* and collect all variable definitions into *defined_vars*.
|
|
177
|
+
|
|
178
|
+
This populates the set with every variable name that could be defined at
|
|
179
|
+
runtime — ``SUB``, ``SUB_EMPTY``, ``SUB_ADD``, ``SUB_APPEND``,
|
|
180
|
+
``SUBDATA``, and ``SUB_INI`` (by reading the INI file on disk). It also
|
|
181
|
+
descends into ``EXECUTE SCRIPT`` targets to collect their definitions.
|
|
182
|
+
|
|
183
|
+
No issues are reported; structural checks and variable-reference validation
|
|
184
|
+
happen in pass 2 (:func:`_lint_cmdlist`).
|
|
185
|
+
"""
|
|
186
|
+
visited = _visited_scripts if _visited_scripts is not None else set()
|
|
187
|
+
|
|
188
|
+
for cmd in cmdlist.cmdlist:
|
|
189
|
+
if cmd.command_type == "sql":
|
|
190
|
+
continue
|
|
191
|
+
stmt = cmd.command.statement
|
|
192
|
+
|
|
193
|
+
# SUB <name> <value>
|
|
194
|
+
sub_m = _RX_SUB.match(stmt)
|
|
195
|
+
if sub_m:
|
|
196
|
+
defined_vars.add(sub_m.group("name").lstrip("+~").upper())
|
|
197
|
+
|
|
198
|
+
# SUB_EMPTY / SUB_ADD / SUB_APPEND / SUBDATA
|
|
199
|
+
for rx in (_RX_SUB_EMPTY, _RX_SUB_ADD, _RX_SUB_APPEND, _RX_SUBDATA):
|
|
200
|
+
m = rx.match(stmt)
|
|
201
|
+
if m:
|
|
202
|
+
defined_vars.add(m.group("name").lstrip("+~").upper())
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
# SUB_INI — read INI file keys
|
|
206
|
+
ini_m = _RX_SUB_INI.match(stmt)
|
|
207
|
+
if ini_m:
|
|
208
|
+
ini_file = ini_m.group("qfile") or ini_m.group("file")
|
|
209
|
+
ini_section = ini_m.group("section")
|
|
210
|
+
if ini_file and not _RX_VAR_REF.search(ini_file):
|
|
211
|
+
_read_ini_vars(ini_file, ini_section, script_dir, defined_vars)
|
|
212
|
+
|
|
213
|
+
# EXECUTE SCRIPT — descend into named script block
|
|
214
|
+
exec_m = _RX_EXEC_SCRIPT.match(stmt)
|
|
215
|
+
if exec_m and _savedscripts is not None:
|
|
216
|
+
script_id = exec_m.group("script_id").lower()
|
|
217
|
+
if script_id in _savedscripts and script_id not in visited:
|
|
218
|
+
visited.add(script_id)
|
|
219
|
+
_collect_defined_vars(
|
|
220
|
+
_savedscripts[script_id],
|
|
221
|
+
script_dir,
|
|
222
|
+
defined_vars,
|
|
223
|
+
_savedscripts=_savedscripts,
|
|
224
|
+
_visited_scripts=visited,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
162
228
|
def _lint_cmdlist(
|
|
163
229
|
cmdlist: CommandList,
|
|
164
230
|
script_dir: Path | None,
|
|
165
231
|
defined_vars: set[str],
|
|
232
|
+
*,
|
|
233
|
+
_savedscripts: dict | None = None,
|
|
234
|
+
_visited_scripts: set[str] | None = None,
|
|
166
235
|
) -> list[_Issue]:
|
|
167
|
-
"""
|
|
236
|
+
"""Pass 2: lint a :class:`CommandList` for structural and variable issues.
|
|
168
237
|
|
|
169
238
|
Args:
|
|
170
239
|
cmdlist: The parsed command list to analyse.
|
|
171
240
|
script_dir: Directory of the top-level script file, used for resolving
|
|
172
241
|
relative INCLUDE paths. ``None`` for inline (``-c``) scripts.
|
|
173
|
-
defined_vars:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
242
|
+
defined_vars: Set of variable names (without sigil) that have been
|
|
243
|
+
pre-collected by :func:`_collect_defined_vars`. This includes
|
|
244
|
+
*all* top-level and script-block definitions so that ordering
|
|
245
|
+
does not matter.
|
|
246
|
+
_savedscripts: Dictionary of named script blocks (from
|
|
247
|
+
``_state.savedscripts``). Passed explicitly so the function can
|
|
248
|
+
descend into EXECUTE SCRIPT targets.
|
|
249
|
+
_visited_scripts: Set of script IDs already descended into, shared
|
|
250
|
+
across recursive calls to prevent infinite recursion from circular
|
|
251
|
+
EXECUTE SCRIPT references.
|
|
179
252
|
|
|
180
253
|
Returns:
|
|
181
254
|
List of ``(severity, source, line_no, message)`` issue tuples.
|
|
@@ -191,6 +264,10 @@ def _lint_cmdlist(
|
|
|
191
264
|
batch_depth = 0
|
|
192
265
|
batch_open_locs: list[tuple[str, int]] = []
|
|
193
266
|
|
|
267
|
+
# Track which EXECUTE SCRIPT targets we've already descended into to
|
|
268
|
+
# prevent infinite recursion from circular script references.
|
|
269
|
+
visited_scripts: set[str] = _visited_scripts if _visited_scripts is not None else set()
|
|
270
|
+
|
|
194
271
|
for cmd in cmdlist.cmdlist:
|
|
195
272
|
src = cmd.source
|
|
196
273
|
lno = cmd.line_no
|
|
@@ -202,7 +279,7 @@ def _lint_cmdlist(
|
|
|
202
279
|
_check_var_ref(m.group(1), src, lno, defined_vars, issues)
|
|
203
280
|
continue
|
|
204
281
|
|
|
205
|
-
# Metacommand checks
|
|
282
|
+
# Metacommand checks — variable references
|
|
206
283
|
for m in _RX_VAR_REF.finditer(stmt):
|
|
207
284
|
_check_var_ref(m.group(1), src, lno, defined_vars, issues)
|
|
208
285
|
|
|
@@ -247,11 +324,27 @@ def _lint_cmdlist(
|
|
|
247
324
|
batch_depth -= 1
|
|
248
325
|
batch_open_locs.pop()
|
|
249
326
|
|
|
250
|
-
# --
|
|
251
|
-
|
|
252
|
-
if
|
|
253
|
-
|
|
254
|
-
|
|
327
|
+
# -- EXECUTE SCRIPT — descend into named script block --
|
|
328
|
+
exec_m = _RX_EXEC_SCRIPT.match(stmt)
|
|
329
|
+
if exec_m and _savedscripts is not None:
|
|
330
|
+
script_id = exec_m.group("script_id").lower()
|
|
331
|
+
if script_id not in _savedscripts:
|
|
332
|
+
# Warn unless it's EXECUTE SCRIPT IF EXISTS
|
|
333
|
+
if not re.search(r"\bIF\s+EXISTS\b", stmt, re.I):
|
|
334
|
+
issues.append(
|
|
335
|
+
_warning(src, lno, f"EXECUTE SCRIPT target not found: '{script_id}'"),
|
|
336
|
+
)
|
|
337
|
+
elif script_id not in visited_scripts:
|
|
338
|
+
visited_scripts.add(script_id)
|
|
339
|
+
sub_issues = _lint_cmdlist(
|
|
340
|
+
_savedscripts[script_id],
|
|
341
|
+
script_dir,
|
|
342
|
+
defined_vars,
|
|
343
|
+
_savedscripts=_savedscripts,
|
|
344
|
+
_visited_scripts=visited_scripts,
|
|
345
|
+
)
|
|
346
|
+
for sev, ssrc, slno, msg in sub_issues:
|
|
347
|
+
issues.append((sev, ssrc, slno, f"[script '{script_id}'] {msg}"))
|
|
255
348
|
|
|
256
349
|
# -- INCLUDE file existence --
|
|
257
350
|
inc_m = _RX_INCLUDE.match(stmt)
|
|
@@ -307,6 +400,10 @@ def _check_var_ref(
|
|
|
307
400
|
if re.match(r"^ARG_\d+$", name, re.I):
|
|
308
401
|
return
|
|
309
402
|
|
|
403
|
+
# $COUNTER_N is managed by CounterVars (@@counter metacommands)
|
|
404
|
+
if re.match(r"^COUNTER_\d+$", name, re.I):
|
|
405
|
+
return
|
|
406
|
+
|
|
310
407
|
# Built-in system variables
|
|
311
408
|
if name.upper() in _BUILTIN_VARS:
|
|
312
409
|
return
|
|
@@ -325,6 +422,35 @@ def _check_var_ref(
|
|
|
325
422
|
)
|
|
326
423
|
|
|
327
424
|
|
|
425
|
+
def _read_ini_vars(
|
|
426
|
+
ini_file: str,
|
|
427
|
+
section: str,
|
|
428
|
+
script_dir: Path | None,
|
|
429
|
+
defined_vars: set[str],
|
|
430
|
+
) -> None:
|
|
431
|
+
"""Read an INI file and register its section keys as defined variables.
|
|
432
|
+
|
|
433
|
+
Mirrors what ``SUB_INI`` does at runtime: reads a
|
|
434
|
+
:class:`~configparser.ConfigParser` section and defines each key as a
|
|
435
|
+
substitution variable. If the file does not exist or the section is
|
|
436
|
+
missing, silently does nothing (the runtime handler behaves the same way).
|
|
437
|
+
"""
|
|
438
|
+
from configparser import ConfigParser
|
|
439
|
+
|
|
440
|
+
p = Path(ini_file)
|
|
441
|
+
if not p.is_absolute() and script_dir is not None:
|
|
442
|
+
p = script_dir / p
|
|
443
|
+
|
|
444
|
+
if not p.exists():
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
cp = ConfigParser()
|
|
448
|
+
cp.read(p)
|
|
449
|
+
if cp.has_section(section):
|
|
450
|
+
for key, _value in cp.items(section):
|
|
451
|
+
defined_vars.add(key.upper())
|
|
452
|
+
|
|
453
|
+
|
|
328
454
|
def _check_include_path(
|
|
329
455
|
raw_path: str,
|
|
330
456
|
script_dir: Path | None,
|
|
@@ -393,19 +519,68 @@ def _lint_script(
|
|
|
393
519
|
return issues
|
|
394
520
|
|
|
395
521
|
script_dir = Path(script_path).resolve().parent if script_path else None
|
|
522
|
+
savedscripts: dict = getattr(_state, "savedscripts", {})
|
|
523
|
+
|
|
524
|
+
# ------------------------------------------------------------------
|
|
525
|
+
# Pass 1: collect all variable definitions from the top-level script
|
|
526
|
+
# and all reachable script blocks. This ensures definition order does
|
|
527
|
+
# not matter — a script block executed early can reference variables
|
|
528
|
+
# defined later in the top-level script.
|
|
529
|
+
# ------------------------------------------------------------------
|
|
530
|
+
all_defined: set[str] = set()
|
|
531
|
+
collect_visited: set[str] = set()
|
|
532
|
+
_collect_defined_vars(
|
|
533
|
+
cmdlist,
|
|
534
|
+
script_dir,
|
|
535
|
+
all_defined,
|
|
536
|
+
_savedscripts=savedscripts,
|
|
537
|
+
_visited_scripts=collect_visited,
|
|
538
|
+
)
|
|
539
|
+
# Also collect from every saved script block (they may define vars
|
|
540
|
+
# referenced by other blocks). Share the visited set so each block
|
|
541
|
+
# is only traversed once (O(N) instead of O(N²)).
|
|
542
|
+
for saved_cl in savedscripts.values():
|
|
543
|
+
_collect_defined_vars(
|
|
544
|
+
saved_cl,
|
|
545
|
+
script_dir,
|
|
546
|
+
all_defined,
|
|
547
|
+
_savedscripts=savedscripts,
|
|
548
|
+
_visited_scripts=collect_visited,
|
|
549
|
+
)
|
|
396
550
|
|
|
397
|
-
#
|
|
398
|
-
#
|
|
399
|
-
#
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
551
|
+
# ------------------------------------------------------------------
|
|
552
|
+
# Pass 2: lint for structural issues and undefined-variable warnings
|
|
553
|
+
# using the complete variable set from pass 1.
|
|
554
|
+
# ------------------------------------------------------------------
|
|
555
|
+
# Shared visited-scripts tracker — prevents duplicate lint warnings
|
|
556
|
+
# when the same script block is reached via multiple paths.
|
|
557
|
+
visited: set[str] = set()
|
|
558
|
+
|
|
559
|
+
issues.extend(
|
|
560
|
+
_lint_cmdlist(
|
|
561
|
+
cmdlist,
|
|
562
|
+
script_dir,
|
|
563
|
+
all_defined,
|
|
564
|
+
_savedscripts=savedscripts,
|
|
565
|
+
_visited_scripts=visited,
|
|
566
|
+
),
|
|
567
|
+
)
|
|
403
568
|
|
|
404
|
-
# Analyse each named SCRIPT block
|
|
405
|
-
|
|
406
|
-
|
|
569
|
+
# Analyse each named SCRIPT block that was NOT already visited via
|
|
570
|
+
# EXECUTE SCRIPT (standalone analysis catches structural issues like
|
|
571
|
+
# unmatched IF/ENDIF in script blocks that are never executed).
|
|
572
|
+
for script_name, saved_cl in savedscripts.items():
|
|
573
|
+
if script_name in visited:
|
|
574
|
+
continue
|
|
575
|
+
visited.add(script_name)
|
|
576
|
+
saved_issues = _lint_cmdlist(
|
|
577
|
+
saved_cl,
|
|
578
|
+
script_dir,
|
|
579
|
+
set(all_defined),
|
|
580
|
+
_savedscripts=savedscripts,
|
|
581
|
+
_visited_scripts=visited,
|
|
582
|
+
)
|
|
407
583
|
for sev, src, lno, msg in saved_issues:
|
|
408
|
-
# Annotate with the script name if the source is the same file
|
|
409
584
|
issues.append((sev, src, lno, f"[script '{script_name}'] {msg}"))
|
|
410
585
|
|
|
411
586
|
return issues
|
|
@@ -440,12 +615,22 @@ def _print_lint_results(issues: list[_Issue], script_label: str) -> int:
|
|
|
440
615
|
_console.print()
|
|
441
616
|
return 0
|
|
442
617
|
|
|
443
|
-
|
|
444
|
-
|
|
618
|
+
# Sort: errors first, then warnings; within each group sort by line number.
|
|
619
|
+
_sev_order = {"error": 0, "warning": 1}
|
|
620
|
+
sorted_issues = sorted(issues, key=lambda i: (_sev_order.get(i[0], 9), i[2]))
|
|
621
|
+
|
|
622
|
+
# Compute the widest location string so columns align.
|
|
623
|
+
locs: list[str] = []
|
|
624
|
+
for _, source, line_no, _ in sorted_issues:
|
|
625
|
+
locs.append(f"{source}:{line_no}" if line_no else source)
|
|
626
|
+
loc_width = max(len(loc) for loc in locs) if locs else 0
|
|
627
|
+
|
|
628
|
+
for (severity, _source, _line_no, message), loc in zip(sorted_issues, locs):
|
|
629
|
+
pad = " " * (loc_width - len(loc))
|
|
445
630
|
if severity == "error":
|
|
446
|
-
_console.print(f" [bold red]ERROR [/bold red] [dim]{loc}[/dim] {message}")
|
|
631
|
+
_console.print(f" [bold red]ERROR [/bold red] [dim]{loc}[/dim]{pad} {message}")
|
|
447
632
|
else:
|
|
448
|
-
_console.print(f" [bold yellow]WARNING[/bold yellow] [dim]{loc}[/dim] {message}")
|
|
633
|
+
_console.print(f" [bold yellow]WARNING[/bold yellow] [dim]{loc}[/dim]{pad} {message}")
|
|
449
634
|
|
|
450
635
|
_console.print()
|
|
451
636
|
parts = []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.13.
|
|
3
|
+
Version: 2.13.2
|
|
4
4
|
Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
|
|
5
5
|
Project-URL: Homepage, https://execsql2.readthedocs.io
|
|
6
6
|
Project-URL: Repository, https://github.com/geocoug/execsql
|
|
@@ -51,7 +51,7 @@ Requires-Dist: keyring; extra == 'all'
|
|
|
51
51
|
Requires-Dist: odfpy; extra == 'all'
|
|
52
52
|
Requires-Dist: openpyxl; extra == 'all'
|
|
53
53
|
Requires-Dist: oracledb; extra == 'all'
|
|
54
|
-
Requires-Dist: pg-upsert>=1.
|
|
54
|
+
Requires-Dist: pg-upsert>=1.20.0; extra == 'all'
|
|
55
55
|
Requires-Dist: polars; extra == 'all'
|
|
56
56
|
Requires-Dist: psycopg2-binary; extra == 'all'
|
|
57
57
|
Requires-Dist: pymysql; extra == 'all'
|
|
@@ -109,7 +109,7 @@ Requires-Dist: oracledb; extra == 'oracle'
|
|
|
109
109
|
Provides-Extra: postgres
|
|
110
110
|
Requires-Dist: psycopg2-binary; extra == 'postgres'
|
|
111
111
|
Provides-Extra: upsert
|
|
112
|
-
Requires-Dist: pg-upsert>=1.
|
|
112
|
+
Requires-Dist: pg-upsert>=1.20.0; extra == 'upsert'
|
|
113
113
|
Description-Content-Type: text/markdown
|
|
114
114
|
|
|
115
115
|
> [!NOTE]
|
|
@@ -12,7 +12,7 @@ execsql/types.py,sha256=HVWb4umIB9lpxCGgqk3xy1hoGYPfN39xci5mHF0Izq4,31882
|
|
|
12
12
|
execsql/cli/__init__.py,sha256=YXxOVF2lNkCkifXyjoC7yWrhHJFT9PzI7cnCzsLJwT8,16488
|
|
13
13
|
execsql/cli/dsn.py,sha256=svaZtrUXFRL2W5G6FRRiKtR6kehOp7urrVhIx_642Z8,2820
|
|
14
14
|
execsql/cli/help.py,sha256=Sn_TgSJiQeBx-xZH0fuP5OvR_wasSTumjWF9UHfIX5k,5414
|
|
15
|
-
execsql/cli/lint.py,sha256=
|
|
15
|
+
execsql/cli/lint.py,sha256=mVZu3Knp13-yQZkofiaeDmxLTWP7DkbaZoDeHPsfAEQ,24094
|
|
16
16
|
execsql/cli/run.py,sha256=JGfndnBnJMkEqbz26pflhEdXDScZNIdGu6b6jTRLYl8,30681
|
|
17
17
|
execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
|
|
18
18
|
execsql/db/access.py,sha256=L79gUnAnnM9EJ_f4k42jr7DI0qGcKtLOnJTlBC7uPm0,17879
|
|
@@ -94,24 +94,24 @@ execsql/utils/numeric.py,sha256=xh02ANSRk3nUpQ-rtm66ILoMqoi7HtzCoRMIOT9U8QI,1570
|
|
|
94
94
|
execsql/utils/regex.py,sha256=diEzTZqU_HHwVMadPAvN1Vgzhl7I03eVaEFGCXyGGL8,3770
|
|
95
95
|
execsql/utils/strings.py,sha256=5Dvzrk-9SIw2lpxXZQkiJbNyo1sy7iXXAtSULlZ0KG8,8488
|
|
96
96
|
execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
|
|
97
|
-
execsql2-2.13.
|
|
98
|
-
execsql2-2.13.
|
|
99
|
-
execsql2-2.13.
|
|
100
|
-
execsql2-2.13.
|
|
101
|
-
execsql2-2.13.
|
|
102
|
-
execsql2-2.13.
|
|
103
|
-
execsql2-2.13.
|
|
104
|
-
execsql2-2.13.
|
|
105
|
-
execsql2-2.13.
|
|
106
|
-
execsql2-2.13.
|
|
107
|
-
execsql2-2.13.
|
|
108
|
-
execsql2-2.13.
|
|
109
|
-
execsql2-2.13.
|
|
110
|
-
execsql2-2.13.
|
|
111
|
-
execsql2-2.13.
|
|
112
|
-
execsql2-2.13.
|
|
113
|
-
execsql2-2.13.
|
|
114
|
-
execsql2-2.13.
|
|
115
|
-
execsql2-2.13.
|
|
116
|
-
execsql2-2.13.
|
|
117
|
-
execsql2-2.13.
|
|
97
|
+
execsql2-2.13.2.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
|
|
98
|
+
execsql2-2.13.2.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
|
|
99
|
+
execsql2-2.13.2.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
|
|
100
|
+
execsql2-2.13.2.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
|
|
101
|
+
execsql2-2.13.2.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
|
|
102
|
+
execsql2-2.13.2.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
|
|
103
|
+
execsql2-2.13.2.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
|
|
104
|
+
execsql2-2.13.2.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
|
|
105
|
+
execsql2-2.13.2.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
|
|
106
|
+
execsql2-2.13.2.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
|
|
107
|
+
execsql2-2.13.2.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
|
|
108
|
+
execsql2-2.13.2.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
|
|
109
|
+
execsql2-2.13.2.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
|
|
110
|
+
execsql2-2.13.2.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
|
|
111
|
+
execsql2-2.13.2.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
|
|
112
|
+
execsql2-2.13.2.dist-info/METADATA,sha256=R_jJi0OgB5NoPJ6g8mLse9YttgHU2EIZxlCZADPe9d0,17566
|
|
113
|
+
execsql2-2.13.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
114
|
+
execsql2-2.13.2.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
|
|
115
|
+
execsql2-2.13.2.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
|
|
116
|
+
execsql2-2.13.2.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
|
|
117
|
+
execsql2-2.13.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{execsql2-2.13.0.data → execsql2-2.13.2.data}/data/execsql2_extras/example_config_prompt.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|