execsql2 2.2.1__py3-none-any.whl → 2.4.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/config.py +52 -0
- execsql/db/access.py +11 -3
- execsql/db/base.py +180 -135
- execsql/db/dsn.py +4 -0
- execsql/db/duckdb.py +4 -0
- execsql/db/factory.py +21 -0
- execsql/db/firebird.py +4 -0
- execsql/db/mysql.py +4 -0
- execsql/db/oracle.py +4 -0
- execsql/db/postgres.py +3 -0
- execsql/db/sqlite.py +3 -0
- execsql/db/sqlserver.py +11 -2
- execsql/exceptions.py +18 -0
- execsql/exporters/base.py +6 -0
- execsql/exporters/delimited.py +36 -0
- execsql/exporters/duckdb.py +4 -0
- execsql/exporters/feather.py +4 -0
- execsql/exporters/html.py +6 -0
- execsql/exporters/json.py +5 -6
- execsql/exporters/latex.py +4 -0
- execsql/exporters/ods.py +28 -7
- execsql/exporters/parquet.py +3 -0
- execsql/exporters/pretty.py +5 -0
- execsql/exporters/raw.py +5 -3
- execsql/exporters/sqlite.py +4 -0
- execsql/exporters/templates.py +16 -6
- execsql/exporters/values.py +4 -0
- execsql/exporters/xls.py +26 -7
- execsql/exporters/xml.py +3 -0
- execsql/exporters/zip.py +15 -0
- execsql/importers/base.py +2 -0
- execsql/importers/csv.py +2 -0
- execsql/importers/feather.py +2 -0
- execsql/importers/ods.py +2 -0
- execsql/importers/xls.py +2 -0
- execsql/metacommands/__init__.py +177 -1968
- execsql/metacommands/dispatch.py +2011 -0
- execsql/models.py +7 -0
- execsql/parser.py +10 -0
- execsql/script/__init__.py +95 -0
- execsql/script/control.py +162 -0
- execsql/{script.py → script/engine.py} +144 -406
- execsql/script/variables.py +281 -0
- execsql/types.py +29 -0
- execsql/utils/auth.py +2 -0
- execsql/utils/crypto.py +4 -6
- execsql/utils/datetime.py +1 -0
- execsql/utils/errors.py +11 -0
- execsql/utils/fileio.py +18 -0
- execsql/utils/gui.py +46 -0
- execsql/utils/mail.py +7 -17
- execsql/utils/numeric.py +2 -0
- execsql/utils/regex.py +9 -0
- execsql/utils/strings.py +16 -0
- execsql/utils/timer.py +2 -0
- execsql2-2.4.0.data/data/execsql2_extras/README.md +65 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/METADATA +8 -1
- execsql2-2.4.0.dist-info/RECORD +108 -0
- execsql2-2.2.1.data/data/execsql2_extras/READ_ME.rst +0 -127
- execsql2-2.2.1.dist-info/RECORD +0 -104
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.0.dist-info}/licenses/NOTICE +0 -0
execsql/db/base.py
CHANGED
|
@@ -14,8 +14,10 @@ open :class:`Database` instances and tracks which connection is currently
|
|
|
14
14
|
active. It is the canonical ``_state.dbs`` object.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
import contextlib
|
|
17
18
|
import datetime
|
|
18
19
|
import re
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
19
21
|
from decimal import Decimal
|
|
20
22
|
from typing import Any
|
|
21
23
|
from collections.abc import Callable, Generator, Iterator
|
|
@@ -24,6 +26,8 @@ from execsql.exceptions import ErrInfo
|
|
|
24
26
|
from execsql.utils.errors import exception_desc
|
|
25
27
|
import execsql.state as _state
|
|
26
28
|
|
|
29
|
+
__all__ = ["Database", "DatabasePool"]
|
|
30
|
+
|
|
27
31
|
|
|
28
32
|
def _default_dt_cast() -> dict[type, Callable]:
|
|
29
33
|
"""Build the default type-cast mapping used by all database backends."""
|
|
@@ -41,13 +45,14 @@ def _default_dt_cast() -> dict[type, Callable]:
|
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
|
|
44
|
-
class Database:
|
|
48
|
+
class Database(ABC):
|
|
45
49
|
"""Abstract base class for all database connections."""
|
|
46
50
|
|
|
47
51
|
_dt_cast: dict[type, Callable] | None = None
|
|
48
52
|
|
|
49
53
|
@property
|
|
50
54
|
def dt_cast(self) -> dict[type, Callable]:
|
|
55
|
+
"""Return the type-cast mapping, initialising it lazily on first access."""
|
|
51
56
|
if self._dt_cast is None:
|
|
52
57
|
self._dt_cast = _default_dt_cast()
|
|
53
58
|
return self._dt_cast
|
|
@@ -85,22 +90,38 @@ class Database:
|
|
|
85
90
|
)
|
|
86
91
|
|
|
87
92
|
def name(self) -> str:
|
|
93
|
+
"""Return a human-readable description of this connection (DBMS + server/file)."""
|
|
88
94
|
if self.server_name:
|
|
89
95
|
return f"{self.type.dbms_id}(server {self.server_name}; database {self.db_name})"
|
|
90
96
|
else:
|
|
91
97
|
return f"{self.type.dbms_id}(file {self.db_name})"
|
|
92
98
|
|
|
99
|
+
@abstractmethod
|
|
93
100
|
def open_db(self) -> None:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
raise DatabaseNotImplementedError(self.name(), "open_db")
|
|
101
|
+
"""Open the underlying database connection."""
|
|
102
|
+
...
|
|
97
103
|
|
|
98
104
|
def cursor(self):
|
|
105
|
+
"""Return a new cursor, opening the connection first if it has not been opened yet."""
|
|
99
106
|
if self.conn is None:
|
|
100
107
|
self.open_db()
|
|
101
108
|
return self.conn.cursor()
|
|
102
109
|
|
|
110
|
+
@contextlib.contextmanager
|
|
111
|
+
def _cursor(self):
|
|
112
|
+
"""Context manager that yields a cursor and closes it on exit.
|
|
113
|
+
|
|
114
|
+
Works with any DB-API 2.0 cursor regardless of whether the driver
|
|
115
|
+
natively supports the context manager protocol.
|
|
116
|
+
"""
|
|
117
|
+
curs = self.cursor()
|
|
118
|
+
try:
|
|
119
|
+
yield curs
|
|
120
|
+
finally:
|
|
121
|
+
curs.close()
|
|
122
|
+
|
|
103
123
|
def close(self) -> None:
|
|
124
|
+
"""Close the database connection, logging a warning if autocommit is off."""
|
|
104
125
|
if self.conn:
|
|
105
126
|
if not self.autocommit:
|
|
106
127
|
_state.exec_log.log_status_info(
|
|
@@ -115,24 +136,26 @@ class Database:
|
|
|
115
136
|
return '"' + identifier.replace('"', '""') + '"'
|
|
116
137
|
|
|
117
138
|
def paramsubs(self, paramcount: int) -> str:
|
|
139
|
+
"""Return a comma-separated string of *paramcount* parameter placeholders."""
|
|
118
140
|
return ",".join((self.paramstr,) * paramcount)
|
|
119
141
|
|
|
120
142
|
def execute(self, sql: Any, paramlist: list | None = None) -> None:
|
|
121
|
-
|
|
122
|
-
|
|
143
|
+
"""Execute *sql* (optionally with *paramlist*), updating ``$LAST_ROWCOUNT``.
|
|
144
|
+
|
|
145
|
+
Rolls back the current transaction and re-raises on any driver error.
|
|
146
|
+
"""
|
|
123
147
|
if type(sql) in (tuple, list):
|
|
124
148
|
sql = " ".join(sql)
|
|
125
149
|
try:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
pass # Non-critical: some drivers lack rowcount support.
|
|
150
|
+
with self._cursor() as curs:
|
|
151
|
+
if paramlist is None:
|
|
152
|
+
curs.execute(sql)
|
|
153
|
+
else:
|
|
154
|
+
curs.execute(sql, paramlist)
|
|
155
|
+
try:
|
|
156
|
+
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
157
|
+
except Exception:
|
|
158
|
+
pass # Non-critical: some drivers lack rowcount support.
|
|
136
159
|
except Exception:
|
|
137
160
|
try:
|
|
138
161
|
self.rollback()
|
|
@@ -140,22 +163,26 @@ class Database:
|
|
|
140
163
|
pass # Rollback is best-effort after a failed execute.
|
|
141
164
|
raise
|
|
142
165
|
|
|
166
|
+
@abstractmethod
|
|
143
167
|
def exec_cmd(self, querycommand: str) -> None:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
raise DatabaseNotImplementedError(self.name(), "exec_cmd")
|
|
168
|
+
"""Execute a stored procedure or function by name."""
|
|
169
|
+
...
|
|
147
170
|
|
|
148
171
|
def autocommit_on(self) -> None:
|
|
172
|
+
"""Enable autocommit mode so each statement is committed immediately."""
|
|
149
173
|
self.autocommit = True
|
|
150
174
|
|
|
151
175
|
def autocommit_off(self) -> None:
|
|
176
|
+
"""Disable autocommit mode, grouping subsequent statements into a transaction."""
|
|
152
177
|
self.autocommit = False
|
|
153
178
|
|
|
154
179
|
def commit(self) -> None:
|
|
180
|
+
"""Commit the current transaction if autocommit is enabled."""
|
|
155
181
|
if self.conn and self.autocommit:
|
|
156
182
|
self.conn.commit()
|
|
157
183
|
|
|
158
184
|
def rollback(self) -> None:
|
|
185
|
+
"""Roll back the current transaction; swallows errors (best-effort)."""
|
|
159
186
|
if self.conn:
|
|
160
187
|
try:
|
|
161
188
|
self.conn.rollback()
|
|
@@ -163,6 +190,7 @@ class Database:
|
|
|
163
190
|
pass # Best-effort; connection may already be closed.
|
|
164
191
|
|
|
165
192
|
def schema_qualified_table_name(self, schema_name: str | None, table_name: str) -> str:
|
|
193
|
+
"""Return the quoted, optionally schema-qualified form of *table_name*."""
|
|
166
194
|
table_name = self.type.quoted(table_name)
|
|
167
195
|
if schema_name:
|
|
168
196
|
schema_name = self.type.quoted(schema_name)
|
|
@@ -170,21 +198,22 @@ class Database:
|
|
|
170
198
|
return table_name
|
|
171
199
|
|
|
172
200
|
def select_data(self, sql: str) -> tuple[list[str], list]:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
201
|
+
"""Execute *sql* and return ``(column_names, rows)`` with all rows fetched into memory."""
|
|
202
|
+
with self._cursor() as curs:
|
|
203
|
+
try:
|
|
204
|
+
curs.execute(sql)
|
|
205
|
+
except Exception:
|
|
206
|
+
self.rollback()
|
|
207
|
+
raise
|
|
208
|
+
try:
|
|
209
|
+
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
210
|
+
except Exception:
|
|
211
|
+
pass # Non-critical: some drivers lack rowcount support.
|
|
212
|
+
rows = curs.fetchall()
|
|
213
|
+
return [d[0] for d in curs.description], rows
|
|
186
214
|
|
|
187
215
|
def select_rowsource(self, sql: str) -> tuple[list[str], Generator]:
|
|
216
|
+
"""Execute *sql* and return ``(column_names, row_generator)`` for streaming large result sets."""
|
|
188
217
|
# Return 1) a list of column names, and 2) an iterable that yields rows.
|
|
189
218
|
curs = self.cursor()
|
|
190
219
|
try:
|
|
@@ -219,6 +248,7 @@ class Database:
|
|
|
219
248
|
return [d[0] for d in curs.description], decode_row()
|
|
220
249
|
|
|
221
250
|
def select_rowdict(self, sql: str) -> tuple[list[str], Iterator]:
|
|
251
|
+
"""Execute *sql* and return ``(column_names, row_iterator)`` where each row is a ``dict``."""
|
|
222
252
|
# Return an iterable that yields dictionaries of row data
|
|
223
253
|
curs = self.cursor()
|
|
224
254
|
try:
|
|
@@ -246,35 +276,35 @@ class Database:
|
|
|
246
276
|
return hdrs, iter(dict_row, None)
|
|
247
277
|
|
|
248
278
|
def schema_exists(self, schema_name: str) -> bool:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
279
|
+
"""Return ``True`` if *schema_name* exists in this database."""
|
|
280
|
+
with self._cursor() as curs:
|
|
281
|
+
sql = f"SELECT schema_name FROM information_schema.schemata WHERE schema_name = {self.paramstr};"
|
|
282
|
+
curs.execute(sql, (schema_name,))
|
|
283
|
+
rows = curs.fetchall()
|
|
254
284
|
return len(rows) > 0
|
|
255
285
|
|
|
256
286
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
287
|
+
"""Return ``True`` if *table_name* (optionally in *schema_name*) exists."""
|
|
288
|
+
with self._cursor() as curs:
|
|
289
|
+
params: list = [table_name]
|
|
290
|
+
schema_clause = ""
|
|
291
|
+
if schema_name:
|
|
292
|
+
schema_clause = f" and table_schema={self.paramstr}"
|
|
293
|
+
params.append(schema_name)
|
|
294
|
+
sql = f"select table_name from information_schema.tables where table_name = {self.paramstr}{schema_clause};"
|
|
295
|
+
try:
|
|
296
|
+
curs.execute(sql, params)
|
|
297
|
+
except ErrInfo:
|
|
298
|
+
raise
|
|
299
|
+
except Exception as e:
|
|
300
|
+
self.rollback()
|
|
301
|
+
raise ErrInfo(
|
|
302
|
+
type="db",
|
|
303
|
+
command_text=sql,
|
|
304
|
+
exception_msg=exception_desc(),
|
|
305
|
+
other_msg=f"Failed test for existence of table {table_name} in {self.name()}",
|
|
306
|
+
) from e
|
|
307
|
+
rows = curs.fetchall()
|
|
278
308
|
return len(rows) > 0
|
|
279
309
|
|
|
280
310
|
def column_exists(
|
|
@@ -283,92 +313,94 @@ class Database:
|
|
|
283
313
|
column_name: str,
|
|
284
314
|
schema_name: str | None = None,
|
|
285
315
|
) -> bool:
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
316
|
+
"""Return ``True`` if *column_name* exists in *table_name* (optionally in *schema_name*)."""
|
|
317
|
+
with self._cursor() as curs:
|
|
318
|
+
params: list = [table_name]
|
|
319
|
+
schema_clause = ""
|
|
320
|
+
if schema_name:
|
|
321
|
+
schema_clause = f" and table_schema={self.paramstr}"
|
|
322
|
+
params.append(schema_name)
|
|
323
|
+
params.append(column_name)
|
|
324
|
+
sql = (
|
|
325
|
+
f"select column_name from information_schema.columns "
|
|
326
|
+
f"where table_name={self.paramstr}{schema_clause} "
|
|
327
|
+
f"and column_name={self.paramstr};"
|
|
328
|
+
)
|
|
329
|
+
try:
|
|
330
|
+
curs.execute(sql, params)
|
|
331
|
+
except ErrInfo:
|
|
332
|
+
raise
|
|
333
|
+
except Exception as e:
|
|
334
|
+
self.rollback()
|
|
335
|
+
raise ErrInfo(
|
|
336
|
+
type="db",
|
|
337
|
+
command_text=sql,
|
|
338
|
+
exception_msg=exception_desc(),
|
|
339
|
+
other_msg=f"Failed test for existence of column {column_name} in table {table_name} of {self.name()}",
|
|
340
|
+
) from e
|
|
341
|
+
rows = curs.fetchall()
|
|
312
342
|
return len(rows) > 0
|
|
313
343
|
|
|
314
344
|
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
345
|
+
"""Return the ordered list of column names for *table_name*."""
|
|
346
|
+
with self._cursor() as curs:
|
|
347
|
+
params: list = [table_name]
|
|
348
|
+
schema_clause = ""
|
|
349
|
+
if schema_name:
|
|
350
|
+
schema_clause = f" and table_schema={self.paramstr}"
|
|
351
|
+
params.append(schema_name)
|
|
352
|
+
sql = (
|
|
353
|
+
f"select column_name from information_schema.columns "
|
|
354
|
+
f"where table_name={self.paramstr}{schema_clause} "
|
|
355
|
+
f"order by ordinal_position;"
|
|
356
|
+
)
|
|
357
|
+
try:
|
|
358
|
+
curs.execute(sql, params)
|
|
359
|
+
except ErrInfo:
|
|
360
|
+
raise
|
|
361
|
+
except Exception as e:
|
|
362
|
+
self.rollback()
|
|
363
|
+
raise ErrInfo(
|
|
364
|
+
type="db",
|
|
365
|
+
command_text=sql,
|
|
366
|
+
exception_msg=exception_desc(),
|
|
367
|
+
other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
|
|
368
|
+
) from e
|
|
369
|
+
rows = curs.fetchall()
|
|
340
370
|
return [row[0] for row in rows]
|
|
341
371
|
|
|
342
372
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
373
|
+
"""Return ``True`` if *view_name* (optionally in *schema_name*) exists."""
|
|
374
|
+
with self._cursor() as curs:
|
|
375
|
+
params: list = [view_name]
|
|
376
|
+
schema_clause = ""
|
|
377
|
+
if schema_name:
|
|
378
|
+
schema_clause = f" and table_schema={self.paramstr}"
|
|
379
|
+
params.append(schema_name)
|
|
380
|
+
sql = f"select table_name from information_schema.views where table_name = {self.paramstr}{schema_clause};"
|
|
381
|
+
try:
|
|
382
|
+
curs.execute(sql, params)
|
|
383
|
+
except ErrInfo:
|
|
384
|
+
raise
|
|
385
|
+
except Exception as e:
|
|
386
|
+
self.rollback()
|
|
387
|
+
raise ErrInfo(
|
|
388
|
+
type="db",
|
|
389
|
+
command_text=sql,
|
|
390
|
+
exception_msg=exception_desc(),
|
|
391
|
+
other_msg=f"Failed test for existence of view {view_name} in {self.name()}",
|
|
392
|
+
) from e
|
|
393
|
+
rows = curs.fetchall()
|
|
364
394
|
return len(rows) > 0
|
|
365
395
|
|
|
366
396
|
def role_exists(self, rolename: str) -> bool:
|
|
397
|
+
"""Return ``True`` if *rolename* exists; subclasses must override this."""
|
|
367
398
|
from execsql.exceptions import DatabaseNotImplementedError
|
|
368
399
|
|
|
369
400
|
raise DatabaseNotImplementedError(self.name(), "role_exists")
|
|
370
401
|
|
|
371
402
|
def drop_table(self, tablename: str) -> None:
|
|
403
|
+
"""Drop *tablename* if it exists; *tablename* must already be schema-qualified and quoted."""
|
|
372
404
|
# The 'tablename' argument should be schema-qualified and quoted as necessary.
|
|
373
405
|
self.execute(f"drop table if exists {tablename} cascade;")
|
|
374
406
|
self.commit()
|
|
@@ -381,6 +413,11 @@ class Database:
|
|
|
381
413
|
column_list: list[str],
|
|
382
414
|
tablespec_src: Callable,
|
|
383
415
|
) -> None:
|
|
416
|
+
"""Bulk-insert rows from *rowsource* into *table_name* using the columns in *column_list*.
|
|
417
|
+
|
|
418
|
+
*rowsource* must be a generator yielding lists of values in column order.
|
|
419
|
+
*tablespec_src* is a zero-argument callable that returns the table's type specification.
|
|
420
|
+
"""
|
|
384
421
|
# The rowsource argument must be a generator yielding a list of values for the columns of the table.
|
|
385
422
|
# The column_list argument must an iterable containing column names. This may be a subset of
|
|
386
423
|
# the names of columns in the rowsource.
|
|
@@ -542,6 +579,7 @@ class Database:
|
|
|
542
579
|
csv_file_obj: Any,
|
|
543
580
|
skipheader: bool,
|
|
544
581
|
) -> None:
|
|
582
|
+
"""Import a CSV/tabular file into *table_name*; column names must be compatible."""
|
|
545
583
|
# Import a text (CSV) file containing tabular data to a table. Columns must be compatible.
|
|
546
584
|
if not self.table_exists(table_name, schema_name):
|
|
547
585
|
raise ErrInfo(
|
|
@@ -584,12 +622,14 @@ class Database:
|
|
|
584
622
|
column_name: str,
|
|
585
623
|
file_name: str,
|
|
586
624
|
) -> None:
|
|
625
|
+
"""Insert the raw binary content of *file_name* as a single row into *column_name* of *table_name*."""
|
|
587
626
|
with open(file_name, "rb") as f:
|
|
588
627
|
filedata = f.read()
|
|
589
628
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
590
629
|
quoted_col = self.quote_identifier(column_name)
|
|
591
630
|
sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
|
|
592
|
-
self.
|
|
631
|
+
with self._cursor() as curs:
|
|
632
|
+
curs.execute(sql, (filedata,))
|
|
593
633
|
|
|
594
634
|
|
|
595
635
|
class DatabasePool:
|
|
@@ -606,6 +646,7 @@ class DatabasePool:
|
|
|
606
646
|
return "DatabasePool()"
|
|
607
647
|
|
|
608
648
|
def add(self, db_alias: str, db_obj: Database) -> None:
|
|
649
|
+
"""Register *db_obj* under *db_alias*, setting it as initial/current if this is the first connection."""
|
|
609
650
|
db_alias = db_alias.lower()
|
|
610
651
|
if db_alias == "initial" and len(self.pool) > 0:
|
|
611
652
|
raise ErrInfo(
|
|
@@ -629,25 +670,27 @@ class DatabasePool:
|
|
|
629
670
|
self.pool[db_alias] = db_obj
|
|
630
671
|
|
|
631
672
|
def aliases(self) -> list[str]:
|
|
632
|
-
|
|
673
|
+
"""Return a list of all currently registered database aliases."""
|
|
633
674
|
return list(self.pool)
|
|
634
675
|
|
|
635
676
|
def current(self) -> Database:
|
|
636
|
-
|
|
677
|
+
"""Return the currently active ``Database`` object."""
|
|
637
678
|
return self.pool[self.current_db]
|
|
638
679
|
|
|
639
680
|
def current_alias(self) -> str:
|
|
640
|
-
|
|
681
|
+
"""Return the alias string for the currently active database."""
|
|
641
682
|
return self.current_db
|
|
642
683
|
|
|
643
684
|
def initial(self) -> Database:
|
|
685
|
+
"""Return the first ``Database`` that was added to the pool."""
|
|
644
686
|
return self.pool[self.initial_db]
|
|
645
687
|
|
|
646
688
|
def aliased_as(self, db_alias: str) -> Database:
|
|
689
|
+
"""Return the ``Database`` registered under *db_alias*."""
|
|
647
690
|
return self.pool[db_alias]
|
|
648
691
|
|
|
649
692
|
def make_current(self, db_alias: str) -> None:
|
|
650
|
-
|
|
693
|
+
"""Set the active database to *db_alias*; raises ``ErrInfo`` if the alias is unknown."""
|
|
651
694
|
db_alias = db_alias.lower()
|
|
652
695
|
if db_alias not in self.pool:
|
|
653
696
|
raise ErrInfo(
|
|
@@ -657,6 +700,7 @@ class DatabasePool:
|
|
|
657
700
|
self.current_db = db_alias
|
|
658
701
|
|
|
659
702
|
def disconnect(self, alias: str) -> None:
|
|
703
|
+
"""Close and remove the connection registered under *alias* from the pool."""
|
|
660
704
|
if alias == self.current_db or (alias == "initial" and "initial" in self.pool):
|
|
661
705
|
raise ErrInfo(
|
|
662
706
|
type="error",
|
|
@@ -667,6 +711,7 @@ class DatabasePool:
|
|
|
667
711
|
del self.pool[alias]
|
|
668
712
|
|
|
669
713
|
def closeall(self) -> None:
|
|
714
|
+
"""Roll back and close every connection in the pool, then reset the pool to empty."""
|
|
670
715
|
for alias, db in self.pool.items():
|
|
671
716
|
nm = db.name()
|
|
672
717
|
try:
|
execsql/db/dsn.py
CHANGED
|
@@ -15,8 +15,12 @@ from execsql.utils.errors import exception_desc, fatal_error
|
|
|
15
15
|
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
16
16
|
import execsql.state as _state
|
|
17
17
|
|
|
18
|
+
__all__ = ["DsnDatabase"]
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
class DsnDatabase(Database):
|
|
22
|
+
"""Generic ODBC adapter that connects to any data source registered as an ODBC DSN via pyodbc."""
|
|
23
|
+
|
|
20
24
|
# There's no telling what is actually connected to a DSN, so this uses
|
|
21
25
|
# generic Database methods almost exclusively. Only 'exec_cmd()' is
|
|
22
26
|
# overridden, and that uses the method for SQL Server because the DAO
|
execsql/db/duckdb.py
CHANGED
|
@@ -15,8 +15,12 @@ from execsql.exceptions import ErrInfo
|
|
|
15
15
|
from execsql.utils.errors import exception_desc, fatal_error
|
|
16
16
|
import execsql.state as _state
|
|
17
17
|
|
|
18
|
+
__all__ = ["DuckDBDatabase"]
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
class DuckDBDatabase(Database):
|
|
22
|
+
"""DuckDB in-process analytics adapter using the duckdb package."""
|
|
23
|
+
|
|
20
24
|
def __init__(self, DuckDB_fn: str) -> None:
|
|
21
25
|
try:
|
|
22
26
|
import duckdb # noqa: F401
|
execsql/db/factory.py
CHANGED
|
@@ -25,6 +25,18 @@ from execsql.db.duckdb import DuckDBDatabase
|
|
|
25
25
|
from execsql.db.mysql import MySQLDatabase
|
|
26
26
|
from execsql.db.firebird import FirebirdDatabase
|
|
27
27
|
|
|
28
|
+
__all__ = [
|
|
29
|
+
"db_Access",
|
|
30
|
+
"db_Dsn",
|
|
31
|
+
"db_DuckDB",
|
|
32
|
+
"db_Firebird",
|
|
33
|
+
"db_MySQL",
|
|
34
|
+
"db_Oracle",
|
|
35
|
+
"db_Postgres",
|
|
36
|
+
"db_SQLite",
|
|
37
|
+
"db_SqlServer",
|
|
38
|
+
]
|
|
39
|
+
|
|
28
40
|
|
|
29
41
|
def db_Access(
|
|
30
42
|
Access_fn: str,
|
|
@@ -32,6 +44,7 @@ def db_Access(
|
|
|
32
44
|
user: str | None = None,
|
|
33
45
|
encoding: str | None = None,
|
|
34
46
|
) -> AccessDatabase:
|
|
47
|
+
"""Open an MS Access database file (.mdb or .accdb) via DAO/ODBC."""
|
|
35
48
|
if not Path(Access_fn).exists():
|
|
36
49
|
raise ErrInfo(
|
|
37
50
|
type="error",
|
|
@@ -50,6 +63,7 @@ def db_Postgres(
|
|
|
50
63
|
new_db: bool = False,
|
|
51
64
|
password: str | None = None,
|
|
52
65
|
) -> PostgresDatabase:
|
|
66
|
+
"""Open a new PostgreSQL connection via psycopg2."""
|
|
53
67
|
return PostgresDatabase(server_name, database_name, user, pw_needed, port, new_db=new_db, password=password)
|
|
54
68
|
|
|
55
69
|
|
|
@@ -58,6 +72,7 @@ def db_SQLite(
|
|
|
58
72
|
new_db: bool = False,
|
|
59
73
|
encoding: str | None = None,
|
|
60
74
|
) -> SQLiteDatabase:
|
|
75
|
+
"""Open a SQLite database file via the standard-library sqlite3 module."""
|
|
61
76
|
if new_db:
|
|
62
77
|
from execsql.utils.fileio import check_dir
|
|
63
78
|
|
|
@@ -79,6 +94,7 @@ def db_SqlServer(
|
|
|
79
94
|
port: int | None = None,
|
|
80
95
|
encoding: str | None = None,
|
|
81
96
|
) -> SqlServerDatabase:
|
|
97
|
+
"""Open a Microsoft SQL Server connection via pyodbc."""
|
|
82
98
|
return SqlServerDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
83
99
|
|
|
84
100
|
|
|
@@ -90,6 +106,7 @@ def db_MySQL(
|
|
|
90
106
|
port: int | None = None,
|
|
91
107
|
encoding: str | None = None,
|
|
92
108
|
) -> MySQLDatabase:
|
|
109
|
+
"""Open a MySQL or MariaDB connection via pymysql."""
|
|
93
110
|
return MySQLDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
94
111
|
|
|
95
112
|
|
|
@@ -98,6 +115,7 @@ def db_DuckDB(
|
|
|
98
115
|
new_db: bool = False,
|
|
99
116
|
encoding: str | None = None,
|
|
100
117
|
) -> DuckDBDatabase:
|
|
118
|
+
"""Open a DuckDB in-process analytics database file via the duckdb package."""
|
|
101
119
|
if new_db:
|
|
102
120
|
from execsql.utils.fileio import check_dir
|
|
103
121
|
|
|
@@ -119,6 +137,7 @@ def db_Oracle(
|
|
|
119
137
|
port: int | None = None,
|
|
120
138
|
encoding: str | None = None,
|
|
121
139
|
) -> OracleDatabase:
|
|
140
|
+
"""Open an Oracle database connection via cx_Oracle (python-oracledb)."""
|
|
122
141
|
return OracleDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
123
142
|
|
|
124
143
|
|
|
@@ -130,6 +149,7 @@ def db_Firebird(
|
|
|
130
149
|
port: int | None = None,
|
|
131
150
|
encoding: str | None = None,
|
|
132
151
|
) -> FirebirdDatabase:
|
|
152
|
+
"""Open a Firebird database connection via the firebird-driver package."""
|
|
133
153
|
return FirebirdDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
134
154
|
|
|
135
155
|
|
|
@@ -139,4 +159,5 @@ def db_Dsn(
|
|
|
139
159
|
pw_needed: bool = True,
|
|
140
160
|
encoding: str | None = None,
|
|
141
161
|
) -> DsnDatabase:
|
|
162
|
+
"""Open a connection to any ODBC data source registered under *dsn_name*."""
|
|
142
163
|
return DsnDatabase(dsn_name=dsn_name, user_name=user, need_passwd=pw_needed, encoding=encoding)
|
execsql/db/firebird.py
CHANGED
|
@@ -14,8 +14,12 @@ from execsql.utils.errors import exception_desc, fatal_error
|
|
|
14
14
|
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
15
15
|
import execsql.state as _state
|
|
16
16
|
|
|
17
|
+
__all__ = ["FirebirdDatabase"]
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
class FirebirdDatabase(Database):
|
|
21
|
+
"""Firebird adapter using the firebird-driver (fdb) package."""
|
|
22
|
+
|
|
19
23
|
def __init__(
|
|
20
24
|
self,
|
|
21
25
|
server_name: str,
|
execsql/db/mysql.py
CHANGED
|
@@ -16,8 +16,12 @@ from execsql.utils.errors import exception_desc, fatal_error
|
|
|
16
16
|
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
17
17
|
import execsql.state as _state
|
|
18
18
|
|
|
19
|
+
__all__ = ["MySQLDatabase"]
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
class MySQLDatabase(Database):
|
|
23
|
+
"""MySQL and MariaDB adapter using the pymysql package."""
|
|
24
|
+
|
|
21
25
|
def __init__(
|
|
22
26
|
self,
|
|
23
27
|
server_name: str,
|