execsql2 2.4.5__py3-none-any.whl → 2.6.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/__init__.py +14 -0
- execsql/cli/dsn.py +2 -0
- execsql/cli/help.py +2 -0
- execsql/cli/run.py +4 -2
- execsql/constants.py +11 -0
- execsql/db/access.py +20 -0
- execsql/db/base.py +4 -0
- execsql/db/dsn.py +11 -8
- execsql/db/duckdb.py +12 -8
- execsql/db/firebird.py +17 -8
- execsql/db/mysql.py +13 -8
- execsql/db/oracle.py +22 -8
- execsql/db/postgres.py +21 -9
- execsql/db/sqlite.py +18 -8
- execsql/db/sqlserver.py +14 -8
- execsql/exporters/__init__.py +6 -1
- execsql/exporters/base.py +2 -0
- execsql/exporters/delimited.py +10 -0
- execsql/exporters/protocol.py +128 -0
- execsql/exporters/xls.py +8 -0
- execsql/format.py +3 -1
- execsql/gui/__init__.py +2 -0
- execsql/gui/base.py +2 -0
- execsql/gui/console.py +2 -0
- execsql/gui/desktop.py +1 -0
- execsql/gui/tui.py +134 -0
- execsql/importers/base.py +1 -0
- execsql/importers/csv.py +2 -0
- execsql/importers/feather.py +2 -0
- execsql/importers/ods.py +1 -0
- execsql/importers/xls.py +1 -0
- execsql/metacommands/__init__.py +386 -180
- execsql/metacommands/dispatch.py +2 -0
- execsql/metacommands/io.py +41 -0
- execsql/models.py +17 -0
- execsql/parser.py +41 -0
- execsql/script/control.py +2 -0
- execsql/script/engine.py +19 -0
- execsql/script/variables.py +9 -5
- execsql/state.py +312 -199
- execsql/types.py +46 -0
- {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/METADATA +2 -2
- {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/RECORD +62 -61
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/WHEEL +0 -0
- {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/licenses/NOTICE +0 -0
execsql/cli/__init__.py
CHANGED
|
@@ -24,6 +24,20 @@ from execsql.cli.help import _console, _err_console, _print_encodings, _print_me
|
|
|
24
24
|
from execsql.cli.run import _connect_initial_db, _run # noqa: F401 — re-export
|
|
25
25
|
from execsql.exceptions import ConfigError, ErrInfo
|
|
26
26
|
|
|
27
|
+
__all__ = [
|
|
28
|
+
"_SCHEME_TO_DBTYPE",
|
|
29
|
+
"_connect_initial_db",
|
|
30
|
+
"_console",
|
|
31
|
+
"_err_console",
|
|
32
|
+
"_legacy_main",
|
|
33
|
+
"_parse_connection_string",
|
|
34
|
+
"_print_encodings",
|
|
35
|
+
"_print_metacommands",
|
|
36
|
+
"_run",
|
|
37
|
+
"app",
|
|
38
|
+
"main",
|
|
39
|
+
]
|
|
40
|
+
|
|
27
41
|
|
|
28
42
|
# ---------------------------------------------------------------------------
|
|
29
43
|
# Typer app
|
execsql/cli/dsn.py
CHANGED
execsql/cli/help.py
CHANGED
|
@@ -11,6 +11,8 @@ from encodings.aliases import aliases as codec_dict
|
|
|
11
11
|
from rich.console import Console
|
|
12
12
|
from rich.table import Table
|
|
13
13
|
|
|
14
|
+
__all__ = ["_console", "_err_console", "_print_encodings", "_print_metacommands"]
|
|
15
|
+
|
|
14
16
|
_console = Console()
|
|
15
17
|
_err_console = Console(stderr=True)
|
|
16
18
|
|
execsql/cli/run.py
CHANGED
|
@@ -24,6 +24,8 @@ from execsql.script import SubVarSet, current_script_line, read_sqlfile, read_sq
|
|
|
24
24
|
from execsql.utils.fileio import FileWriter, Logger, filewriter_end
|
|
25
25
|
from execsql.utils.gui import gui_connect, gui_console_isrunning, gui_console_off, gui_console_on, gui_console_wait_user
|
|
26
26
|
|
|
27
|
+
__all__ = ["_connect_initial_db", "_print_dry_run", "_run"]
|
|
28
|
+
|
|
27
29
|
|
|
28
30
|
# ---------------------------------------------------------------------------
|
|
29
31
|
# Dry-run helper
|
|
@@ -92,7 +94,7 @@ def _run(
|
|
|
92
94
|
# Security note: ALL environment variables are exposed as &-prefixed
|
|
93
95
|
# substitution variables. Sensitive values (API keys, tokens) in the
|
|
94
96
|
# process environment will be accessible to scripts. See the
|
|
95
|
-
# "Environment Variables" section in docs/substitution_vars.md.
|
|
97
|
+
# "Environment Variables" section in docs/reference/substitution_vars.md.
|
|
96
98
|
for k in os.environ:
|
|
97
99
|
try:
|
|
98
100
|
_state.subvars.add_substitution("&" + k, os.environ[k])
|
|
@@ -138,7 +140,7 @@ def _run(
|
|
|
138
140
|
parsed_dsn = _parse_connection_string(dsn)
|
|
139
141
|
except ConfigError as exc:
|
|
140
142
|
_err_console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
141
|
-
raise SystemExit(1)
|
|
143
|
+
raise SystemExit(1) from None
|
|
142
144
|
db_type = db_type or parsed_dsn["db_type"]
|
|
143
145
|
conf.db_type = db_type
|
|
144
146
|
# DSN values override conf-file values — the CLI flag is explicit.
|
execsql/constants.py
CHANGED
|
@@ -17,6 +17,17 @@ Contains:
|
|
|
17
17
|
colour+icon combinations.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
+
__all__ = [
|
|
21
|
+
"bm_servers",
|
|
22
|
+
"cancel_xbm",
|
|
23
|
+
"color_names",
|
|
24
|
+
"custom_icons",
|
|
25
|
+
"expand_xbm",
|
|
26
|
+
"icon_xbm",
|
|
27
|
+
"wedge_sm_xbm",
|
|
28
|
+
"wedges_3_xbm",
|
|
29
|
+
]
|
|
30
|
+
|
|
20
31
|
# Tile servers for map basemap layers
|
|
21
32
|
bm_servers: dict[str, str] = {
|
|
22
33
|
"OpenStreetMap": "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
execsql/db/access.py
CHANGED
|
@@ -88,6 +88,7 @@ class AccessDatabase(Database):
|
|
|
88
88
|
return f"AccessDatabase({self.db_name}, {self.encoding})"
|
|
89
89
|
|
|
90
90
|
def open_db(self) -> None:
|
|
91
|
+
"""Open an ODBC connection to the Access database."""
|
|
91
92
|
# Open an ODBC connection.
|
|
92
93
|
import pyodbc
|
|
93
94
|
|
|
@@ -136,6 +137,7 @@ class AccessDatabase(Database):
|
|
|
136
137
|
)
|
|
137
138
|
|
|
138
139
|
def open_dao(self) -> None:
|
|
140
|
+
"""Open a DAO connection to the Access database."""
|
|
139
141
|
import win32com.client
|
|
140
142
|
|
|
141
143
|
if self.dao_conn is not None:
|
|
@@ -186,6 +188,7 @@ class AccessDatabase(Database):
|
|
|
186
188
|
)
|
|
187
189
|
|
|
188
190
|
def exec_dao(self, querystring: str) -> None:
|
|
191
|
+
"""Execute a query using the DAO connection."""
|
|
189
192
|
# Execute a query using DAO.
|
|
190
193
|
if self.dao_conn is None:
|
|
191
194
|
self.open_dao()
|
|
@@ -193,6 +196,7 @@ class AccessDatabase(Database):
|
|
|
193
196
|
self.last_dao_time = time.time()
|
|
194
197
|
|
|
195
198
|
def close(self) -> None:
|
|
199
|
+
"""Close both the DAO and ODBC connections."""
|
|
196
200
|
if self.dao_conn:
|
|
197
201
|
for qn in self.temp_query_names:
|
|
198
202
|
try:
|
|
@@ -206,10 +210,13 @@ class AccessDatabase(Database):
|
|
|
206
210
|
self.conn = None
|
|
207
211
|
|
|
208
212
|
def dao_flush_check(self) -> None:
|
|
213
|
+
"""Wait if needed for Jet's read buffer to flush after a DAO command."""
|
|
209
214
|
if time.time() - self.last_dao_time < 5.0:
|
|
210
215
|
time.sleep(5 - (time.time() - self.last_dao_time))
|
|
211
216
|
|
|
212
217
|
def execute(self, sqlcmd: Any, paramlist: list | None = None) -> None:
|
|
218
|
+
"""Execute a SQL command, handling encoding, DAO flush, and temporary queries."""
|
|
219
|
+
|
|
213
220
|
# A shortcut to self.cursor().execute() that handles encoding and that
|
|
214
221
|
# ensures that at least 5 seconds have passed since the last DAO command,
|
|
215
222
|
# to allow Jet's read buffer to be flushed (see https://support.microsoft.com/en-us/kb/225048).
|
|
@@ -254,9 +261,11 @@ class AccessDatabase(Database):
|
|
|
254
261
|
exec1(sqlcmd, paramlist)
|
|
255
262
|
|
|
256
263
|
def exec_cmd(self, querycommand: str) -> None:
|
|
264
|
+
"""Execute a stored query command via DAO."""
|
|
257
265
|
self.exec_dao(querycommand)
|
|
258
266
|
|
|
259
267
|
def select_data(self, sql: str) -> tuple[list[str], list]:
|
|
268
|
+
"""Return column names and all rows from a SELECT statement."""
|
|
260
269
|
# Returns the results of the sql select statement.
|
|
261
270
|
# The Access driver returns data as unicode, so no decoding is necessary.
|
|
262
271
|
self.dao_flush_check()
|
|
@@ -266,6 +275,7 @@ class AccessDatabase(Database):
|
|
|
266
275
|
return [d[0] for d in curs.description], rows
|
|
267
276
|
|
|
268
277
|
def select_rowsource(self, sql: str) -> tuple[list[str], Any]:
|
|
278
|
+
"""Return column names and an iterable that yields rows one at a time."""
|
|
269
279
|
# Return 1) a list of column names, and 2) an iterable that yields rows.
|
|
270
280
|
self.dao_flush_check()
|
|
271
281
|
curs = self.cursor()
|
|
@@ -274,6 +284,7 @@ class AccessDatabase(Database):
|
|
|
274
284
|
return [d[0] for d in curs.description], iter(curs.fetchone, None)
|
|
275
285
|
|
|
276
286
|
def select_rowdict(self, sql: str) -> tuple[list[str], Any]:
|
|
287
|
+
"""Return column names and an iterable that yields rows as dictionaries."""
|
|
277
288
|
# Return an iterable that yields dictionaries of row data.
|
|
278
289
|
self.dao_flush_check()
|
|
279
290
|
curs = self.cursor()
|
|
@@ -295,6 +306,7 @@ class AccessDatabase(Database):
|
|
|
295
306
|
return headers, iter(dict_row, None)
|
|
296
307
|
|
|
297
308
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
309
|
+
"""Return True if the named table exists in the Access database."""
|
|
298
310
|
self.dao_flush_check()
|
|
299
311
|
curs = self.cursor()
|
|
300
312
|
try:
|
|
@@ -318,6 +330,7 @@ class AccessDatabase(Database):
|
|
|
318
330
|
column_name: str,
|
|
319
331
|
schema_name: str | None = None,
|
|
320
332
|
) -> bool:
|
|
333
|
+
"""Return True if the named column exists in the given Access table."""
|
|
321
334
|
self.dao_flush_check()
|
|
322
335
|
curs = self.cursor()
|
|
323
336
|
quoted_col = self.quote_identifier(column_name)
|
|
@@ -330,6 +343,7 @@ class AccessDatabase(Database):
|
|
|
330
343
|
return True
|
|
331
344
|
|
|
332
345
|
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
346
|
+
"""Return a list of column names for the given Access table."""
|
|
333
347
|
self.dao_flush_check()
|
|
334
348
|
curs = self.cursor()
|
|
335
349
|
quoted_tbl = self.quote_identifier(table_name)
|
|
@@ -337,6 +351,7 @@ class AccessDatabase(Database):
|
|
|
337
351
|
return [d[0] for d in curs.description]
|
|
338
352
|
|
|
339
353
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
354
|
+
"""Return True if the named view or query exists in the Access database."""
|
|
340
355
|
self.dao_flush_check()
|
|
341
356
|
curs = self.cursor()
|
|
342
357
|
try:
|
|
@@ -355,14 +370,17 @@ class AccessDatabase(Database):
|
|
|
355
370
|
return len(rows) > 0
|
|
356
371
|
|
|
357
372
|
def schema_exists(self, schema_name: str) -> bool:
|
|
373
|
+
"""Return False; Access does not support schemas."""
|
|
358
374
|
return False
|
|
359
375
|
|
|
360
376
|
def drop_table(self, tablename: str) -> None:
|
|
377
|
+
"""Drop the named table from the Access database."""
|
|
361
378
|
self.dao_flush_check()
|
|
362
379
|
tablename = self.type.quoted(tablename)
|
|
363
380
|
self.execute(f"drop table {tablename};")
|
|
364
381
|
|
|
365
382
|
def as_datetime(self, val: Any) -> datetime.datetime | None:
|
|
383
|
+
"""Convert a value to a datetime object suitable for Access."""
|
|
366
384
|
from execsql.types import DT_Timestamp, DT_Date, DT_Time, DataTypeError
|
|
367
385
|
|
|
368
386
|
if val is None or (isinstance(val, _state.stringtypes) and len(val) == 0):
|
|
@@ -393,6 +411,7 @@ class AccessDatabase(Database):
|
|
|
393
411
|
return v
|
|
394
412
|
|
|
395
413
|
def int_or_bool(self, val: Any) -> int | None:
|
|
414
|
+
"""Convert a value to an integer, recognizing Access boolean values."""
|
|
396
415
|
# Because Booleans are stored as integers in Access (at least, if execsql
|
|
397
416
|
# creates the table), we have to recognize Boolean values as legitimate
|
|
398
417
|
# integers.
|
|
@@ -420,6 +439,7 @@ class AccessDatabase(Database):
|
|
|
420
439
|
column_name: str,
|
|
421
440
|
file_name: str,
|
|
422
441
|
) -> None:
|
|
442
|
+
"""Import an entire binary file into a single column of a table."""
|
|
423
443
|
import pyodbc
|
|
424
444
|
|
|
425
445
|
with open(file_name, "rb") as f:
|
execsql/db/base.py
CHANGED
|
@@ -70,6 +70,7 @@ class Database(ABC):
|
|
|
70
70
|
port: int | None = None,
|
|
71
71
|
encoding: str | None = None,
|
|
72
72
|
) -> None:
|
|
73
|
+
"""Initialize common connection attributes for a database backend."""
|
|
73
74
|
self.type = None
|
|
74
75
|
self.server_name = server_name
|
|
75
76
|
self.db_name = db_name
|
|
@@ -84,6 +85,7 @@ class Database(ABC):
|
|
|
84
85
|
self.autocommit = True
|
|
85
86
|
|
|
86
87
|
def __repr__(self) -> str:
|
|
88
|
+
"""Return a developer-friendly string representation of this connection."""
|
|
87
89
|
return (
|
|
88
90
|
f"Database({self.server_name!r}, {self.db_name!r}, {self.user!r}, "
|
|
89
91
|
f"{self.need_passwd!r}, {self.port!r}, {self.encoding!r})"
|
|
@@ -637,12 +639,14 @@ class DatabasePool:
|
|
|
637
639
|
and with the current and initial databases identified."""
|
|
638
640
|
|
|
639
641
|
def __init__(self) -> None:
|
|
642
|
+
"""Initialize an empty connection pool with no active database."""
|
|
640
643
|
self.pool: dict[str, Database] = {}
|
|
641
644
|
self.initial_db: str | None = None
|
|
642
645
|
self.current_db: str | None = None
|
|
643
646
|
self.do_rollback: bool = True
|
|
644
647
|
|
|
645
648
|
def __repr__(self) -> str:
|
|
649
|
+
"""Return a string representation of the pool."""
|
|
646
650
|
return "DatabasePool()"
|
|
647
651
|
|
|
648
652
|
def add(self, db_alias: str, db_obj: Database) -> None:
|
execsql/db/dsn.py
CHANGED
|
@@ -59,6 +59,7 @@ class DsnDatabase(Database):
|
|
|
59
59
|
return f"DsnDatabase({self.db_name!r}, {self.user!r}, {self.need_passwd!r}, {self.port!r}, {self.encoding!r})"
|
|
60
60
|
|
|
61
61
|
def open_db(self) -> None:
|
|
62
|
+
"""Open an ODBC connection using the configured DSN."""
|
|
62
63
|
# Open an ODBC connection using a DSN.
|
|
63
64
|
import pyodbc
|
|
64
65
|
|
|
@@ -118,15 +119,16 @@ class DsnDatabase(Database):
|
|
|
118
119
|
_try_connect()
|
|
119
120
|
|
|
120
121
|
def exec_cmd(self, querycommand: str) -> None:
|
|
122
|
+
"""Execute a stored procedure by name."""
|
|
121
123
|
# The querycommand must be a stored procedure
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
with self._cursor() as curs:
|
|
125
|
+
cmd = f"execute {querycommand};"
|
|
126
|
+
try:
|
|
127
|
+
curs.execute(cmd.encode(self.encoding))
|
|
128
|
+
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
129
|
+
except Exception:
|
|
130
|
+
self.rollback()
|
|
131
|
+
raise
|
|
130
132
|
|
|
131
133
|
def import_entire_file(
|
|
132
134
|
self,
|
|
@@ -135,6 +137,7 @@ class DsnDatabase(Database):
|
|
|
135
137
|
column_name: str,
|
|
136
138
|
file_name: str,
|
|
137
139
|
) -> None:
|
|
140
|
+
"""Import an entire binary file into a single column of a table."""
|
|
138
141
|
import pyodbc
|
|
139
142
|
|
|
140
143
|
with open(file_name, "rb") as f:
|
execsql/db/duckdb.py
CHANGED
|
@@ -45,6 +45,7 @@ class DuckDBDatabase(Database):
|
|
|
45
45
|
return f"DuckDBDatabase({self.db_name!r})"
|
|
46
46
|
|
|
47
47
|
def open_db(self) -> None:
|
|
48
|
+
"""Open a connection to the DuckDB database file."""
|
|
48
49
|
import duckdb
|
|
49
50
|
|
|
50
51
|
if self.conn is None:
|
|
@@ -60,22 +61,25 @@ class DuckDBDatabase(Database):
|
|
|
60
61
|
) from e
|
|
61
62
|
|
|
62
63
|
def exec_cmd(self, querycommand: str) -> None:
|
|
64
|
+
"""Execute a query command as a view selection, since DuckDB lacks stored procedures."""
|
|
63
65
|
# DuckDB does not support stored functions, so the querycommand
|
|
64
66
|
# is treated as (and therefore must be) a view.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
with self._cursor() as curs:
|
|
68
|
+
cmd = f"select * from {querycommand};"
|
|
69
|
+
try:
|
|
70
|
+
curs.execute(cmd.encode(self.encoding))
|
|
71
|
+
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
72
|
+
except Exception:
|
|
73
|
+
self.rollback()
|
|
74
|
+
raise
|
|
73
75
|
|
|
74
76
|
def view_exists(self, view_name: str) -> bool:
|
|
77
|
+
"""Return True if the named view exists in the DuckDB database."""
|
|
75
78
|
# DuckDB information_schema has no 'views' table; views are listed in 'tables'
|
|
76
79
|
return self.table_exists(view_name)
|
|
77
80
|
|
|
78
81
|
def schema_exists(self, schema_name: str) -> bool:
|
|
82
|
+
"""Return True if the named schema exists in the current DuckDB catalog."""
|
|
79
83
|
# In DuckDB, the 'schemata' view is not limited to the current database.
|
|
80
84
|
curs = self.cursor()
|
|
81
85
|
curs.execute(
|
execsql/db/firebird.py
CHANGED
|
@@ -59,6 +59,7 @@ class FirebirdDatabase(Database):
|
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
def open_db(self) -> None:
|
|
62
|
+
"""Open a connection to the Firebird database."""
|
|
62
63
|
import fdb as firebird_lib
|
|
63
64
|
|
|
64
65
|
def db_conn():
|
|
@@ -113,17 +114,19 @@ class FirebirdDatabase(Database):
|
|
|
113
114
|
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
|
|
114
115
|
|
|
115
116
|
def exec_cmd(self, querycommand: str) -> None:
|
|
117
|
+
"""Execute a stored procedure by name."""
|
|
116
118
|
# The querycommand must be a stored function (/procedure)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
with self._cursor() as curs:
|
|
120
|
+
cmd = f"execute procedure {querycommand};"
|
|
121
|
+
try:
|
|
122
|
+
curs.execute(cmd)
|
|
123
|
+
except Exception:
|
|
124
|
+
self.rollback()
|
|
125
|
+
raise
|
|
126
|
+
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
125
127
|
|
|
126
128
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
129
|
+
"""Return True if the named table exists in the Firebird database."""
|
|
127
130
|
curs = self.cursor()
|
|
128
131
|
sql = (
|
|
129
132
|
"SELECT RDB$RELATION_NAME FROM RDB$RELATIONS "
|
|
@@ -156,6 +159,7 @@ class FirebirdDatabase(Database):
|
|
|
156
159
|
column_name: str,
|
|
157
160
|
schema_name: str | None = None,
|
|
158
161
|
) -> bool:
|
|
162
|
+
"""Return True if the named column exists in the given Firebird table."""
|
|
159
163
|
curs = self.cursor()
|
|
160
164
|
quoted_col = self.quote_identifier(column_name)
|
|
161
165
|
quoted_tbl = self.quote_identifier(table_name)
|
|
@@ -167,6 +171,7 @@ class FirebirdDatabase(Database):
|
|
|
167
171
|
return True
|
|
168
172
|
|
|
169
173
|
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
174
|
+
"""Return a list of column names for the given Firebird table."""
|
|
170
175
|
curs = self.cursor()
|
|
171
176
|
quoted_tbl = self.quote_identifier(table_name)
|
|
172
177
|
sql = f"select first 1 * from {quoted_tbl};"
|
|
@@ -185,6 +190,7 @@ class FirebirdDatabase(Database):
|
|
|
185
190
|
return [d[0] for d in curs.description]
|
|
186
191
|
|
|
187
192
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
193
|
+
"""Return True if the named view exists in the Firebird database."""
|
|
188
194
|
curs = self.cursor()
|
|
189
195
|
sql = "select distinct rdb$view_name from rdb$view_relations where rdb$view_name = ?;"
|
|
190
196
|
try:
|
|
@@ -204,9 +210,11 @@ class FirebirdDatabase(Database):
|
|
|
204
210
|
return len(rows) > 0
|
|
205
211
|
|
|
206
212
|
def schema_exists(self, schema_name: str) -> bool:
|
|
213
|
+
"""Return False; Firebird does not support schemas."""
|
|
207
214
|
return False
|
|
208
215
|
|
|
209
216
|
def role_exists(self, rolename: str) -> bool:
|
|
217
|
+
"""Return True if the named role or user exists in the Firebird database."""
|
|
210
218
|
curs = self.cursor()
|
|
211
219
|
curs.execute(
|
|
212
220
|
"SELECT DISTINCT USER FROM RDB$USER_PRIVILEGES WHERE USER = ? union "
|
|
@@ -218,6 +226,7 @@ class FirebirdDatabase(Database):
|
|
|
218
226
|
return len(rows) > 0
|
|
219
227
|
|
|
220
228
|
def drop_table(self, tablename: str) -> None:
|
|
229
|
+
"""Drop the named table from the Firebird database."""
|
|
221
230
|
# Firebird will thrown an error if there are foreign keys into the table.
|
|
222
231
|
tablename = self.type.quoted(tablename)
|
|
223
232
|
self.execute(f"DROP TABLE {tablename};")
|
execsql/db/mysql.py
CHANGED
|
@@ -73,6 +73,7 @@ class MySQLDatabase(Database):
|
|
|
73
73
|
)
|
|
74
74
|
|
|
75
75
|
def open_db(self) -> None:
|
|
76
|
+
"""Open a connection to the MySQL or MariaDB server."""
|
|
76
77
|
import pymysql as mysql_lib
|
|
77
78
|
|
|
78
79
|
def db_conn():
|
|
@@ -130,20 +131,23 @@ class MySQLDatabase(Database):
|
|
|
130
131
|
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
|
|
131
132
|
|
|
132
133
|
def exec_cmd(self, querycommand: str) -> None:
|
|
134
|
+
"""Execute a stored procedure by name."""
|
|
133
135
|
# The querycommand must be a stored function (/procedure)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
with self._cursor() as curs:
|
|
137
|
+
cmd = f"call {querycommand}();"
|
|
138
|
+
try:
|
|
139
|
+
curs.execute(cmd)
|
|
140
|
+
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
141
|
+
except Exception:
|
|
142
|
+
self.rollback()
|
|
143
|
+
raise
|
|
142
144
|
|
|
143
145
|
def schema_exists(self, schema_name: str) -> bool:
|
|
146
|
+
"""Return False; MySQL does not support schemas in the execsql sense."""
|
|
144
147
|
return False
|
|
145
148
|
|
|
146
149
|
def role_exists(self, rolename: str) -> bool:
|
|
150
|
+
"""Return True if the named role or user exists in the MySQL server."""
|
|
147
151
|
curs = self.cursor()
|
|
148
152
|
curs.execute(
|
|
149
153
|
"select distinct user as role from mysql.user where user = %s"
|
|
@@ -162,6 +166,7 @@ class MySQLDatabase(Database):
|
|
|
162
166
|
csv_file_obj: Any,
|
|
163
167
|
skipheader: bool,
|
|
164
168
|
) -> None:
|
|
169
|
+
"""Import a delimited file into a MySQL table."""
|
|
165
170
|
# Import a file to a table. Columns must be compatible.
|
|
166
171
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
167
172
|
if not self.table_exists(table_name, schema_name):
|
execsql/db/oracle.py
CHANGED
|
@@ -61,6 +61,7 @@ class OracleDatabase(Database):
|
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
def open_db(self) -> None:
|
|
64
|
+
"""Open a connection to the Oracle database."""
|
|
64
65
|
import cx_Oracle
|
|
65
66
|
|
|
66
67
|
def db_conn(db: OracleDatabase, db_name: str):
|
|
@@ -104,6 +105,7 @@ class OracleDatabase(Database):
|
|
|
104
105
|
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
|
|
105
106
|
|
|
106
107
|
def execute(self, sql: Any, paramlist: list | None = None) -> None:
|
|
108
|
+
"""Execute a SQL command, stripping any trailing semicolon for Oracle."""
|
|
107
109
|
# Strip any semicolon off the end and pass to the parent method.
|
|
108
110
|
if sql[-1:] == ";":
|
|
109
111
|
super().execute(sql[:-1], paramlist)
|
|
@@ -111,29 +113,34 @@ class OracleDatabase(Database):
|
|
|
111
113
|
super().execute(sql, paramlist)
|
|
112
114
|
|
|
113
115
|
def select_data(self, sql: str) -> tuple[list[str], list]:
|
|
116
|
+
"""Return column names and all rows from a SELECT statement."""
|
|
114
117
|
if sql[-1:] == ";":
|
|
115
118
|
return super().select_data(sql[:-1])
|
|
116
119
|
else:
|
|
117
120
|
return super().select_data(sql)
|
|
118
121
|
|
|
119
122
|
def select_rowsource(self, sql: str) -> Any:
|
|
123
|
+
"""Return column names and an iterable that yields rows one at a time."""
|
|
120
124
|
if sql[-1:] == ";":
|
|
121
125
|
return super().select_rowsource(sql[:-1])
|
|
122
126
|
else:
|
|
123
127
|
return super().select_rowsource(sql)
|
|
124
128
|
|
|
125
129
|
def select_rowdict(self, sql: str) -> Any:
|
|
130
|
+
"""Return column names and an iterable that yields rows as dictionaries."""
|
|
126
131
|
if sql[-1:] == ";":
|
|
127
132
|
return super().select_rowdict(sql[:-1])
|
|
128
133
|
else:
|
|
129
134
|
return super().select_rowdict(sql)
|
|
130
135
|
|
|
131
136
|
def schema_exists(self, schema_name: str) -> bool:
|
|
137
|
+
"""Raise DatabaseNotImplementedError; schema_exists is not supported for Oracle."""
|
|
132
138
|
from execsql.exceptions import DatabaseNotImplementedError
|
|
133
139
|
|
|
134
140
|
raise DatabaseNotImplementedError(self.name(), "schema_exists")
|
|
135
141
|
|
|
136
142
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
143
|
+
"""Return True if the named table exists in the Oracle database."""
|
|
137
144
|
curs = self.cursor()
|
|
138
145
|
params = {"tname": table_name}
|
|
139
146
|
owner_clause = ""
|
|
@@ -163,6 +170,7 @@ class OracleDatabase(Database):
|
|
|
163
170
|
column_name: str,
|
|
164
171
|
schema_name: str | None = None,
|
|
165
172
|
) -> bool:
|
|
173
|
+
"""Return True if the named column exists in the given Oracle table."""
|
|
166
174
|
curs = self.cursor()
|
|
167
175
|
params = {"tname": table_name, "cname": column_name}
|
|
168
176
|
owner_clause = ""
|
|
@@ -187,6 +195,7 @@ class OracleDatabase(Database):
|
|
|
187
195
|
return len(rows) > 0
|
|
188
196
|
|
|
189
197
|
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
198
|
+
"""Return a list of column names for the given Oracle table."""
|
|
190
199
|
curs = self.cursor()
|
|
191
200
|
params = {"tname": table_name}
|
|
192
201
|
owner_clause = ""
|
|
@@ -211,6 +220,7 @@ class OracleDatabase(Database):
|
|
|
211
220
|
return [row[0] for row in rows]
|
|
212
221
|
|
|
213
222
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
223
|
+
"""Return True if the named view exists in the Oracle database."""
|
|
214
224
|
curs = self.cursor()
|
|
215
225
|
params = {"vname": view_name}
|
|
216
226
|
owner_clause = ""
|
|
@@ -235,6 +245,7 @@ class OracleDatabase(Database):
|
|
|
235
245
|
return len(rows) > 0
|
|
236
246
|
|
|
237
247
|
def role_exists(self, rolename: str) -> bool:
|
|
248
|
+
"""Return True if the named role or user exists in the Oracle database."""
|
|
238
249
|
curs = self.cursor()
|
|
239
250
|
curs.execute(
|
|
240
251
|
"select role from dba_roles where role = :rname union "
|
|
@@ -246,19 +257,22 @@ class OracleDatabase(Database):
|
|
|
246
257
|
return len(rows) > 0
|
|
247
258
|
|
|
248
259
|
def drop_table(self, tablename: str) -> None:
|
|
260
|
+
"""Drop the named table with cascade constraints."""
|
|
249
261
|
tablename = self.type.quoted(tablename)
|
|
250
262
|
self.execute(f"drop table {tablename} cascade constraints")
|
|
251
263
|
|
|
252
264
|
def paramsubs(self, paramcount: int) -> str:
|
|
265
|
+
"""Return Oracle-style positional parameter placeholders (:1, :2, ...)."""
|
|
253
266
|
return ",".join(":" + str(d) for d in range(1, paramcount + 1))
|
|
254
267
|
|
|
255
268
|
def exec_cmd(self, querycommand: str) -> None:
|
|
269
|
+
"""Execute a stored function by name."""
|
|
256
270
|
# The querycommand must be a stored function (/procedure)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
271
|
+
with self._cursor() as curs:
|
|
272
|
+
cmd = f"select {querycommand}()"
|
|
273
|
+
try:
|
|
274
|
+
curs.execute(cmd)
|
|
275
|
+
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
276
|
+
except Exception:
|
|
277
|
+
self.rollback()
|
|
278
|
+
raise
|
execsql/db/postgres.py
CHANGED
|
@@ -70,6 +70,7 @@ class PostgresDatabase(Database):
|
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
def open_db(self) -> None:
|
|
73
|
+
"""Open a connection to the PostgreSQL database."""
|
|
73
74
|
import psycopg2
|
|
74
75
|
|
|
75
76
|
def db_conn(db: PostgresDatabase, db_name: str):
|
|
@@ -145,17 +146,19 @@ class PostgresDatabase(Database):
|
|
|
145
146
|
self.encoding = self.conn.encoding
|
|
146
147
|
|
|
147
148
|
def exec_cmd(self, querycommand: str) -> None:
|
|
149
|
+
"""Execute a stored function by name."""
|
|
148
150
|
# The querycommand must be a stored function (/procedure)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
with self._cursor() as curs:
|
|
152
|
+
cmd = f"select {querycommand}()"
|
|
153
|
+
try:
|
|
154
|
+
curs.execute(cmd)
|
|
155
|
+
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
156
|
+
except Exception:
|
|
157
|
+
self.rollback()
|
|
158
|
+
raise
|
|
157
159
|
|
|
158
160
|
def role_exists(self, rolename: str) -> bool:
|
|
161
|
+
"""Return True if the named role exists in the PostgreSQL cluster."""
|
|
159
162
|
curs = self.cursor()
|
|
160
163
|
curs.execute("select rolname from pg_roles where rolname = %s;", (rolename,))
|
|
161
164
|
rows = curs.fetchall()
|
|
@@ -163,6 +166,7 @@ class PostgresDatabase(Database):
|
|
|
163
166
|
return len(rows) > 0
|
|
164
167
|
|
|
165
168
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
169
|
+
"""Return True if the named table exists in the PostgreSQL database."""
|
|
166
170
|
curs = self.cursor()
|
|
167
171
|
if schema_name is not None:
|
|
168
172
|
params: list = [table_name]
|
|
@@ -195,6 +199,7 @@ class PostgresDatabase(Database):
|
|
|
195
199
|
return len(rows) > 0
|
|
196
200
|
|
|
197
201
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
202
|
+
"""Return True if the named view exists in the PostgreSQL database."""
|
|
198
203
|
curs = self.cursor()
|
|
199
204
|
if schema_name is not None:
|
|
200
205
|
params: list = [view_name]
|
|
@@ -227,9 +232,14 @@ class PostgresDatabase(Database):
|
|
|
227
232
|
return len(rows) > 0
|
|
228
233
|
|
|
229
234
|
def vacuum(self, argstring: str) -> None:
|
|
235
|
+
"""Run VACUUM with the given arguments in autocommit mode."""
|
|
230
236
|
self.commit()
|
|
231
237
|
self.conn.set_session(autocommit=True)
|
|
232
|
-
self.conn.cursor()
|
|
238
|
+
curs = self.conn.cursor()
|
|
239
|
+
try:
|
|
240
|
+
curs.execute(f"VACUUM {argstring};")
|
|
241
|
+
finally:
|
|
242
|
+
curs.close()
|
|
233
243
|
self.conn.set_session(autocommit=False)
|
|
234
244
|
|
|
235
245
|
def import_tabular_file(
|
|
@@ -239,6 +249,7 @@ class PostgresDatabase(Database):
|
|
|
239
249
|
csv_file_obj: Any,
|
|
240
250
|
skipheader: bool,
|
|
241
251
|
) -> None:
|
|
252
|
+
"""Import a delimited file into a PostgreSQL table, using COPY when possible."""
|
|
242
253
|
# Import a file to a table. Columns must be compatible.
|
|
243
254
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
244
255
|
if not self.table_exists(table_name, schema_name):
|
|
@@ -440,6 +451,7 @@ class PostgresDatabase(Database):
|
|
|
440
451
|
column_name: str,
|
|
441
452
|
file_name: str,
|
|
442
453
|
) -> None:
|
|
454
|
+
"""Import an entire binary file into a single column of a table."""
|
|
443
455
|
import psycopg2
|
|
444
456
|
|
|
445
457
|
with open(file_name, "rb") as f:
|