execsql2 2.16.12__py3-none-any.whl → 2.16.14__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/db/base.py +2 -0
- execsql/format.py +288 -15
- execsql/metacommands/io_export.py +14 -10
- execsql/metacommands/io_fileops.py +4 -6
- execsql/script/ast.py +3 -0
- execsql/script/executor.py +7 -3
- execsql/script/parser.py +20 -14
- {execsql2-2.16.12.dist-info → execsql2-2.16.14.dist-info}/METADATA +1 -1
- {execsql2-2.16.12.dist-info → execsql2-2.16.14.dist-info}/RECORD +28 -28
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.16.12.data → execsql2-2.16.14.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.16.12.dist-info → execsql2-2.16.14.dist-info}/WHEEL +0 -0
- {execsql2-2.16.12.dist-info → execsql2-2.16.14.dist-info}/entry_points.txt +0 -0
- {execsql2-2.16.12.dist-info → execsql2-2.16.14.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.16.12.dist-info → execsql2-2.16.14.dist-info}/licenses/NOTICE +0 -0
execsql/db/base.py
CHANGED
|
@@ -226,6 +226,7 @@ class Database(ABC):
|
|
|
226
226
|
try:
|
|
227
227
|
curs.execute(sql)
|
|
228
228
|
except Exception:
|
|
229
|
+
curs.close()
|
|
229
230
|
self.rollback()
|
|
230
231
|
raise
|
|
231
232
|
try:
|
|
@@ -264,6 +265,7 @@ class Database(ABC):
|
|
|
264
265
|
try:
|
|
265
266
|
curs.execute(sql)
|
|
266
267
|
except Exception:
|
|
268
|
+
curs.close()
|
|
267
269
|
self.rollback()
|
|
268
270
|
raise
|
|
269
271
|
try:
|
execsql/format.py
CHANGED
|
@@ -148,22 +148,66 @@ def _is_comment_line(line: str, in_block: bool) -> tuple[bool, bool]:
|
|
|
148
148
|
return False, False
|
|
149
149
|
|
|
150
150
|
|
|
151
|
-
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]:
|
|
152
156
|
"""Format a list of SQL-only lines (no comment-only lines) via sqlglot."""
|
|
153
157
|
text = "\n".join(sql_lines)
|
|
154
158
|
protected, replacements = _protect_variables(text)
|
|
159
|
+
|
|
160
|
+
# Count semicolons in input as a rough statement count.
|
|
161
|
+
input_semis = protected.count(";")
|
|
162
|
+
|
|
155
163
|
try:
|
|
156
164
|
with contextlib.redirect_stderr(io.StringIO()):
|
|
157
165
|
ast = sqlglot.parse(protected, read="postgres", error_level=sqlglot.errors.ErrorLevel.IGNORE)
|
|
158
|
-
statements = [
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
)
|
|
163
185
|
stmts = [s for s in statements if s]
|
|
164
186
|
if not stmts:
|
|
165
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
|
+
|
|
166
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
|
|
167
211
|
joined = re.sub(r"\bINTO TEMPORARY\b(?!\s+TABLE)", "INTO TEMPORARY TABLE", joined)
|
|
168
212
|
return _restore_variables(joined, replacements).split("\n")
|
|
169
213
|
except Exception:
|
|
@@ -175,7 +219,194 @@ def _sqlglot_format(sql_lines: list[str]) -> list[str]:
|
|
|
175
219
|
# ---------------------------------------------------------------------------
|
|
176
220
|
|
|
177
221
|
|
|
178
|
-
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]:
|
|
179
410
|
"""Re-indent a SQL block to the current depth, optionally formatting via sqlglot."""
|
|
180
411
|
if not lines:
|
|
181
412
|
return lines
|
|
@@ -191,6 +422,13 @@ def format_sql_block(lines: list[str], depth: int, indent: int, use_sql: bool) -
|
|
|
191
422
|
if not use_sql:
|
|
192
423
|
return [target_prefix + line if line.strip() else "" for line in rebased]
|
|
193
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
|
+
|
|
194
432
|
result: list[str] = []
|
|
195
433
|
seg: list[str] = []
|
|
196
434
|
seg_is_comment: bool | None = None
|
|
@@ -202,7 +440,7 @@ def format_sql_block(lines: list[str], depth: int, indent: int, use_sql: bool) -
|
|
|
202
440
|
if seg_is_comment:
|
|
203
441
|
result.extend(seg)
|
|
204
442
|
else:
|
|
205
|
-
result.extend(_sqlglot_format(seg))
|
|
443
|
+
result.extend(_sqlglot_format(seg, sql_indent=indent, leading_comma=leading_comma))
|
|
206
444
|
seg.clear()
|
|
207
445
|
|
|
208
446
|
for line in rebased:
|
|
@@ -238,34 +476,58 @@ def format_metacommand(payload: str, depth: int, indent: int) -> str:
|
|
|
238
476
|
return f"{prefix}-- !x! {keyword}"
|
|
239
477
|
|
|
240
478
|
|
|
241
|
-
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:
|
|
242
480
|
"""Format the source text of an execsql script and return the result."""
|
|
243
481
|
depth = 0
|
|
244
482
|
sql_acc: list[str] = []
|
|
245
483
|
output: list[str] = []
|
|
246
484
|
|
|
247
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
|
|
248
492
|
|
|
249
493
|
def flush_sql() -> None:
|
|
250
|
-
nonlocal in_dollar_quote
|
|
494
|
+
nonlocal in_dollar_quote, in_sql_statement
|
|
251
495
|
if sql_acc:
|
|
252
496
|
# If any line in the accumulated block is inside a $$-delimited
|
|
253
497
|
# region, skip sqlglot formatting entirely. PL/pgSQL function
|
|
254
498
|
# bodies contain IF/END IF, LOOP, RETURN, etc. that sqlglot does
|
|
255
499
|
# not understand and will corrupt (e.g., rewriting to COMMIT).
|
|
256
500
|
safe_for_sqlglot = use_sql and not in_dollar_quote
|
|
257
|
-
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))
|
|
258
502
|
sql_acc.clear()
|
|
503
|
+
in_sql_statement = False
|
|
259
504
|
|
|
260
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
|
+
|
|
261
521
|
m = METACOMMAND_RE.match(raw_line)
|
|
262
522
|
|
|
263
|
-
if not
|
|
264
|
-
if not in_dollar_quote:
|
|
523
|
+
if not stripped_line:
|
|
524
|
+
if not in_dollar_quote and not in_sql_statement:
|
|
265
525
|
flush_sql()
|
|
526
|
+
output.append("")
|
|
266
527
|
else:
|
|
528
|
+
# Mid-statement blank line stays in the accumulator and
|
|
529
|
+
# will appear in the output when the block is formatted.
|
|
267
530
|
sql_acc.append(raw_line)
|
|
268
|
-
output.append("")
|
|
269
531
|
|
|
270
532
|
elif m:
|
|
271
533
|
flush_sql()
|
|
@@ -296,6 +558,12 @@ def format_file(source: str, indent: int = 4, use_sql: bool = True) -> str:
|
|
|
296
558
|
if "$$" in raw_line and raw_line.count("$$") % 2 == 1:
|
|
297
559
|
in_dollar_quote = not in_dollar_quote
|
|
298
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
|
|
299
567
|
|
|
300
568
|
flush_sql()
|
|
301
569
|
|
|
@@ -350,6 +618,11 @@ def main() -> None:
|
|
|
350
618
|
in_place: bool = typer.Option(False, "-i", "--in-place", help="Modify files in place."),
|
|
351
619
|
no_sql: bool = typer.Option(False, "--no-sql", help="Skip SQL formatting via sqlglot."),
|
|
352
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
|
+
),
|
|
353
626
|
) -> None:
|
|
354
627
|
use_sql = not no_sql
|
|
355
628
|
paths = collect_paths(targets)
|
|
@@ -365,7 +638,7 @@ def main() -> None:
|
|
|
365
638
|
_err_console.print(f"[bold red]Error:[/bold red] reading {path}: {exc}")
|
|
366
639
|
raise typer.Exit(code=1) from None
|
|
367
640
|
|
|
368
|
-
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)
|
|
369
642
|
|
|
370
643
|
if check:
|
|
371
644
|
if formatted != source:
|
|
@@ -136,16 +136,20 @@ def _dispatch_format(
|
|
|
136
136
|
raise
|
|
137
137
|
except Exception as e:
|
|
138
138
|
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
139
|
+
try:
|
|
140
|
+
if filefmt == "raw":
|
|
141
|
+
write_query_raw(outfile, rows, db.encoding, append, zipfile=zipfilename)
|
|
142
|
+
elif filefmt == "b64":
|
|
143
|
+
write_query_b64(outfile, rows, append, zipfile=zipfilename)
|
|
144
|
+
elif filefmt == "feather":
|
|
145
|
+
write_query_to_feather(outfile, hdrs, rows)
|
|
146
|
+
elif filefmt == "parquet":
|
|
147
|
+
write_query_to_parquet(outfile, hdrs, rows)
|
|
148
|
+
else:
|
|
149
|
+
write_delimited_file(outfile, filefmt, hdrs, rows, _state.conf.output_encoding, append, zipfilename)
|
|
150
|
+
except BaseException:
|
|
151
|
+
rows.close()
|
|
152
|
+
raise
|
|
149
153
|
|
|
150
154
|
|
|
151
155
|
# ---------------------------------------------------------------------------
|
|
@@ -106,10 +106,9 @@ def x_copy(**kwargs: Any) -> None:
|
|
|
106
106
|
try:
|
|
107
107
|
db2.populate_table(schema2, table2, rows, hdrs, get_ts)
|
|
108
108
|
db2.commit()
|
|
109
|
-
except
|
|
109
|
+
except BaseException:
|
|
110
|
+
rows.close()
|
|
110
111
|
raise
|
|
111
|
-
except Exception as e:
|
|
112
|
-
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
113
112
|
|
|
114
113
|
|
|
115
114
|
def x_copy_query(**kwargs: Any) -> None:
|
|
@@ -181,10 +180,9 @@ def x_copy_query(**kwargs: Any) -> None:
|
|
|
181
180
|
try:
|
|
182
181
|
db2.populate_table(schema2, table2, rows, hdrs, get_ts)
|
|
183
182
|
db2.commit()
|
|
184
|
-
except
|
|
183
|
+
except BaseException:
|
|
184
|
+
rows.close()
|
|
185
185
|
raise
|
|
186
|
-
except Exception as e:
|
|
187
|
-
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
188
186
|
|
|
189
187
|
|
|
190
188
|
def x_zip(**kwargs: Any) -> None:
|
execsql/script/ast.py
CHANGED
|
@@ -209,11 +209,14 @@ class ElseIfClause:
|
|
|
209
209
|
Attributes:
|
|
210
210
|
condition: The condition expression text (e.g. ``"HAS_ROWS"``).
|
|
211
211
|
span: Source location of the ELSEIF line itself.
|
|
212
|
+
condition_modifiers: ANDIF/ORIF modifiers that compound the ELSEIF
|
|
213
|
+
condition, evaluated left-to-right at runtime.
|
|
212
214
|
body: Nodes executed when this condition is true.
|
|
213
215
|
"""
|
|
214
216
|
|
|
215
217
|
condition: str
|
|
216
218
|
span: SourceSpan
|
|
219
|
+
condition_modifiers: list[ConditionModifier] = field(default_factory=list)
|
|
217
220
|
body: list[Node] = field(default_factory=list)
|
|
218
221
|
|
|
219
222
|
|
execsql/script/executor.py
CHANGED
|
@@ -384,6 +384,12 @@ def _execute_node(
|
|
|
384
384
|
ctx.last_command = _FakeScriptCmd(node)
|
|
385
385
|
_execute_include(ctx, node, localvars)
|
|
386
386
|
|
|
387
|
+
else:
|
|
388
|
+
raise ErrInfo(
|
|
389
|
+
type="error",
|
|
390
|
+
other_msg=f"Unhandled AST node type: {type(node).__name__} at {node.span}",
|
|
391
|
+
)
|
|
392
|
+
|
|
387
393
|
|
|
388
394
|
# ---------------------------------------------------------------------------
|
|
389
395
|
# Block executors
|
|
@@ -404,9 +410,7 @@ def _execute_if(
|
|
|
404
410
|
|
|
405
411
|
# Try ELSEIF clauses
|
|
406
412
|
for clause in node.elseif_clauses:
|
|
407
|
-
|
|
408
|
-
expanded = substitute_vars(clause.condition, effective_locals, ctx=ctx)
|
|
409
|
-
if xcmd_test(expanded):
|
|
413
|
+
if _eval_condition(ctx, clause.condition, clause.condition_modifiers):
|
|
410
414
|
_execute_nodes(ctx, clause.body, node.span.file, localvars, in_loop=in_loop)
|
|
411
415
|
return
|
|
412
416
|
|
execsql/script/parser.py
CHANGED
|
@@ -580,14 +580,17 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
580
580
|
command_text=line,
|
|
581
581
|
other_msg=f"ANDIF without matching IF on line {file_lineno} of {source_name}.",
|
|
582
582
|
)
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
condition=m.group("cond").strip(),
|
|
588
|
-
span=SourceSpan(source_name, file_lineno),
|
|
589
|
-
),
|
|
583
|
+
modifier = ConditionModifier(
|
|
584
|
+
kind="AND",
|
|
585
|
+
condition=m.group("cond").strip(),
|
|
586
|
+
span=SourceSpan(source_name, file_lineno),
|
|
590
587
|
)
|
|
588
|
+
frame = block_stack[-1]
|
|
589
|
+
if_node = frame.node
|
|
590
|
+
if frame._in_elseif and if_node.elseif_clauses: # type: ignore[union-attr]
|
|
591
|
+
if_node.elseif_clauses[-1].condition_modifiers.append(modifier) # type: ignore[union-attr]
|
|
592
|
+
else:
|
|
593
|
+
if_node.condition_modifiers.append(modifier) # type: ignore[union-attr]
|
|
591
594
|
continue
|
|
592
595
|
|
|
593
596
|
# -- ORIF --
|
|
@@ -599,14 +602,17 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
|
|
|
599
602
|
command_text=line,
|
|
600
603
|
other_msg=f"ORIF without matching IF on line {file_lineno} of {source_name}.",
|
|
601
604
|
)
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
condition=m.group("cond").strip(),
|
|
607
|
-
span=SourceSpan(source_name, file_lineno),
|
|
608
|
-
),
|
|
605
|
+
modifier = ConditionModifier(
|
|
606
|
+
kind="OR",
|
|
607
|
+
condition=m.group("cond").strip(),
|
|
608
|
+
span=SourceSpan(source_name, file_lineno),
|
|
609
609
|
)
|
|
610
|
+
frame = block_stack[-1]
|
|
611
|
+
if_node = frame.node
|
|
612
|
+
if frame._in_elseif and if_node.elseif_clauses: # type: ignore[union-attr]
|
|
613
|
+
if_node.elseif_clauses[-1].condition_modifiers.append(modifier) # type: ignore[union-attr]
|
|
614
|
+
else:
|
|
615
|
+
if_node.condition_modifiers.append(modifier) # type: ignore[union-attr]
|
|
610
616
|
continue
|
|
611
617
|
|
|
612
618
|
# -- ELSE --
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.16.
|
|
3
|
+
Version: 2.16.14
|
|
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
|
|
@@ -18,7 +18,7 @@ execsql/cli/lint_ast.py,sha256=c9UEFsZ7PZlFdrK0zJCe-WXfCqvS3WlOUWEycZozqB8,14688
|
|
|
18
18
|
execsql/cli/run.py,sha256=QrJ5SqIGiRO5AXavmxyhlJCMGtgZydC8MtHHAieQ4II,34571
|
|
19
19
|
execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
|
|
20
20
|
execsql/db/access.py,sha256=dAFuP1YeL7e7sy13T-9wll2Av5cr_PcUdxHf1NlvRNY,18204
|
|
21
|
-
execsql/db/base.py,sha256
|
|
21
|
+
execsql/db/base.py,sha256=lUhS9yGNX4Nhxfu1wxjLdGbLoabcDYXZQB_5VUw5kqE,31372
|
|
22
22
|
execsql/db/dsn.py,sha256=ZkKQrRgNr8VilOhE7jzLIZKVhrmEL0Bt3BppegQ03KM,5486
|
|
23
23
|
execsql/db/duckdb.py,sha256=79lRzKRhw1Pjfqcrba27S4Oq8a8AbDO_d0XkaNKKPQo,3197
|
|
24
24
|
execsql/db/factory.py,sha256=YHdgyqQYy16548O3fGyElLC5C7DdIgva4Z29OsDxXjs,5367
|
|
@@ -72,8 +72,8 @@ execsql/metacommands/data.py,sha256=tRQBGTAuW-eJ2tBNWaoZI9OjTyNNyHJISo7gOdL-sm8,
|
|
|
72
72
|
execsql/metacommands/debug.py,sha256=3QVm0N5uc7mcZco36kqlDq8tiWW0sdo8E8BoQZkE_DI,11784
|
|
73
73
|
execsql/metacommands/dispatch.py,sha256=Gao7rwiBFdghmYKcbWVm21Va30evobQuoghLY_FOZto,87602
|
|
74
74
|
execsql/metacommands/io.py,sha256=vlGBje5sgnqeilooMdhJDgSRIhysHy5_7LrKtik9Xjs,3011
|
|
75
|
-
execsql/metacommands/io_export.py,sha256=
|
|
76
|
-
execsql/metacommands/io_fileops.py,sha256=
|
|
75
|
+
execsql/metacommands/io_export.py,sha256=iX4L3lpZGT2P5ca2C3XJ3MYxXJ_loC7nWmfKcWv4UGQ,13379
|
|
76
|
+
execsql/metacommands/io_fileops.py,sha256=K7eHMAuCUEVpNITYnJhWaSmXtAnnx1ZaNIFnOwKtXzc,9456
|
|
77
77
|
execsql/metacommands/io_import.py,sha256=dXI4Bpar0i3Swm7xU-0naY8zLmV96Ie0hsrqBus22Nc,14176
|
|
78
78
|
execsql/metacommands/io_write.py,sha256=eayUxsLnEDqp2R5iqlEjeOK8DxyoOFkG9hBe2JkVR0A,8243
|
|
79
79
|
execsql/metacommands/prompt.py,sha256=E2e7q4pxbl_wEBrhco0B2gm5hO_HG3rNIF75PLdTgGg,36767
|
|
@@ -81,11 +81,11 @@ 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=AviMXseSpZtaPpJtJEs3olaXuk23kN_dU5raHbymy6s,20266
|
|
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=RclV-uG33yEWs8p_sj9KbW5IpHbmkRyG2DO5L4UJK50,37678
|
|
88
|
+
execsql/script/parser.py,sha256=a8ufVvyK6X-E6PbTiOpETscyi38XbiTypkgP8COHrQg,32171
|
|
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
|
|
@@ -99,24 +99,24 @@ 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.14.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
|
|
103
|
+
execsql2-2.16.14.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
|
|
104
|
+
execsql2-2.16.14.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
|
|
105
|
+
execsql2-2.16.14.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
|
|
106
|
+
execsql2-2.16.14.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
|
|
107
|
+
execsql2-2.16.14.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
|
|
108
|
+
execsql2-2.16.14.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
|
|
109
|
+
execsql2-2.16.14.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
|
|
110
|
+
execsql2-2.16.14.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
|
|
111
|
+
execsql2-2.16.14.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
|
|
112
|
+
execsql2-2.16.14.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
|
|
113
|
+
execsql2-2.16.14.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
|
|
114
|
+
execsql2-2.16.14.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
|
|
115
|
+
execsql2-2.16.14.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
|
|
116
|
+
execsql2-2.16.14.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
|
|
117
|
+
execsql2-2.16.14.dist-info/METADATA,sha256=APNebYdwunkij8RDQOlTNkFS-BKMASdRLhNJefPx75o,20921
|
|
118
|
+
execsql2-2.16.14.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
119
|
+
execsql2-2.16.14.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
|
|
120
|
+
execsql2-2.16.14.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
|
|
121
|
+
execsql2-2.16.14.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
|
|
122
|
+
execsql2-2.16.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{execsql2-2.16.12.data → execsql2-2.16.14.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
|