execsql2 2.1.2__py3-none-any.whl → 2.4.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/cli/__init__.py +436 -0
- execsql/cli/dsn.py +86 -0
- execsql/cli/help.py +140 -0
- execsql/{cli.py → cli/run.py} +14 -589
- execsql/config.py +65 -1
- execsql/db/access.py +27 -15
- execsql/db/base.py +328 -215
- execsql/db/dsn.py +10 -5
- execsql/db/duckdb.py +6 -2
- execsql/db/factory.py +21 -0
- execsql/db/firebird.py +27 -19
- execsql/db/mysql.py +12 -7
- execsql/db/oracle.py +15 -11
- execsql/db/postgres.py +31 -16
- execsql/db/sqlite.py +15 -11
- execsql/db/sqlserver.py +16 -5
- execsql/exceptions.py +25 -7
- execsql/exporters/base.py +12 -1
- execsql/exporters/delimited.py +80 -35
- execsql/exporters/duckdb.py +6 -2
- execsql/exporters/feather.py +10 -6
- execsql/exporters/html.py +89 -69
- execsql/exporters/json.py +52 -45
- execsql/exporters/latex.py +37 -27
- execsql/exporters/ods.py +32 -11
- execsql/exporters/parquet.py +5 -2
- execsql/exporters/pretty.py +16 -9
- execsql/exporters/raw.py +22 -16
- execsql/exporters/sqlite.py +6 -2
- execsql/exporters/templates.py +39 -21
- execsql/exporters/values.py +26 -20
- execsql/exporters/xls.py +30 -11
- execsql/exporters/xml.py +31 -13
- execsql/exporters/zip.py +15 -0
- execsql/importers/base.py +6 -4
- execsql/importers/csv.py +8 -6
- execsql/importers/feather.py +6 -4
- execsql/importers/ods.py +6 -4
- execsql/importers/xls.py +6 -4
- execsql/metacommands/__init__.py +208 -1548
- execsql/metacommands/conditions.py +101 -27
- execsql/metacommands/control.py +8 -4
- execsql/metacommands/data.py +6 -6
- execsql/metacommands/debug.py +6 -2
- execsql/metacommands/dispatch.py +2011 -0
- execsql/metacommands/io.py +67 -1310
- execsql/metacommands/io_export.py +442 -0
- execsql/metacommands/io_fileops.py +287 -0
- execsql/metacommands/io_import.py +398 -0
- execsql/metacommands/io_write.py +248 -0
- execsql/metacommands/prompt.py +22 -66
- execsql/metacommands/system.py +7 -2
- execsql/models.py +7 -0
- execsql/parser.py +10 -0
- execsql/py.typed +0 -0
- execsql/script/__init__.py +95 -0
- execsql/script/control.py +162 -0
- execsql/{script.py → script/engine.py} +184 -402
- execsql/script/variables.py +281 -0
- execsql/types.py +49 -20
- execsql/utils/auth.py +2 -0
- execsql/utils/crypto.py +4 -6
- execsql/utils/datetime.py +1 -0
- execsql/utils/errors.py +11 -0
- execsql/utils/fileio.py +33 -8
- execsql/utils/gui.py +46 -0
- execsql/utils/mail.py +7 -17
- execsql/utils/numeric.py +2 -0
- execsql/utils/regex.py +9 -0
- execsql/utils/strings.py +16 -0
- execsql/utils/timer.py +2 -0
- execsql2-2.4.0.data/data/execsql2_extras/README.md +65 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/METADATA +13 -6
- execsql2-2.4.0.dist-info/RECORD +108 -0
- execsql2-2.1.2.data/data/execsql2_extras/READ_ME.rst +0 -127
- execsql2-2.1.2.dist-info/RECORD +0 -96
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/NOTICE +0 -0
execsql/cli/__init__.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""Command-line interface for execsql.
|
|
2
|
+
|
|
3
|
+
Parses arguments via Typer, then delegates to :func:`_run` for state
|
|
4
|
+
initialisation, database connection, and script execution.
|
|
5
|
+
|
|
6
|
+
Submodules:
|
|
7
|
+
|
|
8
|
+
- :mod:`execsql.cli.help` — Rich-formatted help output & console objects
|
|
9
|
+
- :mod:`execsql.cli.dsn` — Connection-string (DSN URL) parser
|
|
10
|
+
- :mod:`execsql.cli.run` — Core execution logic
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
import traceback
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import typer
|
|
20
|
+
|
|
21
|
+
from execsql import __version__
|
|
22
|
+
from execsql.cli.dsn import _parse_connection_string, _SCHEME_TO_DBTYPE # noqa: F401 — re-export
|
|
23
|
+
from execsql.cli.help import _console, _err_console, _print_encodings, _print_metacommands # noqa: F401 — re-export
|
|
24
|
+
from execsql.cli.run import _connect_initial_db, _run # noqa: F401 — re-export
|
|
25
|
+
from execsql.exceptions import ConfigError, ErrInfo
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Typer app
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
app = typer.Typer(
|
|
33
|
+
name="execsql",
|
|
34
|
+
help="Run a SQL script against a database with metacommand support.",
|
|
35
|
+
add_completion=False,
|
|
36
|
+
rich_markup_mode="rich",
|
|
37
|
+
no_args_is_help=True,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _version_callback(value: bool) -> None:
|
|
42
|
+
if value:
|
|
43
|
+
_console.print(f"execsql [bold cyan]{__version__}[/bold cyan]")
|
|
44
|
+
raise typer.Exit()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command(
|
|
48
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
49
|
+
)
|
|
50
|
+
def main(
|
|
51
|
+
ctx: typer.Context,
|
|
52
|
+
# Positional args collected manually (script + optional server/db/file)
|
|
53
|
+
args: list[str] | None = typer.Argument(
|
|
54
|
+
None,
|
|
55
|
+
metavar="SQL_SCRIPT [SERVER DATABASE | DATABASE_FILE]",
|
|
56
|
+
help=(
|
|
57
|
+
"SQL script file to execute. Optionally followed by server and database "
|
|
58
|
+
"name (client-server DBs) or a database file path (file-based DBs)."
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
# Named options — grouped to mirror the original argparse interface
|
|
62
|
+
sub_vars: list[str] | None = typer.Option(
|
|
63
|
+
None,
|
|
64
|
+
"-a",
|
|
65
|
+
"--assign-arg",
|
|
66
|
+
metavar="VALUE",
|
|
67
|
+
help="Define the replacement string for a substitution variable [cyan]\\$ARG_x[/cyan].",
|
|
68
|
+
),
|
|
69
|
+
boolean_int: str | None = typer.Option(
|
|
70
|
+
None,
|
|
71
|
+
"-b",
|
|
72
|
+
"--boolean-int",
|
|
73
|
+
metavar="{0,1,t,f,y,n}",
|
|
74
|
+
help="Treat integers 0 and 1 as boolean values.",
|
|
75
|
+
),
|
|
76
|
+
make_dirs: str | None = typer.Option(
|
|
77
|
+
None,
|
|
78
|
+
"-d",
|
|
79
|
+
"--directories",
|
|
80
|
+
metavar="{0,1,t,f,y,n}",
|
|
81
|
+
help="Auto-create directories for EXPORT metacommand. [dim]n=no (default), y=yes[/dim]",
|
|
82
|
+
),
|
|
83
|
+
database_encoding: str | None = typer.Option(
|
|
84
|
+
None,
|
|
85
|
+
"-e",
|
|
86
|
+
"--database-encoding",
|
|
87
|
+
help="Character encoding used in the database.",
|
|
88
|
+
),
|
|
89
|
+
script_encoding: str | None = typer.Option(
|
|
90
|
+
None,
|
|
91
|
+
"-f",
|
|
92
|
+
"--script-encoding",
|
|
93
|
+
help="Character encoding of the script file. [dim]Default: UTF-8[/dim]",
|
|
94
|
+
),
|
|
95
|
+
output_encoding: str | None = typer.Option(
|
|
96
|
+
None,
|
|
97
|
+
"-g",
|
|
98
|
+
"--output-encoding",
|
|
99
|
+
help="Encoding for WRITE and EXPORT output.",
|
|
100
|
+
),
|
|
101
|
+
import_encoding: str | None = typer.Option(
|
|
102
|
+
None,
|
|
103
|
+
"-i",
|
|
104
|
+
"--import-encoding",
|
|
105
|
+
help="Encoding for data files used with IMPORT.",
|
|
106
|
+
),
|
|
107
|
+
user_logfile: bool = typer.Option(
|
|
108
|
+
False,
|
|
109
|
+
"-l",
|
|
110
|
+
"--user-logfile",
|
|
111
|
+
help="Write a log file to [cyan]~/execsql.log[/cyan].",
|
|
112
|
+
),
|
|
113
|
+
metacommands: bool = typer.Option(
|
|
114
|
+
False,
|
|
115
|
+
"-m",
|
|
116
|
+
"--metacommands",
|
|
117
|
+
help="List metacommands and exit.",
|
|
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
|
+
online_help: bool = typer.Option(
|
|
126
|
+
False,
|
|
127
|
+
"-o",
|
|
128
|
+
"--online-help",
|
|
129
|
+
help="Open the online documentation in the default browser.",
|
|
130
|
+
),
|
|
131
|
+
port: int | None = typer.Option(
|
|
132
|
+
None,
|
|
133
|
+
"-p",
|
|
134
|
+
"--port",
|
|
135
|
+
help="Database server port.",
|
|
136
|
+
),
|
|
137
|
+
scanlines: int | None = typer.Option(
|
|
138
|
+
None,
|
|
139
|
+
"-s",
|
|
140
|
+
"--scan-lines",
|
|
141
|
+
metavar="N",
|
|
142
|
+
help="Lines to scan for IMPORT format detection. [dim]0 = scan entire file.[/dim]",
|
|
143
|
+
),
|
|
144
|
+
db_type: str | None = typer.Option(
|
|
145
|
+
None,
|
|
146
|
+
"-t",
|
|
147
|
+
"--type",
|
|
148
|
+
metavar="{a,d,p,s,l,m,k,o,f}",
|
|
149
|
+
help=(
|
|
150
|
+
"Database type: [bold]a[/bold]=MS-Access, [bold]p[/bold]=PostgreSQL, "
|
|
151
|
+
"[bold]s[/bold]=SQL Server, [bold]l[/bold]=SQLite, [bold]m[/bold]=MySQL/MariaDB, "
|
|
152
|
+
"[bold]k[/bold]=DuckDB, [bold]o[/bold]=Oracle, [bold]f[/bold]=Firebird, "
|
|
153
|
+
"[bold]d[/bold]=DSN."
|
|
154
|
+
),
|
|
155
|
+
),
|
|
156
|
+
user: str | None = typer.Option(
|
|
157
|
+
None,
|
|
158
|
+
"-u",
|
|
159
|
+
"--user",
|
|
160
|
+
help="Database user name.",
|
|
161
|
+
),
|
|
162
|
+
use_gui: str | None = typer.Option(
|
|
163
|
+
None,
|
|
164
|
+
"-v",
|
|
165
|
+
"--visible-prompts",
|
|
166
|
+
metavar="{0,1,2,3}",
|
|
167
|
+
help=(
|
|
168
|
+
"GUI level: [bold]0[/bold]=none (default), [bold]1[/bold]=GUI for password/pause, "
|
|
169
|
+
"[bold]2[/bold]=GUI for password/pause + DB selection, [bold]3[/bold]=full GUI console."
|
|
170
|
+
),
|
|
171
|
+
),
|
|
172
|
+
gui_framework: str | None = typer.Option(
|
|
173
|
+
None,
|
|
174
|
+
"--gui-framework",
|
|
175
|
+
metavar="{tkinter,textual}",
|
|
176
|
+
help="GUI framework to use with [cyan]--visible-prompts[/cyan]. [dim]Default: tkinter[/dim]",
|
|
177
|
+
),
|
|
178
|
+
no_passwd: bool = typer.Option(
|
|
179
|
+
False,
|
|
180
|
+
"-w",
|
|
181
|
+
"--no-passwd",
|
|
182
|
+
help="Skip password prompt when user is specified.",
|
|
183
|
+
),
|
|
184
|
+
encodings: bool = typer.Option(
|
|
185
|
+
False,
|
|
186
|
+
"-y",
|
|
187
|
+
"--encodings",
|
|
188
|
+
help="List available encoding names and exit.",
|
|
189
|
+
),
|
|
190
|
+
import_buffer: int | None = typer.Option(
|
|
191
|
+
None,
|
|
192
|
+
"-z",
|
|
193
|
+
"--import-buffer",
|
|
194
|
+
metavar="KB",
|
|
195
|
+
help="Import buffer size in KB. [dim]Default: 32[/dim]",
|
|
196
|
+
),
|
|
197
|
+
command: str | None = typer.Option(
|
|
198
|
+
None,
|
|
199
|
+
"-c",
|
|
200
|
+
"--command",
|
|
201
|
+
metavar="SCRIPT",
|
|
202
|
+
help=(
|
|
203
|
+
"Execute an inline SQL/metacommand script string instead of a script file. "
|
|
204
|
+
"Use shell [cyan]$'line1\\nline2'[/cyan] syntax for multi-line scripts."
|
|
205
|
+
),
|
|
206
|
+
),
|
|
207
|
+
dry_run: bool = typer.Option(
|
|
208
|
+
False,
|
|
209
|
+
"--dry-run",
|
|
210
|
+
help=("Parse the script and print the command list without connecting to a database or executing anything."),
|
|
211
|
+
),
|
|
212
|
+
dsn: str | None = typer.Option(
|
|
213
|
+
None,
|
|
214
|
+
"--dsn",
|
|
215
|
+
"--connection-string",
|
|
216
|
+
metavar="URL",
|
|
217
|
+
help=(
|
|
218
|
+
"Database connection URL, e.g. [cyan]postgresql://user:pass@host:5432/db[/cyan]. "
|
|
219
|
+
"Supported schemes: postgresql, mysql, mssql, oracle, firebird, sqlite, duckdb. "
|
|
220
|
+
"Overrides [cyan]-t[/cyan]/[cyan]-u[/cyan]/[cyan]-p[/cyan] and positional server/db args."
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
output_dir: str | None = typer.Option(
|
|
224
|
+
None,
|
|
225
|
+
"--output-dir",
|
|
226
|
+
metavar="DIR",
|
|
227
|
+
help=(
|
|
228
|
+
"Default base directory for EXPORT output files. "
|
|
229
|
+
"Relative paths in EXPORT metacommands are joined to this directory. "
|
|
230
|
+
"Absolute paths and [cyan]stdout[/cyan] are unaffected."
|
|
231
|
+
),
|
|
232
|
+
),
|
|
233
|
+
progress: bool = typer.Option(
|
|
234
|
+
False,
|
|
235
|
+
"--progress",
|
|
236
|
+
help="Show a progress bar for long-running IMPORT operations.",
|
|
237
|
+
),
|
|
238
|
+
dump_keywords: bool = typer.Option(
|
|
239
|
+
False,
|
|
240
|
+
"--dump-keywords",
|
|
241
|
+
help="Dump all metacommand keywords as JSON and exit.",
|
|
242
|
+
),
|
|
243
|
+
version: bool | None = typer.Option(
|
|
244
|
+
None,
|
|
245
|
+
"--version",
|
|
246
|
+
callback=_version_callback,
|
|
247
|
+
is_eager=True,
|
|
248
|
+
help="Show version and exit.",
|
|
249
|
+
),
|
|
250
|
+
) -> None:
|
|
251
|
+
"""Run [bold]SQL_SCRIPT[/bold] against the specified database.
|
|
252
|
+
|
|
253
|
+
[dim]Positional arguments after the script file:[/dim]
|
|
254
|
+
|
|
255
|
+
[green]Client-server databases:[/green]
|
|
256
|
+
execsql script.sql [SERVER] [DATABASE]
|
|
257
|
+
|
|
258
|
+
[green]File-based databases (SQLite, DuckDB, Access):[/green]
|
|
259
|
+
execsql script.sql [DATABASE_FILE]
|
|
260
|
+
"""
|
|
261
|
+
# ------------------------------------------------------------------
|
|
262
|
+
# Early exits (no script file needed)
|
|
263
|
+
# ------------------------------------------------------------------
|
|
264
|
+
if metacommands:
|
|
265
|
+
_print_metacommands()
|
|
266
|
+
raise typer.Exit()
|
|
267
|
+
|
|
268
|
+
if encodings:
|
|
269
|
+
_print_encodings()
|
|
270
|
+
raise typer.Exit()
|
|
271
|
+
|
|
272
|
+
if dump_keywords:
|
|
273
|
+
import json as _json
|
|
274
|
+
|
|
275
|
+
from execsql.metacommands import (
|
|
276
|
+
ALL_EXPORT_FORMATS,
|
|
277
|
+
DATABASE_TYPES,
|
|
278
|
+
DISPATCH_TABLE,
|
|
279
|
+
JSON_VARIANT_FORMATS,
|
|
280
|
+
METADATA_FORMATS,
|
|
281
|
+
QUERY_EXPORT_FORMATS,
|
|
282
|
+
SERVE_FORMATS,
|
|
283
|
+
TABLE_EXPORT_FORMATS,
|
|
284
|
+
)
|
|
285
|
+
from execsql.metacommands.conditions import CONDITIONAL_TABLE
|
|
286
|
+
|
|
287
|
+
mc_kw = DISPATCH_TABLE.keywords_by_category()
|
|
288
|
+
cond_kw = CONDITIONAL_TABLE.keywords_by_category()
|
|
289
|
+
|
|
290
|
+
data = {
|
|
291
|
+
"metacommands": {
|
|
292
|
+
"control": sorted(mc_kw.get("control", [])),
|
|
293
|
+
"block": sorted(
|
|
294
|
+
mc_kw.get("block", []) + ["BEGIN SCRIPT", "END SCRIPT", "BEGIN SQL", "END SQL"],
|
|
295
|
+
),
|
|
296
|
+
"action": sorted(mc_kw.get("action", [])),
|
|
297
|
+
"config": sorted(mc_kw.get("config", [])),
|
|
298
|
+
"prompt": sorted(mc_kw.get("prompt", [])),
|
|
299
|
+
},
|
|
300
|
+
"conditions": sorted(cond_kw.get("condition", []) + ["IS_FALSE", "NOT", "OR"]),
|
|
301
|
+
"config_options": sorted(mc_kw.get("config_option", [])),
|
|
302
|
+
"export_formats": {
|
|
303
|
+
"query": sorted(QUERY_EXPORT_FORMATS),
|
|
304
|
+
"table": sorted(TABLE_EXPORT_FORMATS),
|
|
305
|
+
"serve": sorted(SERVE_FORMATS),
|
|
306
|
+
"metadata": sorted(METADATA_FORMATS),
|
|
307
|
+
"json_variants": sorted(JSON_VARIANT_FORMATS),
|
|
308
|
+
"all": sorted(ALL_EXPORT_FORMATS),
|
|
309
|
+
},
|
|
310
|
+
"database_types": sorted(DATABASE_TYPES),
|
|
311
|
+
"variable_patterns": {
|
|
312
|
+
"system": "!!$name!!",
|
|
313
|
+
"environment": "!!&name!!",
|
|
314
|
+
"parameter": "!!#name!!",
|
|
315
|
+
"column": "!!@name!!",
|
|
316
|
+
"local": "!!~name!!",
|
|
317
|
+
"local_alt": "!!+name!!",
|
|
318
|
+
"regular": "!!name!!",
|
|
319
|
+
"deferred": "!{name}!",
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
_console.print_json(_json.dumps(data, indent=2))
|
|
323
|
+
raise typer.Exit()
|
|
324
|
+
|
|
325
|
+
if online_help:
|
|
326
|
+
import webbrowser
|
|
327
|
+
|
|
328
|
+
webbrowser.open("https://execsql2.readthedocs.io/en/latest/", new=2, autoraise=True)
|
|
329
|
+
raise typer.Exit()
|
|
330
|
+
|
|
331
|
+
positional = args or []
|
|
332
|
+
if command is not None:
|
|
333
|
+
script_name = None # inline mode — no script file
|
|
334
|
+
else:
|
|
335
|
+
if not positional:
|
|
336
|
+
_err_console.print(
|
|
337
|
+
"[bold red]Error:[/bold red] No SQL script file specified. Use [cyan]-c[/cyan] to run an inline script.",
|
|
338
|
+
)
|
|
339
|
+
raise typer.Exit(code=1)
|
|
340
|
+
script_name = positional[0]
|
|
341
|
+
if not Path(script_name).exists():
|
|
342
|
+
_err_console.print(
|
|
343
|
+
f'[bold red]Error:[/bold red] SQL script file [cyan]"{script_name}"[/cyan] does not exist.',
|
|
344
|
+
)
|
|
345
|
+
raise typer.Exit(code=1)
|
|
346
|
+
|
|
347
|
+
# ------------------------------------------------------------------
|
|
348
|
+
# Validate positional args and db_type choice
|
|
349
|
+
# ------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
if db_type and db_type not in ("a", "d", "p", "s", "l", "m", "k", "o", "f"):
|
|
352
|
+
_err_console.print(
|
|
353
|
+
f"[bold red]Error:[/bold red] Invalid database type [cyan]{db_type!r}[/cyan]. "
|
|
354
|
+
"Choose from: a, d, p, s, l, m, k, o, f",
|
|
355
|
+
)
|
|
356
|
+
raise typer.Exit(code=2)
|
|
357
|
+
|
|
358
|
+
if use_gui and use_gui not in ("0", "1", "2", "3"):
|
|
359
|
+
_err_console.print(
|
|
360
|
+
f"[bold red]Error:[/bold red] Invalid GUI level [cyan]{use_gui!r}[/cyan]. Choose from: 0, 1, 2, 3",
|
|
361
|
+
)
|
|
362
|
+
raise typer.Exit(code=2)
|
|
363
|
+
|
|
364
|
+
if gui_framework and gui_framework.lower() not in ("tkinter", "textual"):
|
|
365
|
+
_err_console.print(
|
|
366
|
+
f"[bold red]Error:[/bold red] Invalid GUI framework [cyan]{gui_framework!r}[/cyan]. Choose from: tkinter, textual",
|
|
367
|
+
)
|
|
368
|
+
raise typer.Exit(code=2)
|
|
369
|
+
|
|
370
|
+
if boolean_int and boolean_int.lower() not in ("0", "1", "t", "f", "y", "n"):
|
|
371
|
+
_err_console.print(
|
|
372
|
+
f"[bold red]Error:[/bold red] Invalid --boolean-int value [cyan]{boolean_int!r}[/cyan].",
|
|
373
|
+
)
|
|
374
|
+
raise typer.Exit(code=2)
|
|
375
|
+
|
|
376
|
+
# ------------------------------------------------------------------
|
|
377
|
+
# Delegate to the real main implementation
|
|
378
|
+
# ------------------------------------------------------------------
|
|
379
|
+
_run(
|
|
380
|
+
positional=positional,
|
|
381
|
+
sub_vars=sub_vars,
|
|
382
|
+
boolean_int=boolean_int,
|
|
383
|
+
make_dirs=make_dirs,
|
|
384
|
+
database_encoding=database_encoding,
|
|
385
|
+
script_encoding=script_encoding,
|
|
386
|
+
output_encoding=output_encoding,
|
|
387
|
+
import_encoding=import_encoding,
|
|
388
|
+
user_logfile=user_logfile,
|
|
389
|
+
new_db=new_db,
|
|
390
|
+
port=port,
|
|
391
|
+
scanlines=scanlines,
|
|
392
|
+
db_type=db_type,
|
|
393
|
+
user=user,
|
|
394
|
+
use_gui=use_gui,
|
|
395
|
+
gui_framework=gui_framework,
|
|
396
|
+
no_passwd=no_passwd,
|
|
397
|
+
import_buffer=import_buffer,
|
|
398
|
+
script_name=script_name,
|
|
399
|
+
command=command,
|
|
400
|
+
dry_run=dry_run,
|
|
401
|
+
dsn=dsn,
|
|
402
|
+
output_dir=output_dir,
|
|
403
|
+
progress=progress,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# ---------------------------------------------------------------------------
|
|
408
|
+
# Legacy entry point (kept for backwards compat with pyproject.toml script)
|
|
409
|
+
# ---------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _legacy_main() -> None:
|
|
413
|
+
"""Entry point that wraps the Typer app for use as a console_scripts target."""
|
|
414
|
+
try:
|
|
415
|
+
app()
|
|
416
|
+
except SystemExit as exc:
|
|
417
|
+
raise exc from exc
|
|
418
|
+
except ErrInfo as exc:
|
|
419
|
+
from execsql.utils.errors import exit_now
|
|
420
|
+
|
|
421
|
+
exit_now(1, exc)
|
|
422
|
+
except ConfigError as exc:
|
|
423
|
+
strace = traceback.extract_tb(sys.exc_info()[2])[-1:]
|
|
424
|
+
lno = strace[0][1]
|
|
425
|
+
sys.exit(f"Configuration error on line {lno} of execsql: {exc}")
|
|
426
|
+
except Exception:
|
|
427
|
+
strace = traceback.extract_tb(sys.exc_info()[2])[-1:]
|
|
428
|
+
lno = strace[0][1]
|
|
429
|
+
msg = f"{Path(sys.argv[0]).name}: Uncaught exception {sys.exc_info()[0]} ({sys.exc_info()[1]}) on line {lno}"
|
|
430
|
+
from execsql.utils.errors import exit_now
|
|
431
|
+
|
|
432
|
+
exit_now(1, ErrInfo("exception", exception_msg=msg))
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
if __name__ == "__main__":
|
|
436
|
+
_legacy_main()
|
execsql/cli/dsn.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Connection-string (DSN URL) parser for the execsql CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from execsql.exceptions import ConfigError
|
|
6
|
+
|
|
7
|
+
#: Mapping from URL scheme to execsql db_type code.
|
|
8
|
+
_SCHEME_TO_DBTYPE: dict[str, str] = {
|
|
9
|
+
"postgresql": "p",
|
|
10
|
+
"postgres": "p",
|
|
11
|
+
"mysql": "m",
|
|
12
|
+
"mariadb": "m",
|
|
13
|
+
"mssql": "s",
|
|
14
|
+
"sqlserver": "s",
|
|
15
|
+
"oracle": "o",
|
|
16
|
+
"oracle+cx_oracle": "o",
|
|
17
|
+
"firebird": "f",
|
|
18
|
+
"sqlite": "l",
|
|
19
|
+
"duckdb": "k",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parse_connection_string(dsn: str) -> dict:
|
|
24
|
+
"""Parse a database URL into a dict of connection parameters.
|
|
25
|
+
|
|
26
|
+
Supports the common form::
|
|
27
|
+
|
|
28
|
+
scheme://[user[:password]@][host[:port]]/database
|
|
29
|
+
|
|
30
|
+
For file-based databases (SQLite, DuckDB) the path after ``//`` is
|
|
31
|
+
treated as the database file path::
|
|
32
|
+
|
|
33
|
+
sqlite:///path/to/file.db -> db_file = /path/to/file.db
|
|
34
|
+
duckdb:///path/to/file.db -> db_file = /path/to/file.db
|
|
35
|
+
|
|
36
|
+
Returns a dict with keys: ``db_type``, ``server``, ``db``, ``db_file``,
|
|
37
|
+
``user``, ``password``, ``port``. Absent components are ``None``.
|
|
38
|
+
|
|
39
|
+
Raises :class:`~execsql.exceptions.ConfigError` for an unrecognised
|
|
40
|
+
URL scheme or a completely un-parseable string.
|
|
41
|
+
"""
|
|
42
|
+
from urllib.parse import urlparse
|
|
43
|
+
|
|
44
|
+
parsed = urlparse(dsn)
|
|
45
|
+
scheme = parsed.scheme.lower()
|
|
46
|
+
if not scheme:
|
|
47
|
+
raise ConfigError(f"Cannot parse connection string (no scheme): {dsn!r}")
|
|
48
|
+
if scheme not in _SCHEME_TO_DBTYPE:
|
|
49
|
+
raise ConfigError(
|
|
50
|
+
f"Unrecognised connection-string scheme {scheme!r}. "
|
|
51
|
+
f"Supported schemes: {', '.join(sorted(_SCHEME_TO_DBTYPE))}",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
db_type = _SCHEME_TO_DBTYPE[scheme]
|
|
55
|
+
port: int | None = parsed.port
|
|
56
|
+
server: str | None = parsed.hostname or None
|
|
57
|
+
user: str | None = parsed.username or None
|
|
58
|
+
password: str | None = parsed.password or None
|
|
59
|
+
|
|
60
|
+
# Database / file path
|
|
61
|
+
# urlparse puts the path in parsed.path. For three-slash URIs like
|
|
62
|
+
# sqlite:///foo.db the path starts with "/"; strip exactly one leading
|
|
63
|
+
# slash for relative paths (sqlite:///foo.db -> foo.db) and leave
|
|
64
|
+
# absolute paths intact (sqlite:////abs/path -> /abs/path).
|
|
65
|
+
raw_path = parsed.path
|
|
66
|
+
if db_type in ("l", "k", "a"):
|
|
67
|
+
# File-based: no server component
|
|
68
|
+
if raw_path.startswith("/") and not raw_path.startswith("//"):
|
|
69
|
+
db_file: str | None = raw_path[1:] or None
|
|
70
|
+
else:
|
|
71
|
+
db_file = raw_path or None
|
|
72
|
+
db: str | None = None
|
|
73
|
+
else:
|
|
74
|
+
db_file = None
|
|
75
|
+
# Remove leading "/"
|
|
76
|
+
db = raw_path.lstrip("/") or None
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
"db_type": db_type,
|
|
80
|
+
"server": server,
|
|
81
|
+
"db": db,
|
|
82
|
+
"db_file": db_file,
|
|
83
|
+
"user": user,
|
|
84
|
+
"password": password,
|
|
85
|
+
"port": port,
|
|
86
|
+
}
|
execsql/cli/help.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Rich-formatted help output for the execsql CLI.
|
|
2
|
+
|
|
3
|
+
Contains the metacommand reference table, encoding list, and the shared
|
|
4
|
+
``Console`` instances used by the other CLI submodules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from encodings.aliases import aliases as codec_dict
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
_console = Console()
|
|
15
|
+
_err_console = Console(stderr=True)
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Metacommand syntax hints — paired with keywords from the dispatch table.
|
|
19
|
+
# Keys must match the ``description`` values used in mcl.add() calls.
|
|
20
|
+
# Entries here are validated by tests/test_registry.py.
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
_SYNTAX: dict[str, tuple[str, str]] = {
|
|
24
|
+
# (display_name, syntax_hint)
|
|
25
|
+
"ASK": ("ASK", '"<question>" SUB <match_string>'),
|
|
26
|
+
"AUTOCOMMIT": ("AUTOCOMMIT", "ON|OFF"),
|
|
27
|
+
"BEGIN BATCH": ("BEGIN BATCH / END BATCH / ROLLBACK BATCH", ""),
|
|
28
|
+
"BEGIN SCRIPT": ("BEGIN SCRIPT / END SCRIPT", ""),
|
|
29
|
+
"BEGIN SQL": ("BEGIN SQL / END SQL", ""),
|
|
30
|
+
"CANCEL_HALT": ("CANCEL_HALT", "ON|OFF"),
|
|
31
|
+
"CD": ("CD", "<directory>"),
|
|
32
|
+
"CONNECT": ("CONNECT", "<alias> [AS <alias_name>]"),
|
|
33
|
+
"COPY": ("COPY", "<source_file> TO <dest_file>"),
|
|
34
|
+
"DEBUG": ("DEBUG", "ON|OFF"),
|
|
35
|
+
"SUB": ("DEFINE SUB", "<variable> [AS] <value>"),
|
|
36
|
+
"EXPORT QUERY": ("EXPORT QUERY", "<queryname> [AS <alias>] ..."),
|
|
37
|
+
"EXPORT": ("EXPORT", "<queryname> TO <format> <filename> ..."),
|
|
38
|
+
"HALT": ("HALT [ON]", "ERROR|CANCEL"),
|
|
39
|
+
"IF": ("IF <condition>", "/ ELSE / ENDIF"),
|
|
40
|
+
"IMPORT_FILE": ("IMPORT FILE", "<filename> [OPTIONS ...]"),
|
|
41
|
+
"IMPORT": ("IMPORT TABLE", "<tablename> FROM FILE <filename> [OPTIONS ...]"),
|
|
42
|
+
"LOOP": ("LOOP <n> TIMES | WHILE | UNTIL", "/ END LOOP"),
|
|
43
|
+
"CONFIG": ("CONFIG", "<option> <value>"),
|
|
44
|
+
"ON CANCEL_HALT": ("ON CANCEL_HALT", "..."),
|
|
45
|
+
"ON ERROR_HALT": ("ON ERROR_HALT", "..."),
|
|
46
|
+
"PAUSE": ("PAUSE", "[<text>]"),
|
|
47
|
+
"PROMPT ACTION": ("PROMPT ACTION", "..."),
|
|
48
|
+
"PROMPT ENTRY_FORM": ("PROMPT ENTRY_FORM", "..."),
|
|
49
|
+
"PROMPT OPENFILE": ("PROMPT OPENFILE", "..."),
|
|
50
|
+
"PROMPT SAVEFILE": ("PROMPT SAVEFILE", "..."),
|
|
51
|
+
"PROMPT DIRECTORY": ("PROMPT DIRECTORY", "..."),
|
|
52
|
+
"PROMPT MAP": ("PROMPT MAP", "..."),
|
|
53
|
+
"ROLLBACK BATCH": ("ROLLBACK", ""),
|
|
54
|
+
"SERVE": ("SERVE", "<queryname> ..."),
|
|
55
|
+
"SYSTEM_CMD": ("SYSTEM_CMD", "(<operating system command line>)"),
|
|
56
|
+
"TIMER": ("TIMER", "ON|OFF"),
|
|
57
|
+
"USE": ("USE", "<alias_name>"),
|
|
58
|
+
"WAIT_UNTIL": ("WAIT_UNTIL", "<Boolean_expression> <HALT|CONTINUE> AFTER <n> SECONDS"),
|
|
59
|
+
"WRITE": ("WRITE", '"<text>" [[TEE] TO <output>]'),
|
|
60
|
+
"WRITE CREATE_TABLE": ("WRITE CREATE_TABLE FROM", "<filename> [TO <output>]"),
|
|
61
|
+
"WRITE SCRIPT": ("WRITE SCRIPT", "<script_name> [[APPEND] TO <output_file>]"),
|
|
62
|
+
"ZIP": ("ZIP", "<filename> [APPEND] TO ZIPFILE <zipfilename>"),
|
|
63
|
+
"SUB_TEMPFILE": ("SUB_TEMPFILE", "<variable>"),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Keys from _SYNTAX that should be skipped when auto-generating from dispatch
|
|
67
|
+
# table (they're variants covered by another entry).
|
|
68
|
+
_SKIP_FROM_DISPATCH = {
|
|
69
|
+
"END BATCH",
|
|
70
|
+
"END SCRIPT",
|
|
71
|
+
"END SQL",
|
|
72
|
+
"ROLLBACK BATCH",
|
|
73
|
+
"BEGIN SCRIPT",
|
|
74
|
+
"BEGIN SQL",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _print_metacommands() -> None:
|
|
79
|
+
"""Print the metacommands table using Rich.
|
|
80
|
+
|
|
81
|
+
Keyword list is derived from the dispatch table; syntax hints come from
|
|
82
|
+
the ``_SYNTAX`` dict above. Keywords not in ``_SYNTAX`` are shown without
|
|
83
|
+
a syntax column.
|
|
84
|
+
"""
|
|
85
|
+
from execsql.metacommands import DISPATCH_TABLE
|
|
86
|
+
|
|
87
|
+
table = Table(
|
|
88
|
+
title="execsql Metacommands",
|
|
89
|
+
caption="Embed in SQL comment lines following the [bold]!x![/bold] token.",
|
|
90
|
+
show_header=True,
|
|
91
|
+
header_style="bold cyan",
|
|
92
|
+
border_style="dim",
|
|
93
|
+
expand=False,
|
|
94
|
+
)
|
|
95
|
+
table.add_column("Metacommand", style="bold green", no_wrap=True)
|
|
96
|
+
table.add_column("Syntax", style="white")
|
|
97
|
+
|
|
98
|
+
# Collect unique keyword names from the dispatch table.
|
|
99
|
+
seen: set[str] = set()
|
|
100
|
+
keywords: list[str] = []
|
|
101
|
+
for mc in DISPATCH_TABLE:
|
|
102
|
+
if mc.description and mc.description not in seen and mc.description not in _SKIP_FROM_DISPATCH:
|
|
103
|
+
seen.add(mc.description)
|
|
104
|
+
keywords.append(mc.description)
|
|
105
|
+
# Add parser-level keywords not in the dispatch table.
|
|
106
|
+
for extra in ("BEGIN BATCH", "BEGIN SCRIPT", "BEGIN SQL"):
|
|
107
|
+
if extra not in seen:
|
|
108
|
+
seen.add(extra)
|
|
109
|
+
keywords.append(extra)
|
|
110
|
+
|
|
111
|
+
for kw in sorted(keywords):
|
|
112
|
+
if kw in _SYNTAX:
|
|
113
|
+
name, syntax = _SYNTAX[kw]
|
|
114
|
+
table.add_row(name, syntax)
|
|
115
|
+
elif kw.startswith("CONFIG ") or kw.startswith("CONSOLE_") or "_" in kw:
|
|
116
|
+
continue # skip config options / internal entries
|
|
117
|
+
else:
|
|
118
|
+
table.add_row(kw, "")
|
|
119
|
+
|
|
120
|
+
_console.print(table)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _print_encodings() -> None:
|
|
124
|
+
"""Print available encodings using Rich."""
|
|
125
|
+
enc = sorted(codec_dict.keys())
|
|
126
|
+
table = Table(
|
|
127
|
+
title="Available Encodings",
|
|
128
|
+
show_header=False,
|
|
129
|
+
border_style="dim",
|
|
130
|
+
expand=True,
|
|
131
|
+
)
|
|
132
|
+
table.add_column("Encoding", style="cyan")
|
|
133
|
+
# 4 columns
|
|
134
|
+
cols = 4
|
|
135
|
+
for i in range(0, len(enc), cols):
|
|
136
|
+
row = enc[i : i + cols]
|
|
137
|
+
while len(row) < cols:
|
|
138
|
+
row.append("")
|
|
139
|
+
table.add_row(*row)
|
|
140
|
+
_console.print(table)
|