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