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.
- execsql/cli/commands/format.py +2 -1
- execsql/cli/commands/lsp.py +33 -9
- execsql/cli/lint.py +45 -22
- execsql/db/access.py +4 -4
- execsql/db/base.py +17 -3
- execsql/db/dsn.py +2 -2
- execsql/db/duckdb.py +14 -2
- execsql/db/firebird.py +2 -3
- execsql/db/mysql.py +2 -2
- execsql/db/oracle.py +5 -5
- execsql/db/postgres.py +2 -3
- execsql/db/sqlserver.py +2 -2
- execsql/exporters/duckdb.py +2 -2
- execsql/exporters/ods.py +2 -2
- execsql/exporters/templates.py +2 -2
- execsql/exporters/xls.py +3 -3
- execsql/exporters/xlsx.py +2 -2
- execsql/exporters/yaml.py +3 -4
- execsql/format.py +71 -21
- execsql/importers/base.py +6 -10
- execsql/lsp/document.py +7 -3
- execsql/metacommands/debug.py +2 -2
- execsql/metacommands/io_fileops.py +6 -6
- execsql/metacommands/upsert.py +2 -5
- execsql/models.py +11 -0
- execsql/utils/errors.py +16 -0
- {execsql2-2.27.0.dist-info → execsql2-2.28.0.dist-info}/METADATA +4 -2
- {execsql2-2.27.0.dist-info → execsql2-2.28.0.dist-info}/RECORD +46 -46
- {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.27.0.dist-info → execsql2-2.28.0.dist-info}/WHEEL +0 -0
- {execsql2-2.27.0.dist-info → execsql2-2.28.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.27.0.dist-info → execsql2-2.28.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.27.0.dist-info → execsql2-2.28.0.dist-info}/licenses/NOTICE +0 -0
execsql/cli/commands/format.py
CHANGED
|
@@ -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(
|
execsql/cli/commands/lsp.py
CHANGED
|
@@ -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.
|
|
21
|
-
"
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
479
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
861
|
-
"""
|
|
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,
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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__(
|
execsql/exporters/duckdb.py
CHANGED
|
@@ -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] = []
|
execsql/exporters/templates.py
CHANGED
|
@@ -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.
|
|
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 (
|
|
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 (
|
|
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=
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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(
|
|
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
|
|
536
|
-
"""
|
|
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
|
|
539
|
-
compared without case; strings and quoted
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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 =
|
|
600
|
-
if
|
|
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
|
|
618
|
-
|
|
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
|
|
626
|
-
# into multiple pseudo-statements.
|
|
627
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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):
|
execsql/metacommands/debug.py
CHANGED
|
@@ -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
|
|
execsql/metacommands/upsert.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
38
|
-
execsql/db/base.py,sha256=
|
|
39
|
-
execsql/db/dsn.py,sha256=
|
|
40
|
-
execsql/db/duckdb.py,sha256=
|
|
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=
|
|
43
|
-
execsql/db/mysql.py,sha256=
|
|
44
|
-
execsql/db/oracle.py,sha256
|
|
45
|
-
execsql/db/postgres.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
66
|
+
execsql/exporters/templates.py,sha256=N9PRFet0mYuDQFu7PNbq5c6qGu2You2qfHpqnxg17NA,7065
|
|
67
67
|
execsql/exporters/values.py,sha256=UPxN5IprasBZLqM5jfUXQihjdTeBFTxbJslOEtmGOcY,3384
|
|
68
|
-
execsql/exporters/xls.py,sha256=
|
|
69
|
-
execsql/exporters/xlsx.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
135
|
-
execsql2-2.
|
|
136
|
-
execsql2-2.
|
|
137
|
-
execsql2-2.
|
|
138
|
-
execsql2-2.
|
|
139
|
-
execsql2-2.
|
|
140
|
-
execsql2-2.
|
|
141
|
-
execsql2-2.
|
|
142
|
-
execsql2-2.
|
|
143
|
-
execsql2-2.
|
|
144
|
-
execsql2-2.
|
|
145
|
-
execsql2-2.
|
|
146
|
-
execsql2-2.
|
|
147
|
-
execsql2-2.
|
|
148
|
-
execsql2-2.
|
|
149
|
-
execsql2-2.
|
|
150
|
-
execsql2-2.
|
|
151
|
-
execsql2-2.
|
|
152
|
-
execsql2-2.
|
|
153
|
-
execsql2-2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
{execsql2-2.27.0.data → execsql2-2.28.0.data}/data/execsql2_extras/example_config_prompt.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|