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/cli/options.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Options shared by more than one command, each defined once.
|
|
2
|
+
|
|
3
|
+
A command declares a shared option by annotating a parameter with one of the
|
|
4
|
+
types below, so ``run`` and ``ping`` cannot drift apart on a flag's spelling,
|
|
5
|
+
metavar or help text. Options that belong to a single command stay in that
|
|
6
|
+
command's module.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Annotated
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ConfigFileOpt",
|
|
18
|
+
"DbTypeOpt",
|
|
19
|
+
"DsnOpt",
|
|
20
|
+
"NoPasswdOpt",
|
|
21
|
+
"OutputFormat",
|
|
22
|
+
"OutputFormatOpt",
|
|
23
|
+
"PortOpt",
|
|
24
|
+
"ScriptEncodingOpt",
|
|
25
|
+
"UserOpt",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# -- Connection (run, ping) ----------------------------------------------------
|
|
29
|
+
|
|
30
|
+
DbTypeOpt = Annotated[
|
|
31
|
+
str | None,
|
|
32
|
+
typer.Option(
|
|
33
|
+
"-t",
|
|
34
|
+
"--type",
|
|
35
|
+
metavar="{a,d,p,s,l,m,k,o,f}",
|
|
36
|
+
help=(
|
|
37
|
+
"Database type: a=MS-Access, p=PostgreSQL, "
|
|
38
|
+
"s=SQL Server, l=SQLite, m=MySQL/MariaDB, "
|
|
39
|
+
"k=DuckDB, o=Oracle, f=Firebird, "
|
|
40
|
+
"d=DSN."
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
DsnOpt = Annotated[
|
|
46
|
+
str | None,
|
|
47
|
+
typer.Option(
|
|
48
|
+
"--dsn",
|
|
49
|
+
"--connection-string",
|
|
50
|
+
metavar="URL",
|
|
51
|
+
help=(
|
|
52
|
+
"Database connection URL, e.g. postgresql://user:pass@host:5432/db. "
|
|
53
|
+
"Supported schemes: postgresql (postgres), mysql (mariadb), mssql (sqlserver), oracle, firebird, sqlite, duckdb. "
|
|
54
|
+
"Overrides -t/-u/-p and positional server/db args."
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
UserOpt = Annotated[str | None, typer.Option("-u", "--user", help="Database user name.")]
|
|
60
|
+
|
|
61
|
+
PortOpt = Annotated[int | None, typer.Option("-p", "--port", help="Database server port.")]
|
|
62
|
+
|
|
63
|
+
NoPasswdOpt = Annotated[
|
|
64
|
+
bool,
|
|
65
|
+
typer.Option("-w", "--no-passwd", help="Skip password prompt when user is specified."),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
# -- Configuration (run, ping, config) -----------------------------------------
|
|
69
|
+
|
|
70
|
+
ConfigFileOpt = Annotated[
|
|
71
|
+
str | None,
|
|
72
|
+
typer.Option(
|
|
73
|
+
"--config",
|
|
74
|
+
metavar="FILE",
|
|
75
|
+
help=(
|
|
76
|
+
"Path to an execsql configuration file. "
|
|
77
|
+
"Loaded after the implicit search paths so its values take precedence. "
|
|
78
|
+
"The file may chain additional configs via its [config] section."
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# -- Script files (run, format, lint) ------------------------------------------
|
|
84
|
+
|
|
85
|
+
ScriptEncodingOpt = Annotated[
|
|
86
|
+
str | None,
|
|
87
|
+
typer.Option(
|
|
88
|
+
"-f",
|
|
89
|
+
"--script-encoding",
|
|
90
|
+
metavar="NAME",
|
|
91
|
+
help="Character encoding of the script file. Default: [encoding] script from a config file, else UTF-8.",
|
|
92
|
+
),
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
# -- Machine-readable output (ping, config, list) ------------------------------
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class OutputFormat(str, Enum):
|
|
99
|
+
"""Output formats for commands that report data rather than check files."""
|
|
100
|
+
|
|
101
|
+
text = "text"
|
|
102
|
+
json = "json"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
OutputFormatOpt = Annotated[
|
|
106
|
+
OutputFormat,
|
|
107
|
+
typer.Option("--output-format", help="text for people; json for tools."),
|
|
108
|
+
]
|
execsql/cli/run.py
CHANGED
|
@@ -33,7 +33,6 @@ __all__ = ["_connect_initial_db", "_ping_db", "_print_dry_run", "_print_profile"
|
|
|
33
33
|
|
|
34
34
|
# Lint helper — imported lazily inside _run() to keep start-up cost low, but
|
|
35
35
|
# re-exported here so that tests and callers can reach it via cli.run.
|
|
36
|
-
from execsql.cli.lint import _print_lint_results # noqa: F401 — re-export (used by cli/__init__.py)
|
|
37
36
|
|
|
38
37
|
|
|
39
38
|
# ---------------------------------------------------------------------------
|
|
@@ -289,6 +288,21 @@ def _load_config(
|
|
|
289
288
|
return ConfigData(script_path, subvars, config_file=config_file)
|
|
290
289
|
|
|
291
290
|
|
|
291
|
+
def _tool_config(config_file: str | None) -> ConfigData:
|
|
292
|
+
"""The config ``execsql format`` and ``execsql lint`` read.
|
|
293
|
+
|
|
294
|
+
Read once per invocation, from the system, user and working-directory
|
|
295
|
+
locations plus *config_file* — not per script, so a script directory's
|
|
296
|
+
own ``execsql.conf`` applies only when passed with ``--config``.
|
|
297
|
+
"""
|
|
298
|
+
return _load_config(None, _seed_early_subvars(), config_file)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _configured_script_encoding(conf: ConfigData) -> str | None:
|
|
302
|
+
"""``[encoding] script`` from *conf*, or ``None`` when no config file sets it."""
|
|
303
|
+
return conf.script_encoding if "script_encoding" in conf.sources else None
|
|
304
|
+
|
|
305
|
+
|
|
292
306
|
def _seed_script_subvars(subvars: SubVarSet, script_name: str | None) -> None:
|
|
293
307
|
"""Add substitution variables that depend on the script path."""
|
|
294
308
|
from execsql.utils.errors import file_size_date
|
|
@@ -492,14 +506,16 @@ def _setup_logging(
|
|
|
492
506
|
use_gui: str | None,
|
|
493
507
|
no_passwd: bool,
|
|
494
508
|
import_buffer: int | None,
|
|
509
|
+
named_vars: list[tuple[str, str]] | None = None,
|
|
495
510
|
) -> Logger:
|
|
496
|
-
"""Create the execution logger, log initial info, and seed ``$RUN_ID
|
|
511
|
+
"""Create the execution logger, log initial info, and seed ``$RUN_ID``, ``-a`` and ``--var`` values."""
|
|
497
512
|
from execsql.utils.errors import file_size_date
|
|
498
513
|
|
|
499
514
|
opts_dict = {
|
|
500
515
|
k: v
|
|
501
516
|
for k, v in {
|
|
502
|
-
|
|
517
|
+
# Count only: -a values are opaque and often secrets.
|
|
518
|
+
"sub_vars": f"{len(sub_vars)} value(s)" if sub_vars else None,
|
|
503
519
|
"boolean_int": boolean_int,
|
|
504
520
|
"make_dirs": make_dirs,
|
|
505
521
|
"database_encoding": database_encoding,
|
|
@@ -520,7 +536,7 @@ def _setup_logging(
|
|
|
520
536
|
}
|
|
521
537
|
logger = Logger(
|
|
522
538
|
script_name or "<inline>",
|
|
523
|
-
conf.db or "",
|
|
539
|
+
conf.db or conf.db_file or "",
|
|
524
540
|
conf.server,
|
|
525
541
|
opts_dict,
|
|
526
542
|
conf.user_logfile,
|
|
@@ -537,6 +553,9 @@ def _setup_logging(
|
|
|
537
553
|
)
|
|
538
554
|
|
|
539
555
|
subvars.add_substitution("$RUN_ID", logger.run_id)
|
|
556
|
+
# Passwords from the DSN or config files, known before the first connection.
|
|
557
|
+
logger.add_redaction_value(getattr(conf, "db_password", None))
|
|
558
|
+
logger.add_redaction_value(getattr(conf, "smtp_password", None))
|
|
540
559
|
|
|
541
560
|
if sub_vars:
|
|
542
561
|
for n, repl in enumerate(sub_vars):
|
|
@@ -550,6 +569,13 @@ def _setup_logging(
|
|
|
550
569
|
logger.log_status_info(
|
|
551
570
|
f"Command-line substitution variable assignment: {var} set to {{***}}",
|
|
552
571
|
)
|
|
572
|
+
# --var values: named, set after config files so they win over
|
|
573
|
+
# [variables]; a SUB in the script can still reassign them. Values are
|
|
574
|
+
# redacted for the same reason as -a's.
|
|
575
|
+
for name, value in named_vars or ():
|
|
576
|
+
subvars.add_substitution(name, value)
|
|
577
|
+
logger.add_redaction_value(value)
|
|
578
|
+
logger.log_status_info(f"Command-line substitution variable assignment: {name} set to {{***}}")
|
|
553
579
|
|
|
554
580
|
return logger
|
|
555
581
|
|
|
@@ -587,12 +613,13 @@ def _run(
|
|
|
587
613
|
profile: bool = False,
|
|
588
614
|
profile_limit: int = 20,
|
|
589
615
|
ping: bool = False,
|
|
590
|
-
lint: bool = False,
|
|
591
616
|
debug: bool = False,
|
|
592
617
|
no_system_cmd: bool = False,
|
|
593
618
|
no_rm_file: bool = False,
|
|
594
619
|
no_serve: bool = False,
|
|
595
620
|
config_file: str | None = None,
|
|
621
|
+
named_vars: list[tuple[str, str]] | None = None,
|
|
622
|
+
manifest_path: str | None = None,
|
|
596
623
|
) -> None:
|
|
597
624
|
"""Initialise state, connect to the database, load the script, and run it.
|
|
598
625
|
|
|
@@ -605,12 +632,6 @@ def _run(
|
|
|
605
632
|
connection details (DBMS name, server version, and location), and calls
|
|
606
633
|
:func:`_ping_db` which raises ``SystemExit(0)``. No script is loaded or
|
|
607
634
|
executed. *script_name* and *command* may both be ``None`` in ping mode.
|
|
608
|
-
|
|
609
|
-
When *lint* is ``True``, the script is parsed and statically analysed for
|
|
610
|
-
structural issues (unmatched IF/ENDIF/LOOP/BATCH blocks, potentially
|
|
611
|
-
undefined variables, missing INCLUDE files, empty scripts) without
|
|
612
|
-
connecting to a database or executing anything. Exits with code 0 if no
|
|
613
|
-
errors were found, or code 1 if errors were found.
|
|
614
635
|
"""
|
|
615
636
|
import execsql.state as _state
|
|
616
637
|
|
|
@@ -698,6 +719,8 @@ def _run(
|
|
|
698
719
|
if sub_vars:
|
|
699
720
|
for n, repl in enumerate(sub_vars):
|
|
700
721
|
_state.subvars.add_substitution(f"$ARG_{n + 1}", repl)
|
|
722
|
+
for name, value in named_vars or ():
|
|
723
|
+
_state.subvars.add_substitution(name, value)
|
|
701
724
|
_ast_tree = _load_script(command, script_name, conf.script_encoding)
|
|
702
725
|
_print_dry_run(_ast_tree)
|
|
703
726
|
raise SystemExit(0)
|
|
@@ -713,27 +736,21 @@ def _run(
|
|
|
713
736
|
_state.dbs.add("initial", db)
|
|
714
737
|
_ping_db(db) # raises SystemExit
|
|
715
738
|
|
|
716
|
-
|
|
739
|
+
manifest = None
|
|
740
|
+
if manifest_path:
|
|
741
|
+
from execsql import manifest as _manifest
|
|
742
|
+
|
|
743
|
+
variables = [f"$ARG_{n + 1}" for n in range(len(sub_vars or ()))] + [name for name, _ in named_vars or ()]
|
|
744
|
+
manifest = _manifest.start(manifest_path, script_name, variables)
|
|
745
|
+
manifest.config_files = list(conf.files_read)
|
|
717
746
|
|
|
718
747
|
if _state.filewriter is None or not _state.filewriter.is_alive():
|
|
719
|
-
|
|
720
|
-
# can't leak responses or unconsumed commands into the new one. On
|
|
721
|
-
# macOS (`spawn`) the OS pipe buffer is small enough that retained
|
|
722
|
-
# entries from a crashed writer would deadlock the next put().
|
|
723
|
-
for q in (_fileio.fw_input, _fileio.fw_output):
|
|
724
|
-
try:
|
|
725
|
-
while True:
|
|
726
|
-
q.get_nowait()
|
|
727
|
-
except Exception:
|
|
728
|
-
pass
|
|
729
|
-
_fileio.filewriter = _state.filewriter = FileWriter(
|
|
730
|
-
_fileio.fw_input,
|
|
731
|
-
_fileio.fw_output,
|
|
748
|
+
_state.filewriter = FileWriter(
|
|
732
749
|
file_encoding=conf.output_encoding,
|
|
733
750
|
open_timeout=getattr(conf, "outfile_open_timeout", 10),
|
|
734
751
|
)
|
|
735
752
|
_state.filewriter.start()
|
|
736
|
-
atexit.register(filewriter_end)
|
|
753
|
+
atexit.register(filewriter_end, _state.filewriter)
|
|
737
754
|
|
|
738
755
|
# ------------------------------------------------------------------
|
|
739
756
|
# Logging
|
|
@@ -758,6 +775,7 @@ def _run(
|
|
|
758
775
|
use_gui=use_gui,
|
|
759
776
|
no_passwd=no_passwd,
|
|
760
777
|
import_buffer=import_buffer,
|
|
778
|
+
named_vars=named_vars,
|
|
761
779
|
)
|
|
762
780
|
|
|
763
781
|
# ------------------------------------------------------------------
|
|
@@ -765,11 +783,6 @@ def _run(
|
|
|
765
783
|
# ------------------------------------------------------------------
|
|
766
784
|
_ast_tree = _load_script(command, script_name, conf.script_encoding)
|
|
767
785
|
|
|
768
|
-
# ------------------------------------------------------------------
|
|
769
|
-
# NOTE: --lint is handled as an early exit in cli/__init__.py (AST
|
|
770
|
-
# linter) before _run() is called. No lint code path here.
|
|
771
|
-
# ------------------------------------------------------------------
|
|
772
|
-
|
|
773
786
|
# ------------------------------------------------------------------
|
|
774
787
|
# Start GUI console if requested
|
|
775
788
|
# ------------------------------------------------------------------
|
|
@@ -794,6 +807,8 @@ def _run(
|
|
|
794
807
|
_state.dbs.add("initial", db)
|
|
795
808
|
|
|
796
809
|
_state.exec_log.log_db_connect(db)
|
|
810
|
+
if manifest is not None:
|
|
811
|
+
manifest.set_database(db)
|
|
797
812
|
_state.subvars.add_substitution("$CURRENT_DBMS", db.type.dbms_id)
|
|
798
813
|
_state.subvars.add_substitution("$CURRENT_DATABASE", db.name())
|
|
799
814
|
_state.subvars.add_substitution("$DB_SERVER", db.server_name)
|
|
@@ -822,6 +837,8 @@ def _run(
|
|
|
822
837
|
|
|
823
838
|
if _ast_tree is not None:
|
|
824
839
|
_execute_script_ast(_ast_tree, conf, profile=profile, profile_limit=profile_limit)
|
|
840
|
+
if manifest is not None:
|
|
841
|
+
manifest.finish(0)
|
|
825
842
|
|
|
826
843
|
|
|
827
844
|
# ---------------------------------------------------------------------------
|
|
@@ -829,6 +846,28 @@ def _run(
|
|
|
829
846
|
# ---------------------------------------------------------------------------
|
|
830
847
|
|
|
831
848
|
|
|
849
|
+
def _end_run_on_sigterm() -> None:
|
|
850
|
+
"""Make SIGTERM end the process through ``SystemExit`` (exit status 143).
|
|
851
|
+
|
|
852
|
+
Python's default SIGTERM action stops the process without running
|
|
853
|
+
``atexit`` handlers, so the run's log (buffered until close) was lost,
|
|
854
|
+
the open transaction was not rolled back explicitly, and a running
|
|
855
|
+
``SYSTEM_CMD`` was left behind. ``timeout``, ``docker stop``, systemd
|
|
856
|
+
and CI cancellation all send SIGTERM. Installed by the console entry
|
|
857
|
+
point only: ``execsql.run()`` leaves the host program's signals alone.
|
|
858
|
+
"""
|
|
859
|
+
import signal
|
|
860
|
+
|
|
861
|
+
def _terminate(signum: int, _frame: Any) -> None:
|
|
862
|
+
import execsql.state as _state
|
|
863
|
+
|
|
864
|
+
if _state.exec_log is not None:
|
|
865
|
+
_state.exec_log.log_exit_terminated()
|
|
866
|
+
raise SystemExit(128 + signum)
|
|
867
|
+
|
|
868
|
+
signal.signal(signal.SIGTERM, _terminate)
|
|
869
|
+
|
|
870
|
+
|
|
832
871
|
def _execute_script_ast(
|
|
833
872
|
tree: Any,
|
|
834
873
|
conf: ConfigData,
|
|
@@ -856,7 +895,8 @@ def _execute_script_ast(
|
|
|
856
895
|
try:
|
|
857
896
|
execute(tree)
|
|
858
897
|
except SystemExit as exc:
|
|
859
|
-
|
|
898
|
+
terminated = _state.exec_log is not None and _state.exec_log.exit_type == "terminated"
|
|
899
|
+
if gui_console_isrunning() and conf.gui_wait_on_exit and not terminated:
|
|
860
900
|
gui_console_wait_user(
|
|
861
901
|
"Script complete; close the console window to exit execsql.",
|
|
862
902
|
)
|
|
@@ -868,6 +908,9 @@ def _execute_script_ast(
|
|
|
868
908
|
sys.exit(exc.code)
|
|
869
909
|
except ConfigError:
|
|
870
910
|
raise
|
|
911
|
+
except KeyboardInterrupt:
|
|
912
|
+
_state.exec_log.log_exit_interrupted()
|
|
913
|
+
raise
|
|
871
914
|
except ErrInfo as exc:
|
|
872
915
|
from execsql.utils.errors import exit_now
|
|
873
916
|
|
|
@@ -881,6 +924,7 @@ def _execute_script_ast(
|
|
|
881
924
|
exit_now(1, ErrInfo("exception", exception_msg=msg))
|
|
882
925
|
|
|
883
926
|
_state.dbs.do_rollback = False
|
|
927
|
+
_flush_output_files()
|
|
884
928
|
if gui_console_isrunning() and conf.gui_wait_on_exit:
|
|
885
929
|
gui_console_wait_user(
|
|
886
930
|
"Script complete; close the console window to exit execsql.",
|
|
@@ -943,10 +987,30 @@ def _execute_script_textual_console(tree: Any, conf: ConfigData) -> None:
|
|
|
943
987
|
exit_now(1, ErrInfo("exception", exception_msg=msg))
|
|
944
988
|
|
|
945
989
|
_state.dbs.do_rollback = False
|
|
990
|
+
_flush_output_files()
|
|
946
991
|
_state.exec_log.log_status_info(f"{_state.cmds_run} commands run")
|
|
947
992
|
_state.exec_log.log_exit_end()
|
|
948
993
|
|
|
949
994
|
|
|
995
|
+
def _flush_output_files() -> None:
|
|
996
|
+
"""Flush file output at the normal end of a script; lost output fails the run.
|
|
997
|
+
|
|
998
|
+
A WRITE/EXPORT target that stays locked past ``outfile_open_timeout`` has
|
|
999
|
+
its output discarded. That must not end in a successful exit status.
|
|
1000
|
+
"""
|
|
1001
|
+
from execsql.utils.fileio import filewriter_close_all_after_write
|
|
1002
|
+
|
|
1003
|
+
try:
|
|
1004
|
+
filewriter_close_all_after_write()
|
|
1005
|
+
except ErrInfo as exc:
|
|
1006
|
+
from execsql.utils.errors import exit_now
|
|
1007
|
+
|
|
1008
|
+
# The loss belongs to no particular statement; keep exit_now from
|
|
1009
|
+
# attributing it to whatever line happened to run last.
|
|
1010
|
+
exc.script_file = ""
|
|
1011
|
+
exit_now(1, exc)
|
|
1012
|
+
|
|
1013
|
+
|
|
950
1014
|
# ---------------------------------------------------------------------------
|
|
951
1015
|
# Utility: build the database connection for the initial/default database
|
|
952
1016
|
# ---------------------------------------------------------------------------
|