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/__init__.py
CHANGED
|
@@ -5,10 +5,13 @@ initialisation, database connection, and script execution.
|
|
|
5
5
|
|
|
6
6
|
Submodules:
|
|
7
7
|
|
|
8
|
-
- :mod:`execsql.cli.
|
|
8
|
+
- :mod:`execsql.cli.application` — the Typer app, its command group, and plain help rendering
|
|
9
|
+
- :mod:`execsql.cli.commands` — one module per command (``run``, ``format``, ``lint``)
|
|
10
|
+
- :mod:`execsql.cli.dispatch` — the rule that inserts ``run`` when no command is given
|
|
11
|
+
- :mod:`execsql.cli.help` — metacommand/encoding tables, --init-config, console objects
|
|
9
12
|
- :mod:`execsql.cli.dsn` — Connection-string (DSN URL) parser
|
|
10
13
|
- :mod:`execsql.cli.run` — Core execution logic (``_run``, ``_connect_initial_db``, ``_ping_db``, ``_print_dry_run``, ``_print_profile``)
|
|
11
|
-
- :mod:`execsql.cli.lint` — AST-based
|
|
14
|
+
- :mod:`execsql.cli.lint` — AST-based static analyser behind ``execsql lint``, and its output formats
|
|
12
15
|
"""
|
|
13
16
|
|
|
14
17
|
from __future__ import annotations
|
|
@@ -17,13 +20,14 @@ import sys
|
|
|
17
20
|
import traceback
|
|
18
21
|
from pathlib import Path
|
|
19
22
|
|
|
20
|
-
import
|
|
21
|
-
|
|
22
|
-
from execsql import
|
|
23
|
+
from execsql.cli import commands # noqa: F401 — registers every command on app
|
|
24
|
+
from execsql.cli.application import app
|
|
25
|
+
from execsql.cli.commands.run import main
|
|
23
26
|
from execsql.cli.dsn import _parse_connection_string, _SCHEME_TO_DBTYPE # noqa: F401 — re-export
|
|
24
27
|
from execsql.cli.help import _console, _err_console, _init_config, _print_encodings, _print_metacommands # noqa: F401 — re-export
|
|
25
28
|
from execsql.cli.run import _connect_initial_db, _run # noqa: F401 — re-export
|
|
26
|
-
from execsql.
|
|
29
|
+
from execsql.cli.run import _end_run_on_sigterm
|
|
30
|
+
from execsql.exceptions import ConfigError, ConfigFileError, ErrInfo
|
|
27
31
|
|
|
28
32
|
__all__ = [
|
|
29
33
|
"_SCHEME_TO_DBTYPE",
|
|
@@ -41,583 +45,6 @@ __all__ = [
|
|
|
41
45
|
]
|
|
42
46
|
|
|
43
47
|
|
|
44
|
-
# ---------------------------------------------------------------------------
|
|
45
|
-
# Typer app
|
|
46
|
-
# ---------------------------------------------------------------------------
|
|
47
|
-
|
|
48
|
-
app = typer.Typer(
|
|
49
|
-
name="execsql",
|
|
50
|
-
help="Run a SQL script against a database with metacommand support.",
|
|
51
|
-
add_completion=False,
|
|
52
|
-
rich_markup_mode="rich",
|
|
53
|
-
no_args_is_help=True,
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _version_callback(value: bool) -> None:
|
|
58
|
-
if value:
|
|
59
|
-
_console.print(f"execsql [bold cyan]{__version__}[/bold cyan]")
|
|
60
|
-
raise typer.Exit()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@app.command(
|
|
64
|
-
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
65
|
-
)
|
|
66
|
-
def main(
|
|
67
|
-
ctx: typer.Context,
|
|
68
|
-
# Positional args collected manually (script + optional server/db/file)
|
|
69
|
-
args: list[str] | None = typer.Argument(
|
|
70
|
-
None,
|
|
71
|
-
metavar="SQL_SCRIPT [SERVER DATABASE | DATABASE_FILE]",
|
|
72
|
-
help=(
|
|
73
|
-
"SQL script file to execute. Optionally followed by server and database "
|
|
74
|
-
"name (client-server DBs) or a database file path (file-based DBs)."
|
|
75
|
-
),
|
|
76
|
-
),
|
|
77
|
-
# -- Connection --------------------------------------------------------
|
|
78
|
-
db_type: str | None = typer.Option(
|
|
79
|
-
None,
|
|
80
|
-
"-t",
|
|
81
|
-
"--type",
|
|
82
|
-
metavar="{a,d,p,s,l,m,k,o,f}",
|
|
83
|
-
help=(
|
|
84
|
-
"Database type: [bold]a[/bold]=MS-Access, [bold]p[/bold]=PostgreSQL, "
|
|
85
|
-
"[bold]s[/bold]=SQL Server, [bold]l[/bold]=SQLite, [bold]m[/bold]=MySQL/MariaDB, "
|
|
86
|
-
"[bold]k[/bold]=DuckDB, [bold]o[/bold]=Oracle, [bold]f[/bold]=Firebird, "
|
|
87
|
-
"[bold]d[/bold]=DSN."
|
|
88
|
-
),
|
|
89
|
-
),
|
|
90
|
-
dsn: str | None = typer.Option(
|
|
91
|
-
None,
|
|
92
|
-
"--dsn",
|
|
93
|
-
"--connection-string",
|
|
94
|
-
metavar="URL",
|
|
95
|
-
help=(
|
|
96
|
-
"Database connection URL, e.g. [cyan]postgresql://user:pass@host:5432/db[/cyan]. "
|
|
97
|
-
"Supported schemes: postgresql, mysql, mssql, oracle, firebird, sqlite, duckdb. "
|
|
98
|
-
"Overrides [cyan]-t[/cyan]/[cyan]-u[/cyan]/[cyan]-p[/cyan] and positional server/db args."
|
|
99
|
-
),
|
|
100
|
-
),
|
|
101
|
-
user: str | None = typer.Option(
|
|
102
|
-
None,
|
|
103
|
-
"-u",
|
|
104
|
-
"--user",
|
|
105
|
-
help="Database user name.",
|
|
106
|
-
),
|
|
107
|
-
port: int | None = typer.Option(
|
|
108
|
-
None,
|
|
109
|
-
"-p",
|
|
110
|
-
"--port",
|
|
111
|
-
help="Database server port.",
|
|
112
|
-
),
|
|
113
|
-
no_passwd: bool = typer.Option(
|
|
114
|
-
False,
|
|
115
|
-
"-w",
|
|
116
|
-
"--no-passwd",
|
|
117
|
-
help="Skip password prompt when user is specified.",
|
|
118
|
-
),
|
|
119
|
-
new_db: bool = typer.Option(
|
|
120
|
-
False,
|
|
121
|
-
"-n",
|
|
122
|
-
"--new-db",
|
|
123
|
-
help="Create a new SQLite or Postgres database if it does not exist.",
|
|
124
|
-
),
|
|
125
|
-
# -- Encoding ----------------------------------------------------------
|
|
126
|
-
database_encoding: str | None = typer.Option(
|
|
127
|
-
None,
|
|
128
|
-
"-e",
|
|
129
|
-
"--database-encoding",
|
|
130
|
-
help="Character encoding used in the database.",
|
|
131
|
-
),
|
|
132
|
-
script_encoding: str | None = typer.Option(
|
|
133
|
-
None,
|
|
134
|
-
"-f",
|
|
135
|
-
"--script-encoding",
|
|
136
|
-
help="Character encoding of the script file. [dim]Default: UTF-8[/dim]",
|
|
137
|
-
),
|
|
138
|
-
output_encoding: str | None = typer.Option(
|
|
139
|
-
None,
|
|
140
|
-
"-g",
|
|
141
|
-
"--output-encoding",
|
|
142
|
-
help="Encoding for WRITE and EXPORT output.",
|
|
143
|
-
),
|
|
144
|
-
import_encoding: str | None = typer.Option(
|
|
145
|
-
None,
|
|
146
|
-
"-i",
|
|
147
|
-
"--import-encoding",
|
|
148
|
-
help="Encoding for data files used with IMPORT.",
|
|
149
|
-
),
|
|
150
|
-
# -- Import/Export -----------------------------------------------------
|
|
151
|
-
import_buffer: int | None = typer.Option(
|
|
152
|
-
None,
|
|
153
|
-
"-z",
|
|
154
|
-
"--import-buffer",
|
|
155
|
-
metavar="KB",
|
|
156
|
-
help="Import buffer size in KB. [dim]Default: 32[/dim]",
|
|
157
|
-
),
|
|
158
|
-
scanlines: int | None = typer.Option(
|
|
159
|
-
None,
|
|
160
|
-
"-s",
|
|
161
|
-
"--scan-lines",
|
|
162
|
-
metavar="N",
|
|
163
|
-
help="Lines to scan for IMPORT format detection. [dim]0 = scan entire file.[/dim]",
|
|
164
|
-
),
|
|
165
|
-
boolean_int: str | None = typer.Option(
|
|
166
|
-
None,
|
|
167
|
-
"-b",
|
|
168
|
-
"--boolean-int",
|
|
169
|
-
metavar="{0,1,t,f,y,n}",
|
|
170
|
-
help="Treat integers 0 and 1 as boolean values.",
|
|
171
|
-
),
|
|
172
|
-
make_dirs: str | None = typer.Option(
|
|
173
|
-
None,
|
|
174
|
-
"-d",
|
|
175
|
-
"--directories",
|
|
176
|
-
metavar="{0,1,t,f,y,n}",
|
|
177
|
-
help="Auto-create directories for EXPORT metacommand. [dim]n=no (default), y=yes[/dim]",
|
|
178
|
-
),
|
|
179
|
-
output_dir: str | None = typer.Option(
|
|
180
|
-
None,
|
|
181
|
-
"--output-dir",
|
|
182
|
-
metavar="DIR",
|
|
183
|
-
help=(
|
|
184
|
-
"Default base directory for EXPORT output files. "
|
|
185
|
-
"Relative paths in EXPORT metacommands are joined to this directory. "
|
|
186
|
-
"Absolute paths and [cyan]stdout[/cyan] are unaffected."
|
|
187
|
-
),
|
|
188
|
-
),
|
|
189
|
-
progress: bool = typer.Option(
|
|
190
|
-
False,
|
|
191
|
-
"--progress",
|
|
192
|
-
help="Show a progress bar for long-running IMPORT operations.",
|
|
193
|
-
),
|
|
194
|
-
# -- Execution ---------------------------------------------------------
|
|
195
|
-
command: str | None = typer.Option(
|
|
196
|
-
None,
|
|
197
|
-
"-c",
|
|
198
|
-
"--command",
|
|
199
|
-
metavar="SCRIPT",
|
|
200
|
-
help=(
|
|
201
|
-
"Execute an inline SQL/metacommand script string instead of a script file. "
|
|
202
|
-
"Use shell [cyan]$'line1\\nline2'[/cyan] syntax for multi-line scripts."
|
|
203
|
-
),
|
|
204
|
-
),
|
|
205
|
-
dry_run: bool = typer.Option(
|
|
206
|
-
False,
|
|
207
|
-
"--dry-run",
|
|
208
|
-
help="Parse the script and print the command list without connecting to a database or executing anything.",
|
|
209
|
-
),
|
|
210
|
-
lint: bool = typer.Option(
|
|
211
|
-
False,
|
|
212
|
-
"--lint",
|
|
213
|
-
help=(
|
|
214
|
-
"Parse the script and perform static analysis without connecting to a database or executing anything. "
|
|
215
|
-
"Reports unmatched IF/ENDIF/LOOP/BATCH blocks (errors), potentially undefined variables, "
|
|
216
|
-
"and missing INCLUDE files (warnings). Exits 0 if no errors, 1 if errors found."
|
|
217
|
-
),
|
|
218
|
-
),
|
|
219
|
-
parse_tree: bool = typer.Option(
|
|
220
|
-
False,
|
|
221
|
-
"--parse-tree",
|
|
222
|
-
help=(
|
|
223
|
-
"Parse the script into an abstract syntax tree and print the tree structure. "
|
|
224
|
-
"Does not connect to a database or execute anything."
|
|
225
|
-
),
|
|
226
|
-
),
|
|
227
|
-
debug: bool = typer.Option(
|
|
228
|
-
False,
|
|
229
|
-
"--debug",
|
|
230
|
-
help="Start in step-through debug mode. The debug REPL pauses before each statement.",
|
|
231
|
-
),
|
|
232
|
-
no_system_cmd: bool = typer.Option(
|
|
233
|
-
False,
|
|
234
|
-
"--no-system-cmd",
|
|
235
|
-
help="Disable the SYSTEM_CMD (SHELL) metacommand. Scripts that use SHELL will fail with an error.",
|
|
236
|
-
),
|
|
237
|
-
no_rm_file: bool = typer.Option(
|
|
238
|
-
False,
|
|
239
|
-
"--no-rm-file",
|
|
240
|
-
help="Disable the RM_FILE metacommand. Scripts that try to delete files will fail with an error.",
|
|
241
|
-
),
|
|
242
|
-
no_serve: bool = typer.Option(
|
|
243
|
-
False,
|
|
244
|
-
"--no-serve",
|
|
245
|
-
help="Disable the SERVE metacommand. Scripts that try to stream a file to stdout will fail with an error.",
|
|
246
|
-
),
|
|
247
|
-
profile: bool = typer.Option(
|
|
248
|
-
False,
|
|
249
|
-
"--profile",
|
|
250
|
-
help="Record per-statement execution times and print a timing summary after the script completes.",
|
|
251
|
-
),
|
|
252
|
-
profile_limit: int = typer.Option(
|
|
253
|
-
20,
|
|
254
|
-
"--profile-limit",
|
|
255
|
-
help="Number of top statements to show in the --profile timing summary (default: 20).",
|
|
256
|
-
),
|
|
257
|
-
# -- GUI ---------------------------------------------------------------
|
|
258
|
-
use_gui: str | None = typer.Option(
|
|
259
|
-
None,
|
|
260
|
-
"-v",
|
|
261
|
-
"--visible-prompts",
|
|
262
|
-
metavar="{0,1,2,3}",
|
|
263
|
-
help=(
|
|
264
|
-
"GUI level: [bold]0[/bold]=none (default), [bold]1[/bold]=GUI for password/pause, "
|
|
265
|
-
"[bold]2[/bold]=GUI for password/pause + DB selection, [bold]3[/bold]=full GUI console."
|
|
266
|
-
),
|
|
267
|
-
),
|
|
268
|
-
gui_framework: str | None = typer.Option(
|
|
269
|
-
None,
|
|
270
|
-
"--gui-framework",
|
|
271
|
-
metavar="{tkinter,textual}",
|
|
272
|
-
help="GUI framework to use with [cyan]--visible-prompts[/cyan]. [dim]Default: tkinter[/dim]",
|
|
273
|
-
),
|
|
274
|
-
# -- Configuration -----------------------------------------------------
|
|
275
|
-
config_file: str | None = typer.Option(
|
|
276
|
-
None,
|
|
277
|
-
"--config",
|
|
278
|
-
metavar="FILE",
|
|
279
|
-
help=(
|
|
280
|
-
"Path to an execsql configuration file. "
|
|
281
|
-
"Loaded after the implicit search paths so its values take precedence. "
|
|
282
|
-
"The file may chain additional configs via its [cyan][config][/cyan] section."
|
|
283
|
-
),
|
|
284
|
-
),
|
|
285
|
-
init_config: bool = typer.Option(
|
|
286
|
-
False,
|
|
287
|
-
"--init-config",
|
|
288
|
-
help="Print a default [cyan]execsql.conf[/cyan] template to stdout and exit.",
|
|
289
|
-
),
|
|
290
|
-
sub_vars: list[str] | None = typer.Option(
|
|
291
|
-
None,
|
|
292
|
-
"-a",
|
|
293
|
-
"--assign-arg",
|
|
294
|
-
metavar="VALUE",
|
|
295
|
-
help="Define the replacement string for a substitution variable [cyan]$ARG_x[/cyan].",
|
|
296
|
-
),
|
|
297
|
-
user_logfile: bool = typer.Option(
|
|
298
|
-
False,
|
|
299
|
-
"-l",
|
|
300
|
-
"--user-logfile",
|
|
301
|
-
help="Write a log file to [cyan]~/execsql.log[/cyan].",
|
|
302
|
-
),
|
|
303
|
-
# -- Information -------------------------------------------------------
|
|
304
|
-
metacommands: bool = typer.Option(
|
|
305
|
-
False,
|
|
306
|
-
"-m",
|
|
307
|
-
"--metacommands",
|
|
308
|
-
help="List metacommands and exit.",
|
|
309
|
-
),
|
|
310
|
-
encodings: bool = typer.Option(
|
|
311
|
-
False,
|
|
312
|
-
"-y",
|
|
313
|
-
"--encodings",
|
|
314
|
-
help="List available encoding names and exit.",
|
|
315
|
-
),
|
|
316
|
-
dump_keywords: bool = typer.Option(
|
|
317
|
-
False,
|
|
318
|
-
"--dump-keywords",
|
|
319
|
-
help="Dump all metacommand keywords as JSON and exit.",
|
|
320
|
-
),
|
|
321
|
-
list_plugins: bool = typer.Option(
|
|
322
|
-
False,
|
|
323
|
-
"--list-plugins",
|
|
324
|
-
help="List all discovered plugins (metacommands, exporters, importers) and exit.",
|
|
325
|
-
),
|
|
326
|
-
ping: bool = typer.Option(
|
|
327
|
-
False,
|
|
328
|
-
"--ping",
|
|
329
|
-
help=(
|
|
330
|
-
"Test database connectivity and exit. "
|
|
331
|
-
"Prints connection details and the server version on success (exit 0), "
|
|
332
|
-
"or the error message on failure (exit 1). "
|
|
333
|
-
"No script file is required."
|
|
334
|
-
),
|
|
335
|
-
),
|
|
336
|
-
online_help: bool = typer.Option(
|
|
337
|
-
False,
|
|
338
|
-
"-o",
|
|
339
|
-
"--online-help",
|
|
340
|
-
help="Open the online documentation in the default browser.",
|
|
341
|
-
),
|
|
342
|
-
version: bool | None = typer.Option(
|
|
343
|
-
None,
|
|
344
|
-
"--version",
|
|
345
|
-
callback=_version_callback,
|
|
346
|
-
is_eager=True,
|
|
347
|
-
help="Show version and exit.",
|
|
348
|
-
),
|
|
349
|
-
) -> None:
|
|
350
|
-
"""Run [bold]SQL_SCRIPT[/bold] against the specified database.
|
|
351
|
-
|
|
352
|
-
[dim]Positional arguments after the script file:[/dim]
|
|
353
|
-
|
|
354
|
-
[green]Client-server databases:[/green]
|
|
355
|
-
execsql script.sql [SERVER] [DATABASE]
|
|
356
|
-
|
|
357
|
-
[green]File-based databases (SQLite, DuckDB, Access):[/green]
|
|
358
|
-
execsql script.sql [DATABASE_FILE]
|
|
359
|
-
"""
|
|
360
|
-
# ------------------------------------------------------------------
|
|
361
|
-
# Early exits (no script file needed)
|
|
362
|
-
# ------------------------------------------------------------------
|
|
363
|
-
if metacommands:
|
|
364
|
-
_print_metacommands()
|
|
365
|
-
raise typer.Exit()
|
|
366
|
-
|
|
367
|
-
if encodings:
|
|
368
|
-
_print_encodings()
|
|
369
|
-
raise typer.Exit()
|
|
370
|
-
|
|
371
|
-
if init_config:
|
|
372
|
-
_init_config()
|
|
373
|
-
raise typer.Exit()
|
|
374
|
-
|
|
375
|
-
if dump_keywords:
|
|
376
|
-
import json as _json
|
|
377
|
-
|
|
378
|
-
from execsql.metacommands import (
|
|
379
|
-
ALL_EXPORT_FORMATS,
|
|
380
|
-
DATABASE_TYPES,
|
|
381
|
-
DISPATCH_TABLE,
|
|
382
|
-
JSON_VARIANT_FORMATS,
|
|
383
|
-
METADATA_FORMATS,
|
|
384
|
-
QUERY_EXPORT_FORMATS,
|
|
385
|
-
SERVE_FORMATS,
|
|
386
|
-
TABLE_EXPORT_FORMATS,
|
|
387
|
-
)
|
|
388
|
-
from execsql.metacommands.conditions import CONDITIONAL_TABLE
|
|
389
|
-
|
|
390
|
-
mc_kw = DISPATCH_TABLE.keywords_by_category()
|
|
391
|
-
cond_kw = CONDITIONAL_TABLE.keywords_by_category()
|
|
392
|
-
|
|
393
|
-
data = {
|
|
394
|
-
"metacommands": {
|
|
395
|
-
"control": sorted(mc_kw.get("control", [])),
|
|
396
|
-
"block": sorted(
|
|
397
|
-
mc_kw.get("block", []) + ["BEGIN SCRIPT", "END SCRIPT", "BEGIN SQL", "END SQL"],
|
|
398
|
-
),
|
|
399
|
-
"action": sorted(mc_kw.get("action", [])),
|
|
400
|
-
"config": sorted(mc_kw.get("config", [])),
|
|
401
|
-
"prompt": sorted(mc_kw.get("prompt", [])),
|
|
402
|
-
},
|
|
403
|
-
"conditions": sorted(cond_kw.get("condition", []) + ["IS_FALSE", "NOT", "OR"]),
|
|
404
|
-
"config_options": sorted(mc_kw.get("config_option", [])),
|
|
405
|
-
"export_formats": {
|
|
406
|
-
"query": sorted(QUERY_EXPORT_FORMATS),
|
|
407
|
-
"table": sorted(TABLE_EXPORT_FORMATS),
|
|
408
|
-
"serve": sorted(SERVE_FORMATS),
|
|
409
|
-
"metadata": sorted(METADATA_FORMATS),
|
|
410
|
-
"json_variants": sorted(JSON_VARIANT_FORMATS),
|
|
411
|
-
"all": sorted(ALL_EXPORT_FORMATS),
|
|
412
|
-
},
|
|
413
|
-
"database_types": sorted(DATABASE_TYPES),
|
|
414
|
-
"variable_patterns": {
|
|
415
|
-
"system": "!!$name!!",
|
|
416
|
-
"environment": "!!&name!!",
|
|
417
|
-
"parameter": "!!#name!!",
|
|
418
|
-
"column": "!!@name!!",
|
|
419
|
-
"local": "!!~name!!",
|
|
420
|
-
"local_alt": "!!+name!!",
|
|
421
|
-
"regular": "!!name!!",
|
|
422
|
-
"deferred": "!{name}!",
|
|
423
|
-
},
|
|
424
|
-
}
|
|
425
|
-
_console.print_json(_json.dumps(data, indent=2))
|
|
426
|
-
raise typer.Exit()
|
|
427
|
-
|
|
428
|
-
if list_plugins:
|
|
429
|
-
from execsql.plugins import (
|
|
430
|
-
EXPORTER_GROUP,
|
|
431
|
-
IMPORTER_GROUP,
|
|
432
|
-
METACOMMAND_GROUP,
|
|
433
|
-
_load_entry_points,
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
_console.print("\n[bold cyan]Installed plugins:[/bold cyan]\n")
|
|
437
|
-
|
|
438
|
-
mc_plugins = _load_entry_points(METACOMMAND_GROUP)
|
|
439
|
-
ex_plugins = _load_entry_points(EXPORTER_GROUP)
|
|
440
|
-
im_plugins = _load_entry_points(IMPORTER_GROUP)
|
|
441
|
-
|
|
442
|
-
if not mc_plugins and not ex_plugins and not im_plugins:
|
|
443
|
-
_console.print(" [dim]No plugins found.[/dim]")
|
|
444
|
-
_console.print()
|
|
445
|
-
_console.print(
|
|
446
|
-
" Plugins are discovered via Python entry points.\n"
|
|
447
|
-
" See the execsql documentation for how to create plugins.",
|
|
448
|
-
)
|
|
449
|
-
else:
|
|
450
|
-
if mc_plugins:
|
|
451
|
-
_console.print(f" [bold]Metacommands[/bold] ({len(mc_plugins)}):")
|
|
452
|
-
for name, _ in mc_plugins:
|
|
453
|
-
_console.print(f" - {name}")
|
|
454
|
-
if ex_plugins:
|
|
455
|
-
_console.print(f" [bold]Exporters[/bold] ({len(ex_plugins)}):")
|
|
456
|
-
for name, _ in ex_plugins:
|
|
457
|
-
_console.print(f" - {name}")
|
|
458
|
-
if im_plugins:
|
|
459
|
-
_console.print(f" [bold]Importers[/bold] ({len(im_plugins)}):")
|
|
460
|
-
for name, _ in im_plugins:
|
|
461
|
-
_console.print(f" - {name}")
|
|
462
|
-
|
|
463
|
-
_console.print()
|
|
464
|
-
raise typer.Exit()
|
|
465
|
-
|
|
466
|
-
if online_help:
|
|
467
|
-
import webbrowser
|
|
468
|
-
|
|
469
|
-
webbrowser.open("https://execsql2.readthedocs.io/en/latest/", new=2, autoraise=True)
|
|
470
|
-
raise typer.Exit()
|
|
471
|
-
|
|
472
|
-
if config_file and not Path(config_file).is_file():
|
|
473
|
-
_err_console.print(
|
|
474
|
-
f"[bold red]Error:[/bold red] Config file [cyan]{config_file!r}[/cyan] does not exist.",
|
|
475
|
-
)
|
|
476
|
-
raise typer.Exit(code=2)
|
|
477
|
-
|
|
478
|
-
positional = args or []
|
|
479
|
-
if command is not None:
|
|
480
|
-
script_name = None # inline mode — no script file
|
|
481
|
-
elif ping:
|
|
482
|
-
# --ping does not require a script file; positional args are still
|
|
483
|
-
# available for server/db arguments if --dsn is not used.
|
|
484
|
-
script_name = None
|
|
485
|
-
else:
|
|
486
|
-
if not positional:
|
|
487
|
-
_err_console.print(
|
|
488
|
-
"[bold red]Error:[/bold red] No SQL script file specified. Use [cyan]-c[/cyan] to run an inline script.",
|
|
489
|
-
)
|
|
490
|
-
raise typer.Exit(code=1)
|
|
491
|
-
script_name = positional[0]
|
|
492
|
-
if not Path(script_name).exists():
|
|
493
|
-
_err_console.print(
|
|
494
|
-
f'[bold red]Error:[/bold red] SQL script file [cyan]"{script_name}"[/cyan] does not exist.',
|
|
495
|
-
)
|
|
496
|
-
raise typer.Exit(code=1)
|
|
497
|
-
|
|
498
|
-
# ------------------------------------------------------------------
|
|
499
|
-
# Validate positional args and db_type choice
|
|
500
|
-
# ------------------------------------------------------------------
|
|
501
|
-
|
|
502
|
-
if db_type and db_type not in ("a", "d", "p", "s", "l", "m", "k", "o", "f"):
|
|
503
|
-
_err_console.print(
|
|
504
|
-
f"[bold red]Error:[/bold red] Invalid database type [cyan]{db_type!r}[/cyan]. "
|
|
505
|
-
"Choose from: a, d, p, s, l, m, k, o, f",
|
|
506
|
-
)
|
|
507
|
-
raise typer.Exit(code=2)
|
|
508
|
-
|
|
509
|
-
if use_gui and use_gui not in ("0", "1", "2", "3"):
|
|
510
|
-
_err_console.print(
|
|
511
|
-
f"[bold red]Error:[/bold red] Invalid GUI level [cyan]{use_gui!r}[/cyan]. Choose from: 0, 1, 2, 3",
|
|
512
|
-
)
|
|
513
|
-
raise typer.Exit(code=2)
|
|
514
|
-
|
|
515
|
-
if gui_framework and gui_framework.lower() not in ("tkinter", "textual"):
|
|
516
|
-
_err_console.print(
|
|
517
|
-
f"[bold red]Error:[/bold red] Invalid GUI framework [cyan]{gui_framework!r}[/cyan]. Choose from: tkinter, textual",
|
|
518
|
-
)
|
|
519
|
-
raise typer.Exit(code=2)
|
|
520
|
-
|
|
521
|
-
if boolean_int and boolean_int.lower() not in ("0", "1", "t", "f", "y", "n"):
|
|
522
|
-
_err_console.print(
|
|
523
|
-
f"[bold red]Error:[/bold red] Invalid --boolean-int value [cyan]{boolean_int!r}[/cyan].",
|
|
524
|
-
)
|
|
525
|
-
raise typer.Exit(code=2)
|
|
526
|
-
|
|
527
|
-
# ------------------------------------------------------------------
|
|
528
|
-
# Parse tree: parse script into AST and print tree structure
|
|
529
|
-
# ------------------------------------------------------------------
|
|
530
|
-
if parse_tree:
|
|
531
|
-
from execsql.script.ast import format_tree
|
|
532
|
-
from execsql.script.parser import parse_script, parse_string
|
|
533
|
-
|
|
534
|
-
try:
|
|
535
|
-
if command is not None:
|
|
536
|
-
tree = parse_string(command.replace("\\n", "\n").replace("\\t", "\t"), "<inline>")
|
|
537
|
-
elif script_name is not None:
|
|
538
|
-
encoding = script_encoding or "utf-8"
|
|
539
|
-
tree = parse_script(script_name, encoding=encoding)
|
|
540
|
-
else:
|
|
541
|
-
_err_console.print(
|
|
542
|
-
"[bold red]Error:[/bold red] --parse-tree requires a script file or -c command.",
|
|
543
|
-
)
|
|
544
|
-
raise typer.Exit(code=1)
|
|
545
|
-
except ErrInfo as exc:
|
|
546
|
-
_err_console.print(f"[bold red]Parse error:[/bold red] {exc.errmsg()}")
|
|
547
|
-
raise typer.Exit(code=1) from exc
|
|
548
|
-
|
|
549
|
-
_console.print(format_tree(tree))
|
|
550
|
-
raise typer.Exit()
|
|
551
|
-
|
|
552
|
-
# ------------------------------------------------------------------
|
|
553
|
-
# Lint: AST-based static analysis (no DB connection needed)
|
|
554
|
-
# ------------------------------------------------------------------
|
|
555
|
-
if lint:
|
|
556
|
-
from execsql.cli.lint import _print_lint_results, lint as _lint_script
|
|
557
|
-
from execsql.script.parser import parse_script, parse_string
|
|
558
|
-
|
|
559
|
-
label = script_name or "<inline>"
|
|
560
|
-
try:
|
|
561
|
-
if command is not None:
|
|
562
|
-
tree = parse_string(command.replace("\\n", "\n").replace("\\t", "\t"), "<inline>")
|
|
563
|
-
elif script_name is not None:
|
|
564
|
-
encoding = script_encoding or "utf-8"
|
|
565
|
-
tree = parse_script(script_name, encoding=encoding)
|
|
566
|
-
else:
|
|
567
|
-
_err_console.print(
|
|
568
|
-
"[bold red]Error:[/bold red] --lint requires a script file or -c command.",
|
|
569
|
-
)
|
|
570
|
-
raise typer.Exit(code=1)
|
|
571
|
-
except ErrInfo as exc:
|
|
572
|
-
# Parse failure IS a lint error — report it
|
|
573
|
-
issues = [("error", label, 0, f"Parse error: {exc.errmsg()}")]
|
|
574
|
-
exit_code = _print_lint_results(issues, label)
|
|
575
|
-
raise typer.Exit(code=exit_code) from exc
|
|
576
|
-
|
|
577
|
-
issues = _lint_script(tree, script_path=script_name)
|
|
578
|
-
exit_code = _print_lint_results(issues, label)
|
|
579
|
-
raise typer.Exit(code=exit_code)
|
|
580
|
-
|
|
581
|
-
# ------------------------------------------------------------------
|
|
582
|
-
# Delegate to the real main implementation
|
|
583
|
-
# ------------------------------------------------------------------
|
|
584
|
-
_run(
|
|
585
|
-
positional=positional,
|
|
586
|
-
sub_vars=sub_vars,
|
|
587
|
-
boolean_int=boolean_int,
|
|
588
|
-
make_dirs=make_dirs,
|
|
589
|
-
database_encoding=database_encoding,
|
|
590
|
-
script_encoding=script_encoding,
|
|
591
|
-
output_encoding=output_encoding,
|
|
592
|
-
import_encoding=import_encoding,
|
|
593
|
-
user_logfile=user_logfile,
|
|
594
|
-
new_db=new_db,
|
|
595
|
-
port=port,
|
|
596
|
-
scanlines=scanlines,
|
|
597
|
-
db_type=db_type,
|
|
598
|
-
user=user,
|
|
599
|
-
use_gui=use_gui,
|
|
600
|
-
gui_framework=gui_framework,
|
|
601
|
-
no_passwd=no_passwd,
|
|
602
|
-
import_buffer=import_buffer,
|
|
603
|
-
script_name=script_name,
|
|
604
|
-
command=command,
|
|
605
|
-
dry_run=dry_run,
|
|
606
|
-
dsn=dsn,
|
|
607
|
-
output_dir=output_dir,
|
|
608
|
-
progress=progress,
|
|
609
|
-
profile=profile,
|
|
610
|
-
profile_limit=profile_limit,
|
|
611
|
-
ping=ping,
|
|
612
|
-
lint=lint,
|
|
613
|
-
debug=debug,
|
|
614
|
-
no_system_cmd=no_system_cmd,
|
|
615
|
-
no_rm_file=no_rm_file,
|
|
616
|
-
no_serve=no_serve,
|
|
617
|
-
config_file=config_file,
|
|
618
|
-
)
|
|
619
|
-
|
|
620
|
-
|
|
621
48
|
# ---------------------------------------------------------------------------
|
|
622
49
|
# Legacy entry point (kept for backwards compat with pyproject.toml script)
|
|
623
50
|
# ---------------------------------------------------------------------------
|
|
@@ -625,6 +52,7 @@ def main(
|
|
|
625
52
|
|
|
626
53
|
def _legacy_main() -> None:
|
|
627
54
|
"""Entry point that wraps the Typer app for use as a console_scripts target."""
|
|
55
|
+
_end_run_on_sigterm()
|
|
628
56
|
try:
|
|
629
57
|
app()
|
|
630
58
|
except SystemExit as exc:
|
|
@@ -634,9 +62,14 @@ def _legacy_main() -> None:
|
|
|
634
62
|
|
|
635
63
|
exit_now(1, exc)
|
|
636
64
|
except ConfigError as exc:
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
65
|
+
# Upstream printed the line of its own source that raised, which
|
|
66
|
+
# located nothing for the user. A file error names the file instead.
|
|
67
|
+
hint = (
|
|
68
|
+
"\nRun `execsql config --validate` to list every problem in the config files."
|
|
69
|
+
if isinstance(exc, ConfigFileError)
|
|
70
|
+
else ""
|
|
71
|
+
)
|
|
72
|
+
sys.exit(f"Configuration error: {exc}{hint}")
|
|
640
73
|
except Exception:
|
|
641
74
|
strace = traceback.extract_tb(sys.exc_info()[2])[-1:]
|
|
642
75
|
lno = strace[0][1]
|