execsql2 2.27.0__py3-none-any.whl → 2.28.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 (46) hide show
  1. execsql/cli/commands/format.py +2 -1
  2. execsql/cli/commands/lsp.py +33 -9
  3. execsql/cli/lint.py +45 -22
  4. execsql/db/access.py +4 -4
  5. execsql/db/base.py +17 -3
  6. execsql/db/dsn.py +2 -2
  7. execsql/db/duckdb.py +14 -2
  8. execsql/db/firebird.py +2 -3
  9. execsql/db/mysql.py +2 -2
  10. execsql/db/oracle.py +5 -5
  11. execsql/db/postgres.py +2 -3
  12. execsql/db/sqlserver.py +2 -2
  13. execsql/exporters/duckdb.py +2 -2
  14. execsql/exporters/ods.py +2 -2
  15. execsql/exporters/templates.py +2 -2
  16. execsql/exporters/xls.py +3 -3
  17. execsql/exporters/xlsx.py +2 -2
  18. execsql/exporters/yaml.py +3 -4
  19. execsql/format.py +71 -21
  20. execsql/importers/base.py +6 -10
  21. execsql/lsp/document.py +7 -3
  22. execsql/metacommands/debug.py +2 -2
  23. execsql/metacommands/io_fileops.py +6 -6
  24. execsql/metacommands/upsert.py +2 -5
  25. execsql/models.py +11 -0
  26. execsql/utils/errors.py +16 -0
  27. {execsql2-2.27.0.dist-info → execsql2-2.28.0.dist-info}/METADATA +4 -2
  28. {execsql2-2.27.0.dist-info → execsql2-2.28.0.dist-info}/RECORD +46 -46
  29. {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/README.md +0 -0
  30. {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  31. {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  32. {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  33. {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  34. {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  35. {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  36. {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  37. {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  38. {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  39. {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/script_template.sql +0 -0
  40. {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  41. {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  42. {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  43. {execsql2-2.27.0.dist-info → execsql2-2.28.0.dist-info}/WHEEL +0 -0
  44. {execsql2-2.27.0.dist-info → execsql2-2.28.0.dist-info}/entry_points.txt +0 -0
  45. {execsql2-2.27.0.dist-info → execsql2-2.28.0.dist-info}/licenses/LICENSE.txt +0 -0
  46. {execsql2-2.27.0.dist-info → execsql2-2.28.0.dist-info}/licenses/NOTICE +0 -0
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from pathlib import Path
6
6
 
7
7
  import typer
8
+ from rich.markup import escape
8
9
 
9
10
  from execsql.cli.application import ExecsqlCommand, app
10
11
  from execsql.cli.help import _err_console
@@ -90,7 +91,7 @@ def format_cmd(
90
91
  try:
91
92
  _require_sqlglot()
92
93
  except ImportError as exc:
93
- _err_console.print(f"[bold red]Error:[/bold red] {exc}", highlight=False)
94
+ _err_console.print(f"[bold red]Error:[/bold red] {escape(str(exc))}", highlight=False)
94
95
  raise typer.Exit(code=1) from exc
95
96
  raise typer.Exit(
96
97
  code=run_formatter(
@@ -3,13 +3,31 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import typer
6
+ from rich.markup import escape
6
7
 
7
8
  from execsql.cli.application import ExecsqlCommand, app
8
9
  from execsql.cli.help import _err_console
10
+ from execsql.utils.errors import install_hint
9
11
 
10
12
  __all__ = ["lsp_cmd"]
11
13
 
12
14
 
15
+ def _lsp_libraries_installed() -> bool:
16
+ """Whether pygls and lsprotocol are installed (whether or not this execsql can use them)."""
17
+ from importlib.util import find_spec
18
+
19
+ return all(find_spec(name) is not None for name in ("pygls", "lsprotocol"))
20
+
21
+
22
+ def _pygls_version() -> str:
23
+ from importlib.metadata import PackageNotFoundError, version
24
+
25
+ try:
26
+ return version("pygls")
27
+ except PackageNotFoundError:
28
+ return "unknown"
29
+
30
+
13
31
  @app.command(
14
32
  cls=ExecsqlCommand,
15
33
  name="lsp",
@@ -17,24 +35,30 @@ __all__ = ["lsp_cmd"]
17
35
  "Run the execsql language server on stdin/stdout, for an editor to start. "
18
36
  "Shows execsql lint findings as you type, completes metacommands, "
19
37
  "conditional tests and variables, explains them on hover, and finds where variables, "
20
- "SCRIPTs and included files are defined, offers quick fixes, and formats documents. Needs the lsp extra "
21
- "(installed with any extras you already have, e.g. execsql2[postgres,lsp])."
38
+ "SCRIPTs and included files are defined, offers quick fixes, and formats documents. Requires the lsp "
39
+ "extra; --check reports whether it is installed."
22
40
  ),
23
41
  )
24
42
  def lsp_cmd(
25
43
  # Editors' language clients pass --stdio (VS Code always does); stdio is the only transport.
26
44
  stdio: bool = typer.Option(False, "--stdio", hidden=True),
45
+ check: bool = typer.Option(False, "--check", help="Report whether the language server can start, and exit."),
27
46
  ) -> None:
28
47
  """The ``lsp`` command; its user-facing help is the decorator's ``help``."""
29
48
  try:
30
49
  from execsql.lsp.server import create_server
31
50
  except ImportError as exc:
32
- _err_console.print(
33
- "[bold red]Error:[/bold red] execsql lsp needs the lsp extra. Install it with the "
34
- 'extras you already have, e.g. uv tool install "execsql2\\[postgres,lsp]" or '
35
- 'pipx install --force "execsql2\\[postgres,lsp]". '
36
- "See https://execsql2.readthedocs.io/en/latest/getting-started/installation/#adding-an-extra-later",
37
- highlight=False,
38
- )
51
+ if _lsp_libraries_installed():
52
+ # pygls is there but this execsql cannot use it (another major version, a broken install).
53
+ message = (
54
+ f"execsql lsp cannot use the installed pygls {_pygls_version()} ({exc}). "
55
+ f"It needs pygls 2.x, which the lsp extra installs. {install_hint('lsp')}"
56
+ )
57
+ else:
58
+ message = f"execsql lsp needs the lsp extra. {install_hint('lsp')}"
59
+ _err_console.print(f"[bold red]Error:[/bold red] {escape(message)}", highlight=False)
39
60
  raise typer.Exit(code=1) from exc
61
+ if check:
62
+ typer.echo(f"execsql lsp is ready (pygls {_pygls_version()}).")
63
+ return
40
64
  create_server().start_io()
execsql/cli/lint.py CHANGED
@@ -48,6 +48,7 @@ Public surface:
48
48
 
49
49
  from __future__ import annotations
50
50
 
51
+ import functools
51
52
  import json
52
53
  import re
53
54
  from collections import Counter
@@ -460,23 +461,40 @@ def _read_ini_vars(
460
461
  language server; a file that is unreadable or not valid INI defines
461
462
  nothing.
462
463
  """
463
- from configparser import ConfigParser
464
- from configparser import Error as ConfigError
465
-
466
464
  p = Path(ini_file)
467
465
  if not p.is_absolute() and script_dir is not None:
468
466
  p = script_dir / p
467
+ try:
468
+ if not p.is_file():
469
+ return
470
+ stat = p.stat()
471
+ if stat.st_size > _MAX_INI_BYTES:
472
+ return
473
+ resolved = str(p.resolve())
474
+ except OSError:
475
+ return
476
+ defined_vars.update(_ini_section_keys(resolved, stat.st_size, stat.st_mtime_ns, section))
477
+
478
+
479
+ @functools.lru_cache(maxsize=64)
480
+ def _ini_section_keys(path: str, size: int, mtime_ns: int, section: str) -> frozenset[str]:
481
+ """The upper-cased keys of *section* in the INI file at *path*.
482
+
483
+ Cached by the file's size and modification time: a script that names
484
+ the same file on many SUB_INI lines, and the language server re-linting
485
+ on every edit, read it once.
486
+ """
487
+ from configparser import ConfigParser
488
+ from configparser import Error as ConfigError
469
489
 
470
490
  cp = ConfigParser()
471
491
  try:
472
- if not p.is_file() or p.stat().st_size > _MAX_INI_BYTES:
473
- return
474
- cp.read_string(p.read_text(encoding="utf-8"), source=str(p))
492
+ cp.read_string(Path(path).read_text(encoding="utf-8"), source=path)
475
493
  except (OSError, UnicodeDecodeError, ConfigError):
476
- return
477
- if cp.has_section(section):
478
- for key, _value in cp.items(section):
479
- defined_vars.add(key.upper())
494
+ return frozenset()
495
+ if not cp.has_section(section):
496
+ return frozenset()
497
+ return frozenset(key.upper() for key, _value in cp.items(section))
480
498
 
481
499
 
482
500
  def _is_known_var(raw_name: str, defined_vars: set[str]) -> bool:
@@ -834,7 +852,12 @@ def _referencing_text(node: Node) -> list[str]:
834
852
  return texts
835
853
 
836
854
 
837
- def _check_unused_variables(script: Script, defined: set[str], issues: list[_Issue]) -> None:
855
+ def _check_unused_variables(
856
+ script: Script,
857
+ defined: set[str],
858
+ issues: list[_Issue],
859
+ script_dir: Path | None = None,
860
+ ) -> None:
838
861
  """Report variables a SUB defines that nothing ever reads.
839
862
 
840
863
  Almost always a typo in either the definition or the reference — the two
@@ -845,8 +868,11 @@ def _check_unused_variables(script: Script, defined: set[str], issues: list[_Iss
845
868
  referenced: set[str] = set()
846
869
  _collect_var_references(script.body, referenced)
847
870
  unused = sorted(defined - referenced)
871
+ lines: dict[str, int] = {}
872
+ if unused:
873
+ _definition_lines(script.body, script_dir, lines)
848
874
  for name in unused:
849
- line = _definition_line(script.body, name)
875
+ line = lines.get(name, 0)
850
876
  issues.append(
851
877
  _issue(
852
878
  "V002",
@@ -857,18 +883,15 @@ def _check_unused_variables(script: Script, defined: set[str], issues: list[_Iss
857
883
  )
858
884
 
859
885
 
860
- def _definition_line(nodes: list[Node], name: str) -> int:
861
- """Line of the SUB that defines *name*, or 0 when it cannot be located."""
886
+ def _definition_lines(nodes: list[Node], script_dir: Path | None, lines: dict[str, int]) -> None:
887
+ """Add to *lines* the line of the first metacommand that defines each variable, in script order."""
862
888
  for node in nodes:
863
889
  if isinstance(node, MetaCommandStatement):
864
890
  probe: set[str] = set()
865
- _extract_var_definition(node.command, None, probe)
866
- if name in probe:
867
- return node.span.start_line
868
- found = _definition_line(list(node.children()), name)
869
- if found:
870
- return found
871
- return 0
891
+ _extract_var_definition(node.command, script_dir, probe)
892
+ for name in probe:
893
+ lines.setdefault(name, node.span.start_line)
894
+ _definition_lines(list(node.children()), script_dir, lines)
872
895
 
873
896
 
874
897
  # ---------------------------------------------------------------------------
@@ -1008,7 +1031,7 @@ def lint(
1008
1031
  )
1009
1032
 
1010
1033
  # Pass 3: whole-script rules, which need every reference collected first
1011
- _check_unused_variables(script, all_defined, issues)
1034
+ _check_unused_variables(script, all_defined, issues, script_dir)
1012
1035
 
1013
1036
  return issues
1014
1037
 
execsql/db/access.py CHANGED
@@ -17,7 +17,7 @@ from typing import Any, cast
17
17
  from execsql.db.base import Database, _fetched
18
18
  from execsql.db.tiers import SupportTier
19
19
  from execsql.exceptions import ErrInfo
20
- from execsql.utils.errors import exception_desc, fatal_error
20
+ from execsql.utils.errors import exception_desc, fatal_error, install_hint
21
21
  from execsql.utils.auth import (
22
22
  begin_login,
23
23
  clear_stored_password,
@@ -68,7 +68,7 @@ class AccessDatabase(Database):
68
68
  try:
69
69
  import pyodbc # noqa: F401
70
70
  except Exception:
71
- fatal_error("The pyodbc module is required. See http://github.com/mkleehammer/pyodbc")
71
+ fatal_error(f"The pyodbc module is required to connect to Access. {install_hint('odbc')}")
72
72
  from execsql.types import dbt_access
73
73
 
74
74
  super().__init__(
@@ -327,8 +327,8 @@ class AccessDatabase(Database):
327
327
  rows = curs.fetchall()
328
328
  return [d[0] for d in curs.description], rows
329
329
 
330
- def select_rowsource(self, sql: str) -> tuple[list[str], Any]:
331
- """Return column names and an iterable that yields rows one at a time."""
330
+ def select_rowsource(self, sql: str, *, decode_bytes: bool = True) -> tuple[list[str], Any]: # noqa: ARG002
331
+ """Return column names and an iterable that yields rows one at a time (values as the driver returns them)."""
332
332
  # Return 1) a list of column names, and 2) an iterable that yields rows.
333
333
  self.dao_flush_check()
334
334
  curs = self.cursor()
execsql/db/base.py CHANGED
@@ -405,8 +405,13 @@ class Database(ABC):
405
405
  rows = curs.fetchall()
406
406
  return [d[0] for d in curs.description], rows
407
407
 
408
- def select_rowsource(self, sql: str) -> tuple[list[str], Generator]:
409
- """Execute *sql* and return ``(column_names, row_generator)`` for streaming large result sets."""
408
+ def select_rowsource(self, sql: str, *, decode_bytes: bool = True) -> tuple[list[str], Generator]:
409
+ """Execute *sql* and return ``(column_names, row_generator)`` for streaming large result sets.
410
+
411
+ A binary value (``bytes``) is decoded to text with the connection's
412
+ encoding, as the exporters expect, unless *decode_bytes* is false:
413
+ COPY passes false so binary columns reach the target unchanged.
414
+ """
410
415
  # Return 1) a list of column names, and 2) an iterable that yields rows.
411
416
  curs = self.cursor()
412
417
  try:
@@ -433,7 +438,7 @@ class Database(ABC):
433
438
  break
434
439
  else:
435
440
  for row in rows:
436
- if self.encoding:
441
+ if self.encoding and decode_bytes:
437
442
  yield [
438
443
  c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c
439
444
  for c in row
@@ -501,6 +506,15 @@ class Database(ABC):
501
506
  """The schema (MySQL: database) an unqualified table name is created in, or ``None`` if unknown."""
502
507
  return None
503
508
 
509
+ def table_exists_where_created(self, table_name: str, schema_name: str | None = None) -> bool:
510
+ """Whether *table_name* exists where a ``CREATE TABLE`` on this connection would put it.
511
+
512
+ The same as :meth:`table_exists`, except on a connection that sees
513
+ tables of other databases (DuckDB ``ATTACH``): only the current
514
+ database counts there.
515
+ """
516
+ return self.table_exists(table_name, schema_name)
517
+
504
518
  def _query_current_schema(self, sql: str) -> str | None:
505
519
  with self._cursor() as curs:
506
520
  curs.execute(sql)
execsql/db/dsn.py CHANGED
@@ -12,7 +12,7 @@ the CLI.
12
12
  from execsql.db.base import Database
13
13
  from execsql.db.tiers import SupportTier
14
14
  from execsql.exceptions import ErrInfo
15
- from execsql.utils.errors import exception_desc, fatal_error
15
+ from execsql.utils.errors import exception_desc, fatal_error, install_hint
16
16
  from execsql.utils.auth import (
17
17
  begin_login,
18
18
  clear_stored_password,
@@ -51,7 +51,7 @@ class DsnDatabase(Database):
51
51
  try:
52
52
  import pyodbc # noqa: F401
53
53
  except Exception:
54
- fatal_error("The pyodbc module is required. See http://github.com/mkleehammer/pyodbc")
54
+ fatal_error(f"The pyodbc module is required to connect through an ODBC DSN. {install_hint('odbc')}")
55
55
  from execsql.types import dbt_dsn
56
56
 
57
57
  super().__init__(
execsql/db/duckdb.py CHANGED
@@ -14,7 +14,7 @@ from typing import Any
14
14
 
15
15
  from execsql.db.base import Database, statement_start
16
16
  from execsql.exceptions import ErrInfo
17
- from execsql.utils.errors import exception_desc, fatal_error
17
+ from execsql.utils.errors import exception_desc, fatal_error, install_hint
18
18
  import execsql.state as _state
19
19
 
20
20
  __all__ = ["DuckDBDatabase"]
@@ -62,7 +62,7 @@ class DuckDBDatabase(Database):
62
62
  try:
63
63
  import duckdb # noqa: F401
64
64
  except Exception:
65
- fatal_error("The duckdb module is required.")
65
+ fatal_error(f"The duckdb module is required to use DuckDB. {install_hint('duckdb')}")
66
66
  from execsql.types import dbt_duckdb
67
67
 
68
68
  super().__init__(
@@ -167,6 +167,18 @@ class DuckDBDatabase(Database):
167
167
  self.rollback()
168
168
  raise
169
169
 
170
+ def table_exists_where_created(self, table_name: str, schema_name: str | None = None) -> bool:
171
+ """Whether *table_name* exists in the current database (not an ``ATTACH``ed one)."""
172
+ # information_schema.tables lists the tables of every attached database.
173
+ sql = "select table_name from information_schema.tables where table_name = ? and table_catalog = current_database()"
174
+ params: list = [table_name]
175
+ if schema_name:
176
+ sql += " and table_schema = ?"
177
+ params.append(schema_name)
178
+ with self._cursor() as curs:
179
+ curs.execute(sql, params)
180
+ return len(curs.fetchall()) > 0
181
+
170
182
  def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
171
183
  """Return True if the named view exists in the DuckDB database."""
172
184
  # DuckDB information_schema has no 'views' table; views are listed in 'tables'
execsql/db/firebird.py CHANGED
@@ -11,7 +11,7 @@ via the ``firebird-driver`` package. Corresponds to ``-t f`` on the CLI.
11
11
  from execsql.db.base import Database
12
12
  from execsql.db.tiers import SupportTier
13
13
  from execsql.exceptions import ErrInfo
14
- from execsql.utils.errors import exception_desc, fatal_error
14
+ from execsql.utils.errors import exception_desc, fatal_error, install_hint
15
15
  from execsql.utils.auth import (
16
16
  begin_login,
17
17
  clear_stored_password,
@@ -46,8 +46,7 @@ class FirebirdDatabase(Database):
46
46
  from firebird import driver as firebird_lib # noqa: F401
47
47
  except Exception:
48
48
  fatal_error(
49
- "The firebird-driver module is required to connect to Firebird. "
50
- "See https://pypi.org/project/firebird-driver/",
49
+ f"The firebird-driver module is required to connect to Firebird. {install_hint('firebird')}",
51
50
  )
52
51
  from execsql.types import dbt_firebird
53
52
 
execsql/db/mysql.py CHANGED
@@ -12,7 +12,7 @@ from typing import Any, cast
12
12
 
13
13
  from execsql.db.base import Database
14
14
  from execsql.exceptions import ErrInfo
15
- from execsql.utils.errors import exception_desc, fatal_error
15
+ from execsql.utils.errors import exception_desc, fatal_error, install_hint
16
16
  from execsql.utils.auth import (
17
17
  begin_login,
18
18
  clear_stored_password,
@@ -81,7 +81,7 @@ class MySQLDatabase(Database):
81
81
  import pymysql as mysql_lib # noqa: F401
82
82
  except Exception:
83
83
  fatal_error(
84
- "The pymysql module is required to connect to MySQL. See https://pypi.python.org/pypi/PyMySQL",
84
+ f"The pymysql module is required to connect to MySQL. {install_hint('mysql')}",
85
85
  )
86
86
  from execsql.types import dbt_mysql
87
87
 
execsql/db/oracle.py CHANGED
@@ -14,7 +14,7 @@ from typing import Any
14
14
  from execsql.db.base import Database
15
15
  from execsql.db.tiers import SupportTier
16
16
  from execsql.exceptions import ErrInfo
17
- from execsql.utils.errors import exception_desc, fatal_error
17
+ from execsql.utils.errors import exception_desc, fatal_error, install_hint
18
18
  from execsql.utils.auth import (
19
19
  begin_login,
20
20
  clear_stored_password,
@@ -76,7 +76,7 @@ class OracleDatabase(Database):
76
76
  ) -> None:
77
77
  if _oracle_driver() is None:
78
78
  fatal_error(
79
- 'The oracledb module is required to connect to Oracle. Install it with: pip install "execsql2[oracle]"',
79
+ f"The oracledb module is required to connect to Oracle. {install_hint('oracle')}",
80
80
  )
81
81
  from execsql.types import dbt_oracle
82
82
 
@@ -164,12 +164,12 @@ class OracleDatabase(Database):
164
164
  else:
165
165
  return super().select_data(sql)
166
166
 
167
- def select_rowsource(self, sql: str) -> Any:
167
+ def select_rowsource(self, sql: str, *, decode_bytes: bool = True) -> Any:
168
168
  """Return column names and an iterable that yields rows one at a time."""
169
169
  if sql[-1:] == ";":
170
- return super().select_rowsource(sql[:-1])
170
+ return super().select_rowsource(sql[:-1], decode_bytes=decode_bytes)
171
171
  else:
172
- return super().select_rowsource(sql)
172
+ return super().select_rowsource(sql, decode_bytes=decode_bytes)
173
173
 
174
174
  def select_rowdict(self, sql: str) -> Any:
175
175
  """Return column names and an iterable that yields rows as dictionaries."""
execsql/db/postgres.py CHANGED
@@ -15,7 +15,7 @@ from typing import Any
15
15
 
16
16
  from execsql.db.base import Database
17
17
  from execsql.exceptions import ErrInfo
18
- from execsql.utils.errors import exception_desc, fatal_error
18
+ from execsql.utils.errors import exception_desc, fatal_error, install_hint
19
19
  from execsql.utils.auth import (
20
20
  begin_login,
21
21
  clear_stored_password,
@@ -51,8 +51,7 @@ class PostgresDatabase(Database):
51
51
  import psycopg # noqa: F401
52
52
  except Exception:
53
53
  fatal_error(
54
- "The psycopg module (psycopg3) is required to connect to PostgreSQL. "
55
- "See https://www.psycopg.org/psycopg3/",
54
+ f"The psycopg module (psycopg3) is required to connect to PostgreSQL. {install_hint('postgres')}",
56
55
  )
57
56
  from execsql.types import dbt_postgres
58
57
 
execsql/db/sqlserver.py CHANGED
@@ -11,7 +11,7 @@ import re
11
11
 
12
12
  from execsql.db.base import Database
13
13
  from execsql.exceptions import ErrInfo
14
- from execsql.utils.errors import fatal_error
14
+ from execsql.utils.errors import fatal_error, install_hint
15
15
  from execsql.utils.auth import (
16
16
  begin_login,
17
17
  clear_stored_password,
@@ -41,7 +41,7 @@ class SqlServerDatabase(Database):
41
41
  try:
42
42
  import pyodbc # noqa: F401
43
43
  except Exception:
44
- fatal_error("The pyodbc module is required. See http://github.com/mkleehammer/pyodbc")
44
+ fatal_error(f"The pyodbc module is required to connect to SQL Server. {install_hint('mssql')}")
45
45
  from execsql.types import dbt_sqlserver
46
46
 
47
47
  super().__init__(
@@ -33,9 +33,9 @@ def export_duckdb(
33
33
  try:
34
34
  import duckdb
35
35
  except Exception:
36
- from execsql.utils.errors import fatal_error
36
+ from execsql.utils.errors import fatal_error, install_hint
37
37
 
38
- fatal_error("The duckdb module is required to export data in that format.")
38
+ fatal_error(f"The duckdb module is required to export data in that format. {install_hint('duckdb')}")
39
39
  return
40
40
 
41
41
  from execsql.models import DataTable
execsql/exporters/ods.py CHANGED
@@ -20,7 +20,7 @@ import execsql.state as _state
20
20
  from execsql.exceptions import OdsFileError
21
21
  from execsql.exceptions import ErrInfo
22
22
  from execsql.script import current_script_line
23
- from execsql.utils.errors import exception_desc, fatal_error
23
+ from execsql.utils.errors import exception_desc, fatal_error, install_hint
24
24
  from execsql.utils.fileio import filewriter_close
25
25
  from execsql.utils.strings import unquoted
26
26
 
@@ -57,7 +57,7 @@ class OdsFile:
57
57
  import odf.number # noqa: F401
58
58
  import odf.style # noqa: F401
59
59
  except ImportError:
60
- fatal_error("The odfpy library is needed to create OpenDocument spreadsheets.")
60
+ fatal_error(f"The odfpy library is needed to create OpenDocument spreadsheets. {install_hint('formats')}")
61
61
  self.filename: str | None = None
62
62
  self.wbk: Any = None
63
63
  self.cell_style_names: list[str] = []
@@ -14,7 +14,7 @@ from typing import Any
14
14
 
15
15
  import execsql.state as _state
16
16
  from execsql.exceptions import ErrInfo
17
- from execsql.utils.errors import fatal_error
17
+ from execsql.utils.errors import fatal_error, install_hint
18
18
  from execsql.utils.fileio import filewriter_close
19
19
 
20
20
  __all__ = ["StrTemplateReport", "JinjaTemplateReport", "report_query"]
@@ -91,7 +91,7 @@ class JinjaTemplateReport:
91
91
  self._jinja2 = jinja2
92
92
  except ImportError:
93
93
  fatal_error(
94
- "The jinja2 library is required to produce reports with the Jinja2 templating system. See http://jinja.pocoo.org/",
94
+ f"The jinja2 library is required to produce reports with the Jinja2 templating system. {install_hint('formats')}",
95
95
  )
96
96
  conf = _state.conf
97
97
  template_root = getattr(conf, "template_root", None)
execsql/exporters/xls.py CHANGED
@@ -22,7 +22,7 @@ from pathlib import Path
22
22
  from typing import Any, cast
23
23
 
24
24
  from execsql.exceptions import XlsFileError, XlsxFileError
25
- from execsql.utils.errors import fatal_error
25
+ from execsql.utils.errors import fatal_error, install_hint
26
26
 
27
27
  __all__ = ["XlsFile", "XlsxFile"]
28
28
 
@@ -51,7 +51,7 @@ class XlsFile:
51
51
 
52
52
  self._xlrd = xlrd
53
53
  except ImportError:
54
- fatal_error("The xlrd library is needed to read Excel (.xls) spreadsheets.")
54
+ fatal_error(f"The xlrd library is needed to read Excel (.xls) spreadsheets. {install_hint('formats')}")
55
55
  self.filename: str | None = None
56
56
  self.encoding: str | None = None
57
57
  self.wbk: Any = None
@@ -190,7 +190,7 @@ class XlsxFile:
190
190
 
191
191
  self._openpyxl = openpyxl
192
192
  except ImportError:
193
- fatal_error("The openpyxl library is needed to read Excel (.xlsx) spreadsheets.")
193
+ fatal_error(f"The openpyxl library is needed to read Excel (.xlsx) spreadsheets. {install_hint('formats')}")
194
194
  self.filename: str | None = None
195
195
  self.encoding: str | None = None
196
196
  self.wbk: Any = None
execsql/exporters/xlsx.py CHANGED
@@ -20,7 +20,7 @@ from execsql.exceptions import ErrInfo
20
20
  from execsql.exporters.base import ExportRecord
21
21
  from execsql.exporters.pretty import prettyprint_query
22
22
  from execsql.script import current_script_line
23
- from execsql.utils.errors import exception_desc, fatal_error
23
+ from execsql.utils.errors import exception_desc, fatal_error, install_hint
24
24
  from execsql.utils.fileio import filewriter_close
25
25
  from execsql.utils.strings import unquoted
26
26
 
@@ -39,7 +39,7 @@ def _require_openpyxl() -> Any:
39
39
  return openpyxl
40
40
  except ImportError:
41
41
  fatal_error(
42
- "The openpyxl library is needed to write Excel (.xlsx) spreadsheets (install the formats extra, execsql2[formats]).",
42
+ f"The openpyxl library is needed to write Excel (.xlsx) spreadsheets. {install_hint('formats')}",
43
43
  )
44
44
 
45
45
 
execsql/exporters/yaml.py CHANGED
@@ -6,8 +6,7 @@ YAML export for execsql.
6
6
  Provides :func:`write_query_to_yaml` which serialises a query result set as a
7
7
  YAML sequence of mappings (one mapping per row).
8
8
 
9
- Requires the ``PyYAML`` package (``pip install PyYAML`` or
10
- ``pip install 'execsql2[formats]'``).
9
+ Requires the ``PyYAML`` package (the ``formats`` extra).
11
10
  """
12
11
 
13
12
  from typing import Any
@@ -15,7 +14,7 @@ from typing import Any
15
14
  import execsql.state as _state
16
15
  from execsql.exceptions import ErrInfo
17
16
  from execsql.exporters.zip import ZipWriter
18
- from execsql.utils.errors import exception_desc
17
+ from execsql.utils.errors import exception_desc, install_hint
19
18
  from execsql.utils.fileio import filewriter_close
20
19
 
21
20
  __all__ = ["write_query_to_yaml"]
@@ -54,7 +53,7 @@ def write_query_to_yaml(
54
53
  except ImportError as exc:
55
54
  raise ErrInfo(
56
55
  "error",
57
- other_msg=("PyYAML is required for FORMAT YAML export. Install it with: pip install PyYAML"),
56
+ other_msg=f"PyYAML is required for FORMAT YAML export. {install_hint('formats')}",
58
57
  ) from exc
59
58
 
60
59
  conf = _state.conf
execsql/format.py CHANGED
@@ -21,11 +21,14 @@ from types import ModuleType
21
21
  __all__ = ["collect_paths", "format_file", "open_dollar_quote", "parse_keyword", "run_formatter"]
22
22
 
23
23
 
24
- _SQLGLOT_MISSING_MSG = (
25
- "execsql format requires sqlglot for SQL reformatting.\n"
26
- ' Install the formatter extra: uv tool install "execsql2[formatter]" (or pip install "execsql2[formatter]")\n'
27
- " Or skip SQL reformatting with the --no-sql flag."
28
- )
24
+ def _sqlglot_missing_msg() -> str:
25
+ from execsql.utils.errors import install_hint
26
+
27
+ return (
28
+ "execsql format requires sqlglot for SQL reformatting.\n"
29
+ f" {install_hint('formatter')}\n"
30
+ " Or skip SQL reformatting with the --no-sql flag."
31
+ )
29
32
 
30
33
 
31
34
  def _require_sqlglot():
@@ -36,7 +39,7 @@ def _require_sqlglot():
36
39
 
37
40
  return sqlglot
38
41
  except ImportError as e:
39
- raise ImportError(_SQLGLOT_MISSING_MSG) from e
42
+ raise ImportError(_sqlglot_missing_msg()) from e
40
43
 
41
44
 
42
45
  # ---------------------------------------------------------------------------
@@ -532,20 +535,57 @@ def _statement_sources(sqlglot: ModuleType, sql: str) -> list[str]:
532
535
  return sources
533
536
 
534
537
 
535
- def _layout_tokens(sqlglot: ModuleType, sql: str) -> list[str]:
536
- """*sql*'s tokens with everything the formatter may change taken out.
538
+ def _ends_with_semicolon(sqlglot: ModuleType, sql: str) -> bool:
539
+ """Whether the last token of *sql* is a statement-ending ``;``."""
540
+ tokens = sqlglot.tokenize(sql, read="postgres")
541
+ return bool(tokens) and tokens[-1].token_type.name == "SEMICOLON"
542
+
543
+
544
+ def _layout_tokens(sqlglot: ModuleType, sql: str) -> list[tuple[str, str]]:
545
+ """*sql*'s tokens, as (type, text), with everything the formatter may change taken out.
537
546
 
538
- Whitespace and comments are not tokens. Keywords and unquoted names are
539
- compared without case; strings and quoted names exactly.
547
+ Whitespace and comments are not tokens. Keywords and unquoted names
548
+ (function names included) are compared without case; strings and quoted
549
+ names exactly. The type is compared too, so a string that came back as a
550
+ quoted name of the same text is a change.
540
551
  """
541
552
  return [
542
- token.text
543
- if token.token_type.name.endswith("STRING") or token.token_type.name == "IDENTIFIER"
544
- else token.text.upper()
553
+ (
554
+ token.token_type.name,
555
+ token.text
556
+ if token.token_type.name.endswith("STRING") or token.token_type.name == "IDENTIFIER"
557
+ else token.text.upper(),
558
+ )
545
559
  for token in sqlglot.tokenize(sql, read="postgres")
546
560
  ]
547
561
 
548
562
 
563
+ # An optimizer hint (``/*+ ... */``, Oracle and MySQL) or a MySQL executable
564
+ # comment (``/*! ... */``) is SQL the server reads, though it looks like a
565
+ # comment. sqlglot keeps neither: it writes them back as ``/* + ... */`` and
566
+ # moves a hint away from its keyword, which disables it.
567
+ _RX_HINT_COMMENT = re.compile(r"/\*[+!]")
568
+
569
+
570
+ def _has_hint_comment(sqlglot: ModuleType, sql: str) -> bool:
571
+ """Whether *sql* holds a hint or executable comment outside its strings and quoted names."""
572
+ if not _RX_HINT_COMMENT.search(sql):
573
+ return False
574
+ tokens = [(t.start, t.end) for t in sqlglot.tokenize(sql, read="postgres")]
575
+ return any(not any(start <= m.start() <= end for start, end in tokens) for m in _RX_HINT_COMMENT.finditer(sql))
576
+
577
+
578
+ def _as_written(source: str) -> str:
579
+ """A statement's source text to keep as written, without the space that followed the previous ``;``.
580
+
581
+ A statement that starts on its own line keeps that line's indentation.
582
+ """
583
+ source = source.rstrip()
584
+ body = source.lstrip()
585
+ lead = source[: len(source) - len(body)]
586
+ return source[lead.rfind("\n") + 1 :] if "\n" in lead else body
587
+
588
+
549
589
  def _sqlglot_format(
550
590
  sql_lines: list[str],
551
591
  sql_indent: int = 4,
@@ -596,9 +636,10 @@ def _sqlglot_format(
596
636
  # parse is left exactly as written.
597
637
  ast = sqlglot.parse(protected, read="postgres", error_level=sqlglot_errors.ErrorLevel.RAISE)
598
638
  nodes = [node for node in ast if node is not None]
599
- sources = None if rewrite_sql else _statement_sources(sqlglot, protected)
600
- if sources is not None and len(sources) != len(nodes):
639
+ sources = _statement_sources(sqlglot, protected)
640
+ if len(sources) != len(nodes) and (not rewrite_sql or _has_hint_comment(sqlglot, protected)):
601
641
  return sql_lines
642
+ aligned = len(sources) == len(nodes)
602
643
  statements: list[str] = []
603
644
  for i, node in enumerate(nodes):
604
645
  # For Command nodes (psql backslash commands, ERROR:, etc.)
@@ -614,20 +655,29 @@ def _sqlglot_format(
614
655
  max_text_width=120,
615
656
  )
616
657
  generated = re.sub(r"\bINTO TEMPORARY\b(?!\s+TABLE)", "INTO TEMPORARY TABLE", generated)
617
- if sources is not None and _layout_tokens(sqlglot, generated) != _layout_tokens(sqlglot, sources[i]):
618
- generated = sources[i].lstrip("\n").rstrip() # layout only: keep it as written
658
+ if aligned and (
659
+ _has_hint_comment(sqlglot, sources[i])
660
+ or (not rewrite_sql and _layout_tokens(sqlglot, generated) != _layout_tokens(sqlglot, sources[i]))
661
+ ):
662
+ generated = _as_written(sources[i])
619
663
  statements.append(generated)
620
664
  stmts = [s for s in statements if s]
621
665
  if not stmts:
622
666
  return sql_lines
623
667
 
668
+ # Terminate the last statement only if the source did: execsql refuses
669
+ # a script whose last statement is unfinished, and formatting must not
670
+ # turn it into one that runs.
671
+ terminated = _ends_with_semicolon(sqlglot, protected)
672
+
624
673
  # Safety: if sqlglot produced more statements than the input had
625
- # semicolons, it likely split a fragment (e.g. a SELECT column list)
626
- # into multiple pseudo-statements. Fall back to the original text.
627
- if len(stmts) > max(input_semis, 1):
674
+ # semicolons (plus an unterminated last one), it likely split a
675
+ # fragment (e.g. a SELECT column list) into multiple pseudo-statements.
676
+ # Fall back to the original text.
677
+ if len(stmts) > max(input_semis + (0 if terminated else 1), 1):
628
678
  return sql_lines
629
679
 
630
- joined = ";\n".join(stmts) + ";"
680
+ joined = ";\n".join(stmts) + (";" if terminated else "")
631
681
 
632
682
  # Content-loss check: sqlglot with IGNORE error level can silently
633
683
  # drop tokens it doesn't understand (e.g. ``ERROR: ...``). If the
execsql/importers/base.py CHANGED
@@ -16,15 +16,7 @@ from execsql.exceptions import ErrInfo
16
16
  from execsql.db.base import Database
17
17
  import execsql.state as _state
18
18
 
19
- __all__ = ["import_data_table", "refuse_existing_table", "table_exists_if_known"]
20
-
21
-
22
- def table_exists_if_known(db: Database, table_name: str, schema_name: str | None = None) -> bool | None:
23
- """Whether the table exists, or ``None`` when the database cannot say (no catalog access, ...)."""
24
- try:
25
- return db.table_exists(table_name, schema_name)
26
- except Exception:
27
- return None
19
+ __all__ = ["import_data_table", "refuse_existing_table"]
28
20
 
29
21
 
30
22
  def refuse_existing_table(db: Database, schema_name: str | None, table_name: str) -> None:
@@ -43,7 +35,11 @@ def refuse_existing_table(db: Database, schema_name: str | None, table_name: str
43
35
  lookup_schema = None
44
36
  if lookup_schema is None:
45
37
  return # An unqualified lookup would search every schema.
46
- if table_exists_if_known(db, table_name, lookup_schema):
38
+ try:
39
+ exists = db.table_exists_where_created(table_name, lookup_schema)
40
+ except Exception:
41
+ return # The database cannot say; CREATE TABLE reports any conflict.
42
+ if exists:
47
43
  name = db.schema_qualified_table_name(schema_name, table_name)
48
44
  raise ErrInfo(
49
45
  type="cmd",
execsql/lsp/document.py CHANGED
@@ -86,6 +86,8 @@ class ScriptIndex:
86
86
  # cannot make every request read a huge file or walk an endless include chain.
87
87
  MAX_INCLUDED_BYTES = 10 * 1024 * 1024
88
88
  MAX_INCLUDED_FILES = 200
89
+ # The files an INCLUDE names, all together; past it indexing follows no more of them.
90
+ MAX_INCLUDED_TOTAL_BYTES = 20 * 1024 * 1024
89
91
 
90
92
 
91
93
  def read_included(path: Path) -> str | None:
@@ -121,7 +123,7 @@ def index_script(
121
123
  prefers the editor's unsaved copy of a file that is open.
122
124
  """
123
125
  index = ScriptIndex(path=path)
124
- _index_into(index, source, path, set(), read, top=True)
126
+ _index_into(index, source, path, set(), read, top=True, budget=[MAX_INCLUDED_TOTAL_BYTES])
125
127
  return index
126
128
 
127
129
 
@@ -133,6 +135,7 @@ def _index_into(
133
135
  read: Callable[[Path], str | None],
134
136
  *,
135
137
  top: bool,
138
+ budget: list[int],
136
139
  ) -> None:
137
140
  if path is not None:
138
141
  seen.add(str(Path(path).resolve()))
@@ -166,8 +169,9 @@ def _index_into(
166
169
  index.includes.append((Location(path, row, start, end), str(resolved)))
167
170
  if "!" not in target and str(resolved.resolve()) not in seen and len(seen) < MAX_INCLUDED_FILES:
168
171
  included = read(resolved)
169
- if included is not None:
170
- _index_into(index, included, str(resolved), seen, read, top=False)
172
+ if included is not None and len(included) <= budget[0]:
173
+ budget[0] -= len(included)
174
+ _index_into(index, included, str(resolved), seen, read, top=False, budget=budget)
171
175
 
172
176
  for row, text in enumerate(lines):
173
177
  for m in _RX_VARIABLE.finditer(text):
@@ -27,7 +27,7 @@ from pathlib import Path
27
27
  from typing import Any
28
28
 
29
29
  import execsql.state as _state
30
- from execsql.utils.errors import fatal_error
30
+ from execsql.utils.errors import fatal_error, install_hint
31
31
  from execsql.utils.fileio import EncodedFile, filewriter_open_as_new, filewriter_write
32
32
 
33
33
 
@@ -110,7 +110,7 @@ def x_debug_write_odbc_drivers(**kwargs: Any) -> None:
110
110
  try:
111
111
  import pyodbc
112
112
  except Exception:
113
- fatal_error("The pyodbc module is required.")
113
+ fatal_error(f"The pyodbc module is required to list ODBC drivers. {install_hint('odbc')}")
114
114
  output_dest = kwargs["filename"]
115
115
  append = kwargs["append"]
116
116
  if output_dest is not None and output_dest != "stdout" and append is None:
@@ -114,7 +114,7 @@ def x_copy(**kwargs: Any) -> None:
114
114
 
115
115
  if new_tbl2:
116
116
  try:
117
- hdrs, rows = db1.select_rowsource(select_stmt)
117
+ hdrs, rows = db1.select_rowsource(select_stmt, decode_bytes=False)
118
118
  except ErrInfo:
119
119
  raise
120
120
  except Exception as e:
@@ -142,7 +142,7 @@ def x_copy(**kwargs: Any) -> None:
142
142
  db2.execute("COMMIT;")
143
143
  else:
144
144
  try:
145
- hdrs, rows = db1.select_rowsource(select_stmt)
145
+ hdrs, rows = db1.select_rowsource(select_stmt, decode_bytes=False)
146
146
  except ErrInfo:
147
147
  raise
148
148
  except Exception as e:
@@ -153,7 +153,7 @@ def x_copy(**kwargs: Any) -> None:
153
153
  def get_ts() -> DataTable:
154
154
  nonlocal get_ts_tablespec
155
155
  if get_ts_tablespec is None:
156
- ts_hdrs, ts_rows = db1.select_rowsource(select_stmt)
156
+ ts_hdrs, ts_rows = db1.select_rowsource(select_stmt, decode_bytes=False)
157
157
  get_ts_tablespec = DataTable(ts_hdrs, ts_rows, infer_strings=False)
158
158
  return get_ts_tablespec
159
159
 
@@ -201,7 +201,7 @@ def x_copy_query(**kwargs: Any) -> None:
201
201
 
202
202
  if new_tbl2:
203
203
  try:
204
- hdrs, rows = db1.select_rowsource(select_stmt)
204
+ hdrs, rows = db1.select_rowsource(select_stmt, decode_bytes=False)
205
205
  except ErrInfo:
206
206
  raise
207
207
  except Exception as e:
@@ -229,7 +229,7 @@ def x_copy_query(**kwargs: Any) -> None:
229
229
  db2.execute("COMMIT;")
230
230
  else:
231
231
  try:
232
- hdrs, rows = db1.select_rowsource(select_stmt)
232
+ hdrs, rows = db1.select_rowsource(select_stmt, decode_bytes=False)
233
233
  except ErrInfo:
234
234
  raise
235
235
  except Exception as e:
@@ -240,7 +240,7 @@ def x_copy_query(**kwargs: Any) -> None:
240
240
  def get_ts() -> DataTable:
241
241
  nonlocal get_ts_tablespec
242
242
  if get_ts_tablespec is None:
243
- ts_hdrs, ts_rows = db1.select_rowsource(select_stmt)
243
+ ts_hdrs, ts_rows = db1.select_rowsource(select_stmt, decode_bytes=False)
244
244
  get_ts_tablespec = DataTable(ts_hdrs, ts_rows, infer_strings=False)
245
245
  return get_ts_tablespec
246
246
 
@@ -17,7 +17,7 @@ from typing import Any
17
17
  import execsql.state as _state
18
18
  from execsql.exceptions import ErrInfo
19
19
  from execsql.types import dbt_postgres
20
- from execsql.utils.errors import exception_desc
20
+ from execsql.utils.errors import exception_desc, install_hint
21
21
 
22
22
 
23
23
  # ---------------------------------------------------------------------------
@@ -213,10 +213,7 @@ def _require_pg_upsert() -> None:
213
213
  except ImportError as exc:
214
214
  raise ErrInfo(
215
215
  "exception",
216
- other_msg=(
217
- 'PG_UPSERT requires the pg-upsert package. Install the upsert extra: uv tool install "execsql2[upsert]"'
218
- ' (or pip install "execsql2[upsert]").'
219
- ),
216
+ other_msg=f"PG_UPSERT requires the pg-upsert package. {install_hint('upsert')}",
220
217
  ) from exc
221
218
 
222
219
 
execsql/models.py CHANGED
@@ -167,8 +167,16 @@ class Column:
167
167
  self.Accum(DT_Text()),
168
168
  self.Accum(DT_Binary()),
169
169
  )
170
+ if not infer_strings:
171
+ # Values from a database cursor: a bytes value is binary data, so
172
+ # Binary is tried before the text types (which accept anything).
173
+ text = [ac for ac in self.accums if isinstance(ac.dt, (DT_Character, DT_Varchar, DT_Text))]
174
+ binary = [ac for ac in self.accums if isinstance(ac.dt, DT_Binary)]
175
+ self.accums = tuple(ac for ac in self.accums if ac not in text and ac not in binary) + tuple(binary + text)
170
176
  self.infer_strings = infer_strings
171
177
  self.str_accums = tuple(ac for ac in self.accums if isinstance(ac.dt, (DT_Character, DT_Varchar, DT_Text)))
178
+ # A bytes value is checked as binary or text only: int(b"12") is 12.
179
+ self.bytes_accums = tuple(ac for ac in self.accums if isinstance(ac.dt, DT_Binary)) + self.str_accums
172
180
  self.dt_eval = False
173
181
  # self.dt is a tuple of: 0: column name; 1: data type class; 2: max length or None;
174
182
  # 3: bool indicating any null values; 4: precision or None; 5: scale or None.
@@ -198,6 +206,9 @@ class Column:
198
206
  if not self.infer_strings and isinstance(column_value, str):
199
207
  for dt in self.str_accums:
200
208
  dt.check(column_value)
209
+ elif not self.infer_strings and isinstance(column_value, (bytes, bytearray, memoryview)):
210
+ for dt in self.bytes_accums:
211
+ dt.check(column_value)
201
212
  else:
202
213
  for dt in self.accums:
203
214
  dt.check(column_value)
execsql/utils/errors.py CHANGED
@@ -37,8 +37,24 @@ __all__ = [
37
37
  "stamp_errinfo",
38
38
  "write_warning",
39
39
  "file_size_date",
40
+ "install_hint",
40
41
  ]
41
42
 
43
+ _ADDING_AN_EXTRA = "https://execsql2.readthedocs.io/en/latest/getting-started/installation/#adding-an-extra-later"
44
+
45
+
46
+ def install_hint(extra: str) -> str:
47
+ """How to install the execsql2 *extra*, for a message about a missing optional package.
48
+
49
+ ``uv tool install`` and ``pipx install --force`` replace an install's
50
+ extras with the ones named, so the hint says to keep those already there.
51
+ """
52
+ return (
53
+ f'Install the {extra} extra along with the extras you already have: uv tool install "execsql2[...,{extra}]" '
54
+ f'or pipx install --force "execsql2[...,{extra}]" (in a project environment: pip install "execsql2[{extra}]"). '
55
+ f"See {_ADDING_AN_EXTRA}"
56
+ )
57
+
42
58
 
43
59
  def exception_info() -> tuple:
44
60
  # Returns the exception type, value, source file name, source line number, and source line text.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execsql2
3
- Version: 2.27.0
3
+ Version: 2.28.0
4
4
  Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
5
5
  Project-URL: Homepage, https://execsql2.readthedocs.io
6
6
  Project-URL: Repository, https://github.com/geocoug/execsql
@@ -95,8 +95,10 @@ Requires-Dist: openpyxl>=3.1; extra == 'dev'
95
95
  Requires-Dist: polars>=1.0; extra == 'dev'
96
96
  Requires-Dist: pre-commit>=3.5.0; extra == 'dev'
97
97
  Requires-Dist: pygls<3,>=2.1; extra == 'dev'
98
+ Requires-Dist: pytest-asyncio>=1.0; extra == 'dev'
98
99
  Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
99
100
  Requires-Dist: pytest-lsp>=1.0; extra == 'dev'
101
+ Requires-Dist: pytest>=8; extra == 'dev'
100
102
  Requires-Dist: pyyaml>=6.0; extra == 'dev'
101
103
  Requires-Dist: ruff>=0.4; extra == 'dev'
102
104
  Requires-Dist: sqlglot<30.22,>=30.21.0; extra == 'dev'
@@ -464,7 +466,7 @@ execsql format --no-sql --in-place scripts/
464
466
  ```yaml
465
467
  repos:
466
468
  - repo: https://github.com/geocoug/execsql
467
- rev: v2.27.0
469
+ rev: v2.28.0
468
470
  hooks:
469
471
  - id: execsql-format
470
472
  - id: execsql-lint
@@ -4,10 +4,10 @@ execsql/api.py,sha256=UJJSlFHySuINeoExaMXqhl00hVSPH1s7G4m8T0NX_xI,26484
4
4
  execsql/config.py,sha256=GUKE3sLGExMMhMkmxi8eXXBWmN74E0vqnt1eTBPRsG4,38166
5
5
  execsql/config_validation.py,sha256=a4ZszyJFGTNgs1jkvXE3yMCaYFtF1Uqih2ojMDVh1v8,10556
6
6
  execsql/exceptions.py,sha256=aHavdblmP7m5ZLxyyKKmPSwwbdmP962j4ShP1Gd9r64,9651
7
- execsql/format.py,sha256=_KNFGTnEihO0Bn-E8k0QNLijAS4NDE8Ee1ldL7_MKZc,57430
7
+ execsql/format.py,sha256=z-KZ8VY1muz8S6iaErZX9E_7wE_VtjEcUB7e_KyhfyI,59565
8
8
  execsql/interactive.py,sha256=7vm7NAlcarBJJOIffcLOcJqH9ZCInvHB_CmC2sQsXf8,8241
9
9
  execsql/manifest.py,sha256=ibceEZtH2FqsUiujbTGy5D3W0WgO4tFCs7bjIHVdep4,8062
10
- execsql/models.py,sha256=yXR9FMCSCRonfyiuWlYwqLBMLzWQcvFQOjGHogM8Veg,15556
10
+ execsql/models.py,sha256=caUGQ3C9hvFaj6c6-GwBzZc8FAGTD1ZSJYd8bFyRh5I,16410
11
11
  execsql/parser.py,sha256=bw26e1lD-PZscV11VtBdqSLkjpcYfNLQC2EYfYD5UwU,16012
12
12
  execsql/plugins.py,sha256=RykMlIjTyWcnQc9ygjLErS3wdcsZSsG9QTIvg4DwfAI,13626
13
13
  execsql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -19,56 +19,56 @@ execsql/cli/application.py,sha256=qKjkhDMWB7fEjEbqOLZ3pNFw80piBnRSeW_DfZg-9MA,12
19
19
  execsql/cli/dispatch.py,sha256=deyN2QQYvdzMSqGGa3CSMpaqdLNeMgc5Uke0mUbl764,2704
20
20
  execsql/cli/dsn.py,sha256=uBspf8DXCn2pXmT1Ug1c4IG7IgR5IFGKp9138puaubo,3289
21
21
  execsql/cli/help.py,sha256=bEy6ssVjyIw73JD_UPLXnCtnCifdVCKJr_cydAAVxhk,8220
22
- execsql/cli/lint.py,sha256=11hmvhCRn7iq-Y1A-a6dxjktsRQChAatz7InF08jwJU,46759
22
+ execsql/cli/lint.py,sha256=8dZhwPrkc-_0WFd1D_qjBdKuql0SP5dpkdBRFd25xC8,47604
23
23
  execsql/cli/options.py,sha256=Srhf1gEOB5yy_7PxkTKBv07xK8Qzw8H-z7PUUnItpwE,2916
24
24
  execsql/cli/run.py,sha256=5f0SX6QyNl8Ur2MwuW2GaN1OqVpWbEI8CQV3k-hBwH8,41987
25
25
  execsql/cli/commands/__init__.py,sha256=FVe_xF7w98krP-USxqf-BIcflA1ctMYVsV8p0i6cChA,572
26
26
  execsql/cli/commands/config.py,sha256=V7CGGkhSCqeJIlAXxAYyG_xNhnsORiwqth2XWEVPzss,8213
27
- execsql/cli/commands/format.py,sha256=4BY2EUExt32DOv871SZW3kimK4x9qaUsElhu3B49yqc,4412
27
+ execsql/cli/commands/format.py,sha256=3awfNmCc6kImeyXKZo1Jt1qXbEuWk86ZllqEOY5vxzw,4456
28
28
  execsql/cli/commands/init.py,sha256=kmuFgGz8sv_1KfiNh190E3A23evqkgYZeBRTU6vdjbs,5465
29
29
  execsql/cli/commands/lint.py,sha256=Lba5IDsXtiUbQ6DkhC9fdLnoSpfoaewoAWF-ei3h59s,8189
30
30
  execsql/cli/commands/listing.py,sha256=thtkJ-ILGd0SPs57oVIMkkCGpXsfS0hQxueMc2ccRVw,3514
31
- execsql/cli/commands/lsp.py,sha256=3Q2Liz3zCiSrlZVkqwvG64PgZOifKn_qB46MVRQqBns,1645
31
+ execsql/cli/commands/lsp.py,sha256=4Xng1xtoCxF6X0eQ8oC4KnV0ovEeGmlDb0tpdjJnskg,2483
32
32
  execsql/cli/commands/run.py,sha256=sGFDku66CazJrorr0mmrhUI3QDvQVSMEYlU8G1Pd5fQ,16492
33
33
  execsql/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  execsql/data/execsql.conf.template,sha256=pqh7G21ifYO0BjZoTgUhGyQAx4eKOmbIr6nToc1V7QA,12494
35
35
  execsql/data/script_header.sql,sha256=a7WKScekNgJoxVEmMRTkWv8NsAzW1jJ4vJfGy9HVy0g,349
36
36
  execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
37
- execsql/db/access.py,sha256=O19alGOCjhBtVFaCnGN8zwVZ2wR57h1spizAF5dhUOg,21207
38
- execsql/db/base.py,sha256=K_NJ5Tpn68qUyc5epnT3V3jUxkv1lCAXF0rGul-8otc,42264
39
- execsql/db/dsn.py,sha256=a3_q7oucR37CMCKNO9S5ShsB_JDwmZY4d8QI1aqeFSU,6893
40
- execsql/db/duckdb.py,sha256=h62nEuwNl8Bz8ZcJJNIAfFK1o91KXxAeh8unlwTivKc,7047
37
+ execsql/db/access.py,sha256=xfBzcgP1qJovxSYwrpu0bdxqyJWqMoMP8i4FsaCTYXA,21306
38
+ execsql/db/base.py,sha256=rMkOxfqhlrG1_LnlkIQYwZXnm-wRd2xN4YQ9oY7OOZM,42990
39
+ execsql/db/dsn.py,sha256=CaISffisqO5IjSE-elEpwK9vXLaArp3ItpV8lvRTu-I,6920
40
+ execsql/db/duckdb.py,sha256=2iVlbjQdOSAwhI2LxWEm7FYZxjjVcKiE8rT_3VJdRIo,7761
41
41
  execsql/db/factory.py,sha256=cMptU3BL1Nsilo7LKLAI5RR-LiC9RTH-3UC9rHT2JLQ,5519
42
- execsql/db/firebird.py,sha256=sU2XqYV0U6hTgUuetFfMdlZwlBlXgPVXXn_wi1XCy1U,10196
43
- execsql/db/mysql.py,sha256=ooH4uNomr_VatEldJ70ynsOth4_STa8-761O33NfndY,21496
44
- execsql/db/oracle.py,sha256=-n8CgbZd9zaGnbfQQmWAdFgPA_B-HkLNC6yvENbzqyc,13720
45
- execsql/db/postgres.py,sha256=QXL34lyg6w7zj2S-1AAlyswPGjbXZz7GZS2qFB4pANY,23604
42
+ execsql/db/firebird.py,sha256=CoNnzFtOFAkp8OJByXXXclitCPAAjM23eFJGfy3C7IE,10171
43
+ execsql/db/mysql.py,sha256=ovadEv8InaK2VZjfMRwZNQ7dTQaE2qaWNUra-FjP0xA,21492
44
+ execsql/db/oracle.py,sha256=i5qPahsy_z1AuZT7l54Hzixknkvbzd-SCQgiVcTN94w,13796
45
+ execsql/db/postgres.py,sha256=PZ2vXKh9Nbs6aAc899vaPz1w9ZntKNCqQ2pCl_giiVQ,23587
46
46
  execsql/db/sqlite.py,sha256=fEpNPkfJGnqm1Wx_rZK5SEqaWsBqLy85Jxk_5NsnqgM,12109
47
- execsql/db/sqlserver.py,sha256=e4UP2Ps0EXjVjX4TOYNslsviBF2dorx8TEvPhxb9rrY,9503
47
+ execsql/db/sqlserver.py,sha256=cdbyHoSUKEwUee2GQTKD5SlLyBraYiOvJ2ro5MJuMFA,9525
48
48
  execsql/db/tiers.py,sha256=PH2sUYwlYqcANGVKXprWQKVIlM6fpWU9q74djn8oFwc,2924
49
49
  execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
50
50
  execsql/debug/repl.py,sha256=jnwrqRac4krc1pES7Ni-j1Dyw3mJIcWl5LQQIX1GkRw,20753
51
51
  execsql/exporters/__init__.py,sha256=-Cnji-OgodJV8ftcDcOyTof0kQMy9J5kKVC8GVFpc3o,670
52
52
  execsql/exporters/base.py,sha256=_GoO0YqwlTxrjogxHI3CxxRR64sMgIEjRogt0KCqzoY,6402
53
53
  execsql/exporters/delimited.py,sha256=N0cP_CNlb_LLCUqbHmTQsOGufWnD_ddGdeFnXPE8Erc,35606
54
- execsql/exporters/duckdb.py,sha256=912TMdYaSVD-kb-_oebTBHQp8FvYMbWTDEDD80w5Z50,3455
54
+ execsql/exporters/duckdb.py,sha256=1Qo6idHvQIJjGbA5trt_s5to9XtoDpcS_wWGXOrVe6I,3495
55
55
  execsql/exporters/feather.py,sha256=tah48aep0cQRikZcmADXUPTzFddgDUT78Ndw_l0Hsj4,4178
56
56
  execsql/exporters/html.py,sha256=hwf9UHs7EZ7xiP3p9ilBIsAukXcDu-KO2dcQHVh2z30,10111
57
57
  execsql/exporters/json.py,sha256=wWGT6zjvIdWlO5FmXbaEw1xOhy23f1YY6bm5uQD_NS0,4393
58
58
  execsql/exporters/latex.py,sha256=bdUN3GwfENUZHgpw6bvjsjfivDgzyf-HRkYTP-eH-pM,5480
59
59
  execsql/exporters/markdown.py,sha256=FuMGFxm9uZUOrdEuXT2ZZLCqmVDly7SMrFlrjP6kVEI,4827
60
- execsql/exporters/ods.py,sha256=Ke9pfcyT6R85LWIWcMI6pzRzkKir5w_gmiptCeeuoBc,20730
60
+ execsql/exporters/ods.py,sha256=5Qubpuz7CLIrl7KgMp1uXIFPrF_7alMjuTuVdj76eh4,20771
61
61
  execsql/exporters/parquet.py,sha256=mbCo2Wlit1lkXmyo5EyLd_ATIHSICS49bouiBT2kdAs,1121
62
62
  execsql/exporters/pretty.py,sha256=SiJHBXIjoFsOHegXcdrtxYa6xSIPga1U0DQEy7pK4QY,4145
63
63
  execsql/exporters/protocol.py,sha256=BxATgz0xKHbB2FpZBeNg7wZfIiCohhD1awlr3JCec0c,4372
64
64
  execsql/exporters/raw.py,sha256=9o3BMt7UYmnjbP8OEzmM2kNKfwwERi2yDcuNfUoEfuQ,2530
65
65
  execsql/exporters/sqlite.py,sha256=QjP_W90S-hecNqp2A-Vy58DD8MZitME1sTq4lqIS43w,3401
66
- execsql/exporters/templates.py,sha256=nRFurwTb5nxoyHpx609FGIp3MDbX_mYQ3IvgswaEbyA,7054
66
+ execsql/exporters/templates.py,sha256=N9PRFet0mYuDQFu7PNbq5c6qGu2You2qfHpqnxg17NA,7065
67
67
  execsql/exporters/values.py,sha256=UPxN5IprasBZLqM5jfUXQihjdTeBFTxbJslOEtmGOcY,3384
68
- execsql/exporters/xls.py,sha256=iZ_N7hneyfaDKbO1XdYv6doJCFPqJWEJql5rduoNyRU,11197
69
- execsql/exporters/xlsx.py,sha256=hmL0i-2HweXFenl5uXjM3-w2DW2Ky8DeYTcZnFzmeOU,12708
68
+ execsql/exporters/xls.py,sha256=n4J4J4sJq-5iU3Fa1JTAxTq8VAXEhv2znNRGoi1arSw,11265
69
+ execsql/exporters/xlsx.py,sha256=7sm-3KniYlB2K-n6xQmIoHbAvl2ryJhFWU95SEkR7e4,12702
70
70
  execsql/exporters/xml.py,sha256=t1PSr3i8xvTCWzd9jaxxAy5NcRpl6sqjnZALafKCqpM,2575
71
- execsql/exporters/yaml.py,sha256=R61Rs_0rd5G3bg344L4hC84GM79IfYXaDO2tkF-BluY,3121
71
+ execsql/exporters/yaml.py,sha256=ex2h91E12wItvh84AarjrDTcZ4SDx9uq0c9wtr-NuQM,3084
72
72
  execsql/exporters/zip.py,sha256=0vDkXB19WzLzgCACBVNnGJYxacgBwEehQDL7Inl-rgw,5002
73
73
  execsql/gui/__init__.py,sha256=oCb-cyhLZzVpWJ4WU5HbqEDBrV-lm0ytEwxemrOZyqs,2048
74
74
  execsql/gui/base.py,sha256=DMTqHLlpZlGCsNKyrLlJPbaxvukC0n1YmR4aHEN3jPY,14983
@@ -76,7 +76,7 @@ execsql/gui/console.py,sha256=dzdSl4NJNvRd9VvlwRCXHvfnXjQPimyTx7NcibcUZC8,18305
76
76
  execsql/gui/desktop.py,sha256=y-T317NzSs9kcA4-NZW-Dx2cZ4Bv1mPoCr7hwQCJYoo,59467
77
77
  execsql/gui/tui.py,sha256=VoUtjSMopYtbtc8_-qV7YQAFvc-bS7zrZ5J2hl1cPxM,66853
78
78
  execsql/importers/__init__.py,sha256=zZwdQxMaValCNqUrVdvaA7XPU3J8NmqVJ4uolNWY2iU,299
79
- execsql/importers/base.py,sha256=ikvJ98vVCcDBqp5ZdwwHwf-6zQCO4f8uI355VCVukHo,5783
79
+ execsql/importers/base.py,sha256=N94kJ_a3_UvjOrZv7TCYOL2b4lshYb_HuLTqpht4D4A,5578
80
80
  execsql/importers/csv.py,sha256=4zIv-K02fuHKxnbqS5iTvLOeVnfssLYYJrs0Gh3qv7I,5021
81
81
  execsql/importers/feather.py,sha256=g2B69d2uv9vmnXcmjFyTVsMP40LYEzFYkhk3gD26mGw,1900
82
82
  execsql/importers/json.py,sha256=fL47h09Zzx4Qw3TTQRQJbdPdD3qVMiU-kK9C7j1IQzg,5534
@@ -86,7 +86,7 @@ execsql/lsp/__init__.py,sha256=KXpbN_CfnfMcvHmE0xPz9ddpp9CAaQH_O7mzBi4O4X8,259
86
86
  execsql/lsp/code_actions.py,sha256=QEzp7KnIS_9QeanClcpmC2miMCT9ehxxOWUth4ZHPzg,8884
87
87
  execsql/lsp/completion.py,sha256=-_HNiWzRgtJDisErNGiBHWwsKeVWBHNnAOu235XV_ys,8148
88
88
  execsql/lsp/diagnostics.py,sha256=Rf5izQN7BX2SUjll-kA5_D12BybFKdj5f4mEPQ0C2q4,2496
89
- execsql/lsp/document.py,sha256=1AtEQ4QKoykxCefZGCessXcB4YjXDHX918XTezOqxRI,8493
89
+ execsql/lsp/document.py,sha256=E9FfnAypzggO__QGWSlbfa6AhYE_1IMpIbyrwkEKx8s,8774
90
90
  execsql/lsp/folding.py,sha256=PgwehzXnPcR68myldo2SYrqbfGla4f6gb_UrhKmaqyQ,2752
91
91
  execsql/lsp/formatting.py,sha256=9YpCfLsi09M1PZovB2HJN-Tp9Jqgk-2xxit5g5NrTqU,1217
92
92
  execsql/lsp/hover.py,sha256=yihlkc0i8f50rtYz8afnd-a81H1ocXOlkYb09m-VkqU,5379
@@ -98,19 +98,19 @@ execsql/metacommands/conditions.py,sha256=4DsMXr6vnfbtcKqgnxYV4UK4NmAfE_hpfgkFjl
98
98
  execsql/metacommands/connect.py,sha256=SPk4h9yiUgTD3w1gNOT_oYx8VShggsO357by5Hoy6OI,15344
99
99
  execsql/metacommands/control.py,sha256=9swW6X8jXDUpu5cHiAnQVx1ahFyWxlx7mjg4dqYfX5Y,9355
100
100
  execsql/metacommands/data.py,sha256=gaKJ0xA-maFnezT9TJv-q4G4Etn-kJnF7vZWZoG4UqI,12305
101
- execsql/metacommands/debug.py,sha256=XyMoKnQKyYHdzcfGUGFEaecIlDOi1LMirwbbpbiRO7I,12922
101
+ execsql/metacommands/debug.py,sha256=mKRT7uLwnw34Xl46fNDI_zH4MsPpZyihoLlonapbXuU,12981
102
102
  execsql/metacommands/dispatch.py,sha256=so0ebcnv66NhVDtA1Bt4GelD0hsC5w7gy7rItz2Mdos,87430
103
103
  execsql/metacommands/effects.py,sha256=cRSo48R4nRJzkRR3OGHLhJzofcdAaNIobaxmn1N9TGw,3813
104
104
  execsql/metacommands/io.py,sha256=vlGBje5sgnqeilooMdhJDgSRIhysHy5_7LrKtik9Xjs,3011
105
105
  execsql/metacommands/io_export.py,sha256=YamxQwURRjZRpCG2z8W3QurncCmEmAyLFqDkxtHo5dY,14434
106
- execsql/metacommands/io_fileops.py,sha256=uXP5PdHOc8yi1rPo8PbWmvAx_yp69ozCkK_SHYoL0NE,12646
106
+ execsql/metacommands/io_fileops.py,sha256=JkuHBsZFDZvAW3wUJtdWwm4qGkpXObqkQNcZalhJVkE,12766
107
107
  execsql/metacommands/io_import.py,sha256=gJICeK1MOfG6D73I7m8zBCalvr_ZJzD-QBKOpKYeYXo,15684
108
108
  execsql/metacommands/io_write.py,sha256=yXDkMbQ3haDARVATVUYZprakyJHSyk1NywdKkFLpQ14,12783
109
109
  execsql/metacommands/prompt.py,sha256=PlXhtBYCembG_1SSCrwpQR10mGF3_jXodu4aCSHlSCM,37950
110
110
  execsql/metacommands/reference.py,sha256=tMHyfpGMNJQubX4yERe_nFZaXOoxjYWI542YHusBVHE,53953
111
111
  execsql/metacommands/script_ext.py,sha256=CsUbyq8QFPnnvfX_k7fPlYNvvRw1xLmAWER4Exo5pwc,3658
112
112
  execsql/metacommands/system.py,sha256=6X2qmMpGUHDzyEluEotXt0Ks9jKQTpc2x0eoYNgN12M,7997
113
- execsql/metacommands/upsert.py,sha256=qez06e41ivxsLlB-Af91fkVzD6XhHVxpvv5sLMPul6M,20429
113
+ execsql/metacommands/upsert.py,sha256=vy8zfvxs9OYsyDiXInNIRT93ovzue4N_KMPBabNH6Ys,20320
114
114
  execsql/script/__init__.py,sha256=I7hVO6wm3iOocxRRGEcFHhjdtL1moKbUYLB-wKbjPdk,3267
115
115
  execsql/script/ast.py,sha256=WqCpFxhxrT2IIGvbArTp3QS0BZryRZAb7IL4ri3I6xg,20591
116
116
  execsql/script/control.py,sha256=WqLy-HLPqHG3vEzYpKMiIJsD7LpORjyQuUWzFzcGz4w,2327
@@ -123,7 +123,7 @@ execsql/utils/auth.py,sha256=BV7VtLM-ID8jNB42vx7GEz8yb3Z1Qs9xCr4cUMqL864,14539
123
123
  execsql/utils/color.py,sha256=goQqhnLg8yERpFolQikA9zYSVcuAm0unKTJiWTbQm-U,634
124
124
  execsql/utils/crypto.py,sha256=KlT6lPk-v0-qQQfAMfWvT9Q6kRdCD3_5JVaLajNocKM,3051
125
125
  execsql/utils/datetime.py,sha256=rqtGrc0t37V00mvirJ8wJKHmDtDSYzTg_UgCyIBzv9E,5583
126
- execsql/utils/errors.py,sha256=JqV5mDKJnsYF8Uam6iLE3HNqkL1cUiiP8ExHpBBFnC8,11630
126
+ execsql/utils/errors.py,sha256=NA-JXP4cyEZ8Y_KNOPLS_ebXTJhQ50gNMBSTTcnsLIc,12350
127
127
  execsql/utils/fileio.py,sha256=ADp8Iltf_zfv0PL0VRdO0BDIHkVpKVVh59_5zmkTfkw,47564
128
128
  execsql/utils/gui.py,sha256=1iiupP9AsLdR6ST0gcLE143o6y6YGUFlNv_pdVPxfCE,23943
129
129
  execsql/utils/mail.py,sha256=4RiKtXa_sqij_YGdYmjQ3PG0u3G9qjs-9caQwBErtBw,7278
@@ -131,23 +131,23 @@ execsql/utils/numeric.py,sha256=_7bi5R6pHjMCkOPne_Sh5nUj-B8e9g6WFmjptFHaunA,1573
131
131
  execsql/utils/regex.py,sha256=B3mSBhRRx_DpUnX_t9IoYd05KvWAebiLMCZqQAmEwcw,3885
132
132
  execsql/utils/strings.py,sha256=UQNjpRCEFa1UO6feU-M-9e24wWAvizs_iu_4fFusLxo,8516
133
133
  execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
134
- execsql2-2.27.0.data/data/execsql2_extras/README.md,sha256=7SG9FbBpdqkgQTTt1aBTbCgymBZyV0rLrQMQrendzxs,5032
135
- execsql2-2.27.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
136
- execsql2-2.27.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
137
- execsql2-2.27.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
138
- execsql2-2.27.0.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
139
- execsql2-2.27.0.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
140
- execsql2-2.27.0.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
141
- execsql2-2.27.0.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
142
- execsql2-2.27.0.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
143
- execsql2-2.27.0.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
144
- execsql2-2.27.0.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
145
- execsql2-2.27.0.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
146
- execsql2-2.27.0.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
147
- execsql2-2.27.0.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
148
- execsql2-2.27.0.dist-info/METADATA,sha256=2sB1585HuNTyYAej0E12cjn1uvpBg9biCkUgn9_g1Dc,27762
149
- execsql2-2.27.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
150
- execsql2-2.27.0.dist-info/entry_points.txt,sha256=jOf53-4IVGjEZeNYUOF1GGaiPNfVZcNKxP3vDFKEyMA,58
151
- execsql2-2.27.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
152
- execsql2-2.27.0.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
153
- execsql2-2.27.0.dist-info/RECORD,,
134
+ execsql2-2.28.0.data/data/execsql2_extras/README.md,sha256=7SG9FbBpdqkgQTTt1aBTbCgymBZyV0rLrQMQrendzxs,5032
135
+ execsql2-2.28.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
136
+ execsql2-2.28.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
137
+ execsql2-2.28.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
138
+ execsql2-2.28.0.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
139
+ execsql2-2.28.0.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
140
+ execsql2-2.28.0.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
141
+ execsql2-2.28.0.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
142
+ execsql2-2.28.0.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
143
+ execsql2-2.28.0.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
144
+ execsql2-2.28.0.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
145
+ execsql2-2.28.0.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
146
+ execsql2-2.28.0.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
147
+ execsql2-2.28.0.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
148
+ execsql2-2.28.0.dist-info/METADATA,sha256=8mBy6p3UOLvFCmFwhoNh77QQIR5qYSotGYH0fUF0XgQ,27854
149
+ execsql2-2.28.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
150
+ execsql2-2.28.0.dist-info/entry_points.txt,sha256=jOf53-4IVGjEZeNYUOF1GGaiPNfVZcNKxP3vDFKEyMA,58
151
+ execsql2-2.28.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
152
+ execsql2-2.28.0.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
153
+ execsql2-2.28.0.dist-info/RECORD,,