execsql2 2.1.2__py3-none-any.whl → 2.2.1__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 +13 -1
- execsql/db/access.py +16 -12
- execsql/db/base.py +158 -90
- execsql/db/dsn.py +6 -5
- execsql/db/duckdb.py +2 -2
- execsql/db/firebird.py +23 -19
- execsql/db/mysql.py +8 -7
- execsql/db/oracle.py +11 -11
- execsql/db/postgres.py +28 -16
- execsql/db/sqlite.py +12 -11
- execsql/db/sqlserver.py +5 -3
- execsql/exceptions.py +7 -7
- execsql/exporters/base.py +6 -1
- execsql/exporters/delimited.py +44 -35
- execsql/exporters/duckdb.py +2 -2
- execsql/exporters/feather.py +6 -6
- execsql/exporters/html.py +83 -69
- execsql/exporters/json.py +50 -42
- execsql/exporters/latex.py +33 -27
- execsql/exporters/ods.py +4 -4
- execsql/exporters/parquet.py +2 -2
- execsql/exporters/pretty.py +11 -9
- execsql/exporters/raw.py +17 -13
- execsql/exporters/sqlite.py +2 -2
- execsql/exporters/templates.py +23 -15
- execsql/exporters/values.py +22 -20
- execsql/exporters/xls.py +4 -4
- execsql/exporters/xml.py +28 -13
- execsql/importers/base.py +4 -4
- execsql/importers/csv.py +6 -6
- execsql/importers/feather.py +4 -4
- execsql/importers/ods.py +4 -4
- execsql/importers/xls.py +4 -4
- execsql/metacommands/__init__.py +518 -67
- 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/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/py.typed +0 -0
- execsql/script.py +49 -5
- execsql/types.py +20 -20
- execsql/utils/fileio.py +15 -8
- {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/METADATA +6 -6
- execsql2-2.2.1.dist-info/RECORD +104 -0
- execsql2-2.1.2.dist-info/RECORD +0 -96
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/READ_ME.rst +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/WHEEL +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/licenses/NOTICE +0 -0
execsql/{cli.py → cli/run.py}
RENAMED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Core execution logic for the execsql CLI.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Initialises global state, connects to the database, loads the SQL script,
|
|
4
|
+
and drives the main execution loop. Separated from argument parsing
|
|
5
|
+
(``cli/__init__.py``) for testability and maintainability.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
from __future__ import annotations
|
|
@@ -13,563 +14,16 @@ import os
|
|
|
13
14
|
import sys
|
|
14
15
|
import traceback
|
|
15
16
|
from pathlib import Path
|
|
16
|
-
from encodings.aliases import aliases as codec_dict
|
|
17
|
-
|
|
18
|
-
import typer
|
|
19
|
-
from rich.console import Console
|
|
20
|
-
from rich.table import Table
|
|
21
17
|
|
|
22
18
|
from execsql import __version__
|
|
19
|
+
from execsql.cli.dsn import _parse_connection_string
|
|
20
|
+
from execsql.cli.help import _console, _err_console
|
|
23
21
|
from execsql.config import ConfigData, StatObj
|
|
24
22
|
from execsql.exceptions import ConfigError, ErrInfo
|
|
25
23
|
from execsql.script import SubVarSet, current_script_line, read_sqlfile, read_sqlstring, runscripts
|
|
26
24
|
from execsql.utils.fileio import FileWriter, Logger, filewriter_end
|
|
27
25
|
from execsql.utils.gui import gui_connect, gui_console_isrunning, gui_console_off, gui_console_on, gui_console_wait_user
|
|
28
26
|
|
|
29
|
-
_console = Console()
|
|
30
|
-
_err_console = Console(stderr=True)
|
|
31
|
-
|
|
32
|
-
# ---------------------------------------------------------------------------
|
|
33
|
-
# Metacommand help text
|
|
34
|
-
# ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
_METACOMMANDS = [
|
|
37
|
-
("ASK", '"<question>" SUB <match_string>'),
|
|
38
|
-
("AUTOCOMMIT", "ON|OFF"),
|
|
39
|
-
("BEGIN BATCH / END BATCH / ROLLBACK BATCH", ""),
|
|
40
|
-
("BEGIN SCRIPT / END SCRIPT", ""),
|
|
41
|
-
("BEGIN SQL / END SQL", ""),
|
|
42
|
-
("CANCEL_HALT", "ON|OFF"),
|
|
43
|
-
("CD", "<directory>"),
|
|
44
|
-
("CONNECT", "<alias> [AS <alias_name>]"),
|
|
45
|
-
("COPY", "<source_file> TO <dest_file>"),
|
|
46
|
-
("DEBUG", "ON|OFF"),
|
|
47
|
-
("DEFINE SUB", "<variable> [AS] <value>"),
|
|
48
|
-
("EXPORT QUERY", "<queryname> [AS <alias>] ..."),
|
|
49
|
-
("EXPORT", "<queryname> TO <format> <filename> ..."),
|
|
50
|
-
("HALT [ON]", "ERROR|CANCEL"),
|
|
51
|
-
("IF <condition>", "/ ELSE / ENDIF"),
|
|
52
|
-
("IMPORT FILE", "<filename> [OPTIONS ...]"),
|
|
53
|
-
("IMPORT TABLE", "<tablename> FROM FILE <filename> [OPTIONS ...]"),
|
|
54
|
-
("LOOP <n> TIMES", "/ END LOOP"),
|
|
55
|
-
("LOOP WHILE <condition>", "/ END LOOP"),
|
|
56
|
-
("LOOP UNTIL <condition>", "/ END LOOP"),
|
|
57
|
-
("ON CANCEL_HALT", "..."),
|
|
58
|
-
("ON ERROR_HALT", "..."),
|
|
59
|
-
("PAUSE", "[<text>]"),
|
|
60
|
-
("PROMPT ACTION", "..."),
|
|
61
|
-
("PROMPT ENTRY_FORM", "..."),
|
|
62
|
-
("PROMPT MENU", "..."),
|
|
63
|
-
("PROMPT OPENFILE", "..."),
|
|
64
|
-
("PROMPT SAVEFILE", "..."),
|
|
65
|
-
("PROMPT DIRECTORY", "..."),
|
|
66
|
-
("PROMPT MAP", "..."),
|
|
67
|
-
("RECONNECT", ""),
|
|
68
|
-
("ROLLBACK", ""),
|
|
69
|
-
("SERVE", "<queryname> ..."),
|
|
70
|
-
("SET AUTOCOMMIT", "ON|OFF"),
|
|
71
|
-
("SHELL", "(<command>)"),
|
|
72
|
-
("SHOW WARNINGS", "ON|OFF"),
|
|
73
|
-
("SUB", "<variable> [AS] <value>"),
|
|
74
|
-
("SUB_TEMPFILE", "<variable>"),
|
|
75
|
-
("SYSTEM_CMD", "(<operating system command line>)"),
|
|
76
|
-
("TIMER", "ON|OFF"),
|
|
77
|
-
("USE", "<alias_name>"),
|
|
78
|
-
("WAIT_UNTIL", "<Boolean_expression> <HALT|CONTINUE> AFTER <n> SECONDS"),
|
|
79
|
-
("WRITE", '"<text>" [[TEE] TO <output>]'),
|
|
80
|
-
("WRITE CREATE_TABLE FROM", "<filename> [TO <output>]"),
|
|
81
|
-
("WRITE SCRIPT", "<script_name> [[APPEND] TO <output_file>]"),
|
|
82
|
-
("ZIP", "<filename> [APPEND] TO ZIPFILE <zipfilename>"),
|
|
83
|
-
]
|
|
84
|
-
|
|
85
|
-
_METACOMMANDS_PLAIN = """\
|
|
86
|
-
Metacommands are embedded in SQL comment lines following the !x! token.
|
|
87
|
-
See the documentation for more complete descriptions of the metacommands.
|
|
88
|
-
ASK "<question>" SUB <match_string>
|
|
89
|
-
AUTOCOMMIT ON|OFF
|
|
90
|
-
BEGIN BATCH / END BATCH / ROLLBACK BATCH
|
|
91
|
-
BEGIN SCRIPT / END SCRIPT
|
|
92
|
-
BEGIN SQL / END SQL
|
|
93
|
-
CANCEL_HALT ON|OFF
|
|
94
|
-
CD <directory>
|
|
95
|
-
CONNECT <alias> [AS <alias_name>]
|
|
96
|
-
COPY <source_file> TO <dest_file>
|
|
97
|
-
DEBUG ON|OFF
|
|
98
|
-
DEFINE SUB <variable> [AS] <value>
|
|
99
|
-
EXPORT QUERY <queryname> [AS <alias>] ...
|
|
100
|
-
EXPORT <queryname> TO <format> <filename> ...
|
|
101
|
-
HALT [ON] ERROR|CANCEL
|
|
102
|
-
IF <condition> / ELSE / ENDIF
|
|
103
|
-
IMPORT FILE <filename> [OPTIONS ...]
|
|
104
|
-
IMPORT TABLE <tablename> FROM FILE <filename> [OPTIONS ...]
|
|
105
|
-
LOOP <n> TIMES / END LOOP
|
|
106
|
-
LOOP WHILE <condition> / END LOOP
|
|
107
|
-
LOOP UNTIL <condition> / END LOOP
|
|
108
|
-
ON CANCEL_HALT ...
|
|
109
|
-
ON ERROR_HALT ...
|
|
110
|
-
PAUSE [<text>]
|
|
111
|
-
PROMPT ACTION ...
|
|
112
|
-
PROMPT ENTRY_FORM ...
|
|
113
|
-
PROMPT MENU ...
|
|
114
|
-
PROMPT OPENFILE ...
|
|
115
|
-
PROMPT SAVEFILE ...
|
|
116
|
-
PROMPT DIRECTORY ...
|
|
117
|
-
PROMPT MAP ...
|
|
118
|
-
RECONNECT
|
|
119
|
-
ROLLBACK
|
|
120
|
-
SERVE <queryname> ...
|
|
121
|
-
SET AUTOCOMMIT ON|OFF
|
|
122
|
-
SHELL (<command>)
|
|
123
|
-
SHOW WARNINGS ON|OFF
|
|
124
|
-
SUB <variable> [AS] <value>
|
|
125
|
-
SUB_TEMPFILE <variable>
|
|
126
|
-
SYSTEM_CMD (<operating system command line>)
|
|
127
|
-
TIMER ON|OFF
|
|
128
|
-
USE <alias_name>
|
|
129
|
-
WAIT_UNTIL <Boolean_expression> <HALT|CONTINUE> AFTER <n> SECONDS
|
|
130
|
-
WRITE "<text>" [[TEE] TO <output>]
|
|
131
|
-
WRITE CREATE_TABLE FROM <filename> [TO <output>]
|
|
132
|
-
WRITE SCRIPT <script_name> [[APPEND] TO <output_file>]
|
|
133
|
-
ZIP <filename> [APPEND] TO ZIPFILE <zipfilename>"""
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _print_metacommands() -> None:
|
|
137
|
-
"""Print the metacommands table using Rich."""
|
|
138
|
-
table = Table(
|
|
139
|
-
title="execsql Metacommands",
|
|
140
|
-
caption="Embed in SQL comment lines following the [bold]!x![/bold] token.",
|
|
141
|
-
show_header=True,
|
|
142
|
-
header_style="bold cyan",
|
|
143
|
-
border_style="dim",
|
|
144
|
-
expand=False,
|
|
145
|
-
)
|
|
146
|
-
table.add_column("Metacommand", style="bold green", no_wrap=True)
|
|
147
|
-
table.add_column("Syntax", style="white")
|
|
148
|
-
for name, syntax in _METACOMMANDS:
|
|
149
|
-
table.add_row(name, syntax)
|
|
150
|
-
_console.print(table)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def _print_encodings() -> None:
|
|
154
|
-
"""Print available encodings using Rich."""
|
|
155
|
-
enc = sorted(codec_dict.keys())
|
|
156
|
-
table = Table(
|
|
157
|
-
title="Available Encodings",
|
|
158
|
-
show_header=False,
|
|
159
|
-
border_style="dim",
|
|
160
|
-
expand=True,
|
|
161
|
-
)
|
|
162
|
-
table.add_column("Encoding", style="cyan")
|
|
163
|
-
# 4 columns
|
|
164
|
-
cols = 4
|
|
165
|
-
for i in range(0, len(enc), cols):
|
|
166
|
-
row = enc[i : i + cols]
|
|
167
|
-
while len(row) < cols:
|
|
168
|
-
row.append("")
|
|
169
|
-
table.add_row(*row)
|
|
170
|
-
_console.print(table)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
# ---------------------------------------------------------------------------
|
|
174
|
-
# Typer app
|
|
175
|
-
# ---------------------------------------------------------------------------
|
|
176
|
-
|
|
177
|
-
app = typer.Typer(
|
|
178
|
-
name="execsql",
|
|
179
|
-
help="Run a SQL script against a database with metacommand support.",
|
|
180
|
-
add_completion=False,
|
|
181
|
-
rich_markup_mode="rich",
|
|
182
|
-
no_args_is_help=True,
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def _version_callback(value: bool) -> None:
|
|
187
|
-
if value:
|
|
188
|
-
_console.print(f"execsql [bold cyan]{__version__}[/bold cyan]")
|
|
189
|
-
raise typer.Exit()
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
@app.command(
|
|
193
|
-
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
194
|
-
)
|
|
195
|
-
def main(
|
|
196
|
-
ctx: typer.Context,
|
|
197
|
-
# Positional args collected manually (script + optional server/db/file)
|
|
198
|
-
args: list[str] | None = typer.Argument(
|
|
199
|
-
None,
|
|
200
|
-
metavar="SQL_SCRIPT [SERVER DATABASE | DATABASE_FILE]",
|
|
201
|
-
help=(
|
|
202
|
-
"SQL script file to execute. Optionally followed by server and database "
|
|
203
|
-
"name (client-server DBs) or a database file path (file-based DBs)."
|
|
204
|
-
),
|
|
205
|
-
),
|
|
206
|
-
# Named options — grouped to mirror the original argparse interface
|
|
207
|
-
sub_vars: list[str] | None = typer.Option(
|
|
208
|
-
None,
|
|
209
|
-
"-a",
|
|
210
|
-
"--assign-arg",
|
|
211
|
-
metavar="VALUE",
|
|
212
|
-
help="Define the replacement string for a substitution variable [cyan]\\$ARG_x[/cyan].",
|
|
213
|
-
),
|
|
214
|
-
boolean_int: str | None = typer.Option(
|
|
215
|
-
None,
|
|
216
|
-
"-b",
|
|
217
|
-
"--boolean-int",
|
|
218
|
-
metavar="{0,1,t,f,y,n}",
|
|
219
|
-
help="Treat integers 0 and 1 as boolean values.",
|
|
220
|
-
),
|
|
221
|
-
make_dirs: str | None = typer.Option(
|
|
222
|
-
None,
|
|
223
|
-
"-d",
|
|
224
|
-
"--directories",
|
|
225
|
-
metavar="{0,1,t,f,y,n}",
|
|
226
|
-
help="Auto-create directories for EXPORT metacommand. [dim]n=no (default), y=yes[/dim]",
|
|
227
|
-
),
|
|
228
|
-
database_encoding: str | None = typer.Option(
|
|
229
|
-
None,
|
|
230
|
-
"-e",
|
|
231
|
-
"--database-encoding",
|
|
232
|
-
help="Character encoding used in the database.",
|
|
233
|
-
),
|
|
234
|
-
script_encoding: str | None = typer.Option(
|
|
235
|
-
None,
|
|
236
|
-
"-f",
|
|
237
|
-
"--script-encoding",
|
|
238
|
-
help="Character encoding of the script file. [dim]Default: UTF-8[/dim]",
|
|
239
|
-
),
|
|
240
|
-
output_encoding: str | None = typer.Option(
|
|
241
|
-
None,
|
|
242
|
-
"-g",
|
|
243
|
-
"--output-encoding",
|
|
244
|
-
help="Encoding for WRITE and EXPORT output.",
|
|
245
|
-
),
|
|
246
|
-
import_encoding: str | None = typer.Option(
|
|
247
|
-
None,
|
|
248
|
-
"-i",
|
|
249
|
-
"--import-encoding",
|
|
250
|
-
help="Encoding for data files used with IMPORT.",
|
|
251
|
-
),
|
|
252
|
-
user_logfile: bool = typer.Option(
|
|
253
|
-
False,
|
|
254
|
-
"-l",
|
|
255
|
-
"--user-logfile",
|
|
256
|
-
help="Write a log file to [cyan]~/execsql.log[/cyan].",
|
|
257
|
-
),
|
|
258
|
-
metacommands: bool = typer.Option(
|
|
259
|
-
False,
|
|
260
|
-
"-m",
|
|
261
|
-
"--metacommands",
|
|
262
|
-
help="List metacommands and exit.",
|
|
263
|
-
),
|
|
264
|
-
new_db: bool = typer.Option(
|
|
265
|
-
False,
|
|
266
|
-
"-n",
|
|
267
|
-
"--new-db",
|
|
268
|
-
help="Create a new SQLite or Postgres database if it does not exist.",
|
|
269
|
-
),
|
|
270
|
-
online_help: bool = typer.Option(
|
|
271
|
-
False,
|
|
272
|
-
"-o",
|
|
273
|
-
"--online-help",
|
|
274
|
-
help="Open the online documentation in the default browser.",
|
|
275
|
-
),
|
|
276
|
-
port: int | None = typer.Option(
|
|
277
|
-
None,
|
|
278
|
-
"-p",
|
|
279
|
-
"--port",
|
|
280
|
-
help="Database server port.",
|
|
281
|
-
),
|
|
282
|
-
scanlines: int | None = typer.Option(
|
|
283
|
-
None,
|
|
284
|
-
"-s",
|
|
285
|
-
"--scan-lines",
|
|
286
|
-
metavar="N",
|
|
287
|
-
help="Lines to scan for IMPORT format detection. [dim]0 = scan entire file.[/dim]",
|
|
288
|
-
),
|
|
289
|
-
db_type: str | None = typer.Option(
|
|
290
|
-
None,
|
|
291
|
-
"-t",
|
|
292
|
-
"--type",
|
|
293
|
-
metavar="{a,d,p,s,l,m,k,o,f}",
|
|
294
|
-
help=(
|
|
295
|
-
"Database type: [bold]a[/bold]=MS-Access, [bold]p[/bold]=PostgreSQL, "
|
|
296
|
-
"[bold]s[/bold]=SQL Server, [bold]l[/bold]=SQLite, [bold]m[/bold]=MySQL/MariaDB, "
|
|
297
|
-
"[bold]k[/bold]=DuckDB, [bold]o[/bold]=Oracle, [bold]f[/bold]=Firebird, "
|
|
298
|
-
"[bold]d[/bold]=DSN."
|
|
299
|
-
),
|
|
300
|
-
),
|
|
301
|
-
user: str | None = typer.Option(
|
|
302
|
-
None,
|
|
303
|
-
"-u",
|
|
304
|
-
"--user",
|
|
305
|
-
help="Database user name.",
|
|
306
|
-
),
|
|
307
|
-
use_gui: str | None = typer.Option(
|
|
308
|
-
None,
|
|
309
|
-
"-v",
|
|
310
|
-
"--visible-prompts",
|
|
311
|
-
metavar="{0,1,2,3}",
|
|
312
|
-
help=(
|
|
313
|
-
"GUI level: [bold]0[/bold]=none (default), [bold]1[/bold]=GUI for password/pause, "
|
|
314
|
-
"[bold]2[/bold]=GUI for password/pause + DB selection, [bold]3[/bold]=full GUI console."
|
|
315
|
-
),
|
|
316
|
-
),
|
|
317
|
-
gui_framework: str | None = typer.Option(
|
|
318
|
-
None,
|
|
319
|
-
"--gui-framework",
|
|
320
|
-
metavar="{tkinter,textual}",
|
|
321
|
-
help="GUI framework to use with [cyan]--visible-prompts[/cyan]. [dim]Default: tkinter[/dim]",
|
|
322
|
-
),
|
|
323
|
-
no_passwd: bool = typer.Option(
|
|
324
|
-
False,
|
|
325
|
-
"-w",
|
|
326
|
-
"--no-passwd",
|
|
327
|
-
help="Skip password prompt when user is specified.",
|
|
328
|
-
),
|
|
329
|
-
encodings: bool = typer.Option(
|
|
330
|
-
False,
|
|
331
|
-
"-y",
|
|
332
|
-
"--encodings",
|
|
333
|
-
help="List available encoding names and exit.",
|
|
334
|
-
),
|
|
335
|
-
import_buffer: int | None = typer.Option(
|
|
336
|
-
None,
|
|
337
|
-
"-z",
|
|
338
|
-
"--import-buffer",
|
|
339
|
-
metavar="KB",
|
|
340
|
-
help="Import buffer size in KB. [dim]Default: 32[/dim]",
|
|
341
|
-
),
|
|
342
|
-
command: str | None = typer.Option(
|
|
343
|
-
None,
|
|
344
|
-
"-c",
|
|
345
|
-
"--command",
|
|
346
|
-
metavar="SCRIPT",
|
|
347
|
-
help=(
|
|
348
|
-
"Execute an inline SQL/metacommand script string instead of a script file. "
|
|
349
|
-
"Use shell [cyan]$'line1\\nline2'[/cyan] syntax for multi-line scripts."
|
|
350
|
-
),
|
|
351
|
-
),
|
|
352
|
-
dry_run: bool = typer.Option(
|
|
353
|
-
False,
|
|
354
|
-
"--dry-run",
|
|
355
|
-
help=("Parse the script and print the command list without connecting to a database or executing anything."),
|
|
356
|
-
),
|
|
357
|
-
dsn: str | None = typer.Option(
|
|
358
|
-
None,
|
|
359
|
-
"--dsn",
|
|
360
|
-
"--connection-string",
|
|
361
|
-
metavar="URL",
|
|
362
|
-
help=(
|
|
363
|
-
"Database connection URL, e.g. [cyan]postgresql://user:pass@host:5432/db[/cyan]. "
|
|
364
|
-
"Supported schemes: postgresql, mysql, mssql, oracle, firebird, sqlite, duckdb. "
|
|
365
|
-
"Overrides [cyan]-t[/cyan]/[cyan]-u[/cyan]/[cyan]-p[/cyan] and positional server/db args."
|
|
366
|
-
),
|
|
367
|
-
),
|
|
368
|
-
output_dir: str | None = typer.Option(
|
|
369
|
-
None,
|
|
370
|
-
"--output-dir",
|
|
371
|
-
metavar="DIR",
|
|
372
|
-
help=(
|
|
373
|
-
"Default base directory for EXPORT output files. "
|
|
374
|
-
"Relative paths in EXPORT metacommands are joined to this directory. "
|
|
375
|
-
"Absolute paths and [cyan]stdout[/cyan] are unaffected."
|
|
376
|
-
),
|
|
377
|
-
),
|
|
378
|
-
version: bool | None = typer.Option(
|
|
379
|
-
None,
|
|
380
|
-
"--version",
|
|
381
|
-
callback=_version_callback,
|
|
382
|
-
is_eager=True,
|
|
383
|
-
help="Show version and exit.",
|
|
384
|
-
),
|
|
385
|
-
) -> None:
|
|
386
|
-
"""Run [bold]SQL_SCRIPT[/bold] against the specified database.
|
|
387
|
-
|
|
388
|
-
[dim]Positional arguments after the script file:[/dim]
|
|
389
|
-
|
|
390
|
-
[green]Client-server databases:[/green]
|
|
391
|
-
execsql script.sql [SERVER] [DATABASE]
|
|
392
|
-
|
|
393
|
-
[green]File-based databases (SQLite, DuckDB, Access):[/green]
|
|
394
|
-
execsql script.sql [DATABASE_FILE]
|
|
395
|
-
"""
|
|
396
|
-
# ------------------------------------------------------------------
|
|
397
|
-
# Early exits (no script file needed)
|
|
398
|
-
# ------------------------------------------------------------------
|
|
399
|
-
if metacommands:
|
|
400
|
-
_print_metacommands()
|
|
401
|
-
raise typer.Exit()
|
|
402
|
-
|
|
403
|
-
if encodings:
|
|
404
|
-
_print_encodings()
|
|
405
|
-
raise typer.Exit()
|
|
406
|
-
|
|
407
|
-
if online_help:
|
|
408
|
-
import webbrowser
|
|
409
|
-
|
|
410
|
-
webbrowser.open("https://execsql2.readthedocs.io/en/latest/", new=2, autoraise=True)
|
|
411
|
-
raise typer.Exit()
|
|
412
|
-
|
|
413
|
-
positional = args or []
|
|
414
|
-
if command is not None:
|
|
415
|
-
script_name = None # inline mode — no script file
|
|
416
|
-
else:
|
|
417
|
-
if not positional:
|
|
418
|
-
_err_console.print(
|
|
419
|
-
"[bold red]Error:[/bold red] No SQL script file specified. Use [cyan]-c[/cyan] to run an inline script.",
|
|
420
|
-
)
|
|
421
|
-
raise typer.Exit(code=1)
|
|
422
|
-
script_name = positional[0]
|
|
423
|
-
if not Path(script_name).exists():
|
|
424
|
-
_err_console.print(
|
|
425
|
-
f'[bold red]Error:[/bold red] SQL script file [cyan]"{script_name}"[/cyan] does not exist.',
|
|
426
|
-
)
|
|
427
|
-
raise typer.Exit(code=1)
|
|
428
|
-
|
|
429
|
-
# ------------------------------------------------------------------
|
|
430
|
-
# Validate positional args and db_type choice
|
|
431
|
-
# ------------------------------------------------------------------
|
|
432
|
-
|
|
433
|
-
if db_type and db_type not in ("a", "d", "p", "s", "l", "m", "k", "o", "f"):
|
|
434
|
-
_err_console.print(
|
|
435
|
-
f"[bold red]Error:[/bold red] Invalid database type [cyan]{db_type!r}[/cyan]. "
|
|
436
|
-
"Choose from: a, d, p, s, l, m, k, o, f",
|
|
437
|
-
)
|
|
438
|
-
raise typer.Exit(code=2)
|
|
439
|
-
|
|
440
|
-
if use_gui and use_gui not in ("0", "1", "2", "3"):
|
|
441
|
-
_err_console.print(
|
|
442
|
-
f"[bold red]Error:[/bold red] Invalid GUI level [cyan]{use_gui!r}[/cyan]. Choose from: 0, 1, 2, 3",
|
|
443
|
-
)
|
|
444
|
-
raise typer.Exit(code=2)
|
|
445
|
-
|
|
446
|
-
if gui_framework and gui_framework.lower() not in ("tkinter", "textual"):
|
|
447
|
-
_err_console.print(
|
|
448
|
-
f"[bold red]Error:[/bold red] Invalid GUI framework [cyan]{gui_framework!r}[/cyan]. Choose from: tkinter, textual",
|
|
449
|
-
)
|
|
450
|
-
raise typer.Exit(code=2)
|
|
451
|
-
|
|
452
|
-
if boolean_int and boolean_int.lower() not in ("0", "1", "t", "f", "y", "n"):
|
|
453
|
-
_err_console.print(
|
|
454
|
-
f"[bold red]Error:[/bold red] Invalid --boolean-int value [cyan]{boolean_int!r}[/cyan].",
|
|
455
|
-
)
|
|
456
|
-
raise typer.Exit(code=2)
|
|
457
|
-
|
|
458
|
-
# ------------------------------------------------------------------
|
|
459
|
-
# Delegate to the real main implementation
|
|
460
|
-
# ------------------------------------------------------------------
|
|
461
|
-
_run(
|
|
462
|
-
positional=positional,
|
|
463
|
-
sub_vars=sub_vars,
|
|
464
|
-
boolean_int=boolean_int,
|
|
465
|
-
make_dirs=make_dirs,
|
|
466
|
-
database_encoding=database_encoding,
|
|
467
|
-
script_encoding=script_encoding,
|
|
468
|
-
output_encoding=output_encoding,
|
|
469
|
-
import_encoding=import_encoding,
|
|
470
|
-
user_logfile=user_logfile,
|
|
471
|
-
new_db=new_db,
|
|
472
|
-
port=port,
|
|
473
|
-
scanlines=scanlines,
|
|
474
|
-
db_type=db_type,
|
|
475
|
-
user=user,
|
|
476
|
-
use_gui=use_gui,
|
|
477
|
-
gui_framework=gui_framework,
|
|
478
|
-
no_passwd=no_passwd,
|
|
479
|
-
import_buffer=import_buffer,
|
|
480
|
-
script_name=script_name,
|
|
481
|
-
command=command,
|
|
482
|
-
dry_run=dry_run,
|
|
483
|
-
dsn=dsn,
|
|
484
|
-
output_dir=output_dir,
|
|
485
|
-
)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
# ---------------------------------------------------------------------------
|
|
489
|
-
# Connection-string parser
|
|
490
|
-
# ---------------------------------------------------------------------------
|
|
491
|
-
|
|
492
|
-
#: Mapping from URL scheme → execsql db_type code
|
|
493
|
-
_SCHEME_TO_DBTYPE: dict[str, str] = {
|
|
494
|
-
"postgresql": "p",
|
|
495
|
-
"postgres": "p",
|
|
496
|
-
"mysql": "m",
|
|
497
|
-
"mariadb": "m",
|
|
498
|
-
"mssql": "s",
|
|
499
|
-
"sqlserver": "s",
|
|
500
|
-
"oracle": "o",
|
|
501
|
-
"oracle+cx_oracle": "o",
|
|
502
|
-
"firebird": "f",
|
|
503
|
-
"sqlite": "l",
|
|
504
|
-
"duckdb": "k",
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
def _parse_connection_string(dsn: str) -> dict:
|
|
509
|
-
"""Parse a database URL into a dict of connection parameters.
|
|
510
|
-
|
|
511
|
-
Supports the common form::
|
|
512
|
-
|
|
513
|
-
scheme://[user[:password]@][host[:port]]/database
|
|
514
|
-
|
|
515
|
-
For file-based databases (SQLite, DuckDB) the path after ``//`` is
|
|
516
|
-
treated as the database file path::
|
|
517
|
-
|
|
518
|
-
sqlite:///path/to/file.db → db_file = /path/to/file.db
|
|
519
|
-
duckdb:///path/to/file.db → db_file = /path/to/file.db
|
|
520
|
-
|
|
521
|
-
Returns a dict with keys: ``db_type``, ``server``, ``db``, ``db_file``,
|
|
522
|
-
``user``, ``password``, ``port``. Absent components are ``None``.
|
|
523
|
-
|
|
524
|
-
Raises :class:`~execsql.exceptions.ConfigError` for an unrecognised
|
|
525
|
-
URL scheme or a completely un-parseable string.
|
|
526
|
-
"""
|
|
527
|
-
from urllib.parse import urlparse
|
|
528
|
-
|
|
529
|
-
parsed = urlparse(dsn)
|
|
530
|
-
scheme = parsed.scheme.lower()
|
|
531
|
-
if not scheme:
|
|
532
|
-
raise ConfigError(f"Cannot parse connection string (no scheme): {dsn!r}")
|
|
533
|
-
if scheme not in _SCHEME_TO_DBTYPE:
|
|
534
|
-
raise ConfigError(
|
|
535
|
-
f"Unrecognised connection-string scheme {scheme!r}. "
|
|
536
|
-
f"Supported schemes: {', '.join(sorted(_SCHEME_TO_DBTYPE))}",
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
db_type = _SCHEME_TO_DBTYPE[scheme]
|
|
540
|
-
port: int | None = parsed.port
|
|
541
|
-
server: str | None = parsed.hostname or None
|
|
542
|
-
user: str | None = parsed.username or None
|
|
543
|
-
password: str | None = parsed.password or None
|
|
544
|
-
|
|
545
|
-
# Database / file path
|
|
546
|
-
# urlparse puts the path in parsed.path. For three-slash URIs like
|
|
547
|
-
# sqlite:///foo.db the path starts with "/"; strip exactly one leading
|
|
548
|
-
# slash for relative paths (sqlite:///foo.db → foo.db) and leave
|
|
549
|
-
# absolute paths intact (sqlite:////abs/path → /abs/path).
|
|
550
|
-
raw_path = parsed.path
|
|
551
|
-
if db_type in ("l", "k", "a"):
|
|
552
|
-
# File-based: no server component
|
|
553
|
-
if raw_path.startswith("/") and not raw_path.startswith("//"):
|
|
554
|
-
db_file: str | None = raw_path[1:] or None
|
|
555
|
-
else:
|
|
556
|
-
db_file = raw_path or None
|
|
557
|
-
db: str | None = None
|
|
558
|
-
else:
|
|
559
|
-
db_file = None
|
|
560
|
-
# Remove leading "/"
|
|
561
|
-
db = raw_path.lstrip("/") or None
|
|
562
|
-
|
|
563
|
-
return {
|
|
564
|
-
"db_type": db_type,
|
|
565
|
-
"server": server,
|
|
566
|
-
"db": db,
|
|
567
|
-
"db_file": db_file,
|
|
568
|
-
"user": user,
|
|
569
|
-
"password": password,
|
|
570
|
-
"port": port,
|
|
571
|
-
}
|
|
572
|
-
|
|
573
27
|
|
|
574
28
|
# ---------------------------------------------------------------------------
|
|
575
29
|
# Dry-run helper
|
|
@@ -619,6 +73,7 @@ def _run(
|
|
|
619
73
|
dry_run: bool = False,
|
|
620
74
|
dsn: str | None = None,
|
|
621
75
|
output_dir: str | None = None,
|
|
76
|
+
progress: bool = False,
|
|
622
77
|
) -> None:
|
|
623
78
|
"""Initialise state, connect to the database, load the script, and run it.
|
|
624
79
|
|
|
@@ -751,6 +206,8 @@ def _run(
|
|
|
751
206
|
conf.new_db = True
|
|
752
207
|
if output_dir:
|
|
753
208
|
conf.export_output_dir = str(Path(output_dir).resolve())
|
|
209
|
+
if progress:
|
|
210
|
+
conf.show_progress = True
|
|
754
211
|
|
|
755
212
|
# Positional arguments after the script name (or all positionals in inline mode)
|
|
756
213
|
# off=1: script file occupies positional[0]; connection args start at [1]
|
|
@@ -1041,14 +498,14 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
1041
498
|
"""Create and return the initial database object based on conf.db_type."""
|
|
1042
499
|
from execsql.db.factory import (
|
|
1043
500
|
db_Access,
|
|
501
|
+
db_Dsn,
|
|
502
|
+
db_DuckDB,
|
|
503
|
+
db_Firebird,
|
|
504
|
+
db_MySQL,
|
|
505
|
+
db_Oracle,
|
|
1044
506
|
db_Postgres,
|
|
1045
507
|
db_SQLite,
|
|
1046
508
|
db_SqlServer,
|
|
1047
|
-
db_MySQL,
|
|
1048
|
-
db_DuckDB,
|
|
1049
|
-
db_Oracle,
|
|
1050
|
-
db_Firebird,
|
|
1051
|
-
db_Dsn,
|
|
1052
509
|
)
|
|
1053
510
|
|
|
1054
511
|
if conf.db_type == "a":
|
|
@@ -1132,35 +589,3 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
1132
589
|
from execsql.utils.errors import fatal_error
|
|
1133
590
|
|
|
1134
591
|
fatal_error(f"Unknown database type: '{conf.db_type}'")
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
# ---------------------------------------------------------------------------
|
|
1138
|
-
# Legacy entry point (kept for backwards compat with pyproject.toml script)
|
|
1139
|
-
# ---------------------------------------------------------------------------
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
def _legacy_main() -> None:
|
|
1143
|
-
"""Entry point that wraps the Typer app for use as a console_scripts target."""
|
|
1144
|
-
try:
|
|
1145
|
-
app()
|
|
1146
|
-
except SystemExit as exc:
|
|
1147
|
-
raise exc
|
|
1148
|
-
except ErrInfo as exc:
|
|
1149
|
-
from execsql.utils.errors import exit_now
|
|
1150
|
-
|
|
1151
|
-
exit_now(1, exc)
|
|
1152
|
-
except ConfigError as exc:
|
|
1153
|
-
strace = traceback.extract_tb(sys.exc_info()[2])[-1:]
|
|
1154
|
-
lno = strace[0][1]
|
|
1155
|
-
sys.exit(f"Configuration error on line {lno} of execsql: {exc}")
|
|
1156
|
-
except Exception:
|
|
1157
|
-
strace = traceback.extract_tb(sys.exc_info()[2])[-1:]
|
|
1158
|
-
lno = strace[0][1]
|
|
1159
|
-
msg = f"{Path(sys.argv[0]).name}: Uncaught exception {sys.exc_info()[0]} ({sys.exc_info()[1]}) on line {lno}"
|
|
1160
|
-
from execsql.utils.errors import exit_now
|
|
1161
|
-
|
|
1162
|
-
exit_now(1, ErrInfo("exception", exception_msg=msg))
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
if __name__ == "__main__":
|
|
1166
|
-
_legacy_main()
|
execsql/config.py
CHANGED
|
@@ -106,10 +106,12 @@ class ConfigData:
|
|
|
106
106
|
self.quote_all_text = False
|
|
107
107
|
self.import_row_buffer = 1000
|
|
108
108
|
self.import_progress_interval = 0
|
|
109
|
+
self.show_progress = False
|
|
109
110
|
self.export_row_buffer = 1000
|
|
110
111
|
self.template_processor = None
|
|
111
112
|
self.tee_write_log = False
|
|
112
113
|
self.log_datavars = True
|
|
114
|
+
self.log_sql = False
|
|
113
115
|
self.max_log_size_mb = 0
|
|
114
116
|
self.smtp_host = None
|
|
115
117
|
self.smtp_port = None
|
|
@@ -143,7 +145,7 @@ class ConfigData:
|
|
|
143
145
|
cp.read(configfile)
|
|
144
146
|
if cp.has_option(self._CONNECT_SECTION, "db_type"):
|
|
145
147
|
t = cp.get(self._CONNECT_SECTION, "db_type").lower()
|
|
146
|
-
if len(t) != 1 or t not in ("a", "
|
|
148
|
+
if len(t) != 1 or t not in ("a", "d", "f", "k", "l", "m", "o", "p", "s"):
|
|
147
149
|
raise ConfigError(f"Invalid database type: {t}")
|
|
148
150
|
self.db_type = t
|
|
149
151
|
if cp.has_option(self._CONNECT_SECTION, "server"):
|
|
@@ -291,6 +293,11 @@ class ConfigData:
|
|
|
291
293
|
self.import_progress_interval = cp.getint(self._INPUT_SECTION, "import_progress_interval")
|
|
292
294
|
except Exception as e:
|
|
293
295
|
raise ConfigError("Invalid argument for import_progress_interval.") from e
|
|
296
|
+
if cp.has_option(self._INPUT_SECTION, "show_progress"):
|
|
297
|
+
try:
|
|
298
|
+
self.show_progress = cp.getboolean(self._INPUT_SECTION, "show_progress")
|
|
299
|
+
except Exception as e:
|
|
300
|
+
raise ConfigError("Invalid argument for show_progress.") from e
|
|
294
301
|
if cp.has_option(self._INPUT_SECTION, "access_use_numeric"):
|
|
295
302
|
try:
|
|
296
303
|
self.access_use_numeric = cp.getboolean(self._INPUT_SECTION, "access_use_numeric")
|
|
@@ -466,6 +473,11 @@ class ConfigData:
|
|
|
466
473
|
self.log_datavars = cp.getboolean(self._CONFIG_SECTION, "log_datavars")
|
|
467
474
|
except Exception as e:
|
|
468
475
|
raise ConfigError("Invalid argument to log_datavars setting.") from e
|
|
476
|
+
if cp.has_option(self._CONFIG_SECTION, "log_sql"):
|
|
477
|
+
try:
|
|
478
|
+
self.log_sql = cp.getboolean(self._CONFIG_SECTION, "log_sql")
|
|
479
|
+
except Exception as e:
|
|
480
|
+
raise ConfigError("Invalid argument to log_sql setting.") from e
|
|
469
481
|
if cp.has_option(self._CONFIG_SECTION, "max_log_size_mb"):
|
|
470
482
|
try:
|
|
471
483
|
self.max_log_size_mb = cp.getint(self._CONFIG_SECTION, "max_log_size_mb")
|