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.
Files changed (94) hide show
  1. execsql/cli/__init__.py +436 -0
  2. execsql/cli/dsn.py +86 -0
  3. execsql/cli/help.py +140 -0
  4. execsql/{cli.py → cli/run.py} +14 -589
  5. execsql/config.py +65 -1
  6. execsql/db/access.py +27 -15
  7. execsql/db/base.py +328 -215
  8. execsql/db/dsn.py +10 -5
  9. execsql/db/duckdb.py +6 -2
  10. execsql/db/factory.py +21 -0
  11. execsql/db/firebird.py +27 -19
  12. execsql/db/mysql.py +12 -7
  13. execsql/db/oracle.py +15 -11
  14. execsql/db/postgres.py +31 -16
  15. execsql/db/sqlite.py +15 -11
  16. execsql/db/sqlserver.py +16 -5
  17. execsql/exceptions.py +25 -7
  18. execsql/exporters/base.py +12 -1
  19. execsql/exporters/delimited.py +80 -35
  20. execsql/exporters/duckdb.py +6 -2
  21. execsql/exporters/feather.py +10 -6
  22. execsql/exporters/html.py +89 -69
  23. execsql/exporters/json.py +52 -45
  24. execsql/exporters/latex.py +37 -27
  25. execsql/exporters/ods.py +32 -11
  26. execsql/exporters/parquet.py +5 -2
  27. execsql/exporters/pretty.py +16 -9
  28. execsql/exporters/raw.py +22 -16
  29. execsql/exporters/sqlite.py +6 -2
  30. execsql/exporters/templates.py +39 -21
  31. execsql/exporters/values.py +26 -20
  32. execsql/exporters/xls.py +30 -11
  33. execsql/exporters/xml.py +31 -13
  34. execsql/exporters/zip.py +15 -0
  35. execsql/importers/base.py +6 -4
  36. execsql/importers/csv.py +8 -6
  37. execsql/importers/feather.py +6 -4
  38. execsql/importers/ods.py +6 -4
  39. execsql/importers/xls.py +6 -4
  40. execsql/metacommands/__init__.py +208 -1548
  41. execsql/metacommands/conditions.py +101 -27
  42. execsql/metacommands/control.py +8 -4
  43. execsql/metacommands/data.py +6 -6
  44. execsql/metacommands/debug.py +6 -2
  45. execsql/metacommands/dispatch.py +2011 -0
  46. execsql/metacommands/io.py +67 -1310
  47. execsql/metacommands/io_export.py +442 -0
  48. execsql/metacommands/io_fileops.py +287 -0
  49. execsql/metacommands/io_import.py +398 -0
  50. execsql/metacommands/io_write.py +248 -0
  51. execsql/metacommands/prompt.py +22 -66
  52. execsql/metacommands/system.py +7 -2
  53. execsql/models.py +7 -0
  54. execsql/parser.py +10 -0
  55. execsql/py.typed +0 -0
  56. execsql/script/__init__.py +95 -0
  57. execsql/script/control.py +162 -0
  58. execsql/{script.py → script/engine.py} +184 -402
  59. execsql/script/variables.py +281 -0
  60. execsql/types.py +49 -20
  61. execsql/utils/auth.py +2 -0
  62. execsql/utils/crypto.py +4 -6
  63. execsql/utils/datetime.py +1 -0
  64. execsql/utils/errors.py +11 -0
  65. execsql/utils/fileio.py +33 -8
  66. execsql/utils/gui.py +46 -0
  67. execsql/utils/mail.py +7 -17
  68. execsql/utils/numeric.py +2 -0
  69. execsql/utils/regex.py +9 -0
  70. execsql/utils/strings.py +16 -0
  71. execsql/utils/timer.py +2 -0
  72. execsql2-2.4.0.data/data/execsql2_extras/README.md +65 -0
  73. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
  74. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/METADATA +13 -6
  75. execsql2-2.4.0.dist-info/RECORD +108 -0
  76. execsql2-2.1.2.data/data/execsql2_extras/READ_ME.rst +0 -127
  77. execsql2-2.1.2.dist-info/RECORD +0 -96
  78. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  79. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  80. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  81. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  82. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  83. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  84. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  85. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  86. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  87. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
  88. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  89. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  90. {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  91. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
  92. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
  93. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
  94. {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,7 +1,8 @@
1
- """Command-line interface for execsql.
1
+ """Core execution logic for the execsql CLI.
2
2
 
3
- Parses arguments, initialises global state, establishes a database
4
- connection, loads the SQL script, and drives the main execution loop.
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()