execsql2 2.0.1__py3-none-any.whl → 2.1.2__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.py +322 -108
- execsql/config.py +134 -114
- execsql/db/access.py +89 -65
- execsql/db/base.py +97 -68
- execsql/db/dsn.py +45 -29
- execsql/db/duckdb.py +4 -5
- execsql/db/factory.py +27 -27
- execsql/db/firebird.py +30 -18
- execsql/db/mysql.py +38 -14
- execsql/db/oracle.py +58 -33
- execsql/db/postgres.py +68 -28
- execsql/db/sqlite.py +36 -27
- execsql/db/sqlserver.py +45 -30
- execsql/exceptions.py +68 -64
- execsql/exporters/__init__.py +1 -1
- execsql/exporters/base.py +42 -17
- execsql/exporters/delimited.py +60 -59
- execsql/exporters/duckdb.py +8 -12
- execsql/exporters/feather.py +32 -24
- execsql/exporters/html.py +33 -30
- execsql/exporters/json.py +18 -17
- execsql/exporters/latex.py +11 -13
- execsql/exporters/ods.py +50 -46
- execsql/exporters/parquet.py +32 -0
- execsql/exporters/pretty.py +16 -15
- execsql/exporters/raw.py +9 -11
- execsql/exporters/sqlite.py +38 -38
- execsql/exporters/templates.py +15 -72
- execsql/exporters/values.py +13 -12
- execsql/exporters/xls.py +26 -26
- execsql/exporters/xml.py +12 -12
- execsql/exporters/zip.py +0 -3
- execsql/gui/__init__.py +2 -2
- execsql/gui/console.py +0 -1
- execsql/gui/desktop.py +6 -7
- execsql/gui/tui.py +8 -14
- execsql/importers/base.py +6 -9
- execsql/importers/csv.py +10 -17
- execsql/importers/feather.py +16 -22
- execsql/importers/ods.py +3 -4
- execsql/importers/xls.py +5 -6
- execsql/metacommands/__init__.py +8 -8
- execsql/metacommands/conditions.py +41 -33
- execsql/metacommands/connect.py +113 -99
- execsql/metacommands/control.py +38 -26
- execsql/metacommands/data.py +35 -33
- execsql/metacommands/debug.py +13 -9
- execsql/metacommands/io.py +288 -229
- execsql/metacommands/prompt.py +179 -157
- execsql/metacommands/script_ext.py +11 -9
- execsql/metacommands/system.py +44 -25
- execsql/models.py +9 -16
- execsql/parser.py +10 -10
- execsql/script.py +183 -157
- execsql/state.py +170 -208
- execsql/types.py +46 -81
- execsql/utils/auth.py +114 -14
- execsql/utils/crypto.py +31 -4
- execsql/utils/datetime.py +7 -7
- execsql/utils/errors.py +34 -29
- execsql/utils/fileio.py +90 -55
- execsql/utils/gui.py +22 -23
- execsql/utils/mail.py +15 -17
- execsql/utils/numeric.py +2 -3
- execsql/utils/regex.py +9 -12
- execsql/utils/strings.py +10 -12
- execsql/utils/timer.py +0 -2
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/execsql.conf +1 -1
- execsql2-2.1.2.dist-info/METADATA +300 -0
- execsql2-2.1.2.dist-info/RECORD +96 -0
- execsql2-2.0.1.dist-info/METADATA +0 -406
- execsql2-2.0.1.dist-info/RECORD +0 -95
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/READ_ME.rst +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/WHEEL +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/entry_points.txt +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/NOTICE +0 -0
execsql/db/base.py
CHANGED
|
@@ -14,11 +14,9 @@ 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 datetime
|
|
18
|
-
import io
|
|
19
17
|
import re
|
|
20
|
-
from
|
|
21
|
-
from
|
|
18
|
+
from typing import Any
|
|
19
|
+
from collections.abc import Callable, Generator, Iterator
|
|
22
20
|
|
|
23
21
|
from execsql.exceptions import ErrInfo
|
|
24
22
|
from execsql.utils.errors import exception_desc
|
|
@@ -28,23 +26,23 @@ import execsql.state as _state
|
|
|
28
26
|
class Database:
|
|
29
27
|
"""Abstract base class for all database connections."""
|
|
30
28
|
|
|
31
|
-
dt_cast:
|
|
29
|
+
dt_cast: dict[type, Callable] = {} # populated per-subclass or in __init__
|
|
32
30
|
|
|
33
31
|
def __init__(
|
|
34
32
|
self,
|
|
35
|
-
server_name:
|
|
36
|
-
db_name:
|
|
37
|
-
user_name:
|
|
38
|
-
need_passwd:
|
|
39
|
-
port:
|
|
40
|
-
encoding:
|
|
33
|
+
server_name: str | None,
|
|
34
|
+
db_name: str | None,
|
|
35
|
+
user_name: str | None = None,
|
|
36
|
+
need_passwd: bool | None = None,
|
|
37
|
+
port: int | None = None,
|
|
38
|
+
encoding: str | None = None,
|
|
41
39
|
) -> None:
|
|
42
40
|
self.type = None
|
|
43
41
|
self.server_name = server_name
|
|
44
42
|
self.db_name = db_name
|
|
45
43
|
self.user = user_name
|
|
46
44
|
self.need_passwd = need_passwd
|
|
47
|
-
self.password:
|
|
45
|
+
self.password: str | None = None
|
|
48
46
|
self.port = port
|
|
49
47
|
self.encoding = encoding
|
|
50
48
|
self.encode_commands = True
|
|
@@ -83,10 +81,15 @@ class Database:
|
|
|
83
81
|
self.conn.close()
|
|
84
82
|
self.conn = None
|
|
85
83
|
|
|
84
|
+
def quote_identifier(self, identifier: str) -> str:
|
|
85
|
+
"""Return *identifier* wrapped in double-quotes with any embedded
|
|
86
|
+
double-quotes escaped (standard SQL identifier quoting)."""
|
|
87
|
+
return '"' + identifier.replace('"', '""') + '"'
|
|
88
|
+
|
|
86
89
|
def paramsubs(self, paramcount: int) -> str:
|
|
87
90
|
return ",".join((self.paramstr,) * paramcount)
|
|
88
91
|
|
|
89
|
-
def execute(self, sql: Any, paramlist:
|
|
92
|
+
def execute(self, sql: Any, paramlist: list | None = None) -> None:
|
|
90
93
|
# A shortcut to self.cursor().execute() that handles encoding.
|
|
91
94
|
# Whether or not encoding is needed depends on the DBMS.
|
|
92
95
|
if type(sql) in (tuple, list):
|
|
@@ -98,16 +101,16 @@ class Database:
|
|
|
98
101
|
else:
|
|
99
102
|
curs.execute(sql, paramlist)
|
|
100
103
|
try:
|
|
101
|
-
# DuckDB does not support the 'rowcount' attribute
|
|
104
|
+
# DuckDB does not support the 'rowcount' attribute.
|
|
102
105
|
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
103
106
|
except Exception:
|
|
104
|
-
pass
|
|
105
|
-
except Exception
|
|
107
|
+
pass # Non-critical: some drivers lack rowcount support.
|
|
108
|
+
except Exception:
|
|
106
109
|
try:
|
|
107
110
|
self.rollback()
|
|
108
111
|
except Exception:
|
|
109
|
-
pass
|
|
110
|
-
raise
|
|
112
|
+
pass # Rollback is best-effort after a failed execute.
|
|
113
|
+
raise
|
|
111
114
|
|
|
112
115
|
def exec_cmd(self, querycommand: str) -> None:
|
|
113
116
|
from execsql.exceptions import DatabaseNotImplementedError
|
|
@@ -129,16 +132,16 @@ class Database:
|
|
|
129
132
|
try:
|
|
130
133
|
self.conn.rollback()
|
|
131
134
|
except Exception:
|
|
132
|
-
pass
|
|
135
|
+
pass # Best-effort; connection may already be closed.
|
|
133
136
|
|
|
134
|
-
def schema_qualified_table_name(self, schema_name:
|
|
137
|
+
def schema_qualified_table_name(self, schema_name: str | None, table_name: str) -> str:
|
|
135
138
|
table_name = self.type.quoted(table_name)
|
|
136
139
|
if schema_name:
|
|
137
140
|
schema_name = self.type.quoted(schema_name)
|
|
138
141
|
return f"{schema_name}.{table_name}"
|
|
139
142
|
return table_name
|
|
140
143
|
|
|
141
|
-
def select_data(self, sql: str) ->
|
|
144
|
+
def select_data(self, sql: str) -> tuple[list[str], list]:
|
|
142
145
|
# Returns the results of the sql select statement.
|
|
143
146
|
curs = self.cursor()
|
|
144
147
|
try:
|
|
@@ -149,18 +152,18 @@ class Database:
|
|
|
149
152
|
try:
|
|
150
153
|
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
151
154
|
except Exception:
|
|
152
|
-
pass
|
|
155
|
+
pass # Non-critical: some drivers lack rowcount support.
|
|
153
156
|
rows = curs.fetchall()
|
|
154
157
|
return [d[0] for d in curs.description], rows
|
|
155
158
|
|
|
156
|
-
def select_rowsource(self, sql: str) ->
|
|
159
|
+
def select_rowsource(self, sql: str) -> tuple[list[str], Generator]:
|
|
157
160
|
# Return 1) a list of column names, and 2) an iterable that yields rows.
|
|
158
161
|
curs = self.cursor()
|
|
159
162
|
try:
|
|
160
|
-
# DuckDB cursors have no 'arraysize' attribute
|
|
163
|
+
# DuckDB cursors have no 'arraysize' attribute.
|
|
161
164
|
curs.arraysize = _state.conf.export_row_buffer
|
|
162
165
|
except Exception:
|
|
163
|
-
pass
|
|
166
|
+
pass # Non-critical: not all drivers support arraysize.
|
|
164
167
|
try:
|
|
165
168
|
curs.execute(sql)
|
|
166
169
|
except Exception:
|
|
@@ -169,7 +172,7 @@ class Database:
|
|
|
169
172
|
try:
|
|
170
173
|
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
171
174
|
except Exception:
|
|
172
|
-
pass
|
|
175
|
+
pass # Non-critical: some drivers lack rowcount support.
|
|
173
176
|
|
|
174
177
|
def decode_row() -> Generator:
|
|
175
178
|
while True:
|
|
@@ -180,14 +183,14 @@ class Database:
|
|
|
180
183
|
for row in rows:
|
|
181
184
|
if self.encoding:
|
|
182
185
|
yield [
|
|
183
|
-
c.decode(self.encoding, "backslashreplace") if
|
|
186
|
+
c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c for c in row
|
|
184
187
|
]
|
|
185
188
|
else:
|
|
186
189
|
yield row
|
|
187
190
|
|
|
188
191
|
return [d[0] for d in curs.description], decode_row()
|
|
189
192
|
|
|
190
|
-
def select_rowdict(self, sql: str) ->
|
|
193
|
+
def select_rowdict(self, sql: str) -> tuple[list[str], Iterator]:
|
|
191
194
|
# Return an iterable that yields dictionaries of row data
|
|
192
195
|
curs = self.cursor()
|
|
193
196
|
try:
|
|
@@ -198,14 +201,14 @@ class Database:
|
|
|
198
201
|
try:
|
|
199
202
|
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
200
203
|
except Exception:
|
|
201
|
-
pass
|
|
204
|
+
pass # Non-critical: some drivers lack rowcount support.
|
|
202
205
|
hdrs = [d[0] for d in curs.description]
|
|
203
206
|
|
|
204
|
-
def dict_row() ->
|
|
207
|
+
def dict_row() -> dict | None:
|
|
205
208
|
row = curs.fetchone()
|
|
206
209
|
if row:
|
|
207
210
|
if self.encoding:
|
|
208
|
-
r = [c.decode(self.encoding, "backslashreplace") if
|
|
211
|
+
r = [c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c for c in row]
|
|
209
212
|
else:
|
|
210
213
|
r = row
|
|
211
214
|
return dict(zip(hdrs, r))
|
|
@@ -216,19 +219,22 @@ class Database:
|
|
|
216
219
|
|
|
217
220
|
def schema_exists(self, schema_name: str) -> bool:
|
|
218
221
|
curs = self.cursor()
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
222
|
+
sql = f"SELECT schema_name FROM information_schema.schemata WHERE schema_name = {self.paramstr};"
|
|
223
|
+
curs.execute(sql, (schema_name,))
|
|
222
224
|
rows = curs.fetchall()
|
|
223
225
|
curs.close()
|
|
224
226
|
return len(rows) > 0
|
|
225
227
|
|
|
226
|
-
def table_exists(self, table_name: str, schema_name:
|
|
228
|
+
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
227
229
|
curs = self.cursor()
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
params: list = [table_name]
|
|
231
|
+
schema_clause = ""
|
|
232
|
+
if schema_name:
|
|
233
|
+
schema_clause = f" and table_schema={self.paramstr}"
|
|
234
|
+
params.append(schema_name)
|
|
235
|
+
sql = f"select table_name from information_schema.tables where table_name = {self.paramstr}{schema_clause};"
|
|
230
236
|
try:
|
|
231
|
-
curs.execute(sql)
|
|
237
|
+
curs.execute(sql, params)
|
|
232
238
|
except ErrInfo:
|
|
233
239
|
raise
|
|
234
240
|
except Exception:
|
|
@@ -247,17 +253,22 @@ class Database:
|
|
|
247
253
|
self,
|
|
248
254
|
table_name: str,
|
|
249
255
|
column_name: str,
|
|
250
|
-
schema_name:
|
|
256
|
+
schema_name: str | None = None,
|
|
251
257
|
) -> bool:
|
|
252
258
|
curs = self.cursor()
|
|
253
|
-
|
|
259
|
+
params: list = [table_name]
|
|
260
|
+
schema_clause = ""
|
|
261
|
+
if schema_name:
|
|
262
|
+
schema_clause = f" and table_schema={self.paramstr}"
|
|
263
|
+
params.append(schema_name)
|
|
264
|
+
params.append(column_name)
|
|
254
265
|
sql = (
|
|
255
266
|
f"select column_name from information_schema.columns "
|
|
256
|
-
f"where table_name=
|
|
257
|
-
f"and column_name=
|
|
267
|
+
f"where table_name={self.paramstr}{schema_clause} "
|
|
268
|
+
f"and column_name={self.paramstr};"
|
|
258
269
|
)
|
|
259
270
|
try:
|
|
260
|
-
curs.execute(sql)
|
|
271
|
+
curs.execute(sql, params)
|
|
261
272
|
except ErrInfo:
|
|
262
273
|
raise
|
|
263
274
|
except Exception:
|
|
@@ -272,16 +283,20 @@ class Database:
|
|
|
272
283
|
curs.close()
|
|
273
284
|
return len(rows) > 0
|
|
274
285
|
|
|
275
|
-
def table_columns(self, table_name: str, schema_name:
|
|
286
|
+
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
276
287
|
curs = self.cursor()
|
|
277
|
-
|
|
288
|
+
params: list = [table_name]
|
|
289
|
+
schema_clause = ""
|
|
290
|
+
if schema_name:
|
|
291
|
+
schema_clause = f" and table_schema={self.paramstr}"
|
|
292
|
+
params.append(schema_name)
|
|
278
293
|
sql = (
|
|
279
294
|
f"select column_name from information_schema.columns "
|
|
280
|
-
f"where table_name=
|
|
295
|
+
f"where table_name={self.paramstr}{schema_clause} "
|
|
281
296
|
f"order by ordinal_position;"
|
|
282
297
|
)
|
|
283
298
|
try:
|
|
284
|
-
curs.execute(sql)
|
|
299
|
+
curs.execute(sql, params)
|
|
285
300
|
except ErrInfo:
|
|
286
301
|
raise
|
|
287
302
|
except Exception:
|
|
@@ -296,12 +311,16 @@ class Database:
|
|
|
296
311
|
curs.close()
|
|
297
312
|
return [row[0] for row in rows]
|
|
298
313
|
|
|
299
|
-
def view_exists(self, view_name: str, schema_name:
|
|
314
|
+
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
300
315
|
curs = self.cursor()
|
|
301
|
-
|
|
302
|
-
|
|
316
|
+
params: list = [view_name]
|
|
317
|
+
schema_clause = ""
|
|
318
|
+
if schema_name:
|
|
319
|
+
schema_clause = f" and table_schema={self.paramstr}"
|
|
320
|
+
params.append(schema_name)
|
|
321
|
+
sql = f"select table_name from information_schema.views where table_name = {self.paramstr}{schema_clause};"
|
|
303
322
|
try:
|
|
304
|
-
curs.execute(sql)
|
|
323
|
+
curs.execute(sql, params)
|
|
305
324
|
except ErrInfo:
|
|
306
325
|
raise
|
|
307
326
|
except Exception:
|
|
@@ -328,10 +347,10 @@ class Database:
|
|
|
328
347
|
|
|
329
348
|
def populate_table(
|
|
330
349
|
self,
|
|
331
|
-
schema_name:
|
|
350
|
+
schema_name: str | None,
|
|
332
351
|
table_name: str,
|
|
333
352
|
rowsource: Any,
|
|
334
|
-
column_list:
|
|
353
|
+
column_list: list[str],
|
|
335
354
|
tablespec_src: Callable,
|
|
336
355
|
) -> None:
|
|
337
356
|
# The rowsource argument must be a generator yielding a list of values for the columns of the table.
|
|
@@ -362,9 +381,10 @@ class Database:
|
|
|
362
381
|
rows = iter(rowsource)
|
|
363
382
|
curs = self.cursor()
|
|
364
383
|
eof = False
|
|
384
|
+
total_rows = 0
|
|
365
385
|
while True:
|
|
366
386
|
b = []
|
|
367
|
-
for
|
|
387
|
+
for _j in range(_state.conf.import_row_buffer):
|
|
368
388
|
try:
|
|
369
389
|
line = next(rows)
|
|
370
390
|
except StopIteration:
|
|
@@ -409,20 +429,19 @@ class Database:
|
|
|
409
429
|
" ",
|
|
410
430
|
line[i],
|
|
411
431
|
)
|
|
412
|
-
if not _state.conf.empty_strings:
|
|
413
|
-
|
|
414
|
-
line[i] = None
|
|
432
|
+
if not _state.conf.empty_strings and line[i].strip() == "":
|
|
433
|
+
line[i] = None
|
|
415
434
|
lt = [type_objs[i].from_data(val) if val is not None else None for i, val in enumerate(line)]
|
|
416
435
|
lt = [type_mod_fn[i](v) if type_mod_fn[i] else v for i, v in enumerate(lt)]
|
|
417
|
-
|
|
436
|
+
row = []
|
|
418
437
|
for i, v in enumerate(lt):
|
|
419
438
|
if incl_col[i]:
|
|
420
|
-
|
|
439
|
+
row.append(v)
|
|
421
440
|
add_line = True
|
|
422
441
|
if not _state.conf.empty_rows:
|
|
423
|
-
add_line = not all(
|
|
442
|
+
add_line = not all(c is None for c in row)
|
|
424
443
|
if add_line:
|
|
425
|
-
b.append(
|
|
444
|
+
b.append(row)
|
|
426
445
|
if len(b) > 0:
|
|
427
446
|
try:
|
|
428
447
|
curs.executemany(sql, b)
|
|
@@ -436,12 +455,22 @@ class Database:
|
|
|
436
455
|
exception_msg=exception_desc(),
|
|
437
456
|
other_msg=f"Can't load data into table {sq_name} of {self.name()} from line {{{line}}}",
|
|
438
457
|
)
|
|
458
|
+
total_rows += len(b)
|
|
459
|
+
interval = _state.conf.import_progress_interval
|
|
460
|
+
if _state.exec_log and interval > 0 and total_rows % interval == 0:
|
|
461
|
+
_state.exec_log.log_status_info(
|
|
462
|
+
f"IMPORT into {sq_name}: {total_rows} rows imported so far.",
|
|
463
|
+
)
|
|
439
464
|
if eof:
|
|
440
465
|
break
|
|
466
|
+
if _state.exec_log:
|
|
467
|
+
_state.exec_log.log_status_info(
|
|
468
|
+
f"IMPORT into {sq_name} complete: {total_rows} rows imported.",
|
|
469
|
+
)
|
|
441
470
|
|
|
442
471
|
def import_tabular_file(
|
|
443
472
|
self,
|
|
444
|
-
schema_name:
|
|
473
|
+
schema_name: str | None,
|
|
445
474
|
table_name: str,
|
|
446
475
|
csv_file_obj: Any,
|
|
447
476
|
skipheader: bool,
|
|
@@ -483,12 +512,12 @@ class Database:
|
|
|
483
512
|
|
|
484
513
|
def import_entire_file(
|
|
485
514
|
self,
|
|
486
|
-
schema_name:
|
|
515
|
+
schema_name: str | None,
|
|
487
516
|
table_name: str,
|
|
488
517
|
column_name: str,
|
|
489
518
|
file_name: str,
|
|
490
519
|
) -> None:
|
|
491
|
-
with
|
|
520
|
+
with open(file_name, "rb") as f:
|
|
492
521
|
filedata = f.read()
|
|
493
522
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
494
523
|
sql = f"insert into {sq_name} ({column_name}) values ({self.paramsubs(1)});"
|
|
@@ -500,9 +529,9 @@ class DatabasePool:
|
|
|
500
529
|
and with the current and initial databases identified."""
|
|
501
530
|
|
|
502
531
|
def __init__(self) -> None:
|
|
503
|
-
self.pool:
|
|
504
|
-
self.initial_db:
|
|
505
|
-
self.current_db:
|
|
532
|
+
self.pool: dict[str, Database] = {}
|
|
533
|
+
self.initial_db: str | None = None
|
|
534
|
+
self.current_db: str | None = None
|
|
506
535
|
self.do_rollback: bool = True
|
|
507
536
|
|
|
508
537
|
def __repr__(self) -> str:
|
|
@@ -531,7 +560,7 @@ class DatabasePool:
|
|
|
531
560
|
self.pool[db_alias].close()
|
|
532
561
|
self.pool[db_alias] = db_obj
|
|
533
562
|
|
|
534
|
-
def aliases(self) ->
|
|
563
|
+
def aliases(self) -> list[str]:
|
|
535
564
|
# Return a list of the currently defined aliases
|
|
536
565
|
return list(self.pool)
|
|
537
566
|
|
execsql/db/dsn.py
CHANGED
|
@@ -8,13 +8,11 @@ registered as an ODBC DSN via ``pyodbc``. Corresponds to ``-t d`` on
|
|
|
8
8
|
the CLI.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
import io
|
|
12
|
-
from typing import Optional
|
|
13
11
|
|
|
14
12
|
from execsql.db.base import Database
|
|
15
13
|
from execsql.exceptions import ErrInfo
|
|
16
14
|
from execsql.utils.errors import exception_desc, fatal_error
|
|
17
|
-
from execsql.utils.auth import get_password
|
|
15
|
+
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
18
16
|
import execsql.state as _state
|
|
19
17
|
|
|
20
18
|
|
|
@@ -28,10 +26,10 @@ class DsnDatabase(Database):
|
|
|
28
26
|
def __init__(
|
|
29
27
|
self,
|
|
30
28
|
dsn_name: str,
|
|
31
|
-
user_name:
|
|
29
|
+
user_name: str | None,
|
|
32
30
|
need_passwd: bool = False,
|
|
33
|
-
encoding:
|
|
34
|
-
password:
|
|
31
|
+
encoding: str | None = None,
|
|
32
|
+
password: str | None = None,
|
|
35
33
|
) -> None:
|
|
36
34
|
try:
|
|
37
35
|
import pyodbc # noqa: F401
|
|
@@ -65,37 +63,55 @@ class DsnDatabase(Database):
|
|
|
65
63
|
self.conn = None
|
|
66
64
|
if self.need_passwd and self.user and self.password is None:
|
|
67
65
|
self.password = get_password("DSN", self.db_name, self.user)
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
|
|
67
|
+
def _dsn_connect(autocommit: bool = False):
|
|
68
|
+
cs = "DSN=%s;"
|
|
70
69
|
if self.need_passwd:
|
|
70
|
+
kwargs = {"autocommit": autocommit} if autocommit else {}
|
|
71
71
|
self.conn = pyodbc.connect(
|
|
72
72
|
f"{cs % self.db_name} Uid={self.user}; Pwd={self.password};",
|
|
73
|
+
**kwargs,
|
|
73
74
|
)
|
|
74
75
|
else:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
kwargs = {"autocommit": autocommit} if autocommit else {}
|
|
77
|
+
self.conn = pyodbc.connect(cs % self.db_name, **kwargs)
|
|
78
|
+
|
|
79
|
+
def _try_connect():
|
|
80
|
+
try:
|
|
81
|
+
_dsn_connect()
|
|
82
|
+
except Exception:
|
|
83
|
+
excdesc = exception_desc()
|
|
84
|
+
if "Optional feature not implemented" in excdesc:
|
|
85
|
+
try:
|
|
86
|
+
_dsn_connect(autocommit=True)
|
|
87
|
+
except Exception:
|
|
88
|
+
raise ErrInfo(
|
|
89
|
+
type="exception",
|
|
90
|
+
exception_msg=exception_desc(),
|
|
91
|
+
other_msg=f"Can't open DSN database {self.db_name} using ODBC",
|
|
84
92
|
)
|
|
85
|
-
|
|
86
|
-
self.conn = pyodbc.connect(cs % self.db_name, autocommit=True)
|
|
87
|
-
except Exception:
|
|
93
|
+
else:
|
|
88
94
|
raise ErrInfo(
|
|
89
95
|
type="exception",
|
|
90
|
-
exception_msg=
|
|
96
|
+
exception_msg=excdesc,
|
|
91
97
|
other_msg=f"Can't open DSN database {self.db_name} using ODBC",
|
|
92
98
|
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
_try_connect()
|
|
102
|
+
except ErrInfo:
|
|
103
|
+
if not password_from_keyring():
|
|
104
|
+
raise
|
|
105
|
+
clear_stored_password("DSN", self.db_name, self.user)
|
|
106
|
+
self.password = get_password(
|
|
107
|
+
"DSN",
|
|
108
|
+
self.db_name,
|
|
109
|
+
self.user,
|
|
110
|
+
skip_keyring=True,
|
|
111
|
+
other_msg="(stored credential failed — enter current password)",
|
|
112
|
+
)
|
|
113
|
+
self.conn = None
|
|
114
|
+
_try_connect()
|
|
99
115
|
|
|
100
116
|
def exec_cmd(self, querycommand: str) -> None:
|
|
101
117
|
# The querycommand must be a stored procedure
|
|
@@ -110,14 +126,14 @@ class DsnDatabase(Database):
|
|
|
110
126
|
|
|
111
127
|
def import_entire_file(
|
|
112
128
|
self,
|
|
113
|
-
schema_name:
|
|
129
|
+
schema_name: str | None,
|
|
114
130
|
table_name: str,
|
|
115
131
|
column_name: str,
|
|
116
132
|
file_name: str,
|
|
117
133
|
) -> None:
|
|
118
134
|
import pyodbc
|
|
119
135
|
|
|
120
|
-
with
|
|
136
|
+
with open(file_name, "rb") as f:
|
|
121
137
|
filedata = f.read()
|
|
122
138
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
123
139
|
sql = f"insert into {sq_name} ({column_name}) values ({self.paramsubs(1)});"
|
execsql/db/duckdb.py
CHANGED
|
@@ -8,8 +8,7 @@ analytics databases via the ``duckdb`` package. Corresponds to ``-t k``
|
|
|
8
8
|
on the CLI.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
import
|
|
12
|
-
from typing import Optional
|
|
11
|
+
from pathlib import Path
|
|
13
12
|
|
|
14
13
|
from execsql.db.base import Database
|
|
15
14
|
from execsql.exceptions import ErrInfo
|
|
@@ -28,7 +27,7 @@ class DuckDBDatabase(Database):
|
|
|
28
27
|
self.type = dbt_duckdb
|
|
29
28
|
self.server_name = None
|
|
30
29
|
self.db_name = DuckDB_fn
|
|
31
|
-
self.catalog_name =
|
|
30
|
+
self.catalog_name = Path(DuckDB_fn).stem
|
|
32
31
|
self.user = None
|
|
33
32
|
self.need_passwd = False
|
|
34
33
|
self.encoding = "UTF-8"
|
|
@@ -76,8 +75,8 @@ class DuckDBDatabase(Database):
|
|
|
76
75
|
# In DuckDB, the 'schemata' view is not limited to the current database.
|
|
77
76
|
curs = self.cursor()
|
|
78
77
|
curs.execute(
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
"SELECT schema_name FROM information_schema.schemata WHERE schema_name = ? and catalog_name = ?;",
|
|
79
|
+
(schema_name, self.catalog_name),
|
|
81
80
|
)
|
|
82
81
|
rows = curs.fetchall()
|
|
83
82
|
curs.close()
|
execsql/db/factory.py
CHANGED
|
@@ -12,8 +12,7 @@ functions are the canonical way to open a new database connection from
|
|
|
12
12
|
:mod:`execsql.cli` and the connection-related metacommand handlers.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
import
|
|
16
|
-
from typing import Optional
|
|
15
|
+
from pathlib import Path
|
|
17
16
|
|
|
18
17
|
from execsql.exceptions import ErrInfo
|
|
19
18
|
from execsql.db.access import AccessDatabase
|
|
@@ -30,10 +29,10 @@ from execsql.db.firebird import FirebirdDatabase
|
|
|
30
29
|
def db_Access(
|
|
31
30
|
Access_fn: str,
|
|
32
31
|
pw_needed: bool = False,
|
|
33
|
-
user:
|
|
34
|
-
encoding:
|
|
32
|
+
user: str | None = None,
|
|
33
|
+
encoding: str | None = None,
|
|
35
34
|
) -> AccessDatabase:
|
|
36
|
-
if not
|
|
35
|
+
if not Path(Access_fn).exists():
|
|
37
36
|
raise ErrInfo(
|
|
38
37
|
type="error",
|
|
39
38
|
other_msg=f'Access database file "{Access_fn}" does not exist.',
|
|
@@ -44,26 +43,27 @@ def db_Access(
|
|
|
44
43
|
def db_Postgres(
|
|
45
44
|
server_name: str,
|
|
46
45
|
database_name: str,
|
|
47
|
-
user:
|
|
46
|
+
user: str | None = None,
|
|
48
47
|
pw_needed: bool = True,
|
|
49
|
-
port:
|
|
50
|
-
encoding:
|
|
48
|
+
port: int | None = None,
|
|
49
|
+
encoding: str | None = None,
|
|
51
50
|
new_db: bool = False,
|
|
51
|
+
password: str | None = None,
|
|
52
52
|
) -> PostgresDatabase:
|
|
53
|
-
return PostgresDatabase(server_name, database_name, user, pw_needed, port, new_db=new_db)
|
|
53
|
+
return PostgresDatabase(server_name, database_name, user, pw_needed, port, new_db=new_db, password=password)
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def db_SQLite(
|
|
57
57
|
sqlite_fn: str,
|
|
58
58
|
new_db: bool = False,
|
|
59
|
-
encoding:
|
|
59
|
+
encoding: str | None = None,
|
|
60
60
|
) -> SQLiteDatabase:
|
|
61
61
|
if new_db:
|
|
62
62
|
from execsql.utils.fileio import check_dir
|
|
63
63
|
|
|
64
64
|
check_dir(sqlite_fn)
|
|
65
65
|
else:
|
|
66
|
-
if not
|
|
66
|
+
if not Path(sqlite_fn).exists():
|
|
67
67
|
raise ErrInfo(
|
|
68
68
|
type="error",
|
|
69
69
|
other_msg=f'SQLite database file "{sqlite_fn}" does not exist.',
|
|
@@ -74,10 +74,10 @@ def db_SQLite(
|
|
|
74
74
|
def db_SqlServer(
|
|
75
75
|
server_name: str,
|
|
76
76
|
database_name: str,
|
|
77
|
-
user:
|
|
77
|
+
user: str | None = None,
|
|
78
78
|
pw_needed: bool = True,
|
|
79
|
-
port:
|
|
80
|
-
encoding:
|
|
79
|
+
port: int | None = None,
|
|
80
|
+
encoding: str | None = None,
|
|
81
81
|
) -> SqlServerDatabase:
|
|
82
82
|
return SqlServerDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
83
83
|
|
|
@@ -85,10 +85,10 @@ def db_SqlServer(
|
|
|
85
85
|
def db_MySQL(
|
|
86
86
|
server_name: str,
|
|
87
87
|
database_name: str,
|
|
88
|
-
user:
|
|
88
|
+
user: str | None = None,
|
|
89
89
|
pw_needed: bool = True,
|
|
90
|
-
port:
|
|
91
|
-
encoding:
|
|
90
|
+
port: int | None = None,
|
|
91
|
+
encoding: str | None = None,
|
|
92
92
|
) -> MySQLDatabase:
|
|
93
93
|
return MySQLDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
94
94
|
|
|
@@ -96,14 +96,14 @@ def db_MySQL(
|
|
|
96
96
|
def db_DuckDB(
|
|
97
97
|
duckdb_fn: str,
|
|
98
98
|
new_db: bool = False,
|
|
99
|
-
encoding:
|
|
99
|
+
encoding: str | None = None,
|
|
100
100
|
) -> DuckDBDatabase:
|
|
101
101
|
if new_db:
|
|
102
102
|
from execsql.utils.fileio import check_dir
|
|
103
103
|
|
|
104
104
|
check_dir(duckdb_fn)
|
|
105
105
|
else:
|
|
106
|
-
if not
|
|
106
|
+
if not Path(duckdb_fn).exists():
|
|
107
107
|
raise ErrInfo(
|
|
108
108
|
type="error",
|
|
109
109
|
other_msg=f'DuckDB database file "{duckdb_fn}" does not exist.',
|
|
@@ -114,10 +114,10 @@ def db_DuckDB(
|
|
|
114
114
|
def db_Oracle(
|
|
115
115
|
server_name: str,
|
|
116
116
|
database_name: str,
|
|
117
|
-
user:
|
|
117
|
+
user: str | None = None,
|
|
118
118
|
pw_needed: bool = True,
|
|
119
|
-
port:
|
|
120
|
-
encoding:
|
|
119
|
+
port: int | None = None,
|
|
120
|
+
encoding: str | None = None,
|
|
121
121
|
) -> OracleDatabase:
|
|
122
122
|
return OracleDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
123
123
|
|
|
@@ -125,18 +125,18 @@ def db_Oracle(
|
|
|
125
125
|
def db_Firebird(
|
|
126
126
|
server_name: str,
|
|
127
127
|
database_name: str,
|
|
128
|
-
user:
|
|
128
|
+
user: str | None = None,
|
|
129
129
|
pw_needed: bool = True,
|
|
130
|
-
port:
|
|
131
|
-
encoding:
|
|
130
|
+
port: int | None = None,
|
|
131
|
+
encoding: str | None = None,
|
|
132
132
|
) -> FirebirdDatabase:
|
|
133
133
|
return FirebirdDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
134
134
|
|
|
135
135
|
|
|
136
136
|
def db_Dsn(
|
|
137
137
|
dsn_name: str,
|
|
138
|
-
user:
|
|
138
|
+
user: str | None = None,
|
|
139
139
|
pw_needed: bool = True,
|
|
140
|
-
encoding:
|
|
140
|
+
encoding: str | None = None,
|
|
141
141
|
) -> DsnDatabase:
|
|
142
142
|
return DsnDatabase(dsn_name=dsn_name, user_name=user, need_passwd=pw_needed, encoding=encoding)
|