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/options.py ADDED
@@ -0,0 +1,108 @@
1
+ """Options shared by more than one command, each defined once.
2
+
3
+ A command declares a shared option by annotating a parameter with one of the
4
+ types below, so ``run`` and ``ping`` cannot drift apart on a flag's spelling,
5
+ metavar or help text. Options that belong to a single command stay in that
6
+ command's module.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from enum import Enum
12
+ from typing import Annotated
13
+
14
+ import typer
15
+
16
+ __all__ = [
17
+ "ConfigFileOpt",
18
+ "DbTypeOpt",
19
+ "DsnOpt",
20
+ "NoPasswdOpt",
21
+ "OutputFormat",
22
+ "OutputFormatOpt",
23
+ "PortOpt",
24
+ "ScriptEncodingOpt",
25
+ "UserOpt",
26
+ ]
27
+
28
+ # -- Connection (run, ping) ----------------------------------------------------
29
+
30
+ DbTypeOpt = Annotated[
31
+ str | None,
32
+ typer.Option(
33
+ "-t",
34
+ "--type",
35
+ metavar="{a,d,p,s,l,m,k,o,f}",
36
+ help=(
37
+ "Database type: a=MS-Access, p=PostgreSQL, "
38
+ "s=SQL Server, l=SQLite, m=MySQL/MariaDB, "
39
+ "k=DuckDB, o=Oracle, f=Firebird, "
40
+ "d=DSN."
41
+ ),
42
+ ),
43
+ ]
44
+
45
+ DsnOpt = Annotated[
46
+ str | None,
47
+ typer.Option(
48
+ "--dsn",
49
+ "--connection-string",
50
+ metavar="URL",
51
+ help=(
52
+ "Database connection URL, e.g. postgresql://user:pass@host:5432/db. "
53
+ "Supported schemes: postgresql, mysql, mssql, oracle, firebird, sqlite, duckdb. "
54
+ "Overrides -t/-u/-p and positional server/db args."
55
+ ),
56
+ ),
57
+ ]
58
+
59
+ UserOpt = Annotated[str | None, typer.Option("-u", "--user", help="Database user name.")]
60
+
61
+ PortOpt = Annotated[int | None, typer.Option("-p", "--port", help="Database server port.")]
62
+
63
+ NoPasswdOpt = Annotated[
64
+ bool,
65
+ typer.Option("-w", "--no-passwd", help="Skip password prompt when user is specified."),
66
+ ]
67
+
68
+ # -- Configuration (run, ping, config) -----------------------------------------
69
+
70
+ ConfigFileOpt = Annotated[
71
+ str | None,
72
+ typer.Option(
73
+ "--config",
74
+ metavar="FILE",
75
+ help=(
76
+ "Path to an execsql configuration file. "
77
+ "Loaded after the implicit search paths so its values take precedence. "
78
+ "The file may chain additional configs via its [config] section."
79
+ ),
80
+ ),
81
+ ]
82
+
83
+ # -- Script files (run, format, lint) ------------------------------------------
84
+
85
+ ScriptEncodingOpt = Annotated[
86
+ str | None,
87
+ typer.Option(
88
+ "-f",
89
+ "--script-encoding",
90
+ metavar="NAME",
91
+ help="Character encoding of the script file. Default: [encoding] script from a config file, else UTF-8.",
92
+ ),
93
+ ]
94
+
95
+ # -- Machine-readable output (ping, config, list) ------------------------------
96
+
97
+
98
+ class OutputFormat(str, Enum):
99
+ """Output formats for commands that report data rather than check files."""
100
+
101
+ text = "text"
102
+ json = "json"
103
+
104
+
105
+ OutputFormatOpt = Annotated[
106
+ OutputFormat,
107
+ typer.Option("--output-format", help="text for people; json for tools."),
108
+ ]
execsql/cli/run.py CHANGED
@@ -33,7 +33,6 @@ __all__ = ["_connect_initial_db", "_ping_db", "_print_dry_run", "_print_profile"
33
33
 
34
34
  # Lint helper — imported lazily inside _run() to keep start-up cost low, but
35
35
  # re-exported here so that tests and callers can reach it via cli.run.
36
- from execsql.cli.lint import _print_lint_results # noqa: F401 — re-export (used by cli/__init__.py)
37
36
 
38
37
 
39
38
  # ---------------------------------------------------------------------------
@@ -289,6 +288,21 @@ def _load_config(
289
288
  return ConfigData(script_path, subvars, config_file=config_file)
290
289
 
291
290
 
291
+ def _tool_config(config_file: str | None) -> ConfigData:
292
+ """The config ``execsql format`` and ``execsql lint`` read.
293
+
294
+ Read once per invocation, from the system, user and working-directory
295
+ locations plus *config_file* — not per script, so a script directory's
296
+ own ``execsql.conf`` applies only when passed with ``--config``.
297
+ """
298
+ return _load_config(None, _seed_early_subvars(), config_file)
299
+
300
+
301
+ def _configured_script_encoding(conf: ConfigData) -> str | None:
302
+ """``[encoding] script`` from *conf*, or ``None`` when no config file sets it."""
303
+ return conf.script_encoding if "script_encoding" in conf.sources else None
304
+
305
+
292
306
  def _seed_script_subvars(subvars: SubVarSet, script_name: str | None) -> None:
293
307
  """Add substitution variables that depend on the script path."""
294
308
  from execsql.utils.errors import file_size_date
@@ -492,8 +506,9 @@ def _setup_logging(
492
506
  use_gui: str | None,
493
507
  no_passwd: bool,
494
508
  import_buffer: int | None,
509
+ named_vars: list[tuple[str, str]] | None = None,
495
510
  ) -> Logger:
496
- """Create the execution logger, log initial info, and seed ``$RUN_ID``."""
511
+ """Create the execution logger, log initial info, and seed ``$RUN_ID``, ``-a`` and ``--var`` values."""
497
512
  from execsql.utils.errors import file_size_date
498
513
 
499
514
  opts_dict = {
@@ -550,6 +565,13 @@ def _setup_logging(
550
565
  logger.log_status_info(
551
566
  f"Command-line substitution variable assignment: {var} set to {{***}}",
552
567
  )
568
+ # --var values: named, set after config files so they win over
569
+ # [variables]; a SUB in the script can still reassign them. Values are
570
+ # redacted for the same reason as -a's.
571
+ for name, value in named_vars or ():
572
+ subvars.add_substitution(name, value)
573
+ logger.add_redaction_value(value)
574
+ logger.log_status_info(f"Command-line substitution variable assignment: {name} set to {{***}}")
553
575
 
554
576
  return logger
555
577
 
@@ -587,12 +609,13 @@ def _run(
587
609
  profile: bool = False,
588
610
  profile_limit: int = 20,
589
611
  ping: bool = False,
590
- lint: bool = False,
591
612
  debug: bool = False,
592
613
  no_system_cmd: bool = False,
593
614
  no_rm_file: bool = False,
594
615
  no_serve: bool = False,
595
616
  config_file: str | None = None,
617
+ named_vars: list[tuple[str, str]] | None = None,
618
+ manifest_path: str | None = None,
596
619
  ) -> None:
597
620
  """Initialise state, connect to the database, load the script, and run it.
598
621
 
@@ -605,12 +628,6 @@ def _run(
605
628
  connection details (DBMS name, server version, and location), and calls
606
629
  :func:`_ping_db` which raises ``SystemExit(0)``. No script is loaded or
607
630
  executed. *script_name* and *command* may both be ``None`` in ping mode.
608
-
609
- When *lint* is ``True``, the script is parsed and statically analysed for
610
- structural issues (unmatched IF/ENDIF/LOOP/BATCH blocks, potentially
611
- undefined variables, missing INCLUDE files, empty scripts) without
612
- connecting to a database or executing anything. Exits with code 0 if no
613
- errors were found, or code 1 if errors were found.
614
631
  """
615
632
  import execsql.state as _state
616
633
 
@@ -698,6 +715,8 @@ def _run(
698
715
  if sub_vars:
699
716
  for n, repl in enumerate(sub_vars):
700
717
  _state.subvars.add_substitution(f"$ARG_{n + 1}", repl)
718
+ for name, value in named_vars or ():
719
+ _state.subvars.add_substitution(name, value)
701
720
  _ast_tree = _load_script(command, script_name, conf.script_encoding)
702
721
  _print_dry_run(_ast_tree)
703
722
  raise SystemExit(0)
@@ -713,6 +732,14 @@ def _run(
713
732
  _state.dbs.add("initial", db)
714
733
  _ping_db(db) # raises SystemExit
715
734
 
735
+ manifest = None
736
+ if manifest_path:
737
+ from execsql import manifest as _manifest
738
+
739
+ variables = [f"$ARG_{n + 1}" for n in range(len(sub_vars or ()))] + [name for name, _ in named_vars or ()]
740
+ manifest = _manifest.start(manifest_path, script_name, variables)
741
+ manifest.config_files = list(conf.files_read)
742
+
716
743
  import execsql.utils.fileio as _fileio
717
744
 
718
745
  if _state.filewriter is None or not _state.filewriter.is_alive():
@@ -758,6 +785,7 @@ def _run(
758
785
  use_gui=use_gui,
759
786
  no_passwd=no_passwd,
760
787
  import_buffer=import_buffer,
788
+ named_vars=named_vars,
761
789
  )
762
790
 
763
791
  # ------------------------------------------------------------------
@@ -765,11 +793,6 @@ def _run(
765
793
  # ------------------------------------------------------------------
766
794
  _ast_tree = _load_script(command, script_name, conf.script_encoding)
767
795
 
768
- # ------------------------------------------------------------------
769
- # NOTE: --lint is handled as an early exit in cli/__init__.py (AST
770
- # linter) before _run() is called. No lint code path here.
771
- # ------------------------------------------------------------------
772
-
773
796
  # ------------------------------------------------------------------
774
797
  # Start GUI console if requested
775
798
  # ------------------------------------------------------------------
@@ -794,6 +817,8 @@ def _run(
794
817
  _state.dbs.add("initial", db)
795
818
 
796
819
  _state.exec_log.log_db_connect(db)
820
+ if manifest is not None:
821
+ manifest.set_database(db)
797
822
  _state.subvars.add_substitution("$CURRENT_DBMS", db.type.dbms_id)
798
823
  _state.subvars.add_substitution("$CURRENT_DATABASE", db.name())
799
824
  _state.subvars.add_substitution("$DB_SERVER", db.server_name)
@@ -822,6 +847,8 @@ def _run(
822
847
 
823
848
  if _ast_tree is not None:
824
849
  _execute_script_ast(_ast_tree, conf, profile=profile, profile_limit=profile_limit)
850
+ if manifest is not None:
851
+ manifest.finish(0)
825
852
 
826
853
 
827
854
  # ---------------------------------------------------------------------------