execsql2 2.0.1__py3-none-any.whl → 2.1.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.py +322 -108
- execsql/config.py +134 -114
- execsql/db/access.py +89 -65
- execsql/db/base.py +97 -68
- execsql/db/dsn.py +45 -29
- execsql/db/duckdb.py +4 -5
- execsql/db/factory.py +27 -27
- execsql/db/firebird.py +30 -18
- execsql/db/mysql.py +38 -14
- execsql/db/oracle.py +58 -33
- execsql/db/postgres.py +68 -28
- execsql/db/sqlite.py +36 -27
- execsql/db/sqlserver.py +45 -30
- execsql/exceptions.py +68 -64
- execsql/exporters/__init__.py +1 -1
- execsql/exporters/base.py +42 -17
- execsql/exporters/delimited.py +60 -59
- execsql/exporters/duckdb.py +8 -12
- execsql/exporters/feather.py +32 -24
- execsql/exporters/html.py +33 -30
- execsql/exporters/json.py +18 -17
- execsql/exporters/latex.py +11 -13
- execsql/exporters/ods.py +50 -46
- execsql/exporters/parquet.py +32 -0
- execsql/exporters/pretty.py +16 -15
- execsql/exporters/raw.py +9 -11
- execsql/exporters/sqlite.py +38 -38
- execsql/exporters/templates.py +15 -72
- execsql/exporters/values.py +13 -12
- execsql/exporters/xls.py +26 -26
- execsql/exporters/xml.py +12 -12
- execsql/exporters/zip.py +0 -3
- execsql/gui/__init__.py +2 -2
- execsql/gui/console.py +0 -1
- execsql/gui/desktop.py +6 -7
- execsql/gui/tui.py +8 -14
- execsql/importers/base.py +6 -9
- execsql/importers/csv.py +10 -17
- execsql/importers/feather.py +16 -22
- execsql/importers/ods.py +3 -4
- execsql/importers/xls.py +5 -6
- execsql/metacommands/__init__.py +8 -8
- execsql/metacommands/conditions.py +41 -33
- execsql/metacommands/connect.py +113 -99
- execsql/metacommands/control.py +38 -26
- execsql/metacommands/data.py +35 -33
- execsql/metacommands/debug.py +13 -9
- execsql/metacommands/io.py +288 -229
- execsql/metacommands/prompt.py +179 -157
- execsql/metacommands/script_ext.py +11 -9
- execsql/metacommands/system.py +44 -25
- execsql/models.py +9 -16
- execsql/parser.py +10 -10
- execsql/script.py +183 -157
- execsql/state.py +170 -208
- execsql/types.py +46 -81
- execsql/utils/auth.py +114 -14
- execsql/utils/crypto.py +31 -4
- execsql/utils/datetime.py +7 -7
- execsql/utils/errors.py +34 -29
- execsql/utils/fileio.py +90 -55
- execsql/utils/gui.py +22 -23
- execsql/utils/mail.py +15 -17
- execsql/utils/numeric.py +2 -3
- execsql/utils/regex.py +9 -12
- execsql/utils/strings.py +10 -12
- execsql/utils/timer.py +0 -2
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/execsql.conf +1 -1
- execsql2-2.1.1.dist-info/METADATA +295 -0
- execsql2-2.1.1.dist-info/RECORD +96 -0
- execsql2-2.0.1.dist-info/METADATA +0 -406
- execsql2-2.0.1.dist-info/RECORD +0 -95
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/READ_ME.rst +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.1.dist-info}/WHEEL +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.1.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.1.dist-info}/licenses/NOTICE +0 -0
execsql/cli.py
CHANGED
|
@@ -12,17 +12,19 @@ import getpass
|
|
|
12
12
|
import os
|
|
13
13
|
import sys
|
|
14
14
|
import traceback
|
|
15
|
+
from pathlib import Path
|
|
15
16
|
from encodings.aliases import aliases as codec_dict
|
|
16
|
-
from typing import List, Optional
|
|
17
17
|
|
|
18
18
|
import typer
|
|
19
19
|
from rich.console import Console
|
|
20
20
|
from rich.table import Table
|
|
21
|
-
from rich.text import Text
|
|
22
21
|
|
|
23
22
|
from execsql import __version__
|
|
24
23
|
from execsql.config import ConfigData, StatObj
|
|
25
24
|
from execsql.exceptions import ConfigError, ErrInfo
|
|
25
|
+
from execsql.script import SubVarSet, current_script_line, read_sqlfile, read_sqlstring, runscripts
|
|
26
|
+
from execsql.utils.fileio import FileWriter, Logger, filewriter_end
|
|
27
|
+
from execsql.utils.gui import gui_connect, gui_console_isrunning, gui_console_off, gui_console_on, gui_console_wait_user
|
|
26
28
|
|
|
27
29
|
_console = Console()
|
|
28
30
|
_err_console = Console(stderr=True)
|
|
@@ -193,7 +195,7 @@ def _version_callback(value: bool) -> None:
|
|
|
193
195
|
def main(
|
|
194
196
|
ctx: typer.Context,
|
|
195
197
|
# Positional args collected manually (script + optional server/db/file)
|
|
196
|
-
args:
|
|
198
|
+
args: list[str] | None = typer.Argument(
|
|
197
199
|
None,
|
|
198
200
|
metavar="SQL_SCRIPT [SERVER DATABASE | DATABASE_FILE]",
|
|
199
201
|
help=(
|
|
@@ -202,46 +204,46 @@ def main(
|
|
|
202
204
|
),
|
|
203
205
|
),
|
|
204
206
|
# Named options — grouped to mirror the original argparse interface
|
|
205
|
-
sub_vars:
|
|
207
|
+
sub_vars: list[str] | None = typer.Option(
|
|
206
208
|
None,
|
|
207
209
|
"-a",
|
|
208
210
|
"--assign-arg",
|
|
209
211
|
metavar="VALUE",
|
|
210
212
|
help="Define the replacement string for a substitution variable [cyan]\\$ARG_x[/cyan].",
|
|
211
213
|
),
|
|
212
|
-
boolean_int:
|
|
214
|
+
boolean_int: str | None = typer.Option(
|
|
213
215
|
None,
|
|
214
216
|
"-b",
|
|
215
217
|
"--boolean-int",
|
|
216
218
|
metavar="{0,1,t,f,y,n}",
|
|
217
219
|
help="Treat integers 0 and 1 as boolean values.",
|
|
218
220
|
),
|
|
219
|
-
make_dirs:
|
|
221
|
+
make_dirs: str | None = typer.Option(
|
|
220
222
|
None,
|
|
221
223
|
"-d",
|
|
222
224
|
"--directories",
|
|
223
225
|
metavar="{0,1,t,f,y,n}",
|
|
224
226
|
help="Auto-create directories for EXPORT metacommand. [dim]n=no (default), y=yes[/dim]",
|
|
225
227
|
),
|
|
226
|
-
database_encoding:
|
|
228
|
+
database_encoding: str | None = typer.Option(
|
|
227
229
|
None,
|
|
228
230
|
"-e",
|
|
229
231
|
"--database-encoding",
|
|
230
232
|
help="Character encoding used in the database.",
|
|
231
233
|
),
|
|
232
|
-
script_encoding:
|
|
234
|
+
script_encoding: str | None = typer.Option(
|
|
233
235
|
None,
|
|
234
236
|
"-f",
|
|
235
237
|
"--script-encoding",
|
|
236
238
|
help="Character encoding of the script file. [dim]Default: UTF-8[/dim]",
|
|
237
239
|
),
|
|
238
|
-
output_encoding:
|
|
240
|
+
output_encoding: str | None = typer.Option(
|
|
239
241
|
None,
|
|
240
242
|
"-g",
|
|
241
243
|
"--output-encoding",
|
|
242
244
|
help="Encoding for WRITE and EXPORT output.",
|
|
243
245
|
),
|
|
244
|
-
import_encoding:
|
|
246
|
+
import_encoding: str | None = typer.Option(
|
|
245
247
|
None,
|
|
246
248
|
"-i",
|
|
247
249
|
"--import-encoding",
|
|
@@ -271,20 +273,20 @@ def main(
|
|
|
271
273
|
"--online-help",
|
|
272
274
|
help="Open the online documentation in the default browser.",
|
|
273
275
|
),
|
|
274
|
-
port:
|
|
276
|
+
port: int | None = typer.Option(
|
|
275
277
|
None,
|
|
276
278
|
"-p",
|
|
277
279
|
"--port",
|
|
278
280
|
help="Database server port.",
|
|
279
281
|
),
|
|
280
|
-
scanlines:
|
|
282
|
+
scanlines: int | None = typer.Option(
|
|
281
283
|
None,
|
|
282
284
|
"-s",
|
|
283
285
|
"--scan-lines",
|
|
284
286
|
metavar="N",
|
|
285
287
|
help="Lines to scan for IMPORT format detection. [dim]0 = scan entire file.[/dim]",
|
|
286
288
|
),
|
|
287
|
-
db_type:
|
|
289
|
+
db_type: str | None = typer.Option(
|
|
288
290
|
None,
|
|
289
291
|
"-t",
|
|
290
292
|
"--type",
|
|
@@ -296,24 +298,28 @@ def main(
|
|
|
296
298
|
"[bold]d[/bold]=DSN."
|
|
297
299
|
),
|
|
298
300
|
),
|
|
299
|
-
user:
|
|
301
|
+
user: str | None = typer.Option(
|
|
300
302
|
None,
|
|
301
303
|
"-u",
|
|
302
304
|
"--user",
|
|
303
305
|
help="Database user name.",
|
|
304
306
|
),
|
|
305
|
-
use_gui:
|
|
307
|
+
use_gui: str | None = typer.Option(
|
|
306
308
|
None,
|
|
307
309
|
"-v",
|
|
308
310
|
"--visible-prompts",
|
|
309
311
|
metavar="{0,1,2,3}",
|
|
310
312
|
help=(
|
|
311
313
|
"GUI level: [bold]0[/bold]=none (default), [bold]1[/bold]=GUI for password/pause, "
|
|
312
|
-
"[bold]2[/bold]=GUI for password/pause + DB selection, [bold]3[/bold]=full GUI console.
|
|
313
|
-
"Set [cyan]gui_framework[/cyan] in the config [interface] section to choose "
|
|
314
|
-
"'tkinter' (default) or 'textual'."
|
|
314
|
+
"[bold]2[/bold]=GUI for password/pause + DB selection, [bold]3[/bold]=full GUI console."
|
|
315
315
|
),
|
|
316
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
|
+
),
|
|
317
323
|
no_passwd: bool = typer.Option(
|
|
318
324
|
False,
|
|
319
325
|
"-w",
|
|
@@ -326,14 +332,50 @@ def main(
|
|
|
326
332
|
"--encodings",
|
|
327
333
|
help="List available encoding names and exit.",
|
|
328
334
|
),
|
|
329
|
-
import_buffer:
|
|
335
|
+
import_buffer: int | None = typer.Option(
|
|
330
336
|
None,
|
|
331
337
|
"-z",
|
|
332
338
|
"--import-buffer",
|
|
333
339
|
metavar="KB",
|
|
334
340
|
help="Import buffer size in KB. [dim]Default: 32[/dim]",
|
|
335
341
|
),
|
|
336
|
-
|
|
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(
|
|
337
379
|
None,
|
|
338
380
|
"--version",
|
|
339
381
|
callback=_version_callback,
|
|
@@ -365,20 +407,28 @@ def main(
|
|
|
365
407
|
if online_help:
|
|
366
408
|
import webbrowser
|
|
367
409
|
|
|
368
|
-
webbrowser.open("https://
|
|
410
|
+
webbrowser.open("https://execsql2.readthedocs.io/en/latest/", new=2, autoraise=True)
|
|
411
|
+
raise typer.Exit()
|
|
369
412
|
|
|
370
413
|
positional = args or []
|
|
371
|
-
if not
|
|
372
|
-
|
|
373
|
-
|
|
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)
|
|
374
428
|
|
|
375
429
|
# ------------------------------------------------------------------
|
|
376
430
|
# Validate positional args and db_type choice
|
|
377
431
|
# ------------------------------------------------------------------
|
|
378
|
-
script_name = positional[0]
|
|
379
|
-
if not os.path.exists(script_name):
|
|
380
|
-
_err_console.print(f'[bold red]Error:[/bold red] SQL script file [cyan]"{script_name}"[/cyan] does not exist.')
|
|
381
|
-
raise typer.Exit(code=1)
|
|
382
432
|
|
|
383
433
|
if db_type and db_type not in ("a", "d", "p", "s", "l", "m", "k", "o", "f"):
|
|
384
434
|
_err_console.print(
|
|
@@ -393,6 +443,12 @@ def main(
|
|
|
393
443
|
)
|
|
394
444
|
raise typer.Exit(code=2)
|
|
395
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
|
+
|
|
396
452
|
if boolean_int and boolean_int.lower() not in ("0", "1", "t", "f", "y", "n"):
|
|
397
453
|
_err_console.print(
|
|
398
454
|
f"[bold red]Error:[/bold red] Invalid --boolean-int value [cyan]{boolean_int!r}[/cyan].",
|
|
@@ -418,12 +474,122 @@ def main(
|
|
|
418
474
|
db_type=db_type,
|
|
419
475
|
user=user,
|
|
420
476
|
use_gui=use_gui,
|
|
477
|
+
gui_framework=gui_framework,
|
|
421
478
|
no_passwd=no_passwd,
|
|
422
479
|
import_buffer=import_buffer,
|
|
423
480
|
script_name=script_name,
|
|
481
|
+
command=command,
|
|
482
|
+
dry_run=dry_run,
|
|
483
|
+
dsn=dsn,
|
|
484
|
+
output_dir=output_dir,
|
|
424
485
|
)
|
|
425
486
|
|
|
426
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
|
+
|
|
574
|
+
# ---------------------------------------------------------------------------
|
|
575
|
+
# Dry-run helper
|
|
576
|
+
# ---------------------------------------------------------------------------
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _print_dry_run(cmdlist: object) -> None:
|
|
580
|
+
"""Print the parsed command list for --dry-run mode."""
|
|
581
|
+
if cmdlist is None or not cmdlist.cmdlist:
|
|
582
|
+
_console.print("[yellow]No commands found in script.[/yellow]")
|
|
583
|
+
return
|
|
584
|
+
n = len(cmdlist.cmdlist)
|
|
585
|
+
_console.print(f"[bold cyan]Dry Run[/bold cyan] — [dim]{n} command(s) parsed[/dim]")
|
|
586
|
+
_console.print()
|
|
587
|
+
for i, cmd in enumerate(cmdlist.cmdlist, 1):
|
|
588
|
+
ctype = "SQL " if cmd.command_type == "sql" else "METACMD"
|
|
589
|
+
source_info = f"[dim]{cmd.source}:{cmd.line_no}[/dim]"
|
|
590
|
+
_console.print(f" [dim]{i:>4}[/dim] [bold green]{ctype}[/bold green] {source_info} {cmd.commandline()}")
|
|
591
|
+
|
|
592
|
+
|
|
427
593
|
# ---------------------------------------------------------------------------
|
|
428
594
|
# Core execution (split from argument parsing for testability)
|
|
429
595
|
# ---------------------------------------------------------------------------
|
|
@@ -431,44 +597,56 @@ def main(
|
|
|
431
597
|
|
|
432
598
|
def _run(
|
|
433
599
|
positional: list,
|
|
434
|
-
sub_vars:
|
|
435
|
-
boolean_int:
|
|
436
|
-
make_dirs:
|
|
437
|
-
database_encoding:
|
|
438
|
-
script_encoding:
|
|
439
|
-
output_encoding:
|
|
440
|
-
import_encoding:
|
|
600
|
+
sub_vars: list[str] | None,
|
|
601
|
+
boolean_int: str | None,
|
|
602
|
+
make_dirs: str | None,
|
|
603
|
+
database_encoding: str | None,
|
|
604
|
+
script_encoding: str | None,
|
|
605
|
+
output_encoding: str | None,
|
|
606
|
+
import_encoding: str | None,
|
|
441
607
|
user_logfile: bool,
|
|
442
608
|
new_db: bool,
|
|
443
|
-
port:
|
|
444
|
-
scanlines:
|
|
445
|
-
db_type:
|
|
446
|
-
user:
|
|
447
|
-
use_gui:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
609
|
+
port: int | None,
|
|
610
|
+
scanlines: int | None,
|
|
611
|
+
db_type: str | None,
|
|
612
|
+
user: str | None,
|
|
613
|
+
use_gui: str | None,
|
|
614
|
+
gui_framework: str | None = None,
|
|
615
|
+
no_passwd: bool = False,
|
|
616
|
+
import_buffer: int | None = None,
|
|
617
|
+
script_name: str | None = None,
|
|
618
|
+
command: str | None = None,
|
|
619
|
+
dry_run: bool = False,
|
|
620
|
+
dsn: str | None = None,
|
|
621
|
+
output_dir: str | None = None,
|
|
451
622
|
) -> None:
|
|
452
|
-
"""
|
|
623
|
+
"""Initialise state, connect to the database, load the script, and run it.
|
|
624
|
+
|
|
625
|
+
Separated from argument parsing so it can be called directly in tests
|
|
626
|
+
without going through the Typer CLI layer. All parameters mirror the
|
|
627
|
+
corresponding CLI options; see [Syntax & Options](../syntax.md) for
|
|
628
|
+
descriptions.
|
|
629
|
+
"""
|
|
453
630
|
import execsql.state as _state
|
|
454
631
|
|
|
455
632
|
# ------------------------------------------------------------------
|
|
456
633
|
# Early setup: substitution variables seeded before arg parsing
|
|
457
634
|
# ------------------------------------------------------------------
|
|
458
|
-
_state.subvars =
|
|
635
|
+
_state.subvars = SubVarSet()
|
|
459
636
|
|
|
637
|
+
# Security note: ALL environment variables are exposed as &-prefixed
|
|
638
|
+
# substitution variables. Sensitive values (API keys, tokens) in the
|
|
639
|
+
# process environment will be accessible to scripts. See the
|
|
640
|
+
# "Environment Variables" section in docs/substitution_vars.md.
|
|
460
641
|
for k in os.environ:
|
|
461
642
|
try:
|
|
462
643
|
_state.subvars.add_substitution("&" + k, os.environ[k])
|
|
463
644
|
except Exception:
|
|
464
|
-
pass
|
|
645
|
+
pass # Skip env vars with names that can't be substitution keys.
|
|
465
646
|
_state.subvars.add_substitution("$LAST_ROWCOUNT", None)
|
|
466
647
|
|
|
467
648
|
dt_now = datetime.datetime.now()
|
|
468
|
-
|
|
469
|
-
dt_now_utc = datetime.datetime.utcnow()
|
|
470
|
-
else:
|
|
471
|
-
dt_now_utc = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
649
|
+
dt_now_utc = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
472
650
|
|
|
473
651
|
_state.subvars.add_substitution("$SCRIPT_START_TIME", dt_now.strftime("%Y-%m-%d %H:%M"))
|
|
474
652
|
_state.subvars.add_substitution("$SCRIPT_START_TIME_UTC", dt_now_utc.strftime("%Y-%m-%d %H:%M"))
|
|
@@ -492,9 +670,36 @@ def _run(
|
|
|
492
670
|
# ------------------------------------------------------------------
|
|
493
671
|
# Read configuration file
|
|
494
672
|
# ------------------------------------------------------------------
|
|
495
|
-
|
|
673
|
+
script_path = str(Path(script_name).resolve().parent) if script_name else os.getcwd()
|
|
674
|
+
_state.conf = ConfigData(script_path, _state.subvars)
|
|
496
675
|
conf = _state.conf
|
|
497
676
|
|
|
677
|
+
# ------------------------------------------------------------------
|
|
678
|
+
# Connection string (--dsn / --connection-string): overrides -t/-u/-p
|
|
679
|
+
# and positional server/db args when provided.
|
|
680
|
+
# ------------------------------------------------------------------
|
|
681
|
+
if dsn:
|
|
682
|
+
try:
|
|
683
|
+
parsed_dsn = _parse_connection_string(dsn)
|
|
684
|
+
except ConfigError as exc:
|
|
685
|
+
_err_console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
686
|
+
raise SystemExit(1)
|
|
687
|
+
db_type = db_type or parsed_dsn["db_type"]
|
|
688
|
+
conf.db_type = db_type
|
|
689
|
+
if parsed_dsn["server"] and not conf.server:
|
|
690
|
+
conf.server = parsed_dsn["server"]
|
|
691
|
+
if parsed_dsn["db"] and not conf.db:
|
|
692
|
+
conf.db = parsed_dsn["db"]
|
|
693
|
+
if parsed_dsn["db_file"] and not conf.db_file:
|
|
694
|
+
conf.db_file = parsed_dsn["db_file"]
|
|
695
|
+
if parsed_dsn["user"] and not user:
|
|
696
|
+
user = parsed_dsn["user"]
|
|
697
|
+
if parsed_dsn["password"]:
|
|
698
|
+
conf.db_password = parsed_dsn["password"]
|
|
699
|
+
conf.passwd_prompt = False
|
|
700
|
+
if parsed_dsn["port"] and not port:
|
|
701
|
+
port = parsed_dsn["port"]
|
|
702
|
+
|
|
498
703
|
# Apply CLI options over config-file values
|
|
499
704
|
if user:
|
|
500
705
|
conf.username = user
|
|
@@ -530,6 +735,8 @@ def _run(
|
|
|
530
735
|
conf.gui_level = 0
|
|
531
736
|
elif conf.gui_level not in range(4):
|
|
532
737
|
raise ConfigError(f"Invalid GUI level specification: {conf.gui_level}")
|
|
738
|
+
if gui_framework:
|
|
739
|
+
conf.gui_framework = gui_framework.lower()
|
|
533
740
|
if db_type:
|
|
534
741
|
conf.db_type = db_type
|
|
535
742
|
if conf.db_type is None:
|
|
@@ -542,22 +749,27 @@ def _run(
|
|
|
542
749
|
conf.access_username = user
|
|
543
750
|
if new_db:
|
|
544
751
|
conf.new_db = True
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
752
|
+
if output_dir:
|
|
753
|
+
conf.export_output_dir = str(Path(output_dir).resolve())
|
|
754
|
+
|
|
755
|
+
# Positional arguments after the script name (or all positionals in inline mode)
|
|
756
|
+
# off=1: script file occupies positional[0]; connection args start at [1]
|
|
757
|
+
# off=0: no script file; all positionals are connection args
|
|
758
|
+
off = 0 if command is not None else 1
|
|
759
|
+
if len(positional) == off + 1:
|
|
548
760
|
if conf.db_type in ("a", "l", "k"):
|
|
549
|
-
conf.db_file = positional[
|
|
761
|
+
conf.db_file = positional[off]
|
|
550
762
|
elif conf.db_type == "d":
|
|
551
|
-
conf.db = positional[
|
|
763
|
+
conf.db = positional[off]
|
|
552
764
|
else:
|
|
553
765
|
if conf.server and not conf.db:
|
|
554
|
-
conf.db = positional[
|
|
766
|
+
conf.db = positional[off]
|
|
555
767
|
else:
|
|
556
|
-
conf.server = positional[
|
|
557
|
-
elif len(positional) ==
|
|
558
|
-
conf.server = positional[
|
|
559
|
-
conf.db = positional[
|
|
560
|
-
elif len(positional) >
|
|
768
|
+
conf.server = positional[off]
|
|
769
|
+
elif len(positional) == off + 2:
|
|
770
|
+
conf.server = positional[off]
|
|
771
|
+
conf.db = positional[off + 1]
|
|
772
|
+
elif len(positional) > off + 2:
|
|
561
773
|
from execsql.utils.errors import fatal_error
|
|
562
774
|
|
|
563
775
|
fatal_error("Incorrect number of command-line arguments.")
|
|
@@ -567,20 +779,25 @@ def _run(
|
|
|
567
779
|
# ------------------------------------------------------------------
|
|
568
780
|
from execsql.utils.errors import file_size_date
|
|
569
781
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
782
|
+
if script_name is not None:
|
|
783
|
+
_state.subvars.add_substitution("$STARTING_SCRIPT", script_name)
|
|
784
|
+
_state.subvars.add_substitution("$STARTING_SCRIPT_NAME", Path(script_name).name)
|
|
785
|
+
_state.subvars.add_substitution("$STARTING_SCRIPT_REVTIME", file_size_date(script_name)[1])
|
|
786
|
+
else:
|
|
787
|
+
_state.subvars.add_substitution("$STARTING_SCRIPT", "<inline>")
|
|
788
|
+
_state.subvars.add_substitution("$STARTING_SCRIPT_NAME", "<inline>")
|
|
789
|
+
_state.subvars.add_substitution("$STARTING_SCRIPT_REVTIME", "")
|
|
573
790
|
|
|
574
791
|
# ------------------------------------------------------------------
|
|
575
792
|
# Initialise state objects
|
|
576
793
|
# ------------------------------------------------------------------
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
794
|
+
from execsql.metacommands import DISPATCH_TABLE
|
|
795
|
+
from execsql.metacommands.conditions import CONDITIONAL_TABLE
|
|
796
|
+
|
|
797
|
+
_state.initialize(conf, DISPATCH_TABLE, CONDITIONAL_TABLE)
|
|
798
|
+
|
|
799
|
+
# Local-only objects that require CLI-specific args or class definitions
|
|
580
800
|
_state.status = StatObj()
|
|
581
|
-
_state.dbs = _state.DatabasePool()
|
|
582
|
-
_state.tempfiles = _state.TempFileMgr()
|
|
583
|
-
_state.export_metadata = _state.ExportMetadata()
|
|
584
801
|
|
|
585
802
|
from execsql.config import WriteHooks
|
|
586
803
|
|
|
@@ -589,22 +806,14 @@ def _run(
|
|
|
589
806
|
import execsql.utils.fileio as _fileio
|
|
590
807
|
|
|
591
808
|
if _state.filewriter is None or not _state.filewriter.is_alive():
|
|
592
|
-
|
|
809
|
+
_fileio.filewriter = _state.filewriter = FileWriter(
|
|
593
810
|
_fileio.fw_input,
|
|
594
811
|
_fileio.fw_output,
|
|
595
812
|
file_encoding=conf.output_encoding,
|
|
596
813
|
open_timeout=getattr(conf, "outfile_open_timeout", 10),
|
|
597
814
|
)
|
|
598
815
|
_state.filewriter.start()
|
|
599
|
-
atexit.register(
|
|
600
|
-
|
|
601
|
-
from execsql.metacommands import DISPATCH_TABLE
|
|
602
|
-
|
|
603
|
-
_state.metacommandlist = DISPATCH_TABLE
|
|
604
|
-
|
|
605
|
-
from execsql.metacommands.conditions import CONDITIONAL_TABLE
|
|
606
|
-
|
|
607
|
-
_state.conditionallist = CONDITIONAL_TABLE
|
|
816
|
+
atexit.register(filewriter_end)
|
|
608
817
|
|
|
609
818
|
# ------------------------------------------------------------------
|
|
610
819
|
# Logging
|
|
@@ -631,15 +840,15 @@ def _run(
|
|
|
631
840
|
}.items()
|
|
632
841
|
if v
|
|
633
842
|
}
|
|
634
|
-
_state.exec_log =
|
|
635
|
-
script_name,
|
|
843
|
+
_state.exec_log = Logger(
|
|
844
|
+
script_name or "<inline>",
|
|
636
845
|
conf.db,
|
|
637
846
|
conf.server,
|
|
638
847
|
opts_dict,
|
|
639
848
|
conf.user_logfile,
|
|
640
849
|
)
|
|
641
850
|
_state.exec_log.log_status_info(
|
|
642
|
-
"Python version
|
|
851
|
+
f"Python version {sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]} {sys.version_info[3]}",
|
|
643
852
|
)
|
|
644
853
|
_state.exec_log.log_status_info(f"execsql version {__version__}")
|
|
645
854
|
_state.exec_log.log_status_info(f"System user: {getpass.getuser()}")
|
|
@@ -662,20 +871,30 @@ def _run(
|
|
|
662
871
|
# ------------------------------------------------------------------
|
|
663
872
|
# Load the SQL script
|
|
664
873
|
# ------------------------------------------------------------------
|
|
665
|
-
|
|
874
|
+
if command is not None:
|
|
875
|
+
read_sqlstring(command.replace("\\n", "\n").replace("\\t", "\t"), "<inline>")
|
|
876
|
+
else:
|
|
877
|
+
read_sqlfile(script_name)
|
|
878
|
+
|
|
879
|
+
# ------------------------------------------------------------------
|
|
880
|
+
# Dry-run: print command list and exit without connecting to DB
|
|
881
|
+
# ------------------------------------------------------------------
|
|
882
|
+
if dry_run:
|
|
883
|
+
_print_dry_run(_state.commandliststack[-1] if _state.commandliststack else None)
|
|
884
|
+
raise SystemExit(0)
|
|
666
885
|
|
|
667
886
|
# ------------------------------------------------------------------
|
|
668
887
|
# Start GUI console if requested
|
|
669
888
|
# ------------------------------------------------------------------
|
|
670
889
|
if conf.gui_level > 2:
|
|
671
|
-
|
|
890
|
+
gui_console_on()
|
|
672
891
|
|
|
673
892
|
# ------------------------------------------------------------------
|
|
674
893
|
# Establish database connection
|
|
675
894
|
# ------------------------------------------------------------------
|
|
676
895
|
if conf.server is None and conf.db is None and conf.db_file is None:
|
|
677
896
|
if conf.gui_level > 1:
|
|
678
|
-
|
|
897
|
+
gui_connect("initial", f"Select the database to use with {script_name or '<inline>'}.")
|
|
679
898
|
db = _state.dbs.current()
|
|
680
899
|
else:
|
|
681
900
|
from execsql.utils.errors import fatal_error
|
|
@@ -717,7 +936,7 @@ def _execute_script_textual_console(conf: ConfigData) -> None:
|
|
|
717
936
|
_state.gui_manager_queue = dialog_queue
|
|
718
937
|
|
|
719
938
|
app = ConsoleApp(
|
|
720
|
-
script_runner=
|
|
939
|
+
script_runner=runscripts,
|
|
721
940
|
dialog_queue=dialog_queue,
|
|
722
941
|
wait_on_exit=conf.gui_wait_on_exit,
|
|
723
942
|
)
|
|
@@ -745,8 +964,8 @@ def _execute_script_textual_console(conf: ConfigData) -> None:
|
|
|
745
964
|
else:
|
|
746
965
|
strace = traceback.extract_tb(exc.__traceback__)[-1:]
|
|
747
966
|
lno = strace[0][1] if strace else "?"
|
|
748
|
-
msg = f"{
|
|
749
|
-
script, slno =
|
|
967
|
+
msg = f"{Path(sys.argv[0]).name}: Uncaught exception {type(exc)} ({exc}) on line {lno}"
|
|
968
|
+
script, slno = current_script_line()
|
|
750
969
|
if script is not None:
|
|
751
970
|
msg += f" in script {script}, line {slno}"
|
|
752
971
|
from execsql.utils.errors import exit_now
|
|
@@ -775,14 +994,14 @@ def _execute_script_direct(conf: ConfigData) -> None:
|
|
|
775
994
|
pass
|
|
776
995
|
|
|
777
996
|
try:
|
|
778
|
-
|
|
997
|
+
runscripts()
|
|
779
998
|
except SystemExit as exc:
|
|
780
|
-
if
|
|
781
|
-
|
|
999
|
+
if gui_console_isrunning() and conf.gui_wait_on_exit:
|
|
1000
|
+
gui_console_wait_user(
|
|
782
1001
|
"Script complete; close the console window to exit execsql.",
|
|
783
1002
|
)
|
|
784
|
-
if
|
|
785
|
-
|
|
1003
|
+
if gui_console_isrunning():
|
|
1004
|
+
gui_console_off()
|
|
786
1005
|
_state.exec_log.log_status_info(f"{_state.cmds_run} commands run")
|
|
787
1006
|
sys.exit(exc.code)
|
|
788
1007
|
except ConfigError:
|
|
@@ -794,11 +1013,8 @@ def _execute_script_direct(conf: ConfigData) -> None:
|
|
|
794
1013
|
except Exception:
|
|
795
1014
|
strace = traceback.extract_tb(sys.exc_info()[2])[-1:]
|
|
796
1015
|
lno = strace[0][1]
|
|
797
|
-
msg = (
|
|
798
|
-
|
|
799
|
-
f"{sys.exc_info()[0]} ({sys.exc_info()[1]}) on line {lno}"
|
|
800
|
-
)
|
|
801
|
-
script, slno = _state.current_script_line()
|
|
1016
|
+
msg = f"{Path(sys.argv[0]).name}: Uncaught exception {sys.exc_info()[0]} ({sys.exc_info()[1]}) on line {lno}"
|
|
1017
|
+
script, slno = current_script_line()
|
|
802
1018
|
if script is not None:
|
|
803
1019
|
msg += f" in script {script}, line {slno}"
|
|
804
1020
|
from execsql.utils.errors import exit_now
|
|
@@ -806,12 +1022,12 @@ def _execute_script_direct(conf: ConfigData) -> None:
|
|
|
806
1022
|
exit_now(1, ErrInfo("exception", exception_msg=msg))
|
|
807
1023
|
|
|
808
1024
|
_state.dbs.do_rollback = False
|
|
809
|
-
if
|
|
810
|
-
|
|
1025
|
+
if gui_console_isrunning() and conf.gui_wait_on_exit:
|
|
1026
|
+
gui_console_wait_user(
|
|
811
1027
|
"Script complete; close the console window to exit execsql.",
|
|
812
1028
|
)
|
|
813
|
-
if
|
|
814
|
-
|
|
1029
|
+
if gui_console_isrunning():
|
|
1030
|
+
gui_console_off()
|
|
815
1031
|
_state.exec_log.log_status_info(f"{_state.cmds_run} commands run")
|
|
816
1032
|
_state.exec_log.log_exit_end()
|
|
817
1033
|
|
|
@@ -855,6 +1071,7 @@ def _connect_initial_db(conf: ConfigData):
|
|
|
855
1071
|
port=conf.port,
|
|
856
1072
|
encoding=conf.db_encoding,
|
|
857
1073
|
new_db=conf.new_db,
|
|
1074
|
+
password=getattr(conf, "db_password", None),
|
|
858
1075
|
)
|
|
859
1076
|
elif conf.db_type == "s":
|
|
860
1077
|
return db_SqlServer(
|
|
@@ -939,10 +1156,7 @@ def _legacy_main() -> None:
|
|
|
939
1156
|
except Exception:
|
|
940
1157
|
strace = traceback.extract_tb(sys.exc_info()[2])[-1:]
|
|
941
1158
|
lno = strace[0][1]
|
|
942
|
-
msg = (
|
|
943
|
-
f"{os.path.basename(sys.argv[0])}: Uncaught exception "
|
|
944
|
-
f"{sys.exc_info()[0]} ({sys.exc_info()[1]}) on line {lno}"
|
|
945
|
-
)
|
|
1159
|
+
msg = f"{Path(sys.argv[0]).name}: Uncaught exception {sys.exc_info()[0]} ({sys.exc_info()[1]}) on line {lno}"
|
|
946
1160
|
from execsql.utils.errors import exit_now
|
|
947
1161
|
|
|
948
1162
|
exit_now(1, ErrInfo("exception", exception_msg=msg))
|