execsql2 2.23.0__py3-none-any.whl → 2.23.1__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/access.py +22 -6
- execsql/db/base.py +105 -1
- execsql/db/dsn.py +2 -0
- execsql/db/duckdb.py +95 -2
- execsql/db/mysql.py +11 -0
- execsql/db/oracle.py +12 -4
- execsql/db/sqlite.py +16 -0
- {execsql2-2.23.0.dist-info → execsql2-2.23.1.dist-info}/METADATA +2 -2
- {execsql2-2.23.0.dist-info → execsql2-2.23.1.dist-info}/RECORD +27 -27
- {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.23.0.dist-info → execsql2-2.23.1.dist-info}/WHEEL +0 -0
- {execsql2-2.23.0.dist-info → execsql2-2.23.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.23.0.dist-info → execsql2-2.23.1.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.23.0.dist-info → execsql2-2.23.1.dist-info}/licenses/NOTICE +0 -0
execsql/db/access.py
CHANGED
|
@@ -14,7 +14,7 @@ import time
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Any, cast
|
|
16
16
|
|
|
17
|
-
from execsql.db.base import Database
|
|
17
|
+
from execsql.db.base import Database, _fetched
|
|
18
18
|
from execsql.db.tiers import SupportTier
|
|
19
19
|
from execsql.exceptions import ErrInfo
|
|
20
20
|
from execsql.utils.errors import exception_desc, fatal_error
|
|
@@ -231,7 +231,13 @@ class AccessDatabase(Database):
|
|
|
231
231
|
if time.time() - self.last_dao_time < 5.0:
|
|
232
232
|
time.sleep(5 - (time.time() - self.last_dao_time))
|
|
233
233
|
|
|
234
|
-
def execute(
|
|
234
|
+
def execute(
|
|
235
|
+
self,
|
|
236
|
+
sqlcmd: Any,
|
|
237
|
+
paramlist: list | None = None,
|
|
238
|
+
*,
|
|
239
|
+
fetch: bool = False,
|
|
240
|
+
) -> tuple[list[str], list] | None:
|
|
235
241
|
"""Execute a SQL command, handling encoding, DAO flush, and temporary queries."""
|
|
236
242
|
|
|
237
243
|
# A shortcut to self.cursor().execute() that handles encoding and that
|
|
@@ -239,9 +245,15 @@ class AccessDatabase(Database):
|
|
|
239
245
|
# to allow Jet's read buffer to be flushed (see https://support.microsoft.com/en-us/kb/225048).
|
|
240
246
|
# This also handles the 'CREATE TEMPORARY QUERY' extension to Access.
|
|
241
247
|
# For Access, commands in a tuple (batch) are executed singly.
|
|
242
|
-
def exec1(sql: str, paramlist: list | None) -> None:
|
|
248
|
+
def exec1(sql: str, paramlist: list | None) -> tuple[list[str], list] | None:
|
|
243
249
|
tqd = self.temp_rx.match(sql)
|
|
244
250
|
if tqd:
|
|
251
|
+
if self.conn is not None and self.holding():
|
|
252
|
+
self.warn_once(
|
|
253
|
+
"temporary query",
|
|
254
|
+
"CREATE TEMPORARY QUERY reopens the Access connection, which rolls back statements "
|
|
255
|
+
"not yet committed under AUTOCOMMIT OFF or BEGIN BATCH.",
|
|
256
|
+
)
|
|
245
257
|
qn = tqd.group(3)
|
|
246
258
|
qsql = sql[tqd.end() :]
|
|
247
259
|
if self.dao_conn is None:
|
|
@@ -258,7 +270,9 @@ class AccessDatabase(Database):
|
|
|
258
270
|
self.conn = None
|
|
259
271
|
if tqd.group(1) and tqd.group(1).strip().lower()[:4] == "temp" and qn not in self.temp_query_names:
|
|
260
272
|
self.temp_query_names.append(qn)
|
|
273
|
+
return None
|
|
261
274
|
else:
|
|
275
|
+
self.before_statement(sql)
|
|
262
276
|
self.dao_flush_check()
|
|
263
277
|
with self._cursor() as curs:
|
|
264
278
|
encoded_sql: Any
|
|
@@ -271,12 +285,14 @@ class AccessDatabase(Database):
|
|
|
271
285
|
else:
|
|
272
286
|
curs.execute(encoded_sql, paramlist)
|
|
273
287
|
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
288
|
+
return _fetched(curs) if fetch else None
|
|
274
289
|
|
|
275
290
|
if type(sqlcmd) in (list, tuple):
|
|
291
|
+
result = None
|
|
276
292
|
for sql in sqlcmd:
|
|
277
|
-
exec1(sql, paramlist)
|
|
278
|
-
|
|
279
|
-
|
|
293
|
+
result = exec1(sql, paramlist)
|
|
294
|
+
return result
|
|
295
|
+
return exec1(sqlcmd, paramlist)
|
|
280
296
|
|
|
281
297
|
def exec_cmd(self, querycommand: str) -> None:
|
|
282
298
|
"""Execute a stored query command via DAO."""
|
execsql/db/base.py
CHANGED
|
@@ -29,6 +29,21 @@ import execsql.state as _state
|
|
|
29
29
|
|
|
30
30
|
__all__ = ["Database", "DatabasePool"]
|
|
31
31
|
|
|
32
|
+
# Leading whitespace and comments, skipped to find a statement's first words.
|
|
33
|
+
_LEADING_NOISE_RX = re.compile(r"(?:\s+|--[^\n]*(?:\n|$)|/\*.*?\*/)*", re.S)
|
|
34
|
+
|
|
35
|
+
# Statements that manage a transaction themselves; one is never opened in front of them.
|
|
36
|
+
_TRANSACTION_CONTROL_RX = re.compile(
|
|
37
|
+
r"(?:BEGIN|START\s+TRANSACTION|COMMIT|END|ROLLBACK|ABORT|SAVEPOINT|RELEASE)\b",
|
|
38
|
+
re.I,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def statement_start(sql: str) -> str:
|
|
43
|
+
"""*sql* from its first keyword on: leading whitespace and comments removed."""
|
|
44
|
+
match = _LEADING_NOISE_RX.match(sql)
|
|
45
|
+
return sql[match.end() :] if match else sql
|
|
46
|
+
|
|
32
47
|
|
|
33
48
|
def _default_dt_cast() -> dict[type, Callable]:
|
|
34
49
|
"""Build the default type-cast mapping used by all database backends."""
|
|
@@ -46,6 +61,13 @@ def _default_dt_cast() -> dict[type, Callable]:
|
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
|
|
64
|
+
def _fetched(curs: Any) -> tuple[list[str], list] | None:
|
|
65
|
+
"""The rows a just-executed cursor produced, as ``(column_names, rows)``; ``None`` if none."""
|
|
66
|
+
if curs.description is None:
|
|
67
|
+
return None
|
|
68
|
+
return [d[0] for d in curs.description], curs.fetchall()
|
|
69
|
+
|
|
70
|
+
|
|
49
71
|
class Database(ABC):
|
|
50
72
|
"""Abstract base class for every DBMS adapter.
|
|
51
73
|
|
|
@@ -215,13 +237,17 @@ class Database(ABC):
|
|
|
215
237
|
"""Return a comma-separated string of *paramcount* parameter placeholders."""
|
|
216
238
|
return ",".join((self.paramstr,) * paramcount)
|
|
217
239
|
|
|
218
|
-
def execute(self, sql: Any, paramlist: list | None = None) -> None:
|
|
240
|
+
def execute(self, sql: Any, paramlist: list | None = None, *, fetch: bool = False) -> tuple[list[str], list] | None:
|
|
219
241
|
"""Execute *sql* (optionally with *paramlist*), updating ``$LAST_ROWCOUNT``.
|
|
220
242
|
|
|
243
|
+
With *fetch*, return ``(column_names, rows)`` when the statement
|
|
244
|
+
produces rows (a prompt shows them), else ``None``.
|
|
245
|
+
|
|
221
246
|
Rolls back the current transaction and re-raises on any driver error.
|
|
222
247
|
"""
|
|
223
248
|
if type(sql) in (tuple, list):
|
|
224
249
|
sql = " ".join(sql)
|
|
250
|
+
self.before_statement(sql)
|
|
225
251
|
try:
|
|
226
252
|
with self._cursor() as curs:
|
|
227
253
|
if paramlist is None:
|
|
@@ -232,6 +258,7 @@ class Database(ABC):
|
|
|
232
258
|
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
233
259
|
except Exception:
|
|
234
260
|
pass # Non-critical: some drivers lack rowcount support.
|
|
261
|
+
return _fetched(curs) if fetch else None
|
|
235
262
|
except Exception:
|
|
236
263
|
try:
|
|
237
264
|
self.rollback()
|
|
@@ -244,6 +271,81 @@ class Database(ABC):
|
|
|
244
271
|
"""Execute a stored procedure or function by name."""
|
|
245
272
|
...
|
|
246
273
|
|
|
274
|
+
# ------------------------------------------------------------------
|
|
275
|
+
# Holding statements until commit (AUTOCOMMIT OFF, BEGIN BATCH)
|
|
276
|
+
# ------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
#: Statements the DBMS commits on its own, with everything run before
|
|
279
|
+
#: them, even inside a transaction (MySQL and Oracle DDL). Matched against
|
|
280
|
+
#: the start of the statement; a warning names each one run while holding.
|
|
281
|
+
implicit_commit_rx: ClassVar[re.Pattern[str] | None] = None
|
|
282
|
+
|
|
283
|
+
#: Statements that cannot run inside a transaction on this DBMS; one is
|
|
284
|
+
#: not opened in front of them.
|
|
285
|
+
no_transaction_rx: ClassVar[re.Pattern[str] | None] = None
|
|
286
|
+
|
|
287
|
+
#: Set on a connection whose driver could only connect in autocommit mode
|
|
288
|
+
#: (an ODBC DSN without transactions): nothing can be held back.
|
|
289
|
+
driver_autocommits: bool = False
|
|
290
|
+
|
|
291
|
+
def holding(self) -> bool:
|
|
292
|
+
"""Whether statements on this connection wait for a commit.
|
|
293
|
+
|
|
294
|
+
True while AUTOCOMMIT is OFF for this database, or inside BEGIN BATCH
|
|
295
|
+
(which holds every database).
|
|
296
|
+
"""
|
|
297
|
+
if not self.autocommit:
|
|
298
|
+
return True
|
|
299
|
+
status = _state.status
|
|
300
|
+
return status is not None and status.batch.in_batch()
|
|
301
|
+
|
|
302
|
+
def before_statement(self, sql: str) -> None:
|
|
303
|
+
"""Make sure *sql* is held until commit when execsql is holding statements.
|
|
304
|
+
|
|
305
|
+
Most drivers already hold every statement: they open a transaction
|
|
306
|
+
before the first one and keep it open until commit or rollback. An
|
|
307
|
+
adapter whose driver does not (SQLite for DDL, DuckDB for everything)
|
|
308
|
+
overrides :meth:`begin_transaction`. Where the DBMS itself commits a
|
|
309
|
+
statement regardless, a warning says so, since the work cannot be
|
|
310
|
+
rolled back.
|
|
311
|
+
"""
|
|
312
|
+
if not self.holding():
|
|
313
|
+
return
|
|
314
|
+
if self.driver_autocommits:
|
|
315
|
+
self.warn_once(
|
|
316
|
+
"driver",
|
|
317
|
+
f"{self.name()} cannot hold statements until COMMIT: its driver commits each one as it runs. "
|
|
318
|
+
"AUTOCOMMIT OFF and BEGIN BATCH have no effect on it.",
|
|
319
|
+
)
|
|
320
|
+
return
|
|
321
|
+
start = statement_start(sql)
|
|
322
|
+
if self.implicit_commit_rx is not None and (m := self.implicit_commit_rx.match(start)):
|
|
323
|
+
from execsql.utils.errors import write_warning
|
|
324
|
+
|
|
325
|
+
write_warning(
|
|
326
|
+
f"{self.type.dbms_id} commits {' '.join(m.group(0).upper().split())} immediately, together with "
|
|
327
|
+
"everything run before it; it cannot be rolled back.",
|
|
328
|
+
always=True,
|
|
329
|
+
)
|
|
330
|
+
if _TRANSACTION_CONTROL_RX.match(start):
|
|
331
|
+
return
|
|
332
|
+
if self.no_transaction_rx is not None and self.no_transaction_rx.match(start):
|
|
333
|
+
return
|
|
334
|
+
self.begin_transaction()
|
|
335
|
+
|
|
336
|
+
def begin_transaction(self) -> None:
|
|
337
|
+
"""Open a transaction if none is open. Most drivers do this themselves; a no-op here."""
|
|
338
|
+
|
|
339
|
+
def warn_once(self, key: str, message: str) -> None:
|
|
340
|
+
"""Write *message* as a warning the first time *key* comes up on this connection."""
|
|
341
|
+
warned: set[str] = self.__dict__.setdefault("_warned", set())
|
|
342
|
+
if key in warned:
|
|
343
|
+
return
|
|
344
|
+
warned.add(key)
|
|
345
|
+
from execsql.utils.errors import write_warning
|
|
346
|
+
|
|
347
|
+
write_warning(message, always=True)
|
|
348
|
+
|
|
247
349
|
def autocommit_on(self) -> None:
|
|
248
350
|
"""Enable autocommit mode so each statement is committed immediately."""
|
|
249
351
|
self.autocommit = True
|
|
@@ -562,6 +664,7 @@ class Database(ABC):
|
|
|
562
664
|
rows = iter(rowsource)
|
|
563
665
|
eof = False
|
|
564
666
|
total_rows = 0
|
|
667
|
+
self.before_statement(sql)
|
|
565
668
|
|
|
566
669
|
# Optional rich progress bar for long-running imports.
|
|
567
670
|
use_progress = getattr(_state.conf, "show_progress", False)
|
|
@@ -745,6 +848,7 @@ class Database(ABC):
|
|
|
745
848
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
746
849
|
quoted_col = self.quote_identifier(column_name)
|
|
747
850
|
sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
|
|
851
|
+
self.before_statement(sql)
|
|
748
852
|
with self._cursor() as curs:
|
|
749
853
|
curs.execute(sql, (filedata,))
|
|
750
854
|
|
execsql/db/dsn.py
CHANGED
|
@@ -109,6 +109,8 @@ class DsnDatabase(Database):
|
|
|
109
109
|
if "Optional feature not implemented" in excdesc:
|
|
110
110
|
try:
|
|
111
111
|
_dsn_connect(autocommit=True)
|
|
112
|
+
# This ODBC driver has no transactions: nothing can be held.
|
|
113
|
+
self.driver_autocommits = True
|
|
112
114
|
except Exception as e:
|
|
113
115
|
raise ErrInfo(
|
|
114
116
|
type="exception",
|
execsql/db/duckdb.py
CHANGED
|
@@ -8,18 +8,55 @@ analytics databases via the ``duckdb`` package. Corresponds to ``-t k``
|
|
|
8
8
|
on the CLI.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import re
|
|
11
12
|
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
12
14
|
|
|
13
|
-
from execsql.db.base import Database
|
|
15
|
+
from execsql.db.base import Database, statement_start
|
|
14
16
|
from execsql.exceptions import ErrInfo
|
|
15
17
|
from execsql.utils.errors import exception_desc, fatal_error
|
|
16
18
|
import execsql.state as _state
|
|
17
19
|
|
|
18
20
|
__all__ = ["DuckDBDatabase"]
|
|
19
21
|
|
|
22
|
+
# Statements that return rows; any other statement DuckDB answers with a
|
|
23
|
+
# one-column "Count" result (rows inserted, updated or deleted).
|
|
24
|
+
_QUERY_RX = re.compile(r"(?:SELECT|WITH|VALUES|FROM|TABLE|SHOW|DESCRIBE|SUMMARIZE|EXPLAIN|PRAGMA|CALL)\b|\(", re.I)
|
|
25
|
+
|
|
26
|
+
# A statement that opens a transaction, or one that ends it.
|
|
27
|
+
_TRANSACTION_RX = re.compile(r"(?P<begin>BEGIN|START\s+TRANSACTION)\b|(?P<end>COMMIT|END|ROLLBACK|ABORT)\b", re.I)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _SharedCursor:
|
|
31
|
+
"""The connection, used as a cursor so a statement joins its open transaction.
|
|
32
|
+
|
|
33
|
+
A DuckDB cursor is a separate connection with a transaction of its own,
|
|
34
|
+
and closing it rolls that transaction back. ``close()`` therefore leaves
|
|
35
|
+
the connection open.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, conn: Any) -> None:
|
|
39
|
+
self._conn = conn
|
|
40
|
+
|
|
41
|
+
def __getattr__(self, name: str) -> Any:
|
|
42
|
+
return getattr(self._conn, name)
|
|
43
|
+
|
|
44
|
+
def close(self) -> None:
|
|
45
|
+
pass
|
|
46
|
+
|
|
20
47
|
|
|
21
48
|
class DuckDBDatabase(Database):
|
|
22
|
-
"""DuckDB in-process analytics adapter using the duckdb package.
|
|
49
|
+
"""DuckDB in-process analytics adapter using the duckdb package.
|
|
50
|
+
|
|
51
|
+
The duckdb driver commits each statement unless a transaction was begun,
|
|
52
|
+
and has no way to ask whether one is open, so this adapter begins one
|
|
53
|
+
itself while statements are held (AUTOCOMMIT OFF, BEGIN BATCH) and keeps
|
|
54
|
+
track of it. While it is open, every statement runs on the connection
|
|
55
|
+
itself rather than on a cursor (see :class:`_SharedCursor`).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
#: A transaction this adapter or a script's own BEGIN opened, not yet ended.
|
|
59
|
+
in_transaction: bool = False
|
|
23
60
|
|
|
24
61
|
def __init__(self, DuckDB_fn: str) -> None:
|
|
25
62
|
try:
|
|
@@ -43,6 +80,62 @@ class DuckDBDatabase(Database):
|
|
|
43
80
|
def __repr__(self) -> str:
|
|
44
81
|
return f"DuckDBDatabase({self.db_name!r})"
|
|
45
82
|
|
|
83
|
+
def cursor(self) -> Any:
|
|
84
|
+
"""A cursor, or while a transaction is open, the connection itself."""
|
|
85
|
+
if self.conn is None:
|
|
86
|
+
self.open_db()
|
|
87
|
+
if self.in_transaction:
|
|
88
|
+
return _SharedCursor(self.conn)
|
|
89
|
+
return self.conn.cursor()
|
|
90
|
+
|
|
91
|
+
def begin_transaction(self) -> None:
|
|
92
|
+
"""Open a transaction if none is open."""
|
|
93
|
+
if self.conn is None:
|
|
94
|
+
self.open_db()
|
|
95
|
+
if not self.in_transaction:
|
|
96
|
+
# A second BEGIN would abort the open transaction, hence the tracking.
|
|
97
|
+
self.conn.execute("BEGIN TRANSACTION")
|
|
98
|
+
self.in_transaction = True
|
|
99
|
+
|
|
100
|
+
def execute(self, sql: Any, paramlist: list | None = None, *, fetch: bool = False) -> tuple[list[str], list] | None:
|
|
101
|
+
"""Execute *sql*, noting a transaction the statement itself begins or ends.
|
|
102
|
+
|
|
103
|
+
The driver's ``rowcount`` is always -1: DuckDB answers an INSERT,
|
|
104
|
+
UPDATE or DELETE with a one-column ``Count`` result instead, which is
|
|
105
|
+
read here to set ``$LAST_ROWCOUNT`` as other drivers do.
|
|
106
|
+
"""
|
|
107
|
+
text = " ".join(sql) if type(sql) in (tuple, list) else sql
|
|
108
|
+
start = statement_start(text)
|
|
109
|
+
m = _TRANSACTION_RX.match(start)
|
|
110
|
+
if m and m.group("begin"):
|
|
111
|
+
self.in_transaction = True # so the BEGIN itself runs on the connection
|
|
112
|
+
query = bool(_QUERY_RX.match(start))
|
|
113
|
+
result = super().execute(sql, paramlist, fetch=fetch or not query)
|
|
114
|
+
if m and m.group("end"):
|
|
115
|
+
self.in_transaction = False
|
|
116
|
+
if not query and result is not None and result[0] == ["Count"]:
|
|
117
|
+
rows = result[1]
|
|
118
|
+
if _state.subvars is not None:
|
|
119
|
+
_state.subvars.add_substitution("$LAST_ROWCOUNT", rows[0][0] if rows else -1)
|
|
120
|
+
return None
|
|
121
|
+
return result if fetch else None
|
|
122
|
+
|
|
123
|
+
def commit(self) -> None:
|
|
124
|
+
"""Commit the open transaction if autocommit is enabled."""
|
|
125
|
+
super().commit()
|
|
126
|
+
if self.autocommit:
|
|
127
|
+
self.in_transaction = False
|
|
128
|
+
|
|
129
|
+
def rollback(self) -> None:
|
|
130
|
+
"""Roll back the open transaction."""
|
|
131
|
+
super().rollback()
|
|
132
|
+
self.in_transaction = False
|
|
133
|
+
|
|
134
|
+
def close(self) -> None:
|
|
135
|
+
"""Close the connection; an open transaction is rolled back."""
|
|
136
|
+
super().close()
|
|
137
|
+
self.in_transaction = False
|
|
138
|
+
|
|
46
139
|
def open_db(self) -> None:
|
|
47
140
|
"""Open a connection to the DuckDB database file."""
|
|
48
141
|
import duckdb
|
execsql/db/mysql.py
CHANGED
|
@@ -49,6 +49,17 @@ def _crlf_terminated(filename: str) -> bool:
|
|
|
49
49
|
class MySQLDatabase(Database):
|
|
50
50
|
"""MySQL and MariaDB adapter using the pymysql package."""
|
|
51
51
|
|
|
52
|
+
#: Statements MySQL commits at once, together with everything before them
|
|
53
|
+
#: ("Statements That Cause an Implicit Commit" in the MySQL manual).
|
|
54
|
+
#: CREATE and DROP TEMPORARY TABLE are the exception.
|
|
55
|
+
implicit_commit_rx = re.compile(
|
|
56
|
+
r"(?:CREATE|ALTER|DROP)\s+(?!TEMPORARY\b)(?:OR\s+REPLACE\s+)?(?:UNIQUE\s+|FULLTEXT\s+|SPATIAL\s+)?\w+"
|
|
57
|
+
r"|RENAME\s+\w+|TRUNCATE\b|GRANT\b|REVOKE\b|LOCK\s+TABLES?\b|UNLOCK\s+TABLES?\b"
|
|
58
|
+
r"|BEGIN\b|START\s+TRANSACTION\b|(?:ANALYZE|CHECK|OPTIMIZE|REPAIR)\s+TABLE\b|FLUSH\b"
|
|
59
|
+
r"|(?:UN)?INSTALL\s+PLUGIN\b|SET\s+PASSWORD\b|SET\s+(?:SESSION\s+|@@)?autocommit\b",
|
|
60
|
+
re.I,
|
|
61
|
+
)
|
|
62
|
+
|
|
52
63
|
def __init__(
|
|
53
64
|
self,
|
|
54
65
|
server_name: str,
|
execsql/db/oracle.py
CHANGED
|
@@ -8,6 +8,7 @@ via the ``oracledb`` driver (python-oracledb). Corresponds to ``-t o``
|
|
|
8
8
|
on the CLI.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import re
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
13
14
|
from execsql.db.base import Database
|
|
@@ -23,6 +24,14 @@ __all__ = ["OracleDatabase"]
|
|
|
23
24
|
class OracleDatabase(Database):
|
|
24
25
|
"""Oracle adapter using the cx_Oracle (python-oracledb) driver."""
|
|
25
26
|
|
|
27
|
+
#: Oracle commits every DDL statement at once, together with everything
|
|
28
|
+
#: before it, including CREATE GLOBAL TEMPORARY TABLE.
|
|
29
|
+
implicit_commit_rx = re.compile(
|
|
30
|
+
r"(?:CREATE|ALTER|DROP)\s+(?:OR\s+REPLACE\s+)?(?:GLOBAL\s+TEMPORARY\s+|UNIQUE\s+|BITMAP\s+)?\w+"
|
|
31
|
+
r"|TRUNCATE\s+\w+|RENAME\b|GRANT\b|REVOKE\b|COMMENT\s+ON\b|(?:NO)?AUDIT\b|ANALYZE\b|PURGE\b|FLASHBACK\b",
|
|
32
|
+
re.I,
|
|
33
|
+
)
|
|
34
|
+
|
|
26
35
|
#: No Oracle server runs in CI; this adapter has no tests.
|
|
27
36
|
support_tier = SupportTier.BEST_EFFORT
|
|
28
37
|
support_tier_name = "Oracle"
|
|
@@ -110,13 +119,12 @@ class OracleDatabase(Database):
|
|
|
110
119
|
msg = f"Failed to open Oracle database {self.db_name} on {self.server_name}"
|
|
111
120
|
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
|
|
112
121
|
|
|
113
|
-
def execute(self, sql: Any, paramlist: list | None = None) -> None:
|
|
122
|
+
def execute(self, sql: Any, paramlist: list | None = None, *, fetch: bool = False) -> tuple[list[str], list] | None:
|
|
114
123
|
"""Execute a SQL command, stripping any trailing semicolon for Oracle."""
|
|
115
124
|
# Strip any semicolon off the end and pass to the parent method.
|
|
116
125
|
if sql[-1:] == ";":
|
|
117
|
-
super().execute(sql[:-1], paramlist)
|
|
118
|
-
|
|
119
|
-
super().execute(sql, paramlist)
|
|
126
|
+
return super().execute(sql[:-1], paramlist, fetch=fetch)
|
|
127
|
+
return super().execute(sql, paramlist, fetch=fetch)
|
|
120
128
|
|
|
121
129
|
def select_data(self, sql: str) -> tuple[list[str], list]:
|
|
122
130
|
"""Return column names and all rows from a SELECT statement."""
|
execsql/db/sqlite.py
CHANGED
|
@@ -26,6 +26,10 @@ DEFAULT_CONNECT_TIMEOUT = 30 # seconds
|
|
|
26
26
|
class SQLiteDatabase(Database):
|
|
27
27
|
"""SQLite adapter using the Python standard-library sqlite3 module."""
|
|
28
28
|
|
|
29
|
+
#: SQLite refuses VACUUM, ATTACH and DETACH inside a transaction, and
|
|
30
|
+
#: ignores some PRAGMAs there (``foreign_keys``).
|
|
31
|
+
no_transaction_rx = re.compile(r"(?:VACUUM|ATTACH|DETACH|PRAGMA)\b", re.I)
|
|
32
|
+
|
|
29
33
|
def __init__(self, SQLite_fn: str, timeout: float = DEFAULT_CONNECT_TIMEOUT) -> None:
|
|
30
34
|
try:
|
|
31
35
|
import sqlite3 # noqa: F401
|
|
@@ -48,6 +52,18 @@ class SQLiteDatabase(Database):
|
|
|
48
52
|
def __repr__(self) -> str:
|
|
49
53
|
return f"SQLiteDatabase({self.db_name!r})"
|
|
50
54
|
|
|
55
|
+
def begin_transaction(self) -> None:
|
|
56
|
+
"""Open a transaction if none is open.
|
|
57
|
+
|
|
58
|
+
Python's sqlite3 module opens one on its own only before INSERT,
|
|
59
|
+
UPDATE, DELETE and REPLACE. Without this, a CREATE TABLE or DROP run
|
|
60
|
+
under AUTOCOMMIT OFF or in a batch would be committed at once.
|
|
61
|
+
"""
|
|
62
|
+
if self.conn is None:
|
|
63
|
+
self.open_db()
|
|
64
|
+
if not self.conn.in_transaction:
|
|
65
|
+
self.conn.execute("BEGIN")
|
|
66
|
+
|
|
51
67
|
def open_db(self) -> None:
|
|
52
68
|
"""Open a connection to the SQLite database file."""
|
|
53
69
|
import sqlite3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.23.
|
|
3
|
+
Version: 2.23.1
|
|
4
4
|
Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
|
|
5
5
|
Project-URL: Homepage, https://execsql2.readthedocs.io
|
|
6
6
|
Project-URL: Repository, https://github.com/geocoug/execsql
|
|
@@ -393,7 +393,7 @@ execsql-format --no-sql --in-place scripts/
|
|
|
393
393
|
```yaml
|
|
394
394
|
repos:
|
|
395
395
|
- repo: https://github.com/geocoug/execsql
|
|
396
|
-
rev: v2.23.
|
|
396
|
+
rev: v2.23.1
|
|
397
397
|
hooks:
|
|
398
398
|
- id: execsql-format
|
|
399
399
|
```
|
|
@@ -19,16 +19,16 @@ execsql/cli/run.py,sha256=Yb-Ri3cds0st1hUn4_w-PHMphTsArHhC4fx919SW9gU,39390
|
|
|
19
19
|
execsql/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
execsql/data/execsql.conf.template,sha256=qqH_G3SiKBepljY3Go4rDDlhe3pIM6gOgsjAnLn0arg,11244
|
|
21
21
|
execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
|
|
22
|
-
execsql/db/access.py,sha256=
|
|
23
|
-
execsql/db/base.py,sha256=
|
|
24
|
-
execsql/db/dsn.py,sha256=
|
|
25
|
-
execsql/db/duckdb.py,sha256=
|
|
22
|
+
execsql/db/access.py,sha256=2VlfEDXvV8xoi1t_hKzEaIidCJ1XGerYFPFvW5FqVBI,20489
|
|
23
|
+
execsql/db/base.py,sha256=2bK1TBU9IAHnQQcdgJ-wEwm5TzrhnwO6j6BGjR9VAHg,40656
|
|
24
|
+
execsql/db/dsn.py,sha256=hC_oWrBz2NSGyAOjZsROxGHs9eI_CSpnnken12wH1hU,6663
|
|
25
|
+
execsql/db/duckdb.py,sha256=h8fh_5Ps1UcXvfyG9UlSyP32Yoa0W8sn246RlQ4xOMU,6970
|
|
26
26
|
execsql/db/factory.py,sha256=C_cyi3GjNx0opuwc6maFFeCSkjfOfoUjuf68YT4VAHE,5377
|
|
27
27
|
execsql/db/firebird.py,sha256=fbzlGB6DeR-S9RFLUD4tJYPN-_76tyRLBkQl5LRJTDo,9640
|
|
28
|
-
execsql/db/mysql.py,sha256=
|
|
29
|
-
execsql/db/oracle.py,sha256=
|
|
28
|
+
execsql/db/mysql.py,sha256=vG_5NPoe82FLvdfY4e7ZnjX0-mgts4D3S40LwZKb7U8,20489
|
|
29
|
+
execsql/db/oracle.py,sha256=jjXwhk_og1qHqdAB16i_dZYXY0Ze7Bg5EvKdCKwjQtE,12667
|
|
30
30
|
execsql/db/postgres.py,sha256=HfrWuJ5-K5le1sY9iGv_E51JHciPIdA0UvyLsTKlnO4,22568
|
|
31
|
-
execsql/db/sqlite.py,sha256=
|
|
31
|
+
execsql/db/sqlite.py,sha256=zfe-KNAuTIHDh_qk3yzCo6fMerlmtt7UFmg-lbdZFm4,12167
|
|
32
32
|
execsql/db/sqlserver.py,sha256=O4E9OD3V3Xowsjl3KMdYHNCBeETlkJS9HUzNKEKuA9A,8943
|
|
33
33
|
execsql/db/tiers.py,sha256=PH2sUYwlYqcANGVKXprWQKVIlM6fpWU9q74djn8oFwc,2924
|
|
34
34
|
execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
|
|
@@ -102,23 +102,23 @@ execsql/utils/numeric.py,sha256=_7bi5R6pHjMCkOPne_Sh5nUj-B8e9g6WFmjptFHaunA,1573
|
|
|
102
102
|
execsql/utils/regex.py,sha256=B3mSBhRRx_DpUnX_t9IoYd05KvWAebiLMCZqQAmEwcw,3885
|
|
103
103
|
execsql/utils/strings.py,sha256=UQNjpRCEFa1UO6feU-M-9e24wWAvizs_iu_4fFusLxo,8516
|
|
104
104
|
execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
|
|
105
|
-
execsql2-2.23.
|
|
106
|
-
execsql2-2.23.
|
|
107
|
-
execsql2-2.23.
|
|
108
|
-
execsql2-2.23.
|
|
109
|
-
execsql2-2.23.
|
|
110
|
-
execsql2-2.23.
|
|
111
|
-
execsql2-2.23.
|
|
112
|
-
execsql2-2.23.
|
|
113
|
-
execsql2-2.23.
|
|
114
|
-
execsql2-2.23.
|
|
115
|
-
execsql2-2.23.
|
|
116
|
-
execsql2-2.23.
|
|
117
|
-
execsql2-2.23.
|
|
118
|
-
execsql2-2.23.
|
|
119
|
-
execsql2-2.23.
|
|
120
|
-
execsql2-2.23.
|
|
121
|
-
execsql2-2.23.
|
|
122
|
-
execsql2-2.23.
|
|
123
|
-
execsql2-2.23.
|
|
124
|
-
execsql2-2.23.
|
|
105
|
+
execsql2-2.23.1.data/data/execsql2_extras/README.md,sha256=vX4NTL095dUoA_hesyRMGYBorEZ_Y_tJ9qrd-MVV09I,5032
|
|
106
|
+
execsql2-2.23.1.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
|
|
107
|
+
execsql2-2.23.1.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
|
|
108
|
+
execsql2-2.23.1.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
|
|
109
|
+
execsql2-2.23.1.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
|
|
110
|
+
execsql2-2.23.1.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
|
|
111
|
+
execsql2-2.23.1.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
|
|
112
|
+
execsql2-2.23.1.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
|
|
113
|
+
execsql2-2.23.1.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
|
|
114
|
+
execsql2-2.23.1.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
|
|
115
|
+
execsql2-2.23.1.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
|
|
116
|
+
execsql2-2.23.1.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
|
|
117
|
+
execsql2-2.23.1.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
|
|
118
|
+
execsql2-2.23.1.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
|
|
119
|
+
execsql2-2.23.1.dist-info/METADATA,sha256=Zp_UKzwuqCVFmA3-yQrsaenr3QI_sFkgWAtNAQpl-OE,21504
|
|
120
|
+
execsql2-2.23.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
121
|
+
execsql2-2.23.1.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
|
|
122
|
+
execsql2-2.23.1.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
|
|
123
|
+
execsql2-2.23.1.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
|
|
124
|
+
execsql2-2.23.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/example_config_prompt.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|