execsql2 2.16.8__py3-none-any.whl → 2.16.13__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/debug/repl.py +59 -0
- execsql/format.py +290 -15
- execsql/metacommands/debug.py +89 -1
- execsql/metacommands/dispatch.py +18 -0
- execsql/script/ast.py +43 -6
- execsql/script/executor.py +28 -13
- execsql/script/parser.py +135 -6
- execsql/utils/fileio.py +9 -1
- {execsql2-2.16.8.dist-info → execsql2-2.16.13.dist-info}/METADATA +1 -1
- {execsql2-2.16.8.dist-info → execsql2-2.16.13.dist-info}/RECORD +29 -29
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.16.8.data → execsql2-2.16.13.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.16.8.dist-info → execsql2-2.16.13.dist-info}/WHEEL +0 -0
- {execsql2-2.16.8.dist-info → execsql2-2.16.13.dist-info}/entry_points.txt +0 -0
- {execsql2-2.16.8.dist-info → execsql2-2.16.13.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.16.8.dist-info → execsql2-2.16.13.dist-info}/licenses/NOTICE +0 -0
execsql/debug/repl.py
CHANGED
|
@@ -111,6 +111,8 @@ _HELP_COMMANDS = [
|
|
|
111
111
|
(".where", ".w", "Show the current script location and upcoming statement"),
|
|
112
112
|
(".stack", "", "Show the command-list stack (script name, line, depth)"),
|
|
113
113
|
(".set VAR VAL", ".s", "Set or update a substitution variable"),
|
|
114
|
+
(".scripts", "", "List all registered SCRIPT definitions"),
|
|
115
|
+
(".scripts NAME", "", "Show detail for a specific SCRIPT"),
|
|
114
116
|
(".help", ".h", "Show this help text"),
|
|
115
117
|
]
|
|
116
118
|
|
|
@@ -290,6 +292,12 @@ def _handle_dot_command(line: str) -> None:
|
|
|
290
292
|
varname = parts[0]
|
|
291
293
|
value = parts[1] if len(parts) > 1 else ""
|
|
292
294
|
_set_var(varname, value)
|
|
295
|
+
elif cmd.startswith("scripts"):
|
|
296
|
+
rest = cmd[7:].strip()
|
|
297
|
+
if rest:
|
|
298
|
+
_print_script_detail(rest)
|
|
299
|
+
else:
|
|
300
|
+
_print_scripts()
|
|
293
301
|
else:
|
|
294
302
|
_write(f" {_c(_RED, 'Unknown command:')} {line!r}. Type '.help' for available commands.\n")
|
|
295
303
|
|
|
@@ -507,3 +515,54 @@ def _set_var(varname: str, value: str) -> None:
|
|
|
507
515
|
else:
|
|
508
516
|
subvars.add_substitution(varname, value)
|
|
509
517
|
_write(f" {_c(_CYAN, varname)} {_c(_DIM, '=')} {value}\n")
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _print_scripts() -> None:
|
|
521
|
+
"""Print all registered SCRIPT definitions."""
|
|
522
|
+
from execsql.metacommands.debug import _format_script_signature, _format_script_source
|
|
523
|
+
|
|
524
|
+
scripts = _state.ast_scripts
|
|
525
|
+
if not scripts:
|
|
526
|
+
_write(" (no scripts registered)\n\n")
|
|
527
|
+
return
|
|
528
|
+
_write_rule(f" {_c(_BOLD + _YELLOW, 'Scripts')} {_c(_DIM, f'({len(scripts)})')} ")
|
|
529
|
+
sigs = {name: _format_script_signature(name, block.param_defs) for name, block in scripts.items()}
|
|
530
|
+
max_sig = max(len(s) for s in sigs.values())
|
|
531
|
+
for name, block in scripts.items():
|
|
532
|
+
sig = sigs[name]
|
|
533
|
+
src = _format_script_source(block.span)
|
|
534
|
+
_write(f" {_c(_CYAN, sig):<{max_sig + len(_CYAN) + len(_RESET)}} {_c(_DIM, src)}\n")
|
|
535
|
+
_write("\n")
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _print_script_detail(name: str) -> None:
|
|
539
|
+
"""Print detail for a single SCRIPT definition."""
|
|
540
|
+
from execsql.metacommands.debug import _format_script_signature, _format_script_source
|
|
541
|
+
|
|
542
|
+
script_name = name.lower()
|
|
543
|
+
scripts = _state.ast_scripts
|
|
544
|
+
if script_name not in scripts:
|
|
545
|
+
_write(f" {_c(_RED, 'No script named')} {_c(_CYAN, repr(script_name))} {_c(_RED, 'is registered.')}\n")
|
|
546
|
+
return
|
|
547
|
+
block = scripts[script_name]
|
|
548
|
+
sig = _format_script_signature(block.name, block.param_defs)
|
|
549
|
+
src = _format_script_source(block.span)
|
|
550
|
+
_write_rule(f" {_c(_BOLD + _YELLOW, 'Script')} {_c(_DIM, '──')} {_c(_CYAN, sig)} ")
|
|
551
|
+
_write(f" {_c(_BOLD, 'Source:')} {src}\n")
|
|
552
|
+
if block.param_defs:
|
|
553
|
+
_write(f" {_c(_BOLD, 'Parameters:')}\n")
|
|
554
|
+
max_name = max(len(p.name) for p in block.param_defs)
|
|
555
|
+
for p in block.param_defs:
|
|
556
|
+
if p.default is not None:
|
|
557
|
+
_write(
|
|
558
|
+
f" {_c(_CYAN, p.name):<{max_name + len(_CYAN) + len(_RESET)}} {_c(_DIM, f'(optional, default: {p.default})')}\n",
|
|
559
|
+
)
|
|
560
|
+
else:
|
|
561
|
+
_write(f" {_c(_CYAN, p.name):<{max_name + len(_CYAN) + len(_RESET)}} {_c(_DIM, '(required)')}\n")
|
|
562
|
+
else:
|
|
563
|
+
_write(f" {_c(_BOLD, 'Parameters:')} (none)\n")
|
|
564
|
+
if block.doc:
|
|
565
|
+
_write("\n")
|
|
566
|
+
for doc_line in block.doc.split("\n"):
|
|
567
|
+
_write(f" {_c(_DIM, doc_line)}\n")
|
|
568
|
+
_write("\n")
|
execsql/format.py
CHANGED
|
@@ -73,6 +73,8 @@ MULTIWORD_KEYWORDS = [
|
|
|
73
73
|
"PROMPT ASK",
|
|
74
74
|
"WITH TEMPLATE",
|
|
75
75
|
"IN ZIPFILE",
|
|
76
|
+
"SHOW SCRIPTS",
|
|
77
|
+
"SHOW SCRIPT",
|
|
76
78
|
]
|
|
77
79
|
|
|
78
80
|
# Depth-tracking sets
|
|
@@ -146,22 +148,66 @@ def _is_comment_line(line: str, in_block: bool) -> tuple[bool, bool]:
|
|
|
146
148
|
return False, False
|
|
147
149
|
|
|
148
150
|
|
|
149
|
-
def _sqlglot_format(
|
|
151
|
+
def _sqlglot_format(
|
|
152
|
+
sql_lines: list[str],
|
|
153
|
+
sql_indent: int = 4,
|
|
154
|
+
leading_comma: bool = False,
|
|
155
|
+
) -> list[str]:
|
|
150
156
|
"""Format a list of SQL-only lines (no comment-only lines) via sqlglot."""
|
|
151
157
|
text = "\n".join(sql_lines)
|
|
152
158
|
protected, replacements = _protect_variables(text)
|
|
159
|
+
|
|
160
|
+
# Count semicolons in input as a rough statement count.
|
|
161
|
+
input_semis = protected.count(";")
|
|
162
|
+
|
|
153
163
|
try:
|
|
154
164
|
with contextlib.redirect_stderr(io.StringIO()):
|
|
155
165
|
ast = sqlglot.parse(protected, read="postgres", error_level=sqlglot.errors.ErrorLevel.IGNORE)
|
|
156
|
-
statements = [
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
statements: list[str] = []
|
|
167
|
+
for node in ast:
|
|
168
|
+
if node is None:
|
|
169
|
+
continue
|
|
170
|
+
# For Command nodes (psql backslash commands, ERROR:, etc.)
|
|
171
|
+
# use unpretty output to avoid mangling.
|
|
172
|
+
if type(node).__name__ == "Command":
|
|
173
|
+
statements.append(node.sql(dialect="postgres"))
|
|
174
|
+
else:
|
|
175
|
+
statements.append(
|
|
176
|
+
node.sql(
|
|
177
|
+
dialect="postgres",
|
|
178
|
+
pretty=True,
|
|
179
|
+
pad=sql_indent,
|
|
180
|
+
indent=sql_indent,
|
|
181
|
+
max_text_width=120,
|
|
182
|
+
leading_comma=leading_comma,
|
|
183
|
+
),
|
|
184
|
+
)
|
|
161
185
|
stmts = [s for s in statements if s]
|
|
162
186
|
if not stmts:
|
|
163
187
|
return sql_lines
|
|
188
|
+
|
|
189
|
+
# Safety: if sqlglot produced more statements than the input had
|
|
190
|
+
# semicolons, it likely split a fragment (e.g. a SELECT column list)
|
|
191
|
+
# into multiple pseudo-statements. Fall back to the original text.
|
|
192
|
+
if len(stmts) > max(input_semis, 1):
|
|
193
|
+
return sql_lines
|
|
194
|
+
|
|
164
195
|
joined = ";\n".join(stmts) + ";"
|
|
196
|
+
|
|
197
|
+
# Content-loss check: sqlglot with IGNORE error level can silently
|
|
198
|
+
# drop tokens it doesn't understand (e.g. ``ERROR: ...``). If the
|
|
199
|
+
# formatted output lost a significant fraction of the alphanumeric
|
|
200
|
+
# content, the formatting is unreliable — fall back.
|
|
201
|
+
# Exclude comment markers from the comparison — they are injected by
|
|
202
|
+
# _format_preserving_comments and are expected to be dropped by sqlglot
|
|
203
|
+
# for certain AST positions (e.g. inside CASE WHEN).
|
|
204
|
+
_alnum = re.compile(r"[^a-zA-Z0-9]")
|
|
205
|
+
_marker_alnum = re.compile(rf"{re.escape(_CMT_MARKER)}\d+")
|
|
206
|
+
input_for_check = _marker_alnum.sub("", protected)
|
|
207
|
+
input_alnum_len = len(_alnum.sub("", input_for_check))
|
|
208
|
+
output_alnum_len = len(_alnum.sub("", joined))
|
|
209
|
+
if input_alnum_len and output_alnum_len < input_alnum_len * 0.7:
|
|
210
|
+
return sql_lines
|
|
165
211
|
joined = re.sub(r"\bINTO TEMPORARY\b(?!\s+TABLE)", "INTO TEMPORARY TABLE", joined)
|
|
166
212
|
return _restore_variables(joined, replacements).split("\n")
|
|
167
213
|
except Exception:
|
|
@@ -173,7 +219,194 @@ def _sqlglot_format(sql_lines: list[str]) -> list[str]:
|
|
|
173
219
|
# ---------------------------------------------------------------------------
|
|
174
220
|
|
|
175
221
|
|
|
176
|
-
def
|
|
222
|
+
def _has_mid_statement_comments(lines: list[str]) -> bool:
|
|
223
|
+
"""Return True if any comment-only line appears inside a SQL statement.
|
|
224
|
+
|
|
225
|
+
A comment is "mid-statement" if it occurs after a SQL line that does not
|
|
226
|
+
end with ``;`` (i.e. the statement is still open). This is a lightweight
|
|
227
|
+
heuristic — it can be fooled by ``;`` inside string literals, but in that
|
|
228
|
+
case the block simply gets the benefit of sqlglot formatting rather than
|
|
229
|
+
being skipped (which is harmless because the SQL isn't fragmented).
|
|
230
|
+
"""
|
|
231
|
+
in_block = False
|
|
232
|
+
in_statement = False
|
|
233
|
+
for line in lines:
|
|
234
|
+
is_comment, in_block = _is_comment_line(line, in_block)
|
|
235
|
+
stripped = line.strip()
|
|
236
|
+
if not stripped:
|
|
237
|
+
continue
|
|
238
|
+
if is_comment:
|
|
239
|
+
if in_statement:
|
|
240
|
+
return True
|
|
241
|
+
else:
|
|
242
|
+
in_statement = True
|
|
243
|
+
if stripped.endswith(";"):
|
|
244
|
+
in_statement = False
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
_CMT_MARKER = "EXECSQL_CMTMARKER_"
|
|
249
|
+
_CMT_MARKER_RE = re.compile(rf"/\*\s*({re.escape(_CMT_MARKER)}\d+)\s*\*/")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _format_preserving_comments(
|
|
253
|
+
lines: list[str],
|
|
254
|
+
sql_indent: int = 4,
|
|
255
|
+
leading_comma: bool = False,
|
|
256
|
+
) -> list[str]:
|
|
257
|
+
"""Format SQL with interleaved comments via marker-based round-tripping.
|
|
258
|
+
|
|
259
|
+
Strategy
|
|
260
|
+
--------
|
|
261
|
+
1. Replace each comment-only line with a unique inline ``/* marker */``
|
|
262
|
+
prepended to the *next* SQL line. This lets sqlglot see the full
|
|
263
|
+
statement without fragmentation while preserving comment anchors.
|
|
264
|
+
2. Format the marker-annotated SQL through ``_sqlglot_format``.
|
|
265
|
+
3. Walk the formatted output: wherever a marker appears, emit the
|
|
266
|
+
original comment on its own line **before** that SQL line, then
|
|
267
|
+
strip the marker from the SQL.
|
|
268
|
+
4. Any marker that sqlglot dropped (e.g. inside a CASE expression)
|
|
269
|
+
is re-inserted by matching key tokens from its anchor SQL line
|
|
270
|
+
against the formatted output.
|
|
271
|
+
"""
|
|
272
|
+
# ---- Step 1: extract comments, replace with inline markers ----------
|
|
273
|
+
comment_store: dict[str, str] = {} # marker → original comment line
|
|
274
|
+
# Track the SQL line that originally followed each comment, for fallback
|
|
275
|
+
anchor_sql: dict[str, str] = {} # marker → next SQL line (stripped)
|
|
276
|
+
pending_markers: list[str] = []
|
|
277
|
+
processed: list[str] = []
|
|
278
|
+
in_block = False
|
|
279
|
+
|
|
280
|
+
for line in lines:
|
|
281
|
+
is_comment, in_block = _is_comment_line(line, in_block)
|
|
282
|
+
stripped = line.strip()
|
|
283
|
+
if not stripped:
|
|
284
|
+
# Blank lines: if we have pending markers, attach blanks as
|
|
285
|
+
# comment entries so they reappear in the right place.
|
|
286
|
+
if pending_markers:
|
|
287
|
+
mid = f"{_CMT_MARKER}{len(comment_store)}"
|
|
288
|
+
comment_store[mid] = line
|
|
289
|
+
pending_markers.append(mid)
|
|
290
|
+
else:
|
|
291
|
+
processed.append(line)
|
|
292
|
+
elif is_comment:
|
|
293
|
+
mid = f"{_CMT_MARKER}{len(comment_store)}"
|
|
294
|
+
comment_store[mid] = line
|
|
295
|
+
pending_markers.append(mid)
|
|
296
|
+
else:
|
|
297
|
+
# SQL line — prepend any pending markers as inline comments
|
|
298
|
+
prefix = " ".join(f"/* {m} */" for m in pending_markers)
|
|
299
|
+
processed.append(f"{prefix} {line}" if prefix else line)
|
|
300
|
+
for m in pending_markers:
|
|
301
|
+
anchor_sql[m] = stripped
|
|
302
|
+
pending_markers.clear()
|
|
303
|
+
|
|
304
|
+
# Trailing comments with no following SQL — preserve as-is
|
|
305
|
+
trailing: list[str] = [comment_store[m] for m in pending_markers]
|
|
306
|
+
|
|
307
|
+
# ---- Step 2: format through sqlglot ---------------------------------
|
|
308
|
+
formatted = _sqlglot_format(processed, sql_indent=sql_indent, leading_comma=leading_comma)
|
|
309
|
+
|
|
310
|
+
# ---- Step 3: restore surviving markers to comment lines -------------
|
|
311
|
+
found_markers: set[str] = set()
|
|
312
|
+
result: list[str] = []
|
|
313
|
+
for fline in formatted:
|
|
314
|
+
markers_here = _CMT_MARKER_RE.findall(fline)
|
|
315
|
+
if markers_here:
|
|
316
|
+
# Strip markers to get the underlying SQL line and its indent
|
|
317
|
+
cleaned = _CMT_MARKER_RE.sub("", fline).strip()
|
|
318
|
+
# Determine indent: use the SQL line's indent from sqlglot
|
|
319
|
+
sql_indent = ""
|
|
320
|
+
if cleaned:
|
|
321
|
+
raw_cleaned = _CMT_MARKER_RE.sub("", fline)
|
|
322
|
+
sql_indent = raw_cleaned[: len(raw_cleaned) - len(raw_cleaned.lstrip())]
|
|
323
|
+
for m in markers_here:
|
|
324
|
+
if m in comment_store:
|
|
325
|
+
orig = comment_store[m]
|
|
326
|
+
# Re-indent the comment to match the SQL line it precedes
|
|
327
|
+
orig_stripped = orig.strip()
|
|
328
|
+
if orig_stripped:
|
|
329
|
+
result.append(sql_indent + orig_stripped)
|
|
330
|
+
else:
|
|
331
|
+
result.append("")
|
|
332
|
+
found_markers.add(m)
|
|
333
|
+
if cleaned:
|
|
334
|
+
result.append(sql_indent + cleaned)
|
|
335
|
+
else:
|
|
336
|
+
result.append(fline)
|
|
337
|
+
|
|
338
|
+
# ---- Step 4: reinsert lost markers ----------------------------------
|
|
339
|
+
lost = [m for m in comment_store if m not in found_markers and m not in set(pending_markers)]
|
|
340
|
+
if lost:
|
|
341
|
+
_reinsert_lost_comments(result, lost, comment_store, anchor_sql)
|
|
342
|
+
|
|
343
|
+
result.extend(trailing)
|
|
344
|
+
return result
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _reinsert_lost_comments(
|
|
348
|
+
result: list[str],
|
|
349
|
+
lost_markers: list[str],
|
|
350
|
+
comment_store: dict[str, str],
|
|
351
|
+
anchor_sql: dict[str, str],
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Best-effort reinsertion of comments that sqlglot dropped.
|
|
354
|
+
|
|
355
|
+
For each lost comment, extract key tokens from its anchor SQL line and
|
|
356
|
+
find the output line that best matches, then insert the comment before
|
|
357
|
+
that line (indented to match). Operates on *result* in place.
|
|
358
|
+
"""
|
|
359
|
+
_word_re = re.compile(r"[a-zA-Z_]\w*")
|
|
360
|
+
|
|
361
|
+
# Process in reverse order so earlier inserts don't shift later indices.
|
|
362
|
+
insertions: list[tuple[int, str]] = []
|
|
363
|
+
for marker in lost_markers:
|
|
364
|
+
anchor = anchor_sql.get(marker, "")
|
|
365
|
+
orig = comment_store[marker]
|
|
366
|
+
orig_stripped = orig.strip()
|
|
367
|
+
if not anchor or not orig_stripped:
|
|
368
|
+
insertions.append((len(result), orig))
|
|
369
|
+
continue
|
|
370
|
+
|
|
371
|
+
anchor_words = [w.lower() for w in _word_re.findall(anchor)]
|
|
372
|
+
if not anchor_words:
|
|
373
|
+
insertions.append((len(result), orig))
|
|
374
|
+
continue
|
|
375
|
+
|
|
376
|
+
# Find the output line with the best token overlap
|
|
377
|
+
best_idx = len(result)
|
|
378
|
+
best_score = 0
|
|
379
|
+
for i, rline in enumerate(result):
|
|
380
|
+
rwords = {w.lower() for w in _word_re.findall(rline)}
|
|
381
|
+
score = sum(1 for w in anchor_words if w in rwords)
|
|
382
|
+
if score > best_score:
|
|
383
|
+
best_score = score
|
|
384
|
+
best_idx = i
|
|
385
|
+
|
|
386
|
+
# Re-indent comment to match the target line
|
|
387
|
+
if best_idx < len(result):
|
|
388
|
+
target = result[best_idx]
|
|
389
|
+
indent_str = target[: len(target) - len(target.lstrip())]
|
|
390
|
+
else:
|
|
391
|
+
indent_str = ""
|
|
392
|
+
insertions.append((best_idx, indent_str + orig_stripped))
|
|
393
|
+
|
|
394
|
+
# Sort descending by index; within the same index, reverse the
|
|
395
|
+
# original order so sequential result.insert() calls produce the
|
|
396
|
+
# correct final ordering (last inserted at a given index ends up first).
|
|
397
|
+
indexed = list(enumerate(insertions))
|
|
398
|
+
indexed.sort(key=lambda x: (x[1][0], x[0]), reverse=True)
|
|
399
|
+
for _, (idx, text) in indexed:
|
|
400
|
+
result.insert(idx, text)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def format_sql_block(
|
|
404
|
+
lines: list[str],
|
|
405
|
+
depth: int,
|
|
406
|
+
indent: int,
|
|
407
|
+
use_sql: bool,
|
|
408
|
+
leading_comma: bool = False,
|
|
409
|
+
) -> list[str]:
|
|
177
410
|
"""Re-indent a SQL block to the current depth, optionally formatting via sqlglot."""
|
|
178
411
|
if not lines:
|
|
179
412
|
return lines
|
|
@@ -189,6 +422,13 @@ def format_sql_block(lines: list[str], depth: int, indent: int, use_sql: bool) -
|
|
|
189
422
|
if not use_sql:
|
|
190
423
|
return [target_prefix + line if line.strip() else "" for line in rebased]
|
|
191
424
|
|
|
425
|
+
# When comments appear mid-statement, use the marker-based approach
|
|
426
|
+
# which preserves both comments AND sqlglot formatting. When all
|
|
427
|
+
# comments are between statements, the simpler segmentation works.
|
|
428
|
+
if _has_mid_statement_comments(rebased):
|
|
429
|
+
result = _format_preserving_comments(rebased, sql_indent=indent, leading_comma=leading_comma)
|
|
430
|
+
return [target_prefix + line if line.strip() else "" for line in result]
|
|
431
|
+
|
|
192
432
|
result: list[str] = []
|
|
193
433
|
seg: list[str] = []
|
|
194
434
|
seg_is_comment: bool | None = None
|
|
@@ -200,7 +440,7 @@ def format_sql_block(lines: list[str], depth: int, indent: int, use_sql: bool) -
|
|
|
200
440
|
if seg_is_comment:
|
|
201
441
|
result.extend(seg)
|
|
202
442
|
else:
|
|
203
|
-
result.extend(_sqlglot_format(seg))
|
|
443
|
+
result.extend(_sqlglot_format(seg, sql_indent=indent, leading_comma=leading_comma))
|
|
204
444
|
seg.clear()
|
|
205
445
|
|
|
206
446
|
for line in rebased:
|
|
@@ -236,34 +476,58 @@ def format_metacommand(payload: str, depth: int, indent: int) -> str:
|
|
|
236
476
|
return f"{prefix}-- !x! {keyword}"
|
|
237
477
|
|
|
238
478
|
|
|
239
|
-
def format_file(source: str, indent: int = 4, use_sql: bool = True) -> str:
|
|
479
|
+
def format_file(source: str, indent: int = 4, use_sql: bool = True, leading_comma: bool = False) -> str:
|
|
240
480
|
"""Format the source text of an execsql script and return the result."""
|
|
241
481
|
depth = 0
|
|
242
482
|
sql_acc: list[str] = []
|
|
243
483
|
output: list[str] = []
|
|
244
484
|
|
|
245
485
|
in_dollar_quote = False
|
|
486
|
+
in_block_comment = False
|
|
487
|
+
# Track whether we are inside an open SQL statement (last SQL line
|
|
488
|
+
# did not end with ';'). Blank lines mid-statement should NOT flush
|
|
489
|
+
# the accumulator — doing so would split a single statement into
|
|
490
|
+
# fragments that sqlglot cannot parse.
|
|
491
|
+
in_sql_statement = False
|
|
246
492
|
|
|
247
493
|
def flush_sql() -> None:
|
|
248
|
-
nonlocal in_dollar_quote
|
|
494
|
+
nonlocal in_dollar_quote, in_sql_statement
|
|
249
495
|
if sql_acc:
|
|
250
496
|
# If any line in the accumulated block is inside a $$-delimited
|
|
251
497
|
# region, skip sqlglot formatting entirely. PL/pgSQL function
|
|
252
498
|
# bodies contain IF/END IF, LOOP, RETURN, etc. that sqlglot does
|
|
253
499
|
# not understand and will corrupt (e.g., rewriting to COMMIT).
|
|
254
500
|
safe_for_sqlglot = use_sql and not in_dollar_quote
|
|
255
|
-
output.extend(format_sql_block(sql_acc, depth, indent, safe_for_sqlglot))
|
|
501
|
+
output.extend(format_sql_block(sql_acc, depth, indent, safe_for_sqlglot, leading_comma=leading_comma))
|
|
256
502
|
sql_acc.clear()
|
|
503
|
+
in_sql_statement = False
|
|
257
504
|
|
|
258
505
|
for raw_line in source.expandtabs(4).splitlines():
|
|
506
|
+
stripped_line = raw_line.strip()
|
|
507
|
+
|
|
508
|
+
# Track /* */ block comment boundaries (but not inside $$ regions).
|
|
509
|
+
# Lines inside block comments must not be processed as metacommands.
|
|
510
|
+
if not in_dollar_quote:
|
|
511
|
+
if in_block_comment:
|
|
512
|
+
sql_acc.append(raw_line)
|
|
513
|
+
if "*/" in raw_line:
|
|
514
|
+
in_block_comment = False
|
|
515
|
+
continue
|
|
516
|
+
if stripped_line.startswith("/*") and "*/" not in stripped_line[2:]:
|
|
517
|
+
in_block_comment = True
|
|
518
|
+
sql_acc.append(raw_line)
|
|
519
|
+
continue
|
|
520
|
+
|
|
259
521
|
m = METACOMMAND_RE.match(raw_line)
|
|
260
522
|
|
|
261
|
-
if not
|
|
262
|
-
if not in_dollar_quote:
|
|
523
|
+
if not stripped_line:
|
|
524
|
+
if not in_dollar_quote and not in_sql_statement:
|
|
263
525
|
flush_sql()
|
|
526
|
+
output.append("")
|
|
264
527
|
else:
|
|
528
|
+
# Mid-statement blank line stays in the accumulator and
|
|
529
|
+
# will appear in the output when the block is formatted.
|
|
265
530
|
sql_acc.append(raw_line)
|
|
266
|
-
output.append("")
|
|
267
531
|
|
|
268
532
|
elif m:
|
|
269
533
|
flush_sql()
|
|
@@ -294,6 +558,12 @@ def format_file(source: str, indent: int = 4, use_sql: bool = True) -> str:
|
|
|
294
558
|
if "$$" in raw_line and raw_line.count("$$") % 2 == 1:
|
|
295
559
|
in_dollar_quote = not in_dollar_quote
|
|
296
560
|
sql_acc.append(raw_line)
|
|
561
|
+
# Update statement tracking: if this SQL line ends with ';'
|
|
562
|
+
# (and isn't a comment), the statement is complete.
|
|
563
|
+
if stripped_line.endswith(";") and not stripped_line.startswith("--"):
|
|
564
|
+
in_sql_statement = False
|
|
565
|
+
elif not stripped_line.startswith("--"):
|
|
566
|
+
in_sql_statement = True
|
|
297
567
|
|
|
298
568
|
flush_sql()
|
|
299
569
|
|
|
@@ -348,6 +618,11 @@ def main() -> None:
|
|
|
348
618
|
in_place: bool = typer.Option(False, "-i", "--in-place", help="Modify files in place."),
|
|
349
619
|
no_sql: bool = typer.Option(False, "--no-sql", help="Skip SQL formatting via sqlglot."),
|
|
350
620
|
indent: int = typer.Option(4, "--indent", metavar="N", help="Spaces per indent level."),
|
|
621
|
+
leading_comma: bool = typer.Option(
|
|
622
|
+
False,
|
|
623
|
+
"--leading-comma",
|
|
624
|
+
help="Place commas at the start of lines instead of the end.",
|
|
625
|
+
),
|
|
351
626
|
) -> None:
|
|
352
627
|
use_sql = not no_sql
|
|
353
628
|
paths = collect_paths(targets)
|
|
@@ -363,7 +638,7 @@ def main() -> None:
|
|
|
363
638
|
_err_console.print(f"[bold red]Error:[/bold red] reading {path}: {exc}")
|
|
364
639
|
raise typer.Exit(code=1) from None
|
|
365
640
|
|
|
366
|
-
formatted = format_file(source, indent=indent, use_sql=use_sql)
|
|
641
|
+
formatted = format_file(source, indent=indent, use_sql=use_sql, leading_comma=leading_comma)
|
|
367
642
|
|
|
368
643
|
if check:
|
|
369
644
|
if formatted != source:
|
execsql/metacommands/debug.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from execsql.utils.errors import fatal_error
|
|
3
2
|
|
|
4
3
|
"""
|
|
5
4
|
Debug metacommand handlers for execsql.
|
|
@@ -7,11 +6,16 @@ Debug metacommand handlers for execsql.
|
|
|
7
6
|
Provides ``x_debug_write_metacommands``, which implements the
|
|
8
7
|
``WRITE METACOMMANDS`` debug metacommand that prints the full registered
|
|
9
8
|
metacommand list to the log/console for troubleshooting.
|
|
9
|
+
|
|
10
|
+
Also provides ``x_show_scripts`` and ``x_show_script`` for runtime
|
|
11
|
+
introspection of registered SCRIPT blocks.
|
|
10
12
|
"""
|
|
11
13
|
|
|
14
|
+
from pathlib import Path
|
|
12
15
|
from typing import Any
|
|
13
16
|
|
|
14
17
|
import execsql.state as _state
|
|
18
|
+
from execsql.utils.errors import fatal_error
|
|
15
19
|
from execsql.utils.fileio import EncodedFile, filewriter_open_as_new, filewriter_write
|
|
16
20
|
|
|
17
21
|
|
|
@@ -173,3 +177,87 @@ def x_debug_write_config(**kwargs: Any) -> None:
|
|
|
173
177
|
|
|
174
178
|
for line in lines:
|
|
175
179
|
write(f"{line}\n")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
# Helpers for SCRIPT introspection (shared by metacommands and REPL)
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _format_script_signature(name: str, param_defs: Any) -> str:
|
|
188
|
+
"""Return ``name(param1, param2, opt=default)`` or ``name()``.
|
|
189
|
+
|
|
190
|
+
*param_defs* may be a list of :class:`ParamDef` objects (preferred) or
|
|
191
|
+
a plain list of strings (backward compat).
|
|
192
|
+
"""
|
|
193
|
+
if not param_defs:
|
|
194
|
+
return f"{name}()"
|
|
195
|
+
parts: list[str] = []
|
|
196
|
+
for p in param_defs:
|
|
197
|
+
if hasattr(p, "default") and p.default is not None:
|
|
198
|
+
parts.append(f"{p.name}={p.default}")
|
|
199
|
+
elif hasattr(p, "name"):
|
|
200
|
+
parts.append(p.name)
|
|
201
|
+
else:
|
|
202
|
+
parts.append(str(p))
|
|
203
|
+
return f"{name}({', '.join(parts)})"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _format_script_source(span: Any) -> str:
|
|
207
|
+
"""Return ``file:start-end`` from a SourceSpan."""
|
|
208
|
+
filename = Path(span.file).name if span and span.file else "<unknown>"
|
|
209
|
+
if span and span.start_line is not None:
|
|
210
|
+
if span.end_line is not None and span.end_line != span.start_line:
|
|
211
|
+
return f"{filename}:{span.start_line}-{span.end_line}"
|
|
212
|
+
return f"{filename}:{span.start_line}"
|
|
213
|
+
return filename
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# SHOW SCRIPTS / SHOW SCRIPT metacommand handlers
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def x_show_scripts(**kwargs: Any) -> None:
|
|
222
|
+
"""List all registered SCRIPT definitions with parameters and source location."""
|
|
223
|
+
scripts = _state.ast_scripts
|
|
224
|
+
if not scripts:
|
|
225
|
+
_state.output.write("No scripts registered.\n")
|
|
226
|
+
return
|
|
227
|
+
_state.output.write(f"Registered scripts ({len(scripts)}):\n\n")
|
|
228
|
+
# Compute column width for alignment
|
|
229
|
+
sigs = {name: _format_script_signature(name, block.param_defs) for name, block in scripts.items()}
|
|
230
|
+
max_sig = max(len(s) for s in sigs.values())
|
|
231
|
+
for name, block in scripts.items():
|
|
232
|
+
sig = sigs[name]
|
|
233
|
+
src = _format_script_source(block.span)
|
|
234
|
+
_state.output.write(f" {sig:<{max_sig}} {src}\n")
|
|
235
|
+
_state.output.write("\n")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def x_show_script(**kwargs: Any) -> None:
|
|
239
|
+
"""Show detail for a single registered SCRIPT definition."""
|
|
240
|
+
script_name = kwargs.get("script_id", "").lower()
|
|
241
|
+
scripts = _state.ast_scripts
|
|
242
|
+
if script_name not in scripts:
|
|
243
|
+
_state.output.write(f"No script named '{script_name}' is registered.\n")
|
|
244
|
+
return
|
|
245
|
+
block = scripts[script_name]
|
|
246
|
+
sig = _format_script_signature(block.name, block.param_defs)
|
|
247
|
+
src = _format_script_source(block.span)
|
|
248
|
+
_state.output.write(f"Script: {sig}\n")
|
|
249
|
+
_state.output.write(f"Source: {src}\n")
|
|
250
|
+
if block.param_defs:
|
|
251
|
+
_state.output.write("Parameters:\n")
|
|
252
|
+
max_name = max(len(p.name) for p in block.param_defs)
|
|
253
|
+
for p in block.param_defs:
|
|
254
|
+
if p.default is not None:
|
|
255
|
+
_state.output.write(f" {p.name:<{max_name}} (optional, default: {p.default})\n")
|
|
256
|
+
else:
|
|
257
|
+
_state.output.write(f" {p.name:<{max_name}} (required)\n")
|
|
258
|
+
else:
|
|
259
|
+
_state.output.write("Parameters: (none)\n")
|
|
260
|
+
if block.doc:
|
|
261
|
+
_state.output.write("\n")
|
|
262
|
+
for doc_line in block.doc.split("\n"):
|
|
263
|
+
_state.output.write(f" {doc_line}\n")
|
execsql/metacommands/dispatch.py
CHANGED
|
@@ -99,6 +99,8 @@ from execsql.metacommands.debug import (
|
|
|
99
99
|
x_debug_write_metacommands,
|
|
100
100
|
x_debug_write_odbc_drivers,
|
|
101
101
|
x_debug_write_subvars,
|
|
102
|
+
x_show_script,
|
|
103
|
+
x_show_scripts,
|
|
102
104
|
)
|
|
103
105
|
from execsql.debug.repl import x_breakpoint
|
|
104
106
|
from execsql.metacommands.io import (
|
|
@@ -1747,6 +1749,22 @@ def build_dispatch_table() -> MetaCommandList:
|
|
|
1747
1749
|
run_when_false=False,
|
|
1748
1750
|
)
|
|
1749
1751
|
|
|
1752
|
+
# ------------------------------------------------------------------
|
|
1753
|
+
# SHOW SCRIPTS / SHOW SCRIPT
|
|
1754
|
+
# ------------------------------------------------------------------
|
|
1755
|
+
mcl.add(
|
|
1756
|
+
r"^\s*SHOW\s+SCRIPTS\s*$",
|
|
1757
|
+
x_show_scripts,
|
|
1758
|
+
description="SHOW SCRIPTS",
|
|
1759
|
+
category="action",
|
|
1760
|
+
)
|
|
1761
|
+
mcl.add(
|
|
1762
|
+
r"^\s*SHOW\s+SCRIPT\s+(?P<script_id>\w+)\s*$",
|
|
1763
|
+
x_show_script,
|
|
1764
|
+
description="SHOW SCRIPT",
|
|
1765
|
+
category="action",
|
|
1766
|
+
)
|
|
1767
|
+
|
|
1750
1768
|
# ------------------------------------------------------------------
|
|
1751
1769
|
# IF / ORIF / ANDIF / ELSEIF / ELSE / ENDIF
|
|
1752
1770
|
# ------------------------------------------------------------------
|
execsql/script/ast.py
CHANGED
|
@@ -52,6 +52,7 @@ __all__ = [
|
|
|
52
52
|
"IfBlock",
|
|
53
53
|
"LoopBlock",
|
|
54
54
|
"BatchBlock",
|
|
55
|
+
"ParamDef",
|
|
55
56
|
"ScriptBlock",
|
|
56
57
|
"SqlBlock",
|
|
57
58
|
"IncludeDirective",
|
|
@@ -290,6 +291,24 @@ class BatchBlock(Node):
|
|
|
290
291
|
return f"BatchBlock({self.span}, body={len(self.body)})"
|
|
291
292
|
|
|
292
293
|
|
|
294
|
+
@dataclass(frozen=True, slots=True)
|
|
295
|
+
class ParamDef:
|
|
296
|
+
"""A single SCRIPT parameter definition with an optional default value.
|
|
297
|
+
|
|
298
|
+
Attributes:
|
|
299
|
+
name: The parameter name (as declared, without ``#`` prefix).
|
|
300
|
+
default: The default value string, or ``None`` for required parameters.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
name: str
|
|
304
|
+
default: str | None = None
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def required(self) -> bool:
|
|
308
|
+
"""Return ``True`` if this parameter has no default value."""
|
|
309
|
+
return self.default is None
|
|
310
|
+
|
|
311
|
+
|
|
293
312
|
@dataclass
|
|
294
313
|
class ScriptBlock(Node):
|
|
295
314
|
"""A BEGIN SCRIPT name ... END SCRIPT structure.
|
|
@@ -299,20 +318,31 @@ class ScriptBlock(Node):
|
|
|
299
318
|
|
|
300
319
|
Attributes:
|
|
301
320
|
name: The script block name (lowercased).
|
|
302
|
-
|
|
321
|
+
param_defs: Optional list of :class:`ParamDef` parameter definitions.
|
|
322
|
+
doc: Optional docstring extracted from comments immediately following
|
|
323
|
+
the BEGIN SCRIPT line.
|
|
303
324
|
body: Nodes within the script block.
|
|
304
325
|
"""
|
|
305
326
|
|
|
306
327
|
name: str
|
|
307
|
-
|
|
328
|
+
param_defs: list[ParamDef] | None = None
|
|
329
|
+
doc: str | None = None
|
|
308
330
|
body: list[Node] = field(default_factory=list)
|
|
309
331
|
|
|
332
|
+
@property
|
|
333
|
+
def param_names(self) -> list[str] | None:
|
|
334
|
+
"""Return parameter names only (backward compatibility)."""
|
|
335
|
+
if self.param_defs is None:
|
|
336
|
+
return None
|
|
337
|
+
return [p.name for p in self.param_defs]
|
|
338
|
+
|
|
310
339
|
def children(self) -> Iterator[Node]:
|
|
311
340
|
yield from self.body
|
|
312
341
|
|
|
313
342
|
def __repr__(self) -> str:
|
|
314
|
-
params = f", params={self.param_names}" if self.
|
|
315
|
-
|
|
343
|
+
params = f", params={self.param_names}" if self.param_defs else ""
|
|
344
|
+
doc_tag = ", doc=True" if self.doc else ""
|
|
345
|
+
return f"ScriptBlock({self.span}, name={self.name!r}{params}{doc_tag}, body={len(self.body)})"
|
|
316
346
|
|
|
317
347
|
|
|
318
348
|
@dataclass
|
|
@@ -547,8 +577,15 @@ def _node_label(node: Node) -> str:
|
|
|
547
577
|
if isinstance(node, BatchBlock):
|
|
548
578
|
return f"{_tag('BATCH')} BEGIN BATCH"
|
|
549
579
|
if isinstance(node, ScriptBlock):
|
|
550
|
-
|
|
551
|
-
|
|
580
|
+
if node.param_defs:
|
|
581
|
+
parts = []
|
|
582
|
+
for p in node.param_defs:
|
|
583
|
+
parts.append(f"{p.name}={p.default}" if p.default is not None else p.name)
|
|
584
|
+
params = f" ({', '.join(parts)})"
|
|
585
|
+
else:
|
|
586
|
+
params = ""
|
|
587
|
+
doc_tag = " [doc]" if node.doc else ""
|
|
588
|
+
return f"{_tag('SCRIPT')} {node.name}{params}{doc_tag}"
|
|
552
589
|
if isinstance(node, SqlBlock):
|
|
553
590
|
return f"{_tag('SQL_BLK')} BEGIN SQL"
|
|
554
591
|
if isinstance(node, IncludeDirective):
|
execsql/script/executor.py
CHANGED
|
@@ -648,7 +648,10 @@ def _execute_include(
|
|
|
648
648
|
WHILE/UNTIL loops are handled natively too.
|
|
649
649
|
"""
|
|
650
650
|
if node.is_execute_script:
|
|
651
|
-
target
|
|
651
|
+
# Substitute variables in the target — the script name may be passed
|
|
652
|
+
# as a parameter (e.g., EXECUTE SCRIPT !!#script_name!!).
|
|
653
|
+
effective_locals = _stack_localvars(ctx) or localvars
|
|
654
|
+
target = substitute_vars(node.target, effective_locals, ctx=ctx).strip().lower()
|
|
652
655
|
|
|
653
656
|
# Native path: target is in our AST registry
|
|
654
657
|
if target in ctx.ast_scripts:
|
|
@@ -713,23 +716,35 @@ def _execute_script_native(
|
|
|
713
716
|
for param, arg in all_prepared_args:
|
|
714
717
|
paramvals.add_substitution(param, arg)
|
|
715
718
|
|
|
716
|
-
# Validate parameter names match
|
|
717
|
-
if script_block.
|
|
719
|
+
# Validate parameter names match — with default parameter support
|
|
720
|
+
if script_block.param_defs is not None:
|
|
718
721
|
passed_names = [p[0].lstrip("#") for p in all_prepared_args]
|
|
719
|
-
|
|
722
|
+
required = [p.name for p in script_block.param_defs if p.required]
|
|
723
|
+
missing = [p for p in required if p not in passed_names]
|
|
724
|
+
if missing:
|
|
720
725
|
raise ErrInfo(
|
|
721
726
|
"error",
|
|
722
|
-
other_msg=f"
|
|
727
|
+
other_msg=(f"Missing required parameter(s) ({', '.join(missing)}) in call to {script_block.name}."),
|
|
723
728
|
)
|
|
729
|
+
# Inject defaults for optional params not provided
|
|
730
|
+
for pdef in script_block.param_defs:
|
|
731
|
+
if pdef.default is not None and pdef.name not in passed_names:
|
|
732
|
+
paramvals.add_substitution(f"#{pdef.name}", pdef.default)
|
|
724
733
|
else:
|
|
725
|
-
if script_block.
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
734
|
+
if script_block.param_defs is not None:
|
|
735
|
+
required = [p.name for p in script_block.param_defs if p.required]
|
|
736
|
+
if required:
|
|
737
|
+
raise ErrInfo(
|
|
738
|
+
"error",
|
|
739
|
+
other_msg=(
|
|
740
|
+
f"Missing required parameter(s) ({', '.join(required)}) in call to {script_block.name}."
|
|
741
|
+
),
|
|
742
|
+
)
|
|
743
|
+
# No args provided but all params have defaults — inject them all
|
|
744
|
+
paramvals = ScriptArgSubVarSet()
|
|
745
|
+
for pdef in script_block.param_defs:
|
|
746
|
+
if pdef.default is not None:
|
|
747
|
+
paramvals.add_substitution(f"#{pdef.name}", pdef.default)
|
|
733
748
|
|
|
734
749
|
# Push a CommandList frame onto the stack so that:
|
|
735
750
|
# - get_subvarset() can find ~local and +outer-scope variables
|
execsql/script/parser.py
CHANGED
|
@@ -42,6 +42,7 @@ from execsql.script.ast import (
|
|
|
42
42
|
LoopBlock,
|
|
43
43
|
MetaCommandStatement,
|
|
44
44
|
Node,
|
|
45
|
+
ParamDef,
|
|
45
46
|
Script,
|
|
46
47
|
ScriptBlock,
|
|
47
48
|
SourceSpan,
|
|
@@ -106,7 +107,7 @@ def _strip_quotes(s: str) -> str:
|
|
|
106
107
|
_EXEC_SCRIPT_RX = re.compile(
|
|
107
108
|
r"^\s*(?:EXEC(?:UTE)?|RUN)\s+SCRIPT"
|
|
108
109
|
r"(?P<exists>\s+IF\s+EXISTS)?"
|
|
109
|
-
r"\s+(?P<script_id
|
|
110
|
+
r"\s+(?P<script_id>(?:\w+|!(?:['\"]?)![^!]+!(?:['\"]?)!))"
|
|
110
111
|
r"(?:(?:\s+WITH)?(?:\s+ARG(?:UMENT)?S?)?\s*\(\s*(?P<argexp>.+?)\s*\))?"
|
|
111
112
|
r"(?:\s+(?P<looptype>WHILE|UNTIL)\s*\(\s*(?P<loopcond>.+)\s*\))?"
|
|
112
113
|
r"\s*$",
|
|
@@ -114,10 +115,52 @@ _EXEC_SCRIPT_RX = re.compile(
|
|
|
114
115
|
)
|
|
115
116
|
|
|
116
117
|
_WITH_PARAMS_RX = re.compile(
|
|
117
|
-
r"(?:\s+WITH)?(?:\s+PARAM(?:ETER)?S)?\s*\(\s*(?P<params
|
|
118
|
+
r"(?:\s+WITH)?(?:\s+PARAM(?:ETER)?S)?\s*\(\s*(?P<params>"
|
|
119
|
+
r"\w+(?:\s*=\s*\S+)?(?:\s*,\s*\w+(?:\s*=\s*\S+)?)*"
|
|
120
|
+
r")\s*\)\s*$",
|
|
118
121
|
re.I,
|
|
119
122
|
)
|
|
120
123
|
|
|
124
|
+
_PARAM_TOKEN_RX = re.compile(r"(\w+)(?:\s*=\s*(\S+))?")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _parse_param_defs(
|
|
128
|
+
params_str: str,
|
|
129
|
+
lineno: int,
|
|
130
|
+
source: str,
|
|
131
|
+
) -> list[ParamDef]:
|
|
132
|
+
"""Parse ``'a, b, c=100, d=false'`` into a list of :class:`ParamDef`.
|
|
133
|
+
|
|
134
|
+
Required parameters (no default) must precede optional parameters
|
|
135
|
+
(with default). Raises :class:`ErrInfo` if ordering is violated.
|
|
136
|
+
"""
|
|
137
|
+
tokens = [t.strip() for t in params_str.split(",")]
|
|
138
|
+
defs: list[ParamDef] = []
|
|
139
|
+
seen_optional: str | None = None # name of first optional param
|
|
140
|
+
for token in tokens:
|
|
141
|
+
m = _PARAM_TOKEN_RX.match(token.strip())
|
|
142
|
+
if not m:
|
|
143
|
+
raise ErrInfo(
|
|
144
|
+
type="cmd",
|
|
145
|
+
other_msg=(f"Invalid parameter token '{token}' on line {lineno} of {source}."),
|
|
146
|
+
)
|
|
147
|
+
name, default = m.group(1), m.group(2)
|
|
148
|
+
if default is not None:
|
|
149
|
+
if seen_optional is None:
|
|
150
|
+
seen_optional = name
|
|
151
|
+
elif seen_optional is not None:
|
|
152
|
+
raise ErrInfo(
|
|
153
|
+
type="cmd",
|
|
154
|
+
other_msg=(
|
|
155
|
+
f"Required parameter '{name}' after optional parameter "
|
|
156
|
+
f"'{seen_optional}' on line {lineno} of {source}. "
|
|
157
|
+
f"Required parameters must precede optional parameters."
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
defs.append(ParamDef(name=name, default=default))
|
|
161
|
+
return defs
|
|
162
|
+
|
|
163
|
+
|
|
121
164
|
# Line classification
|
|
122
165
|
_EXEC_LINE_RX = re.compile(r"^\s*--\s*!x!\s*(?P<cmd>.+)$", re.I)
|
|
123
166
|
_COMMENT_LINE_RX = re.compile(r"^\s*--")
|
|
@@ -135,7 +178,16 @@ class _BlockFrame:
|
|
|
135
178
|
to close the block correctly.
|
|
136
179
|
"""
|
|
137
180
|
|
|
138
|
-
__slots__ = (
|
|
181
|
+
__slots__ = (
|
|
182
|
+
"node",
|
|
183
|
+
"kind",
|
|
184
|
+
"start_line",
|
|
185
|
+
"source",
|
|
186
|
+
"_in_else",
|
|
187
|
+
"_in_elseif",
|
|
188
|
+
"collecting_doc",
|
|
189
|
+
"doc_lines",
|
|
190
|
+
)
|
|
139
191
|
|
|
140
192
|
def __init__(self, node: Node, kind: str, start_line: int, source: str) -> None:
|
|
141
193
|
self.node = node
|
|
@@ -144,6 +196,8 @@ class _BlockFrame:
|
|
|
144
196
|
self.source = source
|
|
145
197
|
self._in_else = False
|
|
146
198
|
self._in_elseif = False
|
|
199
|
+
self.collecting_doc: bool = kind == "script" # auto-collect doc after BEGIN SCRIPT
|
|
200
|
+
self.doc_lines: list[str] = []
|
|
147
201
|
|
|
148
202
|
|
|
149
203
|
# ---------------------------------------------------------------------------
|
|
@@ -206,9 +260,67 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
206
260
|
)
|
|
207
261
|
line_comment_lines = []
|
|
208
262
|
|
|
263
|
+
def _stop_doc_collection() -> None:
|
|
264
|
+
"""Stop collecting docstring lines and finalize the doc on the script node."""
|
|
265
|
+
if block_stack and block_stack[-1].collecting_doc:
|
|
266
|
+
frame = block_stack[-1]
|
|
267
|
+
frame.collecting_doc = False
|
|
268
|
+
if frame.doc_lines:
|
|
269
|
+
doc_text = "\n".join(frame.doc_lines).strip()
|
|
270
|
+
if doc_text and isinstance(frame.node, ScriptBlock):
|
|
271
|
+
frame.node.doc = doc_text
|
|
272
|
+
|
|
209
273
|
for file_lineno, raw_line in enumerate(lines, 1):
|
|
210
274
|
line = raw_line.rstrip()
|
|
211
275
|
|
|
276
|
+
# --- Docstring collection for SCRIPT blocks ---
|
|
277
|
+
# Comments immediately following BEGIN SCRIPT are captured as the
|
|
278
|
+
# docstring. A blank line terminates the doc.
|
|
279
|
+
if block_stack and block_stack[-1].collecting_doc and not in_block_comment:
|
|
280
|
+
frame = block_stack[-1]
|
|
281
|
+
if not line:
|
|
282
|
+
# Blank line terminates doc collection
|
|
283
|
+
_stop_doc_collection()
|
|
284
|
+
_flush_line_comments()
|
|
285
|
+
continue
|
|
286
|
+
# Check for block comment opening
|
|
287
|
+
stripped = line.lstrip()
|
|
288
|
+
if stripped.startswith("/*"):
|
|
289
|
+
# Collect block comment as doc lines
|
|
290
|
+
comment_body = stripped[2:]
|
|
291
|
+
if comment_body.rstrip().endswith("*/"):
|
|
292
|
+
# Single-line block comment: /* text */
|
|
293
|
+
comment_body = comment_body.rstrip()[:-2].strip()
|
|
294
|
+
if comment_body:
|
|
295
|
+
frame.doc_lines.append(comment_body)
|
|
296
|
+
continue
|
|
297
|
+
# Multi-line block comment — collect until */
|
|
298
|
+
if comment_body.strip():
|
|
299
|
+
frame.doc_lines.append(comment_body.rstrip())
|
|
300
|
+
# Let the block comment tracker handle the rest, but mark
|
|
301
|
+
# that we're collecting doc inside a block comment.
|
|
302
|
+
in_block_comment = True
|
|
303
|
+
block_comment_lines = [line]
|
|
304
|
+
block_comment_start = file_lineno
|
|
305
|
+
sql_accum_at_block_comment = False
|
|
306
|
+
# We'll handle the doc finalization when */ is found below.
|
|
307
|
+
# For now, mark doc as still collecting.
|
|
308
|
+
continue
|
|
309
|
+
# Check for single-line comment (not metacommand)
|
|
310
|
+
metacommand_match_doc = _EXEC_LINE_RX.match(line)
|
|
311
|
+
if not metacommand_match_doc and _COMMENT_LINE_RX.match(line):
|
|
312
|
+
# Strip -- prefix and optional leading space
|
|
313
|
+
text = stripped
|
|
314
|
+
if text.startswith("--"):
|
|
315
|
+
text = text[2:]
|
|
316
|
+
if text.startswith(" "):
|
|
317
|
+
text = text[1:]
|
|
318
|
+
frame.doc_lines.append(text.rstrip())
|
|
319
|
+
continue
|
|
320
|
+
# Non-comment line (metacommand or SQL) — stop doc collection
|
|
321
|
+
_stop_doc_collection()
|
|
322
|
+
# Fall through to normal processing
|
|
323
|
+
|
|
212
324
|
# --- Block comment tracking ---
|
|
213
325
|
if not line:
|
|
214
326
|
_flush_line_comments()
|
|
@@ -219,6 +331,23 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
219
331
|
if len(line) > 1 and line.rstrip().endswith("*/"):
|
|
220
332
|
in_block_comment = False
|
|
221
333
|
comment_text = "\n".join(block_comment_lines)
|
|
334
|
+
# If we were collecting doc lines when the block comment opened,
|
|
335
|
+
# feed the content into the docstring instead of creating a node.
|
|
336
|
+
if block_stack and block_stack[-1].collecting_doc:
|
|
337
|
+
frame = block_stack[-1]
|
|
338
|
+
# Extract text between /* and */, stripping delimiters
|
|
339
|
+
for bc_line in block_comment_lines:
|
|
340
|
+
stripped_bc = bc_line.strip()
|
|
341
|
+
if stripped_bc.startswith("/*"):
|
|
342
|
+
stripped_bc = stripped_bc[2:]
|
|
343
|
+
if stripped_bc.endswith("*/"):
|
|
344
|
+
stripped_bc = stripped_bc[:-2]
|
|
345
|
+
stripped_bc = stripped_bc.strip()
|
|
346
|
+
if stripped_bc:
|
|
347
|
+
frame.doc_lines.append(stripped_bc)
|
|
348
|
+
block_comment_lines = []
|
|
349
|
+
sql_accum_at_block_comment = False
|
|
350
|
+
continue
|
|
222
351
|
if sql_accum_at_block_comment:
|
|
223
352
|
# Block comment started inside a SQL statement — fold it
|
|
224
353
|
# back into sql_accum so the statement isn't split.
|
|
@@ -332,7 +461,7 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
332
461
|
if m:
|
|
333
462
|
name = m.group("name").lower()
|
|
334
463
|
paramexpr = m.group("paramexpr")
|
|
335
|
-
|
|
464
|
+
param_defs = None
|
|
336
465
|
if paramexpr:
|
|
337
466
|
wp = _WITH_PARAMS_RX.match(paramexpr)
|
|
338
467
|
if not wp:
|
|
@@ -341,13 +470,13 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
341
470
|
command_text=line,
|
|
342
471
|
other_msg=f"Invalid BEGIN SCRIPT metacommand on line {file_lineno} of file {source_name}.",
|
|
343
472
|
)
|
|
344
|
-
|
|
473
|
+
param_defs = _parse_param_defs(wp.group("params"), file_lineno, source_name)
|
|
345
474
|
block_stack.append(
|
|
346
475
|
_BlockFrame(
|
|
347
476
|
ScriptBlock(
|
|
348
477
|
span=SourceSpan(source_name, file_lineno),
|
|
349
478
|
name=name,
|
|
350
|
-
|
|
479
|
+
param_defs=param_defs,
|
|
351
480
|
),
|
|
352
481
|
kind="script",
|
|
353
482
|
start_line=file_lineno,
|
execsql/utils/fileio.py
CHANGED
|
@@ -284,7 +284,15 @@ class FileWriter(multiprocessing.Process):
|
|
|
284
284
|
self.active = False
|
|
285
285
|
self.close_all()
|
|
286
286
|
|
|
287
|
-
def run(self) -> None:
|
|
287
|
+
def run(self) -> None: # pragma: no cover – runs in a subprocess
|
|
288
|
+
# Ignore SIGINT in the child process — the parent owns Ctrl+C handling
|
|
289
|
+
# and will shut us down via CMD_SHUTDOWN on the queue. Without this,
|
|
290
|
+
# KeyboardInterrupt races through queue.get() and close_all(), producing
|
|
291
|
+
# ugly tracebacks on stderr.
|
|
292
|
+
import signal
|
|
293
|
+
|
|
294
|
+
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
295
|
+
|
|
288
296
|
# Messages in the input queue consist of a 2-tuple, of which the first element
|
|
289
297
|
# is a command and the second is a tuple of arguments for the function indicated
|
|
290
298
|
# by that command.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.16.
|
|
3
|
+
Version: 2.16.13
|
|
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
|
|
@@ -3,7 +3,7 @@ execsql/__main__.py,sha256=HdbK-SAhyUmfB6xINY5AzxdMSxGzWSGEG_2dv42Jn64,315
|
|
|
3
3
|
execsql/api.py,sha256=0D2Rl329fvDXERNrJBUzoWEt_VxqCHwRgaGzrq04rVs,19709
|
|
4
4
|
execsql/config.py,sha256=6icjr8PKenUGfFF6lciSclvejjDzY8GTW1OZ1-IZt-Y,29480
|
|
5
5
|
execsql/exceptions.py,sha256=j8hykBiof9H3Za9hwLIbDcVB2Xn65ODXXplp1jkvdgM,8453
|
|
6
|
-
execsql/format.py,sha256=
|
|
6
|
+
execsql/format.py,sha256=nftyFgzNuv9aGUsp07WD3SjfttZ_dPhY45zJ7ocObL4,24384
|
|
7
7
|
execsql/models.py,sha256=kCTUQg9-vReM6WNFfB_ZrEppuOW5u1uMBQThSkfPC0o,13264
|
|
8
8
|
execsql/parser.py,sha256=P3ea8k7T_XLMrbhpFNZXwytdShrY302MKnhosqza1lo,15493
|
|
9
9
|
execsql/plugins.py,sha256=2voLwT6eFap6BCBoZYndNNC_bMEJO1f_aP6xQTVXwYI,12815
|
|
@@ -29,7 +29,7 @@ execsql/db/postgres.py,sha256=-GpaA9fi6_vdIDTGyRGxNRaPYAAkEB_aCb-EyLFShsM,21120
|
|
|
29
29
|
execsql/db/sqlite.py,sha256=or2JDDt_MyBTAtSXBVhNTcojE9jCE4b98tq39sNOyUg,10401
|
|
30
30
|
execsql/db/sqlserver.py,sha256=d8PwfNumt-Spit_0llafqQ900suv3CZMbrRsHLU1iB4,7683
|
|
31
31
|
execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
|
|
32
|
-
execsql/debug/repl.py,sha256=
|
|
32
|
+
execsql/debug/repl.py,sha256=TlIw6XrxwP3QUxuOaQZKp9vbJI55kEfiMtRNMxQRZo8,20747
|
|
33
33
|
execsql/exporters/__init__.py,sha256=-Cnji-OgodJV8ftcDcOyTof0kQMy9J5kKVC8GVFpc3o,670
|
|
34
34
|
execsql/exporters/base.py,sha256=Uhq0PBz8N_pJ7WlIN9225wY-HyxfzWSb9YabHXicBA8,6387
|
|
35
35
|
execsql/exporters/delimited.py,sha256=Po9RV4UwBLOuRZmPFJk5CErpFvTdLCy--nFJJffZgxM,32383
|
|
@@ -69,8 +69,8 @@ execsql/metacommands/conditions.py,sha256=B6fBumkqoPO4wcQbw_ypYITaSnzPemAA1g5GrN
|
|
|
69
69
|
execsql/metacommands/connect.py,sha256=Wnlp5PeeaNDaVlaWjCetvarTgQIwIeMPYe8cyslPYeA,14969
|
|
70
70
|
execsql/metacommands/control.py,sha256=PlTAq34OkcmnOsPm3bZxF4mg1MaNBpENa8wzIKVEY10,8302
|
|
71
71
|
execsql/metacommands/data.py,sha256=tRQBGTAuW-eJ2tBNWaoZI9OjTyNNyHJISo7gOdL-sm8,11370
|
|
72
|
-
execsql/metacommands/debug.py,sha256=
|
|
73
|
-
execsql/metacommands/dispatch.py,sha256=
|
|
72
|
+
execsql/metacommands/debug.py,sha256=3QVm0N5uc7mcZco36kqlDq8tiWW0sdo8E8BoQZkE_DI,11784
|
|
73
|
+
execsql/metacommands/dispatch.py,sha256=Gao7rwiBFdghmYKcbWVm21Va30evobQuoghLY_FOZto,87602
|
|
74
74
|
execsql/metacommands/io.py,sha256=vlGBje5sgnqeilooMdhJDgSRIhysHy5_7LrKtik9Xjs,3011
|
|
75
75
|
execsql/metacommands/io_export.py,sha256=J2FnXA6452FIGStv6vbZXORmA-Ges70na0W7YwpMESA,13253
|
|
76
76
|
execsql/metacommands/io_fileops.py,sha256=QKFj-94W32zxJiDcMpRsaohSCdtCRPIOG10K0WPigFk,9616
|
|
@@ -81,42 +81,42 @@ execsql/metacommands/script_ext.py,sha256=sw4YKUQl0SRlVlmhIoGbMokOo_hVqh1EcTuYCN
|
|
|
81
81
|
execsql/metacommands/system.py,sha256=azRbv_P8l0t8BkDM9bmAUkhpnLSLHSCcmByqs-a3FxQ,7352
|
|
82
82
|
execsql/metacommands/upsert.py,sha256=XE_P3mjaUkqT-LR4_28n2NbXwdjSgAGXJyZmKZC4Oy4,21136
|
|
83
83
|
execsql/script/__init__.py,sha256=3WaBklMVIWjtCsYQ-BVo9UAVEIATOgeGsuyv21YKnxo,3969
|
|
84
|
-
execsql/script/ast.py,sha256=
|
|
84
|
+
execsql/script/ast.py,sha256=SLn1fp0QDsqvJegNeIuyKV9f1BFnkEdzRota5EaNTfo,20053
|
|
85
85
|
execsql/script/control.py,sha256=s-1eZdGARM6H1FwZ6VDdO_f50j7bvvRtTHesfUm9tbc,6144
|
|
86
86
|
execsql/script/engine.py,sha256=EhuVBniOrFkzAW4I3NIZLt3INHTZJvlYoF7B99rZBLI,29391
|
|
87
|
-
execsql/script/executor.py,sha256=
|
|
88
|
-
execsql/script/parser.py,sha256=
|
|
87
|
+
execsql/script/executor.py,sha256=T-pGa3o5S0oX1ySB5zztzRoJHYRZI58CKw3qcFz_95Q,37602
|
|
88
|
+
execsql/script/parser.py,sha256=huznh4RnW6Y_yJyIIoMG5McUS2T9o3kfOW37CXPR5dk,31689
|
|
89
89
|
execsql/script/variables.py,sha256=ZSBGQUsoii6w3dLDVY9xxoPIV6wY0sAV_BNIQ6pgQAE,14328
|
|
90
90
|
execsql/utils/__init__.py,sha256=0uR6JwVJQRX3vceByNBduCAf5dd5assKjeqJUWvpZoA,278
|
|
91
91
|
execsql/utils/auth.py,sha256=onXzNkNZQZxGC5w7eey06sjvAIAX_Lf9g7nUJtcsel0,7009
|
|
92
92
|
execsql/utils/crypto.py,sha256=KlT6lPk-v0-qQQfAMfWvT9Q6kRdCD3_5JVaLajNocKM,3051
|
|
93
93
|
execsql/utils/datetime.py,sha256=rMCXAbvj6bxKCYzC97vrludO6PU5DYQ39buZ0smDC5A,3573
|
|
94
94
|
execsql/utils/errors.py,sha256=I3xiPyDYS8Ftv7Y8P0uqzrTKlCN1dTXe1pMKKFFjmoA,8651
|
|
95
|
-
execsql/utils/fileio.py,sha256=
|
|
95
|
+
execsql/utils/fileio.py,sha256=x0kl2oLydCIGbhRCAJDHFknUyGcbKEOnV_M4lWcFmTk,24259
|
|
96
96
|
execsql/utils/gui.py,sha256=h_7V2zYfp0l6C3Ft8QtUJAPgxCUrFkgIqwimz5s1bbQ,22821
|
|
97
97
|
execsql/utils/mail.py,sha256=4QXwdPgMh6vQBKLtkdH7IDh3JBczumc3l5up2AUSOZU,5483
|
|
98
98
|
execsql/utils/numeric.py,sha256=xh02ANSRk3nUpQ-rtm66ILoMqoi7HtzCoRMIOT9U8QI,1570
|
|
99
99
|
execsql/utils/regex.py,sha256=diEzTZqU_HHwVMadPAvN1Vgzhl7I03eVaEFGCXyGGL8,3770
|
|
100
100
|
execsql/utils/strings.py,sha256=5Dvzrk-9SIw2lpxXZQkiJbNyo1sy7iXXAtSULlZ0KG8,8488
|
|
101
101
|
execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
|
|
102
|
-
execsql2-2.16.
|
|
103
|
-
execsql2-2.16.
|
|
104
|
-
execsql2-2.16.
|
|
105
|
-
execsql2-2.16.
|
|
106
|
-
execsql2-2.16.
|
|
107
|
-
execsql2-2.16.
|
|
108
|
-
execsql2-2.16.
|
|
109
|
-
execsql2-2.16.
|
|
110
|
-
execsql2-2.16.
|
|
111
|
-
execsql2-2.16.
|
|
112
|
-
execsql2-2.16.
|
|
113
|
-
execsql2-2.16.
|
|
114
|
-
execsql2-2.16.
|
|
115
|
-
execsql2-2.16.
|
|
116
|
-
execsql2-2.16.
|
|
117
|
-
execsql2-2.16.
|
|
118
|
-
execsql2-2.16.
|
|
119
|
-
execsql2-2.16.
|
|
120
|
-
execsql2-2.16.
|
|
121
|
-
execsql2-2.16.
|
|
122
|
-
execsql2-2.16.
|
|
102
|
+
execsql2-2.16.13.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
|
|
103
|
+
execsql2-2.16.13.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
|
|
104
|
+
execsql2-2.16.13.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
|
|
105
|
+
execsql2-2.16.13.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
|
|
106
|
+
execsql2-2.16.13.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
|
|
107
|
+
execsql2-2.16.13.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
|
|
108
|
+
execsql2-2.16.13.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
|
|
109
|
+
execsql2-2.16.13.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
|
|
110
|
+
execsql2-2.16.13.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
|
|
111
|
+
execsql2-2.16.13.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
|
|
112
|
+
execsql2-2.16.13.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
|
|
113
|
+
execsql2-2.16.13.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
|
|
114
|
+
execsql2-2.16.13.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
|
|
115
|
+
execsql2-2.16.13.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
|
|
116
|
+
execsql2-2.16.13.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
|
|
117
|
+
execsql2-2.16.13.dist-info/METADATA,sha256=XBstvMhu4_kEtw4SOYQxdhMPR5UeTa8pEJ3gEkecmFI,20921
|
|
118
|
+
execsql2-2.16.13.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
119
|
+
execsql2-2.16.13.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
|
|
120
|
+
execsql2-2.16.13.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
|
|
121
|
+
execsql2-2.16.13.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
|
|
122
|
+
execsql2-2.16.13.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{execsql2-2.16.8.data → execsql2-2.16.13.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
|