execsql2 2.23.1__py3-none-any.whl → 2.25.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- execsql/api.py +169 -160
- execsql/cli/__init__.py +19 -586
- execsql/cli/application.py +309 -0
- execsql/cli/commands/__init__.py +13 -0
- execsql/cli/commands/config.py +205 -0
- execsql/cli/commands/format.py +100 -0
- execsql/cli/commands/init.py +135 -0
- execsql/cli/commands/lint.py +225 -0
- execsql/cli/commands/listing.py +81 -0
- execsql/cli/commands/run.py +459 -0
- execsql/cli/dispatch.py +66 -0
- execsql/cli/dsn.py +11 -4
- execsql/cli/help.py +175 -24
- execsql/cli/lint.py +780 -100
- execsql/cli/options.py +108 -0
- execsql/cli/run.py +96 -32
- execsql/config.py +256 -173
- execsql/config_validation.py +282 -0
- execsql/data/execsql.conf.template +38 -5
- execsql/data/script_header.sql +23 -0
- execsql/db/access.py +29 -8
- execsql/db/base.py +40 -8
- execsql/db/dsn.py +13 -4
- execsql/db/duckdb.py +3 -1
- execsql/db/factory.py +13 -4
- execsql/db/firebird.py +21 -7
- execsql/db/mysql.py +37 -16
- execsql/db/oracle.py +49 -16
- execsql/db/postgres.py +30 -6
- execsql/db/sqlite.py +7 -9
- execsql/db/sqlserver.py +25 -8
- execsql/debug/repl.py +51 -193
- execsql/exceptions.py +9 -0
- execsql/exporters/delimited.py +87 -28
- execsql/exporters/xlsx.py +24 -9
- execsql/exporters/zip.py +1 -2
- execsql/format.py +115 -76
- execsql/gui/desktop.py +1 -3
- execsql/gui/tui.py +2 -2
- execsql/importers/base.py +35 -1
- execsql/importers/csv.py +3 -1
- execsql/interactive.py +232 -0
- execsql/manifest.py +200 -0
- execsql/metacommands/conditions.py +7 -8
- execsql/metacommands/control.py +2 -3
- execsql/metacommands/data.py +4 -1
- execsql/metacommands/debug.py +2 -4
- execsql/metacommands/effects.py +124 -0
- execsql/metacommands/io_fileops.py +62 -37
- execsql/metacommands/io_import.py +0 -1
- execsql/metacommands/prompt.py +2 -0
- execsql/metacommands/upsert.py +5 -2
- execsql/models.py +3 -2
- execsql/parser.py +14 -5
- execsql/plugins.py +24 -0
- execsql/script/__init__.py +0 -9
- execsql/script/ast.py +1 -1
- execsql/script/engine.py +46 -104
- execsql/script/executor.py +162 -39
- execsql/script/parser.py +167 -118
- execsql/state.py +34 -11
- execsql/state.pyi +11 -0
- execsql/types.py +22 -13
- execsql/utils/auth.py +169 -24
- execsql/utils/color.py +18 -0
- execsql/utils/datetime.py +55 -17
- execsql/utils/errors.py +82 -10
- execsql/utils/fileio.py +404 -117
- execsql/utils/gui.py +3 -0
- execsql/utils/mail.py +37 -12
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/README.md +1 -1
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/METADATA +122 -63
- execsql2-2.25.0.dist-info/RECORD +140 -0
- execsql2-2.25.0.dist-info/entry_points.txt +2 -0
- execsql2-2.23.1.dist-info/RECORD +0 -124
- execsql2-2.23.1.dist-info/entry_points.txt +0 -3
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/WHEEL +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/licenses/NOTICE +0 -0
execsql/api.py
CHANGED
|
@@ -15,7 +15,7 @@ Usage::
|
|
|
15
15
|
result = run(sql="CREATE TABLE t (id INT); INSERT INTO t VALUES (1);",
|
|
16
16
|
dsn="sqlite:///my.db")
|
|
17
17
|
|
|
18
|
-
# With substitution variables
|
|
18
|
+
# With substitution variables (referenced as !!$SCHEMA!! and !!$DATE!!)
|
|
19
19
|
result = run(script="etl.sql",
|
|
20
20
|
dsn="postgresql://user:pass@host/db",
|
|
21
21
|
variables={"SCHEMA": "public", "DATE": "2026-01-01"})
|
|
@@ -29,7 +29,6 @@ Usage::
|
|
|
29
29
|
|
|
30
30
|
from __future__ import annotations
|
|
31
31
|
|
|
32
|
-
import atexit
|
|
33
32
|
import dataclasses
|
|
34
33
|
import datetime
|
|
35
34
|
import io
|
|
@@ -57,7 +56,9 @@ class ScriptError:
|
|
|
57
56
|
|
|
58
57
|
Attributes:
|
|
59
58
|
message: Human-readable error description.
|
|
60
|
-
source: Script file path or ``"<inline>"
|
|
59
|
+
source: Script file path or ``"<inline>"`` for an error in the
|
|
60
|
+
script; ``"<config>"``, ``"<connect>"`` or ``"<output>"`` for a
|
|
61
|
+
config file, connection or output file that failed.
|
|
61
62
|
line: Source line number, or ``None`` if unknown.
|
|
62
63
|
sql: The SQL statement that caused the error, if applicable.
|
|
63
64
|
"""
|
|
@@ -77,10 +78,10 @@ class ScriptResult:
|
|
|
77
78
|
commands_run: Number of SQL statements and metacommands executed.
|
|
78
79
|
elapsed: Wall-clock execution time in seconds.
|
|
79
80
|
errors: List of errors encountered (empty on success).
|
|
80
|
-
variables: Final state of substitution variables, keyed by
|
|
81
|
-
without any sigil. Includes user-defined
|
|
82
|
-
and system variables (the leading ``$`` is
|
|
83
|
-
``$DATE_TAG`` appears as ``"
|
|
81
|
+
variables: Final state of substitution variables, keyed by
|
|
82
|
+
lower-case name without any sigil. Includes user-defined
|
|
83
|
+
variables (no prefix) and system variables (the leading ``$`` is
|
|
84
|
+
stripped, so ``$DATE_TAG`` appears as ``"date_tag"``). Environment (``&``),
|
|
84
85
|
column-data (``@``), script-local (``~``), and script-parameter
|
|
85
86
|
(``#``) variables are excluded.
|
|
86
87
|
"""
|
|
@@ -217,6 +218,8 @@ class _LibraryConfig:
|
|
|
217
218
|
self.smtp_password: str | None = None
|
|
218
219
|
self.smtp_ssl = False
|
|
219
220
|
self.smtp_tls = False
|
|
221
|
+
self.smtp_verify_certificate = True
|
|
222
|
+
self.smtp_ca_file: str | None = None
|
|
220
223
|
self.email_format = "plain"
|
|
221
224
|
self.email_css: str | None = None
|
|
222
225
|
|
|
@@ -352,7 +355,8 @@ def run(
|
|
|
352
355
|
connection: A pre-existing :class:`~execsql.db.base.Database`
|
|
353
356
|
instance. ``run()`` will NOT close this connection on exit.
|
|
354
357
|
variables: Substitution variables as ``{"NAME": "value"}``.
|
|
355
|
-
Keys without a ``$`` prefix get one added automatically
|
|
358
|
+
Keys without a ``$`` prefix get one added automatically, so
|
|
359
|
+
``{"SCHEMA": "public"}`` is referenced as ``!!$SCHEMA!!``.
|
|
356
360
|
config_file: Optional execsql configuration file to load.
|
|
357
361
|
encoding: Script file encoding (default ``"utf-8"``).
|
|
358
362
|
halt_on_error: If ``True`` (default), stop on the first SQL
|
|
@@ -373,7 +377,9 @@ def run(
|
|
|
373
377
|
|
|
374
378
|
Returns:
|
|
375
379
|
A :class:`ScriptResult` with execution outcome, timing, errors,
|
|
376
|
-
and final variable state.
|
|
380
|
+
and final variable state. A script that cannot be read, a bad
|
|
381
|
+
*config_file*, and a *dsn* that cannot be parsed or opened all
|
|
382
|
+
return ``success=False`` rather than raising.
|
|
377
383
|
|
|
378
384
|
Raises:
|
|
379
385
|
ValueError: If the argument combination is invalid (e.g. both
|
|
@@ -404,14 +410,8 @@ def run(
|
|
|
404
410
|
else:
|
|
405
411
|
assert sql is not None
|
|
406
412
|
tree = parse_string(sql, source_name="<inline>")
|
|
407
|
-
except ErrInfo as exc:
|
|
408
|
-
return
|
|
409
|
-
success=False,
|
|
410
|
-
commands_run=0,
|
|
411
|
-
elapsed=0.0,
|
|
412
|
-
errors=[ScriptError(message=exc.errmsg(), source=str(script) if script else "<inline>")],
|
|
413
|
-
variables={},
|
|
414
|
-
)
|
|
413
|
+
except (ErrInfo, OSError) as exc:
|
|
414
|
+
return _failed(_error_text(exc), str(script) if script else "<inline>")
|
|
415
415
|
|
|
416
416
|
# ------------------------------------------------------------------
|
|
417
417
|
# Build an isolated RuntimeContext
|
|
@@ -425,9 +425,14 @@ def run(
|
|
|
425
425
|
# Load a real ConfigData with the explicit config file
|
|
426
426
|
from execsql.script.variables import SubVarSet
|
|
427
427
|
|
|
428
|
+
if not Path(config_file).is_file():
|
|
429
|
+
return _failed(f"Config file {str(config_file)!r} does not exist.", "<config>")
|
|
428
430
|
temp_subvars = SubVarSet()
|
|
429
431
|
script_dir = str(Path(script).resolve().parent) if script else os.getcwd()
|
|
430
|
-
|
|
432
|
+
try:
|
|
433
|
+
conf = ConfigData(script_dir, temp_subvars, config_file=str(config_file))
|
|
434
|
+
except Exception as exc:
|
|
435
|
+
return _failed(_error_text(exc), "<config>")
|
|
431
436
|
# Apply any overrides
|
|
432
437
|
for k, v in conf_overrides.items():
|
|
433
438
|
setattr(conf, k, v)
|
|
@@ -492,89 +497,103 @@ def run(
|
|
|
492
497
|
# Connect to database
|
|
493
498
|
# ------------------------------------------------------------------
|
|
494
499
|
owns_connection = connection is None
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
db = connection
|
|
499
|
-
|
|
500
|
-
assert db is not None
|
|
501
|
-
assert ctx.dbs is not None
|
|
502
|
-
ctx.dbs.add("initial", db)
|
|
503
|
-
ctx.subvars.add_substitution("$CURRENT_DBMS", db.type.dbms_id)
|
|
504
|
-
ctx.subvars.add_substitution("$CURRENT_DATABASE", db.name())
|
|
505
|
-
ctx.subvars.add_substitution("$SYSTEM_CMD_EXIT_STATUS", "0")
|
|
506
|
-
|
|
507
|
-
# ------------------------------------------------------------------
|
|
508
|
-
# Execute
|
|
509
|
-
# ------------------------------------------------------------------
|
|
510
|
-
from execsql.script.executor import execute
|
|
511
|
-
|
|
512
|
-
errors: list[ScriptError] = []
|
|
513
|
-
t0 = time.perf_counter()
|
|
514
|
-
|
|
515
|
-
# WRITE ... TO <file> and TEE are handled by a FileWriter subprocess,
|
|
516
|
-
# and fileio drops every write when none is running. Start one here so
|
|
517
|
-
# the API behaves like the CLI.
|
|
518
|
-
_ensure_filewriter(conf)
|
|
519
|
-
|
|
500
|
+
# Everything from here on runs inside try/finally so an owned
|
|
501
|
+
# connection is closed and temp files removed however run() exits,
|
|
502
|
+
# including KeyboardInterrupt in a notebook.
|
|
520
503
|
try:
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
)
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
504
|
+
if dsn is not None:
|
|
505
|
+
try:
|
|
506
|
+
db = _connect_from_dsn(dsn, new_db=new_db)
|
|
507
|
+
except Exception as exc:
|
|
508
|
+
return _failed(_error_text(exc), "<connect>")
|
|
509
|
+
else:
|
|
510
|
+
db = connection
|
|
511
|
+
|
|
512
|
+
assert db is not None
|
|
513
|
+
assert ctx.dbs is not None
|
|
514
|
+
ctx.dbs.add("initial", db)
|
|
515
|
+
ctx.subvars.add_substitution("$CURRENT_DBMS", db.type.dbms_id)
|
|
516
|
+
ctx.subvars.add_substitution("$CURRENT_DATABASE", db.name())
|
|
517
|
+
ctx.subvars.add_substitution("$SYSTEM_CMD_EXIT_STATUS", "0")
|
|
518
|
+
|
|
519
|
+
# ------------------------------------------------------------------
|
|
520
|
+
# Execute
|
|
521
|
+
# ------------------------------------------------------------------
|
|
522
|
+
from execsql.script.executor import execute
|
|
523
|
+
|
|
524
|
+
errors: list[ScriptError] = []
|
|
525
|
+
t0 = time.perf_counter()
|
|
526
|
+
|
|
527
|
+
# WRITE ... TO <file> and TEE hand their output to a FileWriter, and
|
|
528
|
+
# fileio drops every write when none is running. This run gets its
|
|
529
|
+
# own, so concurrent runs never share files or a halt.
|
|
530
|
+
_start_filewriter(ctx, conf)
|
|
546
531
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
532
|
+
try:
|
|
533
|
+
execute(tree, ctx=ctx)
|
|
534
|
+
except SystemExit as exc:
|
|
535
|
+
# exit_now() calls sys.exit() — catch and convert to error
|
|
536
|
+
_capture_errors(ctx, errors, exc.code)
|
|
537
|
+
except ErrInfo as exc:
|
|
538
|
+
errors.append(
|
|
539
|
+
ScriptError(
|
|
540
|
+
message=exc.errmsg(),
|
|
541
|
+
source=_last_source(ctx),
|
|
542
|
+
line=_last_line(ctx),
|
|
543
|
+
sql=getattr(exc, "command_text", None),
|
|
544
|
+
),
|
|
545
|
+
)
|
|
546
|
+
except Exception as exc:
|
|
547
|
+
errors.append(ScriptError(message=str(exc), source="<runtime>"))
|
|
548
|
+
|
|
549
|
+
# Non-halting SQL errors (halt_on_error=False) are recorded in
|
|
550
|
+
# status.error_history as execution continues; a halting error never
|
|
551
|
+
# reaches the history, so there is no overlap with the exception
|
|
552
|
+
# paths above. They happened first, so they are listed first.
|
|
553
|
+
if ctx.status is not None:
|
|
554
|
+
errors[:0] = [
|
|
555
|
+
ScriptError(message=err_msg, source=err_source, line=err_line, sql=err_cmd)
|
|
556
|
+
for err_source, err_line, err_cmd, err_msg in ctx.status.error_history
|
|
557
|
+
]
|
|
558
|
+
|
|
559
|
+
elapsed = time.perf_counter() - t0
|
|
560
|
+
|
|
561
|
+
# Flush and close every file the script wrote, so the caller can read
|
|
562
|
+
# them the moment run() returns. Output to a file that could not be
|
|
563
|
+
# opened is reported as an error. The writer is stopped below.
|
|
564
|
+
from execsql.utils.fileio import filewriter_close_all_after_write
|
|
557
565
|
|
|
558
|
-
# ------------------------------------------------------------------
|
|
559
|
-
# Collect results
|
|
560
|
-
# ------------------------------------------------------------------
|
|
561
|
-
final_vars = {}
|
|
562
|
-
if ctx.subvars is not None:
|
|
563
|
-
for name, value in ctx.subvars.substitutions:
|
|
564
|
-
# Include user vars and $-prefixed system vars
|
|
565
|
-
# Skip environment (&), column (@), local (~), parameter (#) vars
|
|
566
|
-
if not name or name[0] in ("&", "@", "~", "#"):
|
|
567
|
-
continue
|
|
568
|
-
key = name.lstrip("$")
|
|
569
|
-
final_vars[key] = str(value) if value is not None else ""
|
|
570
|
-
|
|
571
|
-
# Close connection if we own it
|
|
572
|
-
if owns_connection:
|
|
573
566
|
try:
|
|
574
|
-
|
|
575
|
-
|
|
567
|
+
filewriter_close_all_after_write()
|
|
568
|
+
except ErrInfo as exc:
|
|
569
|
+
errors.append(ScriptError(message=exc.errmsg(), source="<output>"))
|
|
576
570
|
except Exception:
|
|
577
|
-
pass
|
|
571
|
+
pass # Best-effort: a failed flush must not mask a script error.
|
|
572
|
+
|
|
573
|
+
# ------------------------------------------------------------------
|
|
574
|
+
# Collect results
|
|
575
|
+
# ------------------------------------------------------------------
|
|
576
|
+
final_vars = {}
|
|
577
|
+
if ctx.subvars is not None:
|
|
578
|
+
for name, value in ctx.subvars.substitutions:
|
|
579
|
+
# Include user vars and $-prefixed system vars
|
|
580
|
+
# Skip environment (&), column (@), local (~), parameter (#) vars
|
|
581
|
+
if not name or name[0] in ("&", "@", "~", "#"):
|
|
582
|
+
continue
|
|
583
|
+
key = name.lstrip("$")
|
|
584
|
+
final_vars[key] = str(value) if value is not None else ""
|
|
585
|
+
finally:
|
|
586
|
+
if ctx.filewriter is not None:
|
|
587
|
+
from execsql.utils.fileio import filewriter_end
|
|
588
|
+
|
|
589
|
+
filewriter_end(ctx.filewriter)
|
|
590
|
+
if owns_connection and ctx.dbs is not None:
|
|
591
|
+
try:
|
|
592
|
+
ctx.dbs.closeall()
|
|
593
|
+
except Exception:
|
|
594
|
+
pass
|
|
595
|
+
if ctx.tempfiles is not None:
|
|
596
|
+
ctx.tempfiles.remove_all()
|
|
578
597
|
|
|
579
598
|
return ScriptResult(
|
|
580
599
|
success=len(errors) == 0,
|
|
@@ -588,80 +607,70 @@ def run(
|
|
|
588
607
|
# ---------------------------------------------------------------------------
|
|
589
608
|
# Helpers
|
|
590
609
|
# ---------------------------------------------------------------------------
|
|
591
|
-
def
|
|
592
|
-
"""
|
|
610
|
+
def _start_filewriter(ctx: RuntimeContext, conf: Any) -> None:
|
|
611
|
+
"""Give this run its own FileWriter thread, held on *ctx*.
|
|
612
|
+
|
|
613
|
+
``WRITE ... TO <file>`` and ``TEE`` hand their output to the active run's
|
|
614
|
+
writer, and every entry point in :mod:`execsql.utils.fileio` drops the
|
|
615
|
+
write when none is running. A writer per run keeps concurrent runs from
|
|
616
|
+
sharing open files or reply queues, and a halt in one run (which stops
|
|
617
|
+
its writer) from discarding another run's output. :func:`run` stops the
|
|
618
|
+
writer before it returns.
|
|
619
|
+
"""
|
|
620
|
+
from execsql.utils.fileio import FileWriter
|
|
593
621
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
silently discarded and the run still reported success.
|
|
622
|
+
ctx.filewriter = FileWriter(
|
|
623
|
+
file_encoding=conf.output_encoding,
|
|
624
|
+
open_timeout=getattr(conf, "outfile_open_timeout", 10),
|
|
625
|
+
)
|
|
626
|
+
ctx.filewriter.start()
|
|
600
627
|
|
|
601
|
-
A writer started here outlives the call and is reused by a later
|
|
602
|
-
:func:`run`, exactly as the CLI keeps one for the life of the process; it
|
|
603
|
-
is reaped by an ``atexit`` handler. A writer the caller started themselves
|
|
604
|
-
is left alone.
|
|
605
628
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
running.
|
|
609
|
-
"""
|
|
610
|
-
import execsql.state as _state
|
|
611
|
-
import execsql.utils.fileio as _fileio
|
|
612
|
-
from execsql.utils.fileio import FileWriter, filewriter_end
|
|
613
|
-
|
|
614
|
-
if _fileio.filewriter is not None and _fileio.filewriter.is_alive():
|
|
615
|
-
return False
|
|
616
|
-
|
|
617
|
-
# Drain stale messages so a previously-dead subprocess cannot leak
|
|
618
|
-
# responses or unconsumed commands into the new one. On macOS (`spawn`)
|
|
619
|
-
# the OS pipe buffer is small enough that retained entries would deadlock
|
|
620
|
-
# the next put().
|
|
621
|
-
for q in (_fileio.fw_input, _fileio.fw_output):
|
|
622
|
-
try:
|
|
623
|
-
while True:
|
|
624
|
-
q.get_nowait()
|
|
625
|
-
except Exception:
|
|
626
|
-
pass
|
|
629
|
+
def _capture_errors(ctx: RuntimeContext, errors: list[ScriptError], exit_code: Any) -> None:
|
|
630
|
+
"""Record the error that halted the run, after ``exit_now()`` raised ``SystemExit``.
|
|
627
631
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
return True
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
def _capture_errors(ctx: RuntimeContext, errors: list[ScriptError]) -> None:
|
|
648
|
-
"""Extract error info from the current context after a SystemExit."""
|
|
649
|
-
last_error = None
|
|
650
|
-
error_msg = None
|
|
651
|
-
if ctx.subvars is not None:
|
|
652
|
-
subs = dict(ctx.subvars.substitutions)
|
|
653
|
-
last_error = subs.get("$LAST_ERROR")
|
|
654
|
-
error_msg = subs.get("$ERROR_MESSAGE")
|
|
655
|
-
|
|
656
|
-
msg = error_msg or last_error or "Script execution failed"
|
|
632
|
+
``exit_now`` leaves the halting :class:`ErrInfo` on the context, and any
|
|
633
|
+
file output lost at the halt as a second error. A failed
|
|
634
|
+
SQL statement's text is in ``$LAST_ERROR``, which the executor sets just
|
|
635
|
+
before halting; it is reported only when the halt came from SQL, so a
|
|
636
|
+
metacommand error is never paired with an earlier statement. A ``HALT``
|
|
637
|
+
carries no error, only its exit status.
|
|
638
|
+
"""
|
|
639
|
+
err = ctx.halt_error
|
|
640
|
+
if err is None:
|
|
641
|
+
msg = f"Script halted by HALT with exit status {exit_code}."
|
|
642
|
+
else:
|
|
643
|
+
msg = err.errmsg()
|
|
644
|
+
sql = None
|
|
645
|
+
lc = ctx.last_command
|
|
646
|
+
if err is not None and lc is not None and lc.command_type == "sql" and ctx.subvars is not None:
|
|
647
|
+
sql = ctx.subvars.varvalue("$LAST_ERROR") or None
|
|
657
648
|
errors.append(
|
|
658
649
|
ScriptError(
|
|
659
|
-
message=
|
|
650
|
+
message=msg,
|
|
660
651
|
source=_last_source(ctx),
|
|
661
652
|
line=_last_line(ctx),
|
|
662
|
-
sql=
|
|
653
|
+
sql=sql,
|
|
663
654
|
),
|
|
664
655
|
)
|
|
656
|
+
if ctx.halt_output_error is not None:
|
|
657
|
+
errors.append(ScriptError(message=ctx.halt_output_error.errmsg(), source="<output>"))
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _failed(message: str, source: str) -> ScriptResult:
|
|
661
|
+
"""A result for a run that failed before any of the script ran."""
|
|
662
|
+
return ScriptResult(
|
|
663
|
+
success=False,
|
|
664
|
+
commands_run=0,
|
|
665
|
+
elapsed=0.0,
|
|
666
|
+
errors=[ScriptError(message=message, source=source)],
|
|
667
|
+
variables={},
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _error_text(exc: BaseException) -> str:
|
|
672
|
+
"""The message of *exc*: the full text for an :class:`ErrInfo`, else ``str()``."""
|
|
673
|
+
return exc.errmsg() if isinstance(exc, ErrInfo) else str(exc)
|
|
665
674
|
|
|
666
675
|
|
|
667
676
|
def _last_source(ctx: RuntimeContext) -> str:
|