execsql2 2.32.0__py3-none-any.whl → 2.34.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/db/base.py +71 -8
- execsql/db/duckdb.py +6 -0
- execsql/db/mysql.py +54 -2
- execsql/db/oracle.py +36 -0
- execsql/db/postgres.py +17 -1
- execsql/db/sqlite.py +7 -1
- execsql/db/sqlserver.py +24 -0
- execsql/db/target_types.py +277 -0
- execsql/exporters/json.py +51 -7
- execsql/exporters/protocol.py +1 -0
- execsql/importers/base.py +1 -1
- execsql/importers/csv.py +1 -1
- execsql/metacommands/__init__.py +17 -1
- execsql/metacommands/io_export.py +3 -1
- execsql/metacommands/io_fileops.py +2 -2
- execsql/metacommands/reference.py +10 -0
- execsql/metacommands/upsert.py +13 -0
- execsql/script/engine.py +5 -7
- execsql/script/executor.py +3 -1
- execsql/script/variables.py +4 -0
- execsql/types.py +2 -2
- {execsql2-2.32.0.dist-info → execsql2-2.34.0.dist-info}/METADATA +3 -3
- {execsql2-2.32.0.dist-info → execsql2-2.34.0.dist-info}/RECORD +40 -39
- {execsql2-2.32.0.data → execsql2-2.34.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.32.0.data → execsql2-2.34.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.32.0.data → execsql2-2.34.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.32.0.data → execsql2-2.34.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.32.0.data → execsql2-2.34.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.32.0.data → execsql2-2.34.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.32.0.data → execsql2-2.34.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.32.0.data → execsql2-2.34.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.32.0.data → execsql2-2.34.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.32.0.data → execsql2-2.34.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.32.0.data → execsql2-2.34.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.32.0.data → execsql2-2.34.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.32.0.data → execsql2-2.34.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.32.0.dist-info → execsql2-2.34.0.dist-info}/WHEEL +0 -0
- {execsql2-2.32.0.dist-info → execsql2-2.34.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.32.0.dist-info → execsql2-2.34.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.32.0.dist-info → execsql2-2.34.0.dist-info}/licenses/NOTICE +0 -0
execsql/db/base.py
CHANGED
|
@@ -24,7 +24,9 @@ from collections.abc import Callable, Generator, Iterator
|
|
|
24
24
|
|
|
25
25
|
from execsql.exceptions import ErrInfo
|
|
26
26
|
from execsql.utils.errors import exception_desc
|
|
27
|
+
from execsql.db.target_types import ColumnKinds, existing_table_converter, kind_for_python_type
|
|
27
28
|
from execsql.db.tiers import SupportTier, announce_tier
|
|
29
|
+
from execsql.types import DataType
|
|
28
30
|
import execsql.state as _state
|
|
29
31
|
|
|
30
32
|
__all__ = ["Database", "DatabasePool"]
|
|
@@ -604,6 +606,42 @@ class Database(ABC):
|
|
|
604
606
|
rows = curs.fetchall()
|
|
605
607
|
return [row[0] for row in rows]
|
|
606
608
|
|
|
609
|
+
def column_kinds(self, sq_name: str) -> ColumnKinds:
|
|
610
|
+
"""The execsql data type each column of table *sq_name* converts values to (see ``db.target_types``).
|
|
611
|
+
|
|
612
|
+
*sq_name* is schema-qualified and quoted, as for an ``INSERT``, and
|
|
613
|
+
is looked up the way the ``INSERT`` finds it. Keys are lower-cased
|
|
614
|
+
column names; a column with no fitting execsql type maps to ``None``.
|
|
615
|
+
This default reads the driver's description of an empty query on the
|
|
616
|
+
table; adapters whose catalog names types more exactly override it.
|
|
617
|
+
"""
|
|
618
|
+
description, _rows = self._column_type_query(sq_name, f"select * from {sq_name} where 1=0;")
|
|
619
|
+
return {d[0].lower(): self._described_kind(d) for d in description}
|
|
620
|
+
|
|
621
|
+
def _described_kind(self, description: Any) -> type[DataType] | None:
|
|
622
|
+
"""The execsql type for one column of a cursor description (a DB-API 7-item sequence)."""
|
|
623
|
+
return kind_for_python_type(description[1], description[5])
|
|
624
|
+
|
|
625
|
+
def _column_type_query(self, sq_name: str, sql: str, params: list | None = None) -> tuple[Any, list]:
|
|
626
|
+
"""Run *sql* for :meth:`column_kinds` of *sq_name*; return the cursor description and the rows."""
|
|
627
|
+
with self._cursor() as curs:
|
|
628
|
+
try:
|
|
629
|
+
if params is None:
|
|
630
|
+
curs.execute(sql)
|
|
631
|
+
else:
|
|
632
|
+
curs.execute(sql, params)
|
|
633
|
+
except ErrInfo:
|
|
634
|
+
raise
|
|
635
|
+
except Exception as e:
|
|
636
|
+
self.rollback()
|
|
637
|
+
raise ErrInfo(
|
|
638
|
+
type="db",
|
|
639
|
+
command_text=sql,
|
|
640
|
+
exception_msg=exception_desc(),
|
|
641
|
+
other_msg=f"Failed to get the column types of table {sq_name} of {self.name()}",
|
|
642
|
+
) from e
|
|
643
|
+
return curs.description, curs.fetchall()
|
|
644
|
+
|
|
607
645
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
608
646
|
"""Return ``True`` if *view_name* (optionally in *schema_name*) exists."""
|
|
609
647
|
with self._cursor() as curs:
|
|
@@ -689,11 +727,17 @@ class Database(ABC):
|
|
|
689
727
|
rowsource: Any,
|
|
690
728
|
column_list: list[str],
|
|
691
729
|
tablespec_src: Callable,
|
|
730
|
+
*,
|
|
731
|
+
existing_table: bool = False,
|
|
692
732
|
) -> None:
|
|
693
733
|
"""Bulk-insert rows from *rowsource* into *table_name* using the columns in *column_list*.
|
|
694
734
|
|
|
695
735
|
*rowsource* must be a generator yielding lists of values in column order.
|
|
696
736
|
*tablespec_src* is a zero-argument callable that returns the table's type specification.
|
|
737
|
+
Each value is converted to the type scanned for its column; with
|
|
738
|
+
*existing_table* (the table was there before the IMPORT or COPY), the
|
|
739
|
+
type of the column it goes into decides instead (see
|
|
740
|
+
``db.target_types.existing_table_converter``).
|
|
697
741
|
"""
|
|
698
742
|
# The rowsource argument must be a generator yielding a list of values for the columns of the table.
|
|
699
743
|
# The column_list argument must an iterable containing column names. This may be a subset of
|
|
@@ -713,8 +757,13 @@ class Database(ABC):
|
|
|
713
757
|
sel_cols = [col for col in ts_colnames if col in column_list]
|
|
714
758
|
incl_col = [col in column_list for col in ts_colnames]
|
|
715
759
|
# Type conversion functions for the rowsource.
|
|
716
|
-
|
|
717
|
-
|
|
760
|
+
converters = [self._scanned_type_converter(col.column_type()[1]) for col in tablespec.cols]
|
|
761
|
+
if existing_table:
|
|
762
|
+
kinds = self.column_kinds(sq_name)
|
|
763
|
+
converters = [
|
|
764
|
+
existing_table_converter(kinds.get(col.name.lower()), col.column_type()[1], convert, col.name, sq_name)
|
|
765
|
+
for col, convert in zip(tablespec.cols, converters)
|
|
766
|
+
]
|
|
718
767
|
# Construct INSERT statement.
|
|
719
768
|
columns = [self.type.quoted(col) for col in sel_cols]
|
|
720
769
|
colspec = ",".join(columns)
|
|
@@ -812,10 +861,7 @@ class Database(ABC):
|
|
|
812
861
|
line[i] = None
|
|
813
862
|
# Trailing values left off the line are NULL.
|
|
814
863
|
line = list(line) + [None] * (len(ts_colnames) - len(line))
|
|
815
|
-
lt = [
|
|
816
|
-
type_objs[i].from_data(val) if val is not None else None for i, val in enumerate(line)
|
|
817
|
-
]
|
|
818
|
-
lt = [type_mod_fn[i](v) if type_mod_fn[i] else v for i, v in enumerate(lt)]
|
|
864
|
+
lt = [converters[i](val) for i, val in enumerate(line)]
|
|
819
865
|
row = []
|
|
820
866
|
for i, v in enumerate(lt):
|
|
821
867
|
if incl_col[i]:
|
|
@@ -869,14 +915,31 @@ class Database(ABC):
|
|
|
869
915
|
f"IMPORT into {sq_name} complete: {total_rows} rows imported.",
|
|
870
916
|
)
|
|
871
917
|
|
|
918
|
+
def _scanned_type_converter(self, data_type: type[DataType]) -> Callable[[Any], Any]:
|
|
919
|
+
"""Convert a value to *data_type*, then apply this DBMS's adjustment for that type, if any.
|
|
920
|
+
|
|
921
|
+
NULL is not converted, but it is adjusted: MySQL's adjustment for
|
|
922
|
+
dates turns it into ``""``.
|
|
923
|
+
"""
|
|
924
|
+
from_data = data_type().from_data
|
|
925
|
+
adjust = self.type.dialect[data_type][3]
|
|
926
|
+
if adjust is None:
|
|
927
|
+
return lambda value: None if value is None else from_data(value)
|
|
928
|
+
return lambda value: adjust(None if value is None else from_data(value))
|
|
929
|
+
|
|
872
930
|
def import_tabular_file(
|
|
873
931
|
self,
|
|
874
932
|
schema_name: str | None,
|
|
875
933
|
table_name: str,
|
|
876
934
|
csv_file_obj: Any,
|
|
877
935
|
skipheader: bool,
|
|
936
|
+
*,
|
|
937
|
+
existing_table: bool = False,
|
|
878
938
|
) -> None:
|
|
879
|
-
"""Import a CSV/tabular file into *table_name*; column names must be compatible.
|
|
939
|
+
"""Import a CSV/tabular file into *table_name*; column names must be compatible.
|
|
940
|
+
|
|
941
|
+
*existing_table*: the table was there before the IMPORT (see :meth:`populate_table`).
|
|
942
|
+
"""
|
|
880
943
|
# Import a text (CSV) file containing tabular data to a table. Columns must be compatible.
|
|
881
944
|
if not self.table_exists(table_name, schema_name):
|
|
882
945
|
raise ErrInfo(
|
|
@@ -912,7 +975,7 @@ class Database(ABC):
|
|
|
912
975
|
|
|
913
976
|
f = csv_file_obj.reader()
|
|
914
977
|
next(f)
|
|
915
|
-
self.populate_table(schema_name, table_name, f, import_cols, get_ts)
|
|
978
|
+
self.populate_table(schema_name, table_name, f, import_cols, get_ts, existing_table=existing_table)
|
|
916
979
|
|
|
917
980
|
def import_entire_file(
|
|
918
981
|
self,
|
execsql/db/duckdb.py
CHANGED
|
@@ -18,6 +18,8 @@ from pathlib import Path
|
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
20
20
|
from execsql.db.base import Database, statement_start
|
|
21
|
+
from execsql.db.target_types import kind_for_type_name
|
|
22
|
+
from execsql.types import DataType
|
|
21
23
|
from execsql.exceptions import ErrInfo
|
|
22
24
|
from execsql.utils.errors import exception_desc, fatal_error, install_hint
|
|
23
25
|
import execsql.state as _state
|
|
@@ -320,6 +322,10 @@ class DuckDBDatabase(Database):
|
|
|
320
322
|
|
|
321
323
|
return insert
|
|
322
324
|
|
|
325
|
+
def _described_kind(self, description: Any) -> type[DataType] | None:
|
|
326
|
+
"""DuckDB describes a column by its type's name (``DECIMAL(18,3)``, ``TIMESTAMP WITH TIME ZONE``)."""
|
|
327
|
+
return kind_for_type_name(str(description[1]))
|
|
328
|
+
|
|
323
329
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
324
330
|
"""Return True if the named view exists in the DuckDB database."""
|
|
325
331
|
# DuckDB information_schema has no 'views' table; views are listed in 'tables'
|
execsql/db/mysql.py
CHANGED
|
@@ -13,6 +13,7 @@ import tempfile
|
|
|
13
13
|
from typing import Any, cast
|
|
14
14
|
|
|
15
15
|
from execsql.db.base import Database
|
|
16
|
+
from execsql.db.target_types import ColumnKinds, kind_for_type_name
|
|
16
17
|
from execsql.exceptions import ErrInfo
|
|
17
18
|
from execsql.utils.errors import exception_desc, fatal_error, install_hint
|
|
18
19
|
from execsql.utils.auth import (
|
|
@@ -41,6 +42,11 @@ _PYTHON_TO_MYSQL_CHARSET: dict[str, str] = {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
def _text(value: Any) -> str:
|
|
46
|
+
"""A catalog value as text: some MySQL versions return ``SHOW COLUMNS`` types as bytes."""
|
|
47
|
+
return value.decode() if isinstance(value, (bytes, bytearray)) else str(value)
|
|
48
|
+
|
|
49
|
+
|
|
44
50
|
def _crlf_terminated(filename: str) -> bool:
|
|
45
51
|
"""True if the file's first line ends with CRLF.
|
|
46
52
|
|
|
@@ -239,6 +245,11 @@ class MySQLDatabase(Database):
|
|
|
239
245
|
self._fold_identifier(schema_name) if schema_name is not None else None,
|
|
240
246
|
)
|
|
241
247
|
|
|
248
|
+
def column_kinds(self, sq_name: str) -> ColumnKinds:
|
|
249
|
+
"""The execsql type of each column of *sq_name*, from ``SHOW COLUMNS`` (which tells BOOLEAN from TINYINT)."""
|
|
250
|
+
_description, rows = self._column_type_query(sq_name, f"show columns from {sq_name};")
|
|
251
|
+
return {str(row[0]).lower(): kind_for_type_name(_text(row[1])) for row in rows}
|
|
252
|
+
|
|
242
253
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
243
254
|
return super().view_exists(
|
|
244
255
|
self._fold_identifier(view_name),
|
|
@@ -305,7 +316,11 @@ class MySQLDatabase(Database):
|
|
|
305
316
|
)
|
|
306
317
|
self.conn = db_conn()
|
|
307
318
|
remember_password("MySQL", self.db_name, self.user, self.server_name, port=self.port)
|
|
308
|
-
|
|
319
|
+
# A fixed mode, not the server's: quote_literal relies on
|
|
320
|
+
# backslash escapes being on (no NO_BACKSLASH_ESCAPES).
|
|
321
|
+
# STRICT_TRANS_TABLES makes a value MySQL can't store as
|
|
322
|
+
# given an error instead of a clamped or truncated one (#101).
|
|
323
|
+
self.execute("set session sql_mode='ANSI,STRICT_TRANS_TABLES';")
|
|
309
324
|
except SystemExit:
|
|
310
325
|
# If the user canceled the password prompt.
|
|
311
326
|
raise
|
|
@@ -343,14 +358,50 @@ class MySQLDatabase(Database):
|
|
|
343
358
|
rows = curs.fetchall()
|
|
344
359
|
return len(rows) > 0
|
|
345
360
|
|
|
361
|
+
def _fail_on_load_warnings(self, file_name: str, sq_name: str) -> None:
|
|
362
|
+
"""Roll back and raise if the LOAD DATA just run changed any value while the session is in strict mode.
|
|
363
|
+
|
|
364
|
+
``LOAD DATA LOCAL`` ignores strict mode: a value MySQL cannot store as
|
|
365
|
+
given is clamped or zeroed with a warning, not rejected (#101). This
|
|
366
|
+
makes the load fail as the same rows inserted one by one would. Notes
|
|
367
|
+
(a decimal rounded to the column's scale) are not warnings and pass, as
|
|
368
|
+
they do for an ``INSERT``. A script that turned strict mode off
|
|
369
|
+
(``SET SESSION sql_mode = 'ANSI'``) keeps MySQL's changed values.
|
|
370
|
+
"""
|
|
371
|
+
with self._cursor() as curs:
|
|
372
|
+
curs.execute("show warnings;")
|
|
373
|
+
warnings = curs.fetchall()
|
|
374
|
+
curs.execute("select @@session.sql_mode;")
|
|
375
|
+
mode = _text(curs.fetchone()[0]).upper()
|
|
376
|
+
if "STRICT_TRANS_TABLES" not in mode and "STRICT_ALL_TABLES" not in mode:
|
|
377
|
+
return
|
|
378
|
+
problems = [_text(row[2]) for row in warnings if _text(row[0]) in ("Warning", "Error")]
|
|
379
|
+
if not problems:
|
|
380
|
+
return
|
|
381
|
+
self.rollback()
|
|
382
|
+
shown = "; ".join(problems[:3])
|
|
383
|
+
more = f" (and {len(problems) - 3} more)" if len(problems) > 3 else ""
|
|
384
|
+
raise ErrInfo(
|
|
385
|
+
type="error",
|
|
386
|
+
other_msg=(
|
|
387
|
+
f"Can't import {file_name} into {sq_name}: MySQL would have stored changed values: {shown}{more}. "
|
|
388
|
+
"Nothing was loaded."
|
|
389
|
+
),
|
|
390
|
+
)
|
|
391
|
+
|
|
346
392
|
def import_tabular_file(
|
|
347
393
|
self,
|
|
348
394
|
schema_name: str | None,
|
|
349
395
|
table_name: str,
|
|
350
396
|
csv_file_obj: Any,
|
|
351
397
|
skipheader: bool,
|
|
398
|
+
*,
|
|
399
|
+
existing_table: bool = False,
|
|
352
400
|
) -> None:
|
|
353
|
-
"""Import a delimited file into a MySQL table.
|
|
401
|
+
"""Import a delimited file into a MySQL table.
|
|
402
|
+
|
|
403
|
+
The file's values go to the server as text, unconverted, so *existing_table* changes nothing here.
|
|
404
|
+
"""
|
|
354
405
|
# Import a file to a table. Columns must be compatible.
|
|
355
406
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
356
407
|
if not self.table_exists(table_name, schema_name):
|
|
@@ -436,6 +487,7 @@ class MySQLDatabase(Database):
|
|
|
436
487
|
)
|
|
437
488
|
try:
|
|
438
489
|
self.execute(import_sql)
|
|
490
|
+
self._fail_on_load_warnings(csv_file_obj.csvfname, sq_name)
|
|
439
491
|
finally:
|
|
440
492
|
if load_file != csv_file_obj.csvfname:
|
|
441
493
|
os.unlink(load_file)
|
execsql/db/oracle.py
CHANGED
|
@@ -12,6 +12,8 @@ import re
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
14
|
from execsql.db.base import Database
|
|
15
|
+
from execsql.db.target_types import kind_for_type_name
|
|
16
|
+
from execsql.types import DataType
|
|
15
17
|
from execsql.db.tiers import SupportTier
|
|
16
18
|
from execsql.exceptions import ErrInfo
|
|
17
19
|
from execsql.utils.errors import exception_desc, fatal_error, install_hint
|
|
@@ -49,6 +51,29 @@ def _oracle_driver() -> Any:
|
|
|
49
51
|
return None
|
|
50
52
|
|
|
51
53
|
|
|
54
|
+
# oracledb DbType names -> the type names db.target_types reads. NUMBER is
|
|
55
|
+
# read from its precision and scale; a type not listed has no execsql type.
|
|
56
|
+
_ORACLE_TYPE_NAMES = {
|
|
57
|
+
"DB_TYPE_VARCHAR": "varchar2",
|
|
58
|
+
"DB_TYPE_NVARCHAR": "nvarchar2",
|
|
59
|
+
"DB_TYPE_CHAR": "char",
|
|
60
|
+
"DB_TYPE_NCHAR": "nchar",
|
|
61
|
+
"DB_TYPE_LONG": "clob",
|
|
62
|
+
"DB_TYPE_CLOB": "clob",
|
|
63
|
+
"DB_TYPE_NCLOB": "nclob",
|
|
64
|
+
"DB_TYPE_BINARY_FLOAT": "binary_float",
|
|
65
|
+
"DB_TYPE_BINARY_DOUBLE": "binary_double",
|
|
66
|
+
"DB_TYPE_BOOLEAN": "boolean",
|
|
67
|
+
"DB_TYPE_DATE": "date",
|
|
68
|
+
"DB_TYPE_TIMESTAMP": "timestamp",
|
|
69
|
+
"DB_TYPE_TIMESTAMP_TZ": "timestamp with time zone",
|
|
70
|
+
"DB_TYPE_TIMESTAMP_LTZ": "timestamp with local time zone",
|
|
71
|
+
"DB_TYPE_RAW": "raw",
|
|
72
|
+
"DB_TYPE_LONG_RAW": "long raw",
|
|
73
|
+
"DB_TYPE_BLOB": "blob",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
52
77
|
class OracleDatabase(Database):
|
|
53
78
|
"""Oracle adapter using the python-oracledb driver."""
|
|
54
79
|
|
|
@@ -268,6 +293,17 @@ class OracleDatabase(Database):
|
|
|
268
293
|
rows = curs.fetchall()
|
|
269
294
|
return [row[0] for row in rows]
|
|
270
295
|
|
|
296
|
+
def _described_kind(self, description: Any) -> type[DataType] | None:
|
|
297
|
+
"""oracledb describes a column by a ``DbType`` (``DB_TYPE_NUMBER``), with its precision and scale."""
|
|
298
|
+
name = getattr(description[1], "name", "")
|
|
299
|
+
if name == "DB_TYPE_NUMBER":
|
|
300
|
+
precision, scale = description[4], description[5]
|
|
301
|
+
# NUMBER with no precision, and FLOAT, report scale -127.
|
|
302
|
+
type_name = f"number({precision},{scale})" if precision and scale is not None and scale >= 0 else "number"
|
|
303
|
+
else:
|
|
304
|
+
type_name = _ORACLE_TYPE_NAMES.get(name, "")
|
|
305
|
+
return kind_for_type_name(type_name, oracle=True)
|
|
306
|
+
|
|
271
307
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
272
308
|
"""Return True if the named view exists in the Oracle database."""
|
|
273
309
|
# Oracle folds unquoted identifiers to uppercase.
|
execsql/db/postgres.py
CHANGED
|
@@ -14,6 +14,7 @@ import re
|
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
16
|
from execsql.db.base import Database
|
|
17
|
+
from execsql.db.target_types import ColumnKinds, kind_for_type_name
|
|
17
18
|
from execsql.exceptions import ErrInfo
|
|
18
19
|
from execsql.utils.errors import exception_desc, fatal_error, install_hint
|
|
19
20
|
from execsql.utils.auth import (
|
|
@@ -224,6 +225,16 @@ class PostgresDatabase(Database):
|
|
|
224
225
|
rows = curs.fetchall()
|
|
225
226
|
return len(rows) > 0
|
|
226
227
|
|
|
228
|
+
def column_kinds(self, sq_name: str) -> ColumnKinds:
|
|
229
|
+
"""The execsql type of each column of *sq_name*, from the catalog (``regclass`` follows the search path)."""
|
|
230
|
+
_description, rows = self._column_type_query(
|
|
231
|
+
sq_name,
|
|
232
|
+
"select attname, format_type(atttypid, atttypmod) from pg_attribute "
|
|
233
|
+
"where attrelid = %s::regclass and attnum > 0 and not attisdropped order by attnum;",
|
|
234
|
+
[sq_name],
|
|
235
|
+
)
|
|
236
|
+
return {name.lower(): kind_for_type_name(type_name) for name, type_name in rows}
|
|
237
|
+
|
|
227
238
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
228
239
|
"""Return True if the named view exists in the PostgreSQL database."""
|
|
229
240
|
if schema_name is not None:
|
|
@@ -279,8 +290,13 @@ class PostgresDatabase(Database):
|
|
|
279
290
|
table_name: str,
|
|
280
291
|
csv_file_obj: Any,
|
|
281
292
|
skipheader: bool,
|
|
293
|
+
*,
|
|
294
|
+
existing_table: bool = False,
|
|
282
295
|
) -> None:
|
|
283
|
-
"""Import a delimited file into a PostgreSQL table, using COPY when possible.
|
|
296
|
+
"""Import a delimited file into a PostgreSQL table, using COPY when possible.
|
|
297
|
+
|
|
298
|
+
The file's values go to the server as text, unconverted, so *existing_table* changes nothing here.
|
|
299
|
+
"""
|
|
284
300
|
# Import a file to a table. Columns must be compatible.
|
|
285
301
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
286
302
|
if not self.table_exists(table_name, schema_name):
|
execsql/db/sqlite.py
CHANGED
|
@@ -182,8 +182,14 @@ class SQLiteDatabase(Database):
|
|
|
182
182
|
rowsource: Any,
|
|
183
183
|
column_list: list[str],
|
|
184
184
|
tablespec_src: Any,
|
|
185
|
+
*,
|
|
186
|
+
existing_table: bool = False,
|
|
185
187
|
) -> None:
|
|
186
|
-
"""Populate a SQLite table from a row source generator.
|
|
188
|
+
"""Populate a SQLite table from a row source generator.
|
|
189
|
+
|
|
190
|
+
Values are inserted as the source gives them (dates, times and
|
|
191
|
+
decimals as text), so *existing_table* changes nothing here.
|
|
192
|
+
"""
|
|
187
193
|
# The rowsource argument must be a generator yielding a list of values for the columns of the table.
|
|
188
194
|
# The column_list argument must an iterable containing column names in the same order as produced by the rowsource.
|
|
189
195
|
sq_name = self.schema_qualified_table_name(None, table_name)
|
execsql/db/sqlserver.py
CHANGED
|
@@ -8,6 +8,7 @@ Server via ``pyodbc``. Corresponds to ``-t s`` on the CLI.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from execsql.db.base import Database
|
|
11
|
+
from execsql.db.target_types import ColumnKinds, kind_for_type_name
|
|
11
12
|
from execsql.db.odbc import connection_string, fresh_sessions, loggable
|
|
12
13
|
from execsql.exceptions import ErrInfo
|
|
13
14
|
from execsql.utils.errors import fatal_error, install_hint
|
|
@@ -228,6 +229,29 @@ class SqlServerDatabase(Database):
|
|
|
228
229
|
rows = curs.fetchall()
|
|
229
230
|
return len(rows) > 0
|
|
230
231
|
|
|
232
|
+
def column_kinds(self, sq_name: str) -> ColumnKinds:
|
|
233
|
+
"""The execsql type of each column of *sq_name*, from ``sys.columns``.
|
|
234
|
+
|
|
235
|
+
pyodbc describes ``datetimeoffset`` and ``varbinary`` alike, so the
|
|
236
|
+
catalog is read instead. A table it does not find through
|
|
237
|
+
``object_id`` (a ``#temp`` table, which lives in tempdb) falls back
|
|
238
|
+
to the driver's description.
|
|
239
|
+
"""
|
|
240
|
+
_description, rows = self._column_type_query(
|
|
241
|
+
sq_name,
|
|
242
|
+
"select c.name, type_name(c.system_type_id), c.precision, c.scale from sys.columns c "
|
|
243
|
+
"where c.object_id = object_id(?) order by c.column_id;",
|
|
244
|
+
[sq_name],
|
|
245
|
+
)
|
|
246
|
+
if not rows:
|
|
247
|
+
return super().column_kinds(sq_name)
|
|
248
|
+
return {
|
|
249
|
+
name.lower(): kind_for_type_name(
|
|
250
|
+
f"{type_name}({precision},{scale})" if type_name in ("decimal", "numeric") else type_name,
|
|
251
|
+
)
|
|
252
|
+
for name, type_name, precision, scale in rows
|
|
253
|
+
}
|
|
254
|
+
|
|
231
255
|
def drop_table(self, tablename: str) -> None:
|
|
232
256
|
"""Drop *tablename*, schema-qualified and quoted by the caller."""
|
|
233
257
|
# SQL Server and Firebird will throw an error if there are foreign keys to the table.
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""The execsql data type of each column of a table that IMPORT or COPY loads.
|
|
2
|
+
|
|
3
|
+
A load into a table that already exists converts each value by the type of
|
|
4
|
+
the column it goes into, not by the type scanned from the source data
|
|
5
|
+
(issue #100). The adapters report each column's type: by the name their
|
|
6
|
+
catalog gives it (PostgreSQL, MySQL, SQL Server, DuckDB, Oracle), or by the
|
|
7
|
+
Python type their driver describes it with (Access, Firebird, ODBC DSNs).
|
|
8
|
+
The functions here turn that
|
|
9
|
+
into one of execsql's data types, or ``None`` when execsql has no
|
|
10
|
+
conversion that fits (arrays, JSON, UUIDs, enums, geometry, intervals, …).
|
|
11
|
+
|
|
12
|
+
Only the types that change how a value is converted are told apart: every
|
|
13
|
+
character type is :class:`DT_Text`, every integer type :class:`DT_Long`,
|
|
14
|
+
and so on. A ``DECIMAL``/``NUMERIC``/``NUMBER`` with scale 0 holds whole
|
|
15
|
+
numbers only, so it is :class:`DT_Long` too.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import datetime
|
|
21
|
+
import math
|
|
22
|
+
import re
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from decimal import Decimal
|
|
25
|
+
from typing import Any, TypeAlias
|
|
26
|
+
|
|
27
|
+
from execsql.exceptions import ErrInfo
|
|
28
|
+
from execsql.types import (
|
|
29
|
+
DataType,
|
|
30
|
+
DT_Binary,
|
|
31
|
+
DT_Boolean,
|
|
32
|
+
DT_Date,
|
|
33
|
+
DT_Decimal,
|
|
34
|
+
DT_Float,
|
|
35
|
+
DT_Long,
|
|
36
|
+
DT_Text,
|
|
37
|
+
DT_Time,
|
|
38
|
+
DT_Timestamp,
|
|
39
|
+
DT_TimestampTZ,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = ["ColumnKinds", "existing_table_converter", "kind_for_python_type", "kind_for_type_name"]
|
|
43
|
+
|
|
44
|
+
#: Lower-cased column name -> the execsql type values for it convert to, or None.
|
|
45
|
+
#: A string, not ``dict[...]``: on Python 3.10 that object passes
|
|
46
|
+
#: ``isinstance(x, type)``, so code that scans the adapter modules for classes
|
|
47
|
+
#: would take it for one.
|
|
48
|
+
ColumnKinds: TypeAlias = "dict[str, type[DataType] | None]"
|
|
49
|
+
|
|
50
|
+
_TEXT = {
|
|
51
|
+
"char",
|
|
52
|
+
"character",
|
|
53
|
+
"varchar",
|
|
54
|
+
"character varying",
|
|
55
|
+
"varchar2",
|
|
56
|
+
"nchar",
|
|
57
|
+
"national character",
|
|
58
|
+
"nvarchar",
|
|
59
|
+
"nvarchar2",
|
|
60
|
+
"national character varying",
|
|
61
|
+
"bpchar",
|
|
62
|
+
"text",
|
|
63
|
+
"ntext",
|
|
64
|
+
"tinytext",
|
|
65
|
+
"mediumtext",
|
|
66
|
+
"longtext",
|
|
67
|
+
"string",
|
|
68
|
+
"clob",
|
|
69
|
+
"nclob",
|
|
70
|
+
}
|
|
71
|
+
_INTEGER = {
|
|
72
|
+
"integer",
|
|
73
|
+
"int",
|
|
74
|
+
"int1",
|
|
75
|
+
"int2",
|
|
76
|
+
"int4",
|
|
77
|
+
"int8",
|
|
78
|
+
"tinyint",
|
|
79
|
+
"smallint",
|
|
80
|
+
"mediumint",
|
|
81
|
+
"bigint",
|
|
82
|
+
"hugeint",
|
|
83
|
+
"utinyint",
|
|
84
|
+
"usmallint",
|
|
85
|
+
"uinteger",
|
|
86
|
+
"ubigint",
|
|
87
|
+
"uhugeint",
|
|
88
|
+
"serial",
|
|
89
|
+
"smallserial",
|
|
90
|
+
"bigserial",
|
|
91
|
+
}
|
|
92
|
+
_DECIMAL = {"decimal", "numeric", "number", "money", "smallmoney"}
|
|
93
|
+
_FLOAT = {"float", "float4", "float8", "real", "double", "double precision", "binary_float", "binary_double"}
|
|
94
|
+
_BOOLEAN = {"boolean", "bool"}
|
|
95
|
+
_TIMESTAMP = {
|
|
96
|
+
"timestamp",
|
|
97
|
+
"timestamp without time zone",
|
|
98
|
+
"datetime",
|
|
99
|
+
"datetime2",
|
|
100
|
+
"smalldatetime",
|
|
101
|
+
"timestamp_s",
|
|
102
|
+
"timestamp_ms",
|
|
103
|
+
"timestamp_ns",
|
|
104
|
+
}
|
|
105
|
+
_TIMESTAMPTZ = {"timestamptz", "timestamp with time zone", "timestamp with local time zone", "datetimeoffset"}
|
|
106
|
+
_TIME = {"time", "time without time zone"}
|
|
107
|
+
_BINARY = {
|
|
108
|
+
"bytea",
|
|
109
|
+
"blob",
|
|
110
|
+
"tinyblob",
|
|
111
|
+
"mediumblob",
|
|
112
|
+
"longblob",
|
|
113
|
+
"binary",
|
|
114
|
+
"varbinary",
|
|
115
|
+
"binary varying",
|
|
116
|
+
"image",
|
|
117
|
+
"raw",
|
|
118
|
+
"long raw",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_PARAMS = re.compile(r"\(([^)]*)\)")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def kind_for_type_name(type_name: str, *, oracle: bool = False) -> type[DataType] | None:
|
|
125
|
+
"""The execsql type for a column declared as *type_name* (as the DBMS's catalog spells it).
|
|
126
|
+
|
|
127
|
+
*oracle*: Oracle's ``DATE`` holds a time of day too, so it is a timestamp there.
|
|
128
|
+
"""
|
|
129
|
+
name = type_name.strip().lower()
|
|
130
|
+
if not name or "[" in name or name.endswith(" array"):
|
|
131
|
+
return None
|
|
132
|
+
params = _PARAMS.search(name)
|
|
133
|
+
args = [a.strip() for a in params.group(1).split(",")] if params else []
|
|
134
|
+
base = " ".join(_PARAMS.sub(" ", name).replace(" unsigned", " ").replace(" zerofill", " ").split())
|
|
135
|
+
if base == "tinyint" and args == ["1"]:
|
|
136
|
+
return DT_Boolean # MySQL's BOOLEAN
|
|
137
|
+
if base == "bit":
|
|
138
|
+
# SQL Server's BIT; a wider BIT(n) is a bit string.
|
|
139
|
+
return DT_Boolean if args in ([], ["1"]) else None
|
|
140
|
+
if base in _TEXT:
|
|
141
|
+
return DT_Text
|
|
142
|
+
if base in _INTEGER:
|
|
143
|
+
return DT_Long
|
|
144
|
+
if base in _DECIMAL:
|
|
145
|
+
if args[1:] == ["0"] or len(args) == 1:
|
|
146
|
+
return DT_Long # DECIMAL(p,0), NUMERIC(p), NUMBER(p): whole numbers
|
|
147
|
+
return DT_Decimal
|
|
148
|
+
if base in _FLOAT:
|
|
149
|
+
return DT_Float
|
|
150
|
+
if base in _BOOLEAN:
|
|
151
|
+
return DT_Boolean
|
|
152
|
+
if base == "date":
|
|
153
|
+
return DT_Timestamp if oracle else DT_Date
|
|
154
|
+
if base in _TIMESTAMP:
|
|
155
|
+
return DT_Timestamp
|
|
156
|
+
if base in _TIMESTAMPTZ:
|
|
157
|
+
return DT_TimestampTZ
|
|
158
|
+
if base in _TIME:
|
|
159
|
+
return DT_Time
|
|
160
|
+
if base in _BINARY:
|
|
161
|
+
return DT_Binary
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def kind_for_python_type(type_code: object, scale: object = None) -> type[DataType] | None:
|
|
166
|
+
"""The execsql type for a column a driver describes by the Python type it returns (pyodbc, firebird-driver).
|
|
167
|
+
|
|
168
|
+
Binary columns are left out: pyodbc describes a SQL Server
|
|
169
|
+
``datetimeoffset`` as ``bytearray`` too.
|
|
170
|
+
"""
|
|
171
|
+
if type_code is bool:
|
|
172
|
+
return DT_Boolean
|
|
173
|
+
if type_code is int:
|
|
174
|
+
return DT_Long
|
|
175
|
+
if type_code is float:
|
|
176
|
+
return DT_Float
|
|
177
|
+
if type_code is Decimal:
|
|
178
|
+
return DT_Long if scale == 0 else DT_Decimal
|
|
179
|
+
if type_code is str:
|
|
180
|
+
return DT_Text
|
|
181
|
+
if type_code is datetime.datetime:
|
|
182
|
+
return DT_Timestamp
|
|
183
|
+
if type_code is datetime.date:
|
|
184
|
+
return DT_Date
|
|
185
|
+
if type_code is datetime.time:
|
|
186
|
+
return DT_Time
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# Converting a value for the column it goes into
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
_INTEGER_TEXT = re.compile(r"^\s*[+-]?[0-9]+\s*$")
|
|
195
|
+
_NUMBER_TEXT = re.compile(r"^\s*[+-]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][+-]?[0-9]+)?\s*$")
|
|
196
|
+
# A whole number longer than this fits no integer column; it goes to the
|
|
197
|
+
# database as given, which rejects it, instead of being expanded here.
|
|
198
|
+
_MAX_INTEGER_DIGITS = 40
|
|
199
|
+
|
|
200
|
+
Converter = Callable[[Any], Any]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _as_given(value: Any) -> Any:
|
|
204
|
+
return value
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _whole_number_converter(column: str, table: str, default: Converter) -> Converter:
|
|
208
|
+
"""Convert a value for integer column *column*: a fraction is an error, never rounded.
|
|
209
|
+
|
|
210
|
+
A value that is not a number (NULL, a date, a word) gets *default*, the
|
|
211
|
+
conversion it had before, and the database takes or rejects that.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
def convert(value: Any) -> Any:
|
|
215
|
+
if isinstance(value, bool):
|
|
216
|
+
return default(value)
|
|
217
|
+
if isinstance(value, int):
|
|
218
|
+
return value
|
|
219
|
+
if isinstance(value, str):
|
|
220
|
+
if _INTEGER_TEXT.match(value):
|
|
221
|
+
return int(value)
|
|
222
|
+
if not _NUMBER_TEXT.match(value):
|
|
223
|
+
return default(value)
|
|
224
|
+
number = Decimal(value)
|
|
225
|
+
elif isinstance(value, Decimal):
|
|
226
|
+
number = value
|
|
227
|
+
elif isinstance(value, float):
|
|
228
|
+
if not math.isfinite(value):
|
|
229
|
+
return default(value)
|
|
230
|
+
number = Decimal(value)
|
|
231
|
+
else:
|
|
232
|
+
return default(value)
|
|
233
|
+
if not number.is_finite() or number.adjusted() >= _MAX_INTEGER_DIGITS:
|
|
234
|
+
return default(value)
|
|
235
|
+
if number == number.to_integral_value():
|
|
236
|
+
return int(number)
|
|
237
|
+
raise ErrInfo(
|
|
238
|
+
type="error",
|
|
239
|
+
other_msg=(
|
|
240
|
+
f"Can't load {value!r} into column {column} of {table}: the column holds whole numbers, "
|
|
241
|
+
"and the database would round the value."
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return convert
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _true_false_as_number(default: Converter) -> Converter:
|
|
249
|
+
"""Convert a value scanned as true/false for a number column: 1 or 0, read as before (``yes`` is 1)."""
|
|
250
|
+
|
|
251
|
+
def convert(value: Any) -> Any:
|
|
252
|
+
flag = default(value)
|
|
253
|
+
return int(flag) if isinstance(flag, bool) else flag
|
|
254
|
+
|
|
255
|
+
return convert
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def existing_table_converter(
|
|
259
|
+
kind: type[DataType] | None,
|
|
260
|
+
scanned: type[DataType],
|
|
261
|
+
default: Converter,
|
|
262
|
+
column: str,
|
|
263
|
+
table: str,
|
|
264
|
+
) -> Converter:
|
|
265
|
+
"""How a value for *column* of existing table *table* is converted.
|
|
266
|
+
|
|
267
|
+
*kind* is the column's type (``None`` if execsql has none that fits),
|
|
268
|
+
*scanned* the type the source column scanned as, and *default* the
|
|
269
|
+
conversion to that scanned type, which a new table's values get.
|
|
270
|
+
"""
|
|
271
|
+
if kind is DT_Text:
|
|
272
|
+
return _as_given
|
|
273
|
+
if kind in (DT_Long, DT_Decimal, DT_Float) and scanned is DT_Boolean:
|
|
274
|
+
return _true_false_as_number(default)
|
|
275
|
+
if kind is DT_Long:
|
|
276
|
+
return _whole_number_converter(column, table, default)
|
|
277
|
+
return default
|
execsql/exporters/json.py
CHANGED
|
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
"""
|
|
4
4
|
JSON export for execsql.
|
|
5
5
|
|
|
6
|
-
Provides :func:`write_query_to_json` (standard JSON array of objects)
|
|
7
|
-
:func:`
|
|
8
|
-
|
|
6
|
+
Provides :func:`write_query_to_json` (standard JSON array of objects),
|
|
7
|
+
:func:`write_query_to_jsonl` (`JSON Lines <https://jsonlines.org/>`_, one
|
|
8
|
+
object per line) and :func:`write_query_to_json_ts` (JSON with a top-level
|
|
9
|
+
timestamp wrapper), all of which serialize a query result set to a file or
|
|
10
|
+
stream.
|
|
9
11
|
"""
|
|
10
12
|
|
|
11
13
|
import json
|
|
@@ -18,7 +20,13 @@ from execsql.models import DataTable, to_json_type
|
|
|
18
20
|
from execsql.utils.errors import exception_desc
|
|
19
21
|
from execsql.utils.fileio import filewriter_close
|
|
20
22
|
|
|
21
|
-
__all__ = ["write_query_to_json", "write_query_to_json_ts"]
|
|
23
|
+
__all__ = ["write_query_to_json", "write_query_to_json_ts", "write_query_to_jsonl"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _row_json(hdrs: list[str], row: Any) -> str:
|
|
27
|
+
"""One row as a compact JSON object; JSON and JSONL write rows alike."""
|
|
28
|
+
dictdata = dict(zip(hdrs, [str(v) if isinstance(v, str) else v for v in row]))
|
|
29
|
+
return json.dumps(dictdata, separators=(",", ":"), default=str)
|
|
22
30
|
|
|
23
31
|
|
|
24
32
|
def write_query_to_json(
|
|
@@ -60,14 +68,50 @@ def write_query_to_json(
|
|
|
60
68
|
else:
|
|
61
69
|
f.write(",\n")
|
|
62
70
|
first = False
|
|
63
|
-
|
|
64
|
-
jsondata = json.dumps(dictdata, separators=(",", ":"), default=str)
|
|
65
|
-
f.write(str(jsondata))
|
|
71
|
+
f.write(_row_json(uhdrs, row))
|
|
66
72
|
f.write("\n]\n")
|
|
67
73
|
finally:
|
|
68
74
|
f.close()
|
|
69
75
|
|
|
70
76
|
|
|
77
|
+
def write_query_to_jsonl(
|
|
78
|
+
select_stmt: str,
|
|
79
|
+
db: Any,
|
|
80
|
+
outfile: str,
|
|
81
|
+
append: bool = False,
|
|
82
|
+
desc: str | None = None,
|
|
83
|
+
zipfile: str | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Execute a SELECT and write the result set as JSON Lines: one JSON object per row, one row per line.
|
|
86
|
+
|
|
87
|
+
Values are written as :func:`write_query_to_json` writes them. A line
|
|
88
|
+
break inside a value is escaped (``\\n``), so each row stays on one line.
|
|
89
|
+
With *append* the rows are added to the end of the file, which stays
|
|
90
|
+
valid JSON Lines.
|
|
91
|
+
"""
|
|
92
|
+
conf = _state.conf
|
|
93
|
+
try:
|
|
94
|
+
hdrs, rows = db.select_rowsource(select_stmt)
|
|
95
|
+
except ErrInfo:
|
|
96
|
+
raise
|
|
97
|
+
except Exception as e:
|
|
98
|
+
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
99
|
+
f: Any
|
|
100
|
+
if zipfile is None:
|
|
101
|
+
filewriter_close(outfile)
|
|
102
|
+
from execsql.utils.fileio import EncodedFile
|
|
103
|
+
|
|
104
|
+
f = EncodedFile(outfile, conf.output_encoding).open("at" if append else "wt")
|
|
105
|
+
else:
|
|
106
|
+
f = ZipWriter(zipfile, outfile, append)
|
|
107
|
+
try:
|
|
108
|
+
uhdrs = [str(h) for h in hdrs]
|
|
109
|
+
for row in rows:
|
|
110
|
+
f.write(_row_json(uhdrs, row) + "\n")
|
|
111
|
+
finally:
|
|
112
|
+
f.close()
|
|
113
|
+
|
|
114
|
+
|
|
71
115
|
def write_query_to_json_ts(
|
|
72
116
|
select_stmt: str,
|
|
73
117
|
db: Any,
|
execsql/exporters/protocol.py
CHANGED
execsql/importers/base.py
CHANGED
|
@@ -142,7 +142,7 @@ def import_data_table(
|
|
|
142
142
|
)
|
|
143
143
|
import_cols = hdrs
|
|
144
144
|
try:
|
|
145
|
-
db.populate_table(schemaname, tablename, data, import_cols, get_ts)
|
|
145
|
+
db.populate_table(schemaname, tablename, data, import_cols, get_ts, existing_table=not is_new)
|
|
146
146
|
db.commit()
|
|
147
147
|
except ErrInfo:
|
|
148
148
|
raise
|
execsql/importers/csv.py
CHANGED
|
@@ -91,7 +91,7 @@ def importtable(
|
|
|
91
91
|
other_msg=f"Non-existent table name ({tablename}) used with the IMPORT_FILE metacommand",
|
|
92
92
|
)
|
|
93
93
|
try:
|
|
94
|
-
db.import_tabular_file(schemaname, tablename, inf, skipheader=True)
|
|
94
|
+
db.import_tabular_file(schemaname, tablename, inf, skipheader=True, existing_table=not is_new)
|
|
95
95
|
db.commit()
|
|
96
96
|
except ErrInfo:
|
|
97
97
|
raise
|
execsql/metacommands/__init__.py
CHANGED
|
@@ -474,13 +474,29 @@ JSON_VARIANT_FORMATS = ["JSON_TS", "JSON_TABLESCHEMA"]
|
|
|
474
474
|
QUERY_EXPORT_FORMATS = (
|
|
475
475
|
DELIMITED_FORMATS
|
|
476
476
|
+ TEXT_FORMATS
|
|
477
|
-
+ [
|
|
477
|
+
+ [
|
|
478
|
+
"ODS",
|
|
479
|
+
"XLSX",
|
|
480
|
+
"JSON",
|
|
481
|
+
"JSONL",
|
|
482
|
+
"HTML",
|
|
483
|
+
"CGI-HTML",
|
|
484
|
+
"VALUES",
|
|
485
|
+
"LATEX",
|
|
486
|
+
"RAW",
|
|
487
|
+
"B64",
|
|
488
|
+
"FEATHER",
|
|
489
|
+
"YAML",
|
|
490
|
+
"MARKDOWN",
|
|
491
|
+
"MD",
|
|
492
|
+
]
|
|
478
493
|
)
|
|
479
494
|
TABLE_EXPORT_FORMATS = (
|
|
480
495
|
DELIMITED_FORMATS
|
|
481
496
|
+ TEXT_FORMATS
|
|
482
497
|
+ [
|
|
483
498
|
"JSON",
|
|
499
|
+
"JSONL",
|
|
484
500
|
"XML",
|
|
485
501
|
"VALUES",
|
|
486
502
|
"HTML",
|
|
@@ -27,7 +27,7 @@ from execsql.exporters.delimited import write_delimited_file
|
|
|
27
27
|
from execsql.exporters.duckdb import write_query_to_duckdb
|
|
28
28
|
from execsql.exporters.feather import write_query_to_feather, write_query_to_hdf5
|
|
29
29
|
from execsql.exporters.html import write_query_to_cgi_html, write_query_to_html
|
|
30
|
-
from execsql.exporters.json import write_query_to_json, write_query_to_json_ts
|
|
30
|
+
from execsql.exporters.json import write_query_to_json, write_query_to_json_ts, write_query_to_jsonl
|
|
31
31
|
from execsql.exporters.latex import write_query_to_latex
|
|
32
32
|
from execsql.exporters.ods import write_queries_to_ods, write_query_to_ods
|
|
33
33
|
from execsql.exporters.xlsx import write_queries_to_xlsx, write_query_to_xlsx
|
|
@@ -123,6 +123,8 @@ def _dispatch_format(
|
|
|
123
123
|
write_query_to_xml(select_stmt, xml_table, db, outfile, append, desc=description, zipfile=zipfilename)
|
|
124
124
|
elif filefmt == "json":
|
|
125
125
|
write_query_to_json(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
|
|
126
|
+
elif filefmt == "jsonl":
|
|
127
|
+
write_query_to_jsonl(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
|
|
126
128
|
elif filefmt in ("json_ts", "json_tableschema"):
|
|
127
129
|
write_query_to_json_ts(select_stmt, db, outfile, append, not notype, desc=description, zipfile=zipfilename)
|
|
128
130
|
elif filefmt == "values":
|
|
@@ -158,7 +158,7 @@ def x_copy(**kwargs: Any) -> None:
|
|
|
158
158
|
return get_ts_tablespec
|
|
159
159
|
|
|
160
160
|
try:
|
|
161
|
-
db2.populate_table(schema2, table2, rows_for_insert, hdrs, get_ts)
|
|
161
|
+
db2.populate_table(schema2, table2, rows_for_insert, hdrs, get_ts, existing_table=not new_tbl2)
|
|
162
162
|
db2.commit()
|
|
163
163
|
except BaseException:
|
|
164
164
|
_close_rowsource(rows_to_close)
|
|
@@ -245,7 +245,7 @@ def x_copy_query(**kwargs: Any) -> None:
|
|
|
245
245
|
return get_ts_tablespec
|
|
246
246
|
|
|
247
247
|
try:
|
|
248
|
-
db2.populate_table(schema2, table2, rows_for_insert, hdrs, get_ts)
|
|
248
|
+
db2.populate_table(schema2, table2, rows_for_insert, hdrs, get_ts, existing_table=not new_tbl2)
|
|
249
249
|
db2.commit()
|
|
250
250
|
except BaseException:
|
|
251
251
|
_close_rowsource(rows_to_close)
|
|
@@ -1364,6 +1364,16 @@ _SYSTEM_VARIABLES: tuple[SystemVariable, ...] = (
|
|
|
1364
1364
|
"TRUE or FALSE: whether the changes were committed.",
|
|
1365
1365
|
"metacommands.md#pg_upsert",
|
|
1366
1366
|
),
|
|
1367
|
+
SystemVariable(
|
|
1368
|
+
"$PG_UPSERT_STATUS",
|
|
1369
|
+
"How the run ended: committed, dry_run, no_changes, qa_failed, cancelled or db_error.",
|
|
1370
|
+
"metacommands.md#pg_upsert",
|
|
1371
|
+
),
|
|
1372
|
+
SystemVariable(
|
|
1373
|
+
"$PG_UPSERT_ERROR",
|
|
1374
|
+
"The database error message when $PG_UPSERT_STATUS is db_error; otherwise empty.",
|
|
1375
|
+
"metacommands.md#pg_upsert",
|
|
1376
|
+
),
|
|
1367
1377
|
SystemVariable("$PG_UPSERT_STAGING_SCHEMA", "Staging schema name used.", "metacommands.md#pg_upsert"),
|
|
1368
1378
|
SystemVariable("$PG_UPSERT_BASE_SCHEMA", "Base schema name used.", "metacommands.md#pg_upsert"),
|
|
1369
1379
|
SystemVariable("$PG_UPSERT_TABLES", "Comma-separated list of table names processed.", "metacommands.md#pg_upsert"),
|
execsql/metacommands/upsert.py
CHANGED
|
@@ -185,6 +185,11 @@ def _set_subvars(result: Any) -> None:
|
|
|
185
185
|
sv("$PG_UPSERT_ROWS_UPDATED", str(result.total_updated))
|
|
186
186
|
sv("$PG_UPSERT_ROWS_INSERTED", str(result.total_inserted))
|
|
187
187
|
sv("$PG_UPSERT_COMMITTED", str(result.committed).upper())
|
|
188
|
+
# How the run ended: committed, dry_run, no_changes, qa_failed, cancelled
|
|
189
|
+
# or db_error. Empty for QA and CHECK, which do not run an upsert.
|
|
190
|
+
status = getattr(result, "status", None)
|
|
191
|
+
sv("$PG_UPSERT_STATUS", status.value if status is not None else "")
|
|
192
|
+
sv("$PG_UPSERT_ERROR", getattr(result, "error", None) or "")
|
|
188
193
|
sv("$PG_UPSERT_STAGING_SCHEMA", result.staging_schema)
|
|
189
194
|
sv("$PG_UPSERT_BASE_SCHEMA", result.base_schema)
|
|
190
195
|
sv("$PG_UPSERT_TABLES", ", ".join(t.table_name for t in result.tables))
|
|
@@ -426,6 +431,14 @@ def _handle_pg_upsert_errors(fn: Any, metacommandline: str | None) -> Any:
|
|
|
426
431
|
try:
|
|
427
432
|
return fn()
|
|
428
433
|
except UserCancelledError as exc:
|
|
434
|
+
# No result describes a run cancelled outside pg-upsert's own run();
|
|
435
|
+
# say what is known rather than leave an earlier run's values.
|
|
436
|
+
sv = _state.subvars.add_substitution
|
|
437
|
+
sv("$PG_UPSERT_STATUS", "cancelled")
|
|
438
|
+
sv("$PG_UPSERT_ERROR", "")
|
|
439
|
+
sv("$PG_UPSERT_COMMITTED", "FALSE")
|
|
440
|
+
sv("$PG_UPSERT_ROWS_UPDATED", "0")
|
|
441
|
+
sv("$PG_UPSERT_ROWS_INSERTED", "0")
|
|
429
442
|
raise ErrInfo(
|
|
430
443
|
"cmd",
|
|
431
444
|
command_text=metacommandline,
|
execsql/script/engine.py
CHANGED
|
@@ -108,6 +108,7 @@ class MetaCommand:
|
|
|
108
108
|
"""Match *cmd_str* against this entry's regex and, if it matches, invoke the handler.
|
|
109
109
|
|
|
110
110
|
Returns ``(True, return_value)`` on a match, ``(False, None)`` otherwise.
|
|
111
|
+
Raises :class:`ErrInfo` when the handler fails.
|
|
111
112
|
"""
|
|
112
113
|
# Runs the metacommand if the command string matches the regex.
|
|
113
114
|
m = self.rx.match(cmd_str.strip())
|
|
@@ -125,13 +126,10 @@ class MetaCommand:
|
|
|
125
126
|
er = ErrInfo("cmd", command_text=cmd_str, exception_msg=exception_desc())
|
|
126
127
|
er.__cause__ = exc
|
|
127
128
|
if er:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if self.set_error_flag:
|
|
133
|
-
_state.status.metacommand_error = True
|
|
134
|
-
return True, None
|
|
129
|
+
# The caller records the error ($LAST_ERROR, $ERROR_MESSAGE,
|
|
130
|
+
# the log, the error history) and halts or carries on.
|
|
131
|
+
if self.set_error_flag or _state.status.halt_on_metacommand_err:
|
|
132
|
+
raise er
|
|
135
133
|
else:
|
|
136
134
|
if self.set_error_flag:
|
|
137
135
|
_state.status.metacommand_error = False
|
execsql/script/executor.py
CHANGED
|
@@ -350,7 +350,9 @@ def _exec_metacommand(
|
|
|
350
350
|
if ctx.exec_log is not None:
|
|
351
351
|
ctx.exec_log.log_status_info(f"Metacommand error: {logged_error(e)}")
|
|
352
352
|
if status.halt_on_metacommand_err:
|
|
353
|
-
|
|
353
|
+
exit_now(1, e)
|
|
354
|
+
if (manifest := _manifest.current()) is not None:
|
|
355
|
+
manifest.record_metacommand(cmd, source, line_no, metacommandlist)
|
|
354
356
|
status.error_history.append((source, line_no, cmd, e.errmsg()))
|
|
355
357
|
return None
|
|
356
358
|
# No handler matched — truly unknown metacommand
|
execsql/script/variables.py
CHANGED
|
@@ -317,6 +317,10 @@ class SubVarSet:
|
|
|
317
317
|
Uses case-insensitive string search instead of per-variable regex
|
|
318
318
|
compilation to avoid O(N) ``re.compile`` calls on every invocation.
|
|
319
319
|
"""
|
|
320
|
+
# Every token below starts with one of these, so text without them
|
|
321
|
+
# can't match; skip the scan over every defined variable.
|
|
322
|
+
if "!!" not in command_str and "!'!" not in command_str and '!"!' not in command_str:
|
|
323
|
+
return command_str, False
|
|
320
324
|
cmd_lower = command_str.lower()
|
|
321
325
|
|
|
322
326
|
def _check_nul(name: str, value: str) -> None:
|
execsql/types.py
CHANGED
|
@@ -855,8 +855,8 @@ dbt_dsn.name_datatype(DT_Binary, "LONGBINARY")
|
|
|
855
855
|
|
|
856
856
|
dbt_mysql = DbType("MySQL")
|
|
857
857
|
dbt_mysql.name_datatype(DT_TimestampTZ, "varchar", True, "char")
|
|
858
|
-
dbt_mysql.name_datatype(DT_Timestamp, "datetime"
|
|
859
|
-
dbt_mysql.name_datatype(DT_Date, "date"
|
|
858
|
+
dbt_mysql.name_datatype(DT_Timestamp, "datetime")
|
|
859
|
+
dbt_mysql.name_datatype(DT_Date, "date")
|
|
860
860
|
dbt_mysql.name_datatype(DT_Time, "time")
|
|
861
861
|
dbt_mysql.name_datatype(DT_Integer, "integer", False, "signed integer")
|
|
862
862
|
dbt_mysql.name_datatype(DT_Long, "bigint", False, "signed integer")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.34.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/en/stable/
|
|
6
6
|
Project-URL: Repository, https://github.com/geocoug/execsql
|
|
@@ -318,7 +318,7 @@ See the [full options reference](https://execsql2.readthedocs.io/en/stable/getti
|
|
|
318
318
|
**Data movement**
|
|
319
319
|
|
|
320
320
|
- Import data from CSV, TSV, JSON, Excel, OpenDocument, Feather, or Parquet files into a database table.
|
|
321
|
-
- Export query results in 20+ formats including CSV, TSV, JSON, YAML, XML, HTML, Markdown, LaTeX, XLSX, OpenDocument, Feather, Parquet, HDF5, DuckDB, SQLite, plain text, and Jinja2 templates.
|
|
321
|
+
- Export query results in 20+ formats including CSV, TSV, JSON, JSON Lines, YAML, XML, HTML, Markdown, LaTeX, XLSX, OpenDocument, Feather, Parquet, HDF5, DuckDB, SQLite, plain text, and Jinja2 templates.
|
|
322
322
|
- Copy data between databases, including across different DBMS types.
|
|
323
323
|
|
|
324
324
|
**Script logic**
|
|
@@ -441,7 +441,7 @@ execsql format --no-sql --in-place scripts/
|
|
|
441
441
|
```yaml
|
|
442
442
|
repos:
|
|
443
443
|
- repo: https://github.com/geocoug/execsql
|
|
444
|
-
rev: v2.
|
|
444
|
+
rev: v2.34.0
|
|
445
445
|
hooks:
|
|
446
446
|
- id: execsql-format
|
|
447
447
|
- id: execsql-lint
|
|
@@ -13,7 +13,7 @@ execsql/plugins.py,sha256=RykMlIjTyWcnQc9ygjLErS3wdcsZSsG9QTIvg4DwfAI,13626
|
|
|
13
13
|
execsql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
execsql/state.py,sha256=rXhxx58dVFjhG58HGOXm3ewloVlvpcaYnW1rij4qjkM,23478
|
|
15
15
|
execsql/state.pyi,sha256=ZDHZ1DPKYu4Qs4bcJT31XNv4GOt6O4s_zH4UHCowovw,4344
|
|
16
|
-
execsql/types.py,sha256=
|
|
16
|
+
execsql/types.py,sha256=HiIhc8wqm9Trt8WYP40VU93uuBSPo1uPP3z-sMsUI1o,33719
|
|
17
17
|
execsql/cli/__init__.py,sha256=FHg2lm2gt4L-XBnlVORn3qWHZ4739Ao-uwBIURfVIjg,2798
|
|
18
18
|
execsql/cli/application.py,sha256=tsSYE8cerJxY6y73GM_Xr1WBwc6A0_al8y969B6lPP8,12542
|
|
19
19
|
execsql/cli/dispatch.py,sha256=deyN2QQYvdzMSqGGa3CSMpaqdLNeMgc5Uke0mUbl764,2704
|
|
@@ -35,17 +35,18 @@ execsql/data/execsql.conf.template,sha256=Mz8wZxu0U2bfL-fsDpRHvdLkwNpfuZrl4QUMjp
|
|
|
35
35
|
execsql/data/script_header.sql,sha256=a7WKScekNgJoxVEmMRTkWv8NsAzW1jJ4vJfGy9HVy0g,349
|
|
36
36
|
execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
|
|
37
37
|
execsql/db/access.py,sha256=fxiC3rtoOJzrNXNxau-w7oh1fd9tyh2_PMliBbFc164,21427
|
|
38
|
-
execsql/db/base.py,sha256=
|
|
38
|
+
execsql/db/base.py,sha256=nMaODzV2_JqJzXJCshlWRQ9ZANPGK6IcIJ4_PBtXyT0,47714
|
|
39
39
|
execsql/db/dsn.py,sha256=Evrc6MUllJVFDn0jZB_iks-aKCsXuNF9XYjAkttqgGQ,6995
|
|
40
|
-
execsql/db/duckdb.py,sha256=
|
|
40
|
+
execsql/db/duckdb.py,sha256=c8VdZcmZdmyH8abrxM8mUk0BGIbittp-oV6UK7lsbgg,13954
|
|
41
41
|
execsql/db/factory.py,sha256=Uz7Xcbff6ZZbtLM55FhHCwGdo4KKfqU_1kyjvOHn-HY,5691
|
|
42
42
|
execsql/db/firebird.py,sha256=CoNnzFtOFAkp8OJByXXXclitCPAAjM23eFJGfy3C7IE,10171
|
|
43
|
-
execsql/db/mysql.py,sha256=
|
|
43
|
+
execsql/db/mysql.py,sha256=X-brnIFiQaOwVbY0uqPrC4esclhBv4sg-gHW3EVxHNw,26923
|
|
44
44
|
execsql/db/odbc.py,sha256=l03UNHi8sFtjHbgOKUNA2teKOisNk-8laQGCsHZs2eo,2526
|
|
45
|
-
execsql/db/oracle.py,sha256=
|
|
46
|
-
execsql/db/postgres.py,sha256=
|
|
47
|
-
execsql/db/sqlite.py,sha256=
|
|
48
|
-
execsql/db/sqlserver.py,sha256=
|
|
45
|
+
execsql/db/oracle.py,sha256=lUqdqzNOdNCkJCwbv9yHgaupeBuayZYsyoIDw76xrh4,15333
|
|
46
|
+
execsql/db/postgres.py,sha256=V3ldh51OIxuKkEAQz8k0TrzczbOFqoGMCSXlX3gH1-4,24433
|
|
47
|
+
execsql/db/sqlite.py,sha256=YTfESfdWdny4hIV23fjJbzFuEchq2IFAmQhRynweJL0,12308
|
|
48
|
+
execsql/db/sqlserver.py,sha256=WIVvTp1MNKaUZn912U7ErcjSqhaLTqnZ2hfv_Gcso74,11601
|
|
49
|
+
execsql/db/target_types.py,sha256=68nSyYY0mj6sgS26zeOYcFuUJA_ik08-0BeNsSM7UrE,8563
|
|
49
50
|
execsql/db/tiers.py,sha256=PH2sUYwlYqcANGVKXprWQKVIlM6fpWU9q74djn8oFwc,2924
|
|
50
51
|
execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
|
|
51
52
|
execsql/debug/repl.py,sha256=jnwrqRac4krc1pES7Ni-j1Dyw3mJIcWl5LQQIX1GkRw,20753
|
|
@@ -55,13 +56,13 @@ execsql/exporters/delimited.py,sha256=N0cP_CNlb_LLCUqbHmTQsOGufWnD_ddGdeFnXPE8Er
|
|
|
55
56
|
execsql/exporters/duckdb.py,sha256=IQp7ayIJHB2lf6mBSxl6k5R2AeUEvgD75Q5Y55mH8jI,3214
|
|
56
57
|
execsql/exporters/feather.py,sha256=tah48aep0cQRikZcmADXUPTzFddgDUT78Ndw_l0Hsj4,4178
|
|
57
58
|
execsql/exporters/html.py,sha256=hwf9UHs7EZ7xiP3p9ilBIsAukXcDu-KO2dcQHVh2z30,10111
|
|
58
|
-
execsql/exporters/json.py,sha256=
|
|
59
|
+
execsql/exporters/json.py,sha256=kCR7HH_tC9xka5UDXLX4jgomXNEhhSHdRCIsYfEKiS4,5814
|
|
59
60
|
execsql/exporters/latex.py,sha256=bdUN3GwfENUZHgpw6bvjsjfivDgzyf-HRkYTP-eH-pM,5480
|
|
60
61
|
execsql/exporters/markdown.py,sha256=FuMGFxm9uZUOrdEuXT2ZZLCqmVDly7SMrFlrjP6kVEI,4827
|
|
61
62
|
execsql/exporters/ods.py,sha256=hMKhUwLTKVf28JY1x7pdCGBl-WAE75oO9Oa7oUITS60,24916
|
|
62
63
|
execsql/exporters/parquet.py,sha256=mbCo2Wlit1lkXmyo5EyLd_ATIHSICS49bouiBT2kdAs,1121
|
|
63
64
|
execsql/exporters/pretty.py,sha256=SiJHBXIjoFsOHegXcdrtxYa6xSIPga1U0DQEy7pK4QY,4145
|
|
64
|
-
execsql/exporters/protocol.py,sha256=
|
|
65
|
+
execsql/exporters/protocol.py,sha256=7PBFmvMlt2M33Aoo7pwze83SuasquozR1KS0vWQffYM,4403
|
|
65
66
|
execsql/exporters/raw.py,sha256=9o3BMt7UYmnjbP8OEzmM2kNKfwwERi2yDcuNfUoEfuQ,2530
|
|
66
67
|
execsql/exporters/sqlite.py,sha256=QjP_W90S-hecNqp2A-Vy58DD8MZitME1sTq4lqIS43w,3401
|
|
67
68
|
execsql/exporters/templates.py,sha256=N9PRFet0mYuDQFu7PNbq5c6qGu2You2qfHpqnxg17NA,7065
|
|
@@ -77,8 +78,8 @@ execsql/gui/console.py,sha256=dzdSl4NJNvRd9VvlwRCXHvfnXjQPimyTx7NcibcUZC8,18305
|
|
|
77
78
|
execsql/gui/desktop.py,sha256=y-T317NzSs9kcA4-NZW-Dx2cZ4Bv1mPoCr7hwQCJYoo,59467
|
|
78
79
|
execsql/gui/tui.py,sha256=VoUtjSMopYtbtc8_-qV7YQAFvc-bS7zrZ5J2hl1cPxM,66853
|
|
79
80
|
execsql/importers/__init__.py,sha256=zZwdQxMaValCNqUrVdvaA7XPU3J8NmqVJ4uolNWY2iU,299
|
|
80
|
-
execsql/importers/base.py,sha256=
|
|
81
|
-
execsql/importers/csv.py,sha256=
|
|
81
|
+
execsql/importers/base.py,sha256=4a4ypTr5qW38ByYbRW6cklveQ4Y7wMIYRa4-lFa_uZo,5635
|
|
82
|
+
execsql/importers/csv.py,sha256=RLEs0QLqaREGBQxo2ovYS6cRqLbTKi4rPBdkANDt56I,5032
|
|
82
83
|
execsql/importers/feather.py,sha256=g2B69d2uv9vmnXcmjFyTVsMP40LYEzFYkhk3gD26mGw,1900
|
|
83
84
|
execsql/importers/json.py,sha256=fL47h09Zzx4Qw3TTQRQJbdPdD3qVMiU-kK9C7j1IQzg,5534
|
|
84
85
|
execsql/importers/ods.py,sha256=scSId1sQpvWxtyPsp6QWTmuXn-h34gbbC4FM2fmhvFA,2980
|
|
@@ -96,7 +97,7 @@ execsql/lsp/hover.py,sha256=JemZqJ8d7FHouMhW92jkfek14fL9rl6JBQU7JIXJHos,5038
|
|
|
96
97
|
execsql/lsp/navigation.py,sha256=_f-zW82P4Wkelbsdedz_5l_ztFt-CFD8VSiVE5JaSAM,6436
|
|
97
98
|
execsql/lsp/rename.py,sha256=fYk1gDSWWhqyoIGZO5hVIHPYYdGKNjgZS0mijFDFpac,3756
|
|
98
99
|
execsql/lsp/server.py,sha256=gTjk1agZbygdbcowa3jkL45WJ37sxhJ58oiLB7BM6Bw,19893
|
|
99
|
-
execsql/metacommands/__init__.py,sha256=
|
|
100
|
+
execsql/metacommands/__init__.py,sha256=aMf-3IRjMpNFSUI-IMAcvL-kXZHq5qF6W_p3UawdQwM,13115
|
|
100
101
|
execsql/metacommands/conditions.py,sha256=4DsMXr6vnfbtcKqgnxYV4UK4NmAfE_hpfgkFjlH0bjo,29844
|
|
101
102
|
execsql/metacommands/connect.py,sha256=SPk4h9yiUgTD3w1gNOT_oYx8VShggsO357by5Hoy6OI,15344
|
|
102
103
|
execsql/metacommands/control.py,sha256=9swW6X8jXDUpu5cHiAnQVx1ahFyWxlx7mjg4dqYfX5Y,9355
|
|
@@ -105,22 +106,22 @@ execsql/metacommands/debug.py,sha256=mKRT7uLwnw34Xl46fNDI_zH4MsPpZyihoLlonapbXuU
|
|
|
105
106
|
execsql/metacommands/dispatch.py,sha256=so0ebcnv66NhVDtA1Bt4GelD0hsC5w7gy7rItz2Mdos,87430
|
|
106
107
|
execsql/metacommands/effects.py,sha256=cRSo48R4nRJzkRR3OGHLhJzofcdAaNIobaxmn1N9TGw,3813
|
|
107
108
|
execsql/metacommands/io.py,sha256=vlGBje5sgnqeilooMdhJDgSRIhysHy5_7LrKtik9Xjs,3011
|
|
108
|
-
execsql/metacommands/io_export.py,sha256=
|
|
109
|
-
execsql/metacommands/io_fileops.py,sha256=
|
|
109
|
+
execsql/metacommands/io_export.py,sha256=_1o5T3Ejqq_0N1hYwaRPgDktn8nZT8sQLMyXPRgXEec,14587
|
|
110
|
+
execsql/metacommands/io_fileops.py,sha256=ru7zYgNV7rg0WtaDwhdYjy5iJTcecvjzR1PCdMNGG0A,13364
|
|
110
111
|
execsql/metacommands/io_import.py,sha256=gJICeK1MOfG6D73I7m8zBCalvr_ZJzD-QBKOpKYeYXo,15684
|
|
111
112
|
execsql/metacommands/io_write.py,sha256=yXDkMbQ3haDARVATVUYZprakyJHSyk1NywdKkFLpQ14,12783
|
|
112
113
|
execsql/metacommands/prompt.py,sha256=PlXhtBYCembG_1SSCrwpQR10mGF3_jXodu4aCSHlSCM,37950
|
|
113
|
-
execsql/metacommands/reference.py,sha256=
|
|
114
|
+
execsql/metacommands/reference.py,sha256=qBsOfcu4pi3XQCL-ywusfbrTtpqwqrWUlVraj8tj5Co,54325
|
|
114
115
|
execsql/metacommands/script_ext.py,sha256=CsUbyq8QFPnnvfX_k7fPlYNvvRw1xLmAWER4Exo5pwc,3658
|
|
115
116
|
execsql/metacommands/system.py,sha256=6X2qmMpGUHDzyEluEotXt0Ks9jKQTpc2x0eoYNgN12M,7997
|
|
116
|
-
execsql/metacommands/upsert.py,sha256=
|
|
117
|
+
execsql/metacommands/upsert.py,sha256=hcYGonmh2sdijbqwNynn1-iajMnLp7x5vbRbtfjaf8c,21461
|
|
117
118
|
execsql/script/__init__.py,sha256=I7hVO6wm3iOocxRRGEcFHhjdtL1moKbUYLB-wKbjPdk,3267
|
|
118
119
|
execsql/script/ast.py,sha256=WqCpFxhxrT2IIGvbArTp3QS0BZryRZAb7IL4ri3I6xg,20591
|
|
119
120
|
execsql/script/control.py,sha256=WqLy-HLPqHG3vEzYpKMiIJsD7LpORjyQuUWzFzcGz4w,2327
|
|
120
|
-
execsql/script/engine.py,sha256=
|
|
121
|
-
execsql/script/executor.py,sha256=
|
|
121
|
+
execsql/script/engine.py,sha256=h1pUSXGqwJASLJizqzd9LBMn_U5Vuqc1UbaZk1xUinw,18468
|
|
122
|
+
execsql/script/executor.py,sha256=p6VWZeTMRPD_621I-CF4K2aaFaAbrY5I82hksrACFJQ,40999
|
|
122
123
|
execsql/script/parser.py,sha256=z73v6t6ZbPUKkRu-UOIL6q1cCRDeoRvG2aXXC8NpMvE,37162
|
|
123
|
-
execsql/script/variables.py,sha256=
|
|
124
|
+
execsql/script/variables.py,sha256=XHRnViFp0zPy-skGpYbpckkYAcczqpw1lqTVLR9eZvo,17735
|
|
124
125
|
execsql/utils/__init__.py,sha256=0uR6JwVJQRX3vceByNBduCAf5dd5assKjeqJUWvpZoA,278
|
|
125
126
|
execsql/utils/auth.py,sha256=0LYKdNMhodq9R0B8WyKp69a-xlP9wOR6Ua9Lgo2aJnQ,14868
|
|
126
127
|
execsql/utils/color.py,sha256=goQqhnLg8yERpFolQikA9zYSVcuAm0unKTJiWTbQm-U,634
|
|
@@ -135,22 +136,22 @@ execsql/utils/paths.py,sha256=7IIHT6vqXTfs6Ltfrzm_I2ZV4pU8IuedOLkHo6a1_K8,1175
|
|
|
135
136
|
execsql/utils/regex.py,sha256=DGzPBl0Kt4--RbwPhoZI6Bm6N_CBUvI0DbqE4EF6qp8,3718
|
|
136
137
|
execsql/utils/strings.py,sha256=UQNjpRCEFa1UO6feU-M-9e24wWAvizs_iu_4fFusLxo,8516
|
|
137
138
|
execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
|
|
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.
|
|
154
|
-
execsql2-2.
|
|
155
|
-
execsql2-2.
|
|
156
|
-
execsql2-2.
|
|
139
|
+
execsql2-2.34.0.data/data/execsql2_extras/README.md,sha256=6f5bWiBlKRn8s6GVrU423-HFrU1Jwbk2AabxraLu3AY,5398
|
|
140
|
+
execsql2-2.34.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
|
|
141
|
+
execsql2-2.34.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
|
|
142
|
+
execsql2-2.34.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
|
|
143
|
+
execsql2-2.34.0.data/data/execsql2_extras/md_compare.sql,sha256=ARYEcOlxWLxfw-KCKDeJRdCqnuCb9PVwGaHtITR2sYU,26535
|
|
144
|
+
execsql2-2.34.0.data/data/execsql2_extras/md_glossary.sql,sha256=gu2PDTm295YvxR7cDp25Mf-xXHly0BvlMudoNOrep5Q,10931
|
|
145
|
+
execsql2-2.34.0.data/data/execsql2_extras/md_upsert.sql,sha256=YNUTFS6yB4bZXs-wRDn6dN8jL5wIumzZyE2uo5arIos,113407
|
|
146
|
+
execsql2-2.34.0.data/data/execsql2_extras/pg_compare.sql,sha256=WcupGxQoWtMwHVPuy42xCpP4gAV4Ttb1W-_hwh-3xb4,24529
|
|
147
|
+
execsql2-2.34.0.data/data/execsql2_extras/pg_glossary.sql,sha256=CfQz2HZj2nMvJfYfQrAmNYdx0uRrOIWsCXwVo5gKv_k,9953
|
|
148
|
+
execsql2-2.34.0.data/data/execsql2_extras/script_template.sql,sha256=s5i0HULxp49N2Cpd24iALleM_dBq5oLuN3Arx_4Z7-o,11140
|
|
149
|
+
execsql2-2.34.0.data/data/execsql2_extras/ss_compare.sql,sha256=PpU-vPyqqC1faBoo5zwVZlZHYKkT79QZisWVlk4o-ss,26415
|
|
150
|
+
execsql2-2.34.0.data/data/execsql2_extras/ss_glossary.sql,sha256=_oTiZHpadPXWrNs8VoRe9PZgX9XdK490NMVWVTVtjl4,13110
|
|
151
|
+
execsql2-2.34.0.data/data/execsql2_extras/ss_upsert.sql,sha256=Dvst877h4KHn8ayYF9EXI_OxqLQ89wyfcnGYYT4szro,118304
|
|
152
|
+
execsql2-2.34.0.dist-info/METADATA,sha256=-If95mpt8uHmRfovA0qRritNaIxwEIW6v3d71fcCAHE,26939
|
|
153
|
+
execsql2-2.34.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
154
|
+
execsql2-2.34.0.dist-info/entry_points.txt,sha256=jOf53-4IVGjEZeNYUOF1GGaiPNfVZcNKxP3vDFKEyMA,58
|
|
155
|
+
execsql2-2.34.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
|
|
156
|
+
execsql2-2.34.0.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
|
|
157
|
+
execsql2-2.34.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{execsql2-2.32.0.data → execsql2-2.34.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
|