execsql2 2.1.2__py3-none-any.whl → 2.2.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/cli/__init__.py +436 -0
- execsql/cli/dsn.py +86 -0
- execsql/cli/help.py +140 -0
- execsql/{cli.py → cli/run.py} +14 -589
- execsql/config.py +13 -1
- execsql/db/access.py +16 -12
- execsql/db/base.py +158 -90
- execsql/db/dsn.py +6 -5
- execsql/db/duckdb.py +2 -2
- execsql/db/firebird.py +23 -19
- execsql/db/mysql.py +8 -7
- execsql/db/oracle.py +11 -11
- execsql/db/postgres.py +28 -16
- execsql/db/sqlite.py +12 -11
- execsql/db/sqlserver.py +5 -3
- execsql/exceptions.py +7 -7
- execsql/exporters/base.py +6 -1
- execsql/exporters/delimited.py +44 -35
- execsql/exporters/duckdb.py +2 -2
- execsql/exporters/feather.py +6 -6
- execsql/exporters/html.py +83 -69
- execsql/exporters/json.py +50 -42
- execsql/exporters/latex.py +33 -27
- execsql/exporters/ods.py +4 -4
- execsql/exporters/parquet.py +2 -2
- execsql/exporters/pretty.py +11 -9
- execsql/exporters/raw.py +17 -13
- execsql/exporters/sqlite.py +2 -2
- execsql/exporters/templates.py +23 -15
- execsql/exporters/values.py +22 -20
- execsql/exporters/xls.py +4 -4
- execsql/exporters/xml.py +28 -13
- execsql/importers/base.py +4 -4
- execsql/importers/csv.py +6 -6
- execsql/importers/feather.py +4 -4
- execsql/importers/ods.py +4 -4
- execsql/importers/xls.py +4 -4
- execsql/metacommands/__init__.py +518 -67
- execsql/metacommands/conditions.py +101 -27
- execsql/metacommands/control.py +8 -4
- execsql/metacommands/data.py +6 -6
- execsql/metacommands/debug.py +6 -2
- execsql/metacommands/io.py +67 -1310
- execsql/metacommands/io_export.py +442 -0
- execsql/metacommands/io_fileops.py +287 -0
- execsql/metacommands/io_import.py +398 -0
- execsql/metacommands/io_write.py +248 -0
- execsql/metacommands/prompt.py +22 -66
- execsql/metacommands/system.py +7 -2
- execsql/py.typed +0 -0
- execsql/script.py +49 -5
- execsql/types.py +20 -20
- execsql/utils/fileio.py +15 -8
- {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/METADATA +6 -6
- execsql2-2.2.1.dist-info/RECORD +104 -0
- execsql2-2.1.2.dist-info/RECORD +0 -96
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/READ_ME.rst +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/WHEEL +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/licenses/NOTICE +0 -0
execsql/db/access.py
CHANGED
|
@@ -68,7 +68,7 @@ class AccessDatabase(Database):
|
|
|
68
68
|
self.dao_conn = None
|
|
69
69
|
self.conn = None # ODBC connection
|
|
70
70
|
self.paramstr = "?"
|
|
71
|
-
self.dt_cast = dict(
|
|
71
|
+
self.dt_cast = dict(self.dt_cast) # Copy the lazy-initialized default before overriding.
|
|
72
72
|
self.dt_cast[datetime.date] = self.as_datetime
|
|
73
73
|
self.dt_cast[datetime.datetime] = self.as_datetime
|
|
74
74
|
self.dt_cast[int] = self.int_or_bool
|
|
@@ -290,17 +290,17 @@ class AccessDatabase(Database):
|
|
|
290
290
|
self.dao_flush_check()
|
|
291
291
|
curs = self.cursor()
|
|
292
292
|
try:
|
|
293
|
-
sql =
|
|
294
|
-
curs.execute(sql)
|
|
293
|
+
sql = "select Name from MSysObjects where Name=? And Type In (1,4,6);"
|
|
294
|
+
curs.execute(sql, (table_name,))
|
|
295
295
|
except ErrInfo:
|
|
296
296
|
raise
|
|
297
|
-
except Exception:
|
|
297
|
+
except Exception as e:
|
|
298
298
|
raise ErrInfo(
|
|
299
299
|
type="db",
|
|
300
300
|
command_text=sql,
|
|
301
301
|
exception_msg=exception_desc(),
|
|
302
302
|
other_msg=f"Failure on test for existence of Access table {table_name}",
|
|
303
|
-
)
|
|
303
|
+
) from e
|
|
304
304
|
rows = curs.fetchall()
|
|
305
305
|
return len(rows) > 0
|
|
306
306
|
|
|
@@ -312,7 +312,9 @@ class AccessDatabase(Database):
|
|
|
312
312
|
) -> bool:
|
|
313
313
|
self.dao_flush_check()
|
|
314
314
|
curs = self.cursor()
|
|
315
|
-
|
|
315
|
+
quoted_col = self.quote_identifier(column_name)
|
|
316
|
+
quoted_tbl = self.quote_identifier(table_name)
|
|
317
|
+
sql = f"select top 1 {quoted_col} from {quoted_tbl};"
|
|
316
318
|
try:
|
|
317
319
|
curs.execute(sql)
|
|
318
320
|
except Exception:
|
|
@@ -322,24 +324,25 @@ class AccessDatabase(Database):
|
|
|
322
324
|
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
323
325
|
self.dao_flush_check()
|
|
324
326
|
curs = self.cursor()
|
|
325
|
-
|
|
327
|
+
quoted_tbl = self.quote_identifier(table_name)
|
|
328
|
+
curs.execute(f"select top 1 * from {quoted_tbl};")
|
|
326
329
|
return [d[0] for d in curs.description]
|
|
327
330
|
|
|
328
331
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
329
332
|
self.dao_flush_check()
|
|
330
333
|
curs = self.cursor()
|
|
331
334
|
try:
|
|
332
|
-
sql =
|
|
333
|
-
curs.execute(sql)
|
|
335
|
+
sql = "select Name from MSysObjects where Name=? And Type = 5;"
|
|
336
|
+
curs.execute(sql, (view_name,))
|
|
334
337
|
except ErrInfo:
|
|
335
338
|
raise
|
|
336
|
-
except Exception:
|
|
339
|
+
except Exception as e:
|
|
337
340
|
raise ErrInfo(
|
|
338
341
|
type="db",
|
|
339
342
|
command_text=sql,
|
|
340
343
|
exception_msg=exception_desc(),
|
|
341
344
|
other_msg=f"Test for existence of Access view/query {view_name}",
|
|
342
|
-
)
|
|
345
|
+
) from e
|
|
343
346
|
rows = curs.fetchall()
|
|
344
347
|
return len(rows) > 0
|
|
345
348
|
|
|
@@ -414,5 +417,6 @@ class AccessDatabase(Database):
|
|
|
414
417
|
with open(file_name, "rb") as f:
|
|
415
418
|
filedata = f.read()
|
|
416
419
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
417
|
-
|
|
420
|
+
quoted_col = self.quote_identifier(column_name)
|
|
421
|
+
sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
|
|
418
422
|
self.cursor().execute(sql, (pyodbc.Binary(filedata),))
|
execsql/db/base.py
CHANGED
|
@@ -14,7 +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
|
|
17
18
|
import re
|
|
19
|
+
from decimal import Decimal
|
|
18
20
|
from typing import Any
|
|
19
21
|
from collections.abc import Callable, Generator, Iterator
|
|
20
22
|
|
|
@@ -23,10 +25,36 @@ from execsql.utils.errors import exception_desc
|
|
|
23
25
|
import execsql.state as _state
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
def _default_dt_cast() -> dict[type, Callable]:
|
|
29
|
+
"""Build the default type-cast mapping used by all database backends."""
|
|
30
|
+
from execsql.types import DT_Boolean, DT_Timestamp, DT_Date, DT_Decimal
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
int: int,
|
|
34
|
+
float: float,
|
|
35
|
+
str: str,
|
|
36
|
+
bool: DT_Boolean().from_data,
|
|
37
|
+
datetime.datetime: DT_Timestamp().from_data,
|
|
38
|
+
datetime.date: DT_Date().from_data,
|
|
39
|
+
Decimal: DT_Decimal().from_data,
|
|
40
|
+
bytearray: bytearray,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
26
44
|
class Database:
|
|
27
45
|
"""Abstract base class for all database connections."""
|
|
28
46
|
|
|
29
|
-
|
|
47
|
+
_dt_cast: dict[type, Callable] | None = None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def dt_cast(self) -> dict[type, Callable]:
|
|
51
|
+
if self._dt_cast is None:
|
|
52
|
+
self._dt_cast = _default_dt_cast()
|
|
53
|
+
return self._dt_cast
|
|
54
|
+
|
|
55
|
+
@dt_cast.setter
|
|
56
|
+
def dt_cast(self, value: dict[type, Callable]) -> None:
|
|
57
|
+
self._dt_cast = value
|
|
30
58
|
|
|
31
59
|
def __init__(
|
|
32
60
|
self,
|
|
@@ -237,14 +265,14 @@ class Database:
|
|
|
237
265
|
curs.execute(sql, params)
|
|
238
266
|
except ErrInfo:
|
|
239
267
|
raise
|
|
240
|
-
except Exception:
|
|
268
|
+
except Exception as e:
|
|
241
269
|
self.rollback()
|
|
242
270
|
raise ErrInfo(
|
|
243
271
|
type="db",
|
|
244
272
|
command_text=sql,
|
|
245
273
|
exception_msg=exception_desc(),
|
|
246
274
|
other_msg=f"Failed test for existence of table {table_name} in {self.name()}",
|
|
247
|
-
)
|
|
275
|
+
) from e
|
|
248
276
|
rows = curs.fetchall()
|
|
249
277
|
curs.close()
|
|
250
278
|
return len(rows) > 0
|
|
@@ -271,14 +299,14 @@ class Database:
|
|
|
271
299
|
curs.execute(sql, params)
|
|
272
300
|
except ErrInfo:
|
|
273
301
|
raise
|
|
274
|
-
except Exception:
|
|
302
|
+
except Exception as e:
|
|
275
303
|
self.rollback()
|
|
276
304
|
raise ErrInfo(
|
|
277
305
|
type="db",
|
|
278
306
|
command_text=sql,
|
|
279
307
|
exception_msg=exception_desc(),
|
|
280
308
|
other_msg=f"Failed test for existence of column {column_name} in table {table_name} of {self.name()}",
|
|
281
|
-
)
|
|
309
|
+
) from e
|
|
282
310
|
rows = curs.fetchall()
|
|
283
311
|
curs.close()
|
|
284
312
|
return len(rows) > 0
|
|
@@ -299,14 +327,14 @@ class Database:
|
|
|
299
327
|
curs.execute(sql, params)
|
|
300
328
|
except ErrInfo:
|
|
301
329
|
raise
|
|
302
|
-
except Exception:
|
|
330
|
+
except Exception as e:
|
|
303
331
|
self.rollback()
|
|
304
332
|
raise ErrInfo(
|
|
305
333
|
type="db",
|
|
306
334
|
command_text=sql,
|
|
307
335
|
exception_msg=exception_desc(),
|
|
308
336
|
other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
|
|
309
|
-
)
|
|
337
|
+
) from e
|
|
310
338
|
rows = curs.fetchall()
|
|
311
339
|
curs.close()
|
|
312
340
|
return [row[0] for row in rows]
|
|
@@ -323,14 +351,14 @@ class Database:
|
|
|
323
351
|
curs.execute(sql, params)
|
|
324
352
|
except ErrInfo:
|
|
325
353
|
raise
|
|
326
|
-
except Exception:
|
|
354
|
+
except Exception as e:
|
|
327
355
|
self.rollback()
|
|
328
356
|
raise ErrInfo(
|
|
329
357
|
type="db",
|
|
330
358
|
command_text=sql,
|
|
331
359
|
exception_msg=exception_desc(),
|
|
332
360
|
other_msg=f"Failed test for existence of view {view_name} in {self.name()}",
|
|
333
|
-
)
|
|
361
|
+
) from e
|
|
334
362
|
rows = curs.fetchall()
|
|
335
363
|
curs.close()
|
|
336
364
|
return len(rows) > 0
|
|
@@ -382,87 +410,126 @@ class Database:
|
|
|
382
410
|
curs = self.cursor()
|
|
383
411
|
eof = False
|
|
384
412
|
total_rows = 0
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if _state.conf.replace_newlines:
|
|
427
|
-
line[i] = re.sub(
|
|
428
|
-
r"[\s\t]*[\r\n]+[\s\t]*",
|
|
429
|
-
" ",
|
|
430
|
-
line[i],
|
|
413
|
+
|
|
414
|
+
# Optional rich progress bar for long-running imports.
|
|
415
|
+
use_progress = getattr(_state.conf, "show_progress", False)
|
|
416
|
+
progress_ctx = None
|
|
417
|
+
task_id = None
|
|
418
|
+
if use_progress:
|
|
419
|
+
try:
|
|
420
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
421
|
+
from rich.console import Console
|
|
422
|
+
|
|
423
|
+
progress_ctx = Progress(
|
|
424
|
+
SpinnerColumn(),
|
|
425
|
+
TextColumn("[bold blue]IMPORT[/bold blue] {task.description}"),
|
|
426
|
+
TextColumn("{task.completed:,} rows"),
|
|
427
|
+
TimeElapsedColumn(),
|
|
428
|
+
console=Console(stderr=True),
|
|
429
|
+
)
|
|
430
|
+
except ImportError:
|
|
431
|
+
use_progress = False
|
|
432
|
+
|
|
433
|
+
def _import_loop() -> int:
|
|
434
|
+
nonlocal eof, total_rows, task_id
|
|
435
|
+
while True:
|
|
436
|
+
b = []
|
|
437
|
+
for _j in range(_state.conf.import_row_buffer):
|
|
438
|
+
try:
|
|
439
|
+
line = next(rows)
|
|
440
|
+
except StopIteration:
|
|
441
|
+
eof = True
|
|
442
|
+
else:
|
|
443
|
+
if len(line) > len(ts_colnames):
|
|
444
|
+
extra_err = True
|
|
445
|
+
if _state.conf.del_empty_cols:
|
|
446
|
+
any_non_empty = False
|
|
447
|
+
for cno in range(len(ts_colnames), len(line)):
|
|
448
|
+
if not (
|
|
449
|
+
line[cno] is None
|
|
450
|
+
or (
|
|
451
|
+
not _state.conf.empty_strings
|
|
452
|
+
and isinstance(line[cno], _state.stringtypes)
|
|
453
|
+
and len(line[cno].strip()) == 0
|
|
431
454
|
)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
455
|
+
and _state.conf.del_empty_cols
|
|
456
|
+
):
|
|
457
|
+
any_non_empty = True
|
|
458
|
+
break
|
|
459
|
+
extra_err = any_non_empty
|
|
460
|
+
if extra_err:
|
|
461
|
+
raise ErrInfo(
|
|
462
|
+
type="error",
|
|
463
|
+
other_msg=f"Too many data columns on line {{{line}}}",
|
|
464
|
+
)
|
|
465
|
+
else:
|
|
466
|
+
line = line[: len(ts_colnames)]
|
|
467
|
+
if not (len(line) == 1 and line[0] is None):
|
|
468
|
+
if (
|
|
469
|
+
_state.conf.trim_strings
|
|
470
|
+
or _state.conf.replace_newlines
|
|
471
|
+
or not _state.conf.empty_strings
|
|
472
|
+
):
|
|
473
|
+
for i in range(len(line)):
|
|
474
|
+
if line[i] is not None and isinstance(
|
|
475
|
+
line[i],
|
|
476
|
+
_state.stringtypes,
|
|
477
|
+
):
|
|
478
|
+
if _state.conf.trim_strings:
|
|
479
|
+
line[i] = line[i].strip()
|
|
480
|
+
if _state.conf.replace_newlines:
|
|
481
|
+
line[i] = re.sub(
|
|
482
|
+
r"[\s\t]*[\r\n]+[\s\t]*",
|
|
483
|
+
" ",
|
|
484
|
+
line[i],
|
|
485
|
+
)
|
|
486
|
+
if not _state.conf.empty_strings and line[i].strip() == "":
|
|
487
|
+
line[i] = None
|
|
488
|
+
lt = [
|
|
489
|
+
type_objs[i].from_data(val) if val is not None else None for i, val in enumerate(line)
|
|
490
|
+
]
|
|
491
|
+
lt = [type_mod_fn[i](v) if type_mod_fn[i] else v for i, v in enumerate(lt)]
|
|
492
|
+
row = []
|
|
493
|
+
for i, v in enumerate(lt):
|
|
494
|
+
if incl_col[i]:
|
|
495
|
+
row.append(v)
|
|
496
|
+
add_line = True
|
|
497
|
+
if not _state.conf.empty_rows:
|
|
498
|
+
add_line = not all(c is None for c in row)
|
|
499
|
+
if add_line:
|
|
500
|
+
b.append(row)
|
|
501
|
+
if len(b) > 0:
|
|
502
|
+
try:
|
|
503
|
+
curs.executemany(sql, b)
|
|
504
|
+
except ErrInfo:
|
|
505
|
+
raise
|
|
506
|
+
except Exception as e:
|
|
507
|
+
self.rollback()
|
|
508
|
+
raise ErrInfo(
|
|
509
|
+
type="db",
|
|
510
|
+
command_text=sql,
|
|
511
|
+
exception_msg=exception_desc(),
|
|
512
|
+
other_msg=f"Can't load data into table {sq_name} of {self.name()} from line {{{line}}}",
|
|
513
|
+
) from e
|
|
514
|
+
total_rows += len(b)
|
|
515
|
+
if use_progress and progress_ctx is not None and task_id is not None:
|
|
516
|
+
progress_ctx.update(task_id, completed=total_rows)
|
|
517
|
+
interval = _state.conf.import_progress_interval
|
|
518
|
+
if _state.exec_log and interval > 0 and total_rows % interval == 0:
|
|
519
|
+
_state.exec_log.log_status_info(
|
|
520
|
+
f"IMPORT into {sq_name}: {total_rows} rows imported so far.",
|
|
521
|
+
)
|
|
522
|
+
if eof:
|
|
523
|
+
break
|
|
524
|
+
return total_rows
|
|
525
|
+
|
|
526
|
+
if use_progress and progress_ctx is not None:
|
|
527
|
+
with progress_ctx:
|
|
528
|
+
task_id = progress_ctx.add_task(sq_name, total=None)
|
|
529
|
+
_import_loop()
|
|
530
|
+
else:
|
|
531
|
+
_import_loop()
|
|
532
|
+
|
|
466
533
|
if _state.exec_log:
|
|
467
534
|
_state.exec_log.log_status_info(
|
|
468
535
|
f"IMPORT into {sq_name} complete: {total_rows} rows imported.",
|
|
@@ -520,7 +587,8 @@ class Database:
|
|
|
520
587
|
with open(file_name, "rb") as f:
|
|
521
588
|
filedata = f.read()
|
|
522
589
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
523
|
-
|
|
590
|
+
quoted_col = self.quote_identifier(column_name)
|
|
591
|
+
sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
|
|
524
592
|
self.cursor().execute(sql, (filedata,))
|
|
525
593
|
|
|
526
594
|
|
execsql/db/dsn.py
CHANGED
|
@@ -79,23 +79,23 @@ class DsnDatabase(Database):
|
|
|
79
79
|
def _try_connect():
|
|
80
80
|
try:
|
|
81
81
|
_dsn_connect()
|
|
82
|
-
except Exception:
|
|
82
|
+
except Exception as e:
|
|
83
83
|
excdesc = exception_desc()
|
|
84
84
|
if "Optional feature not implemented" in excdesc:
|
|
85
85
|
try:
|
|
86
86
|
_dsn_connect(autocommit=True)
|
|
87
|
-
except Exception:
|
|
87
|
+
except Exception as e:
|
|
88
88
|
raise ErrInfo(
|
|
89
89
|
type="exception",
|
|
90
90
|
exception_msg=exception_desc(),
|
|
91
91
|
other_msg=f"Can't open DSN database {self.db_name} using ODBC",
|
|
92
|
-
)
|
|
92
|
+
) from e
|
|
93
93
|
else:
|
|
94
94
|
raise ErrInfo(
|
|
95
95
|
type="exception",
|
|
96
96
|
exception_msg=excdesc,
|
|
97
97
|
other_msg=f"Can't open DSN database {self.db_name} using ODBC",
|
|
98
|
-
)
|
|
98
|
+
) from e
|
|
99
99
|
|
|
100
100
|
try:
|
|
101
101
|
_try_connect()
|
|
@@ -136,5 +136,6 @@ class DsnDatabase(Database):
|
|
|
136
136
|
with open(file_name, "rb") as f:
|
|
137
137
|
filedata = f.read()
|
|
138
138
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
139
|
-
|
|
139
|
+
quoted_col = self.quote_identifier(column_name)
|
|
140
|
+
sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
|
|
140
141
|
self.cursor().execute(sql, (pyodbc.Binary(filedata),))
|
execsql/db/duckdb.py
CHANGED
|
@@ -48,12 +48,12 @@ class DuckDBDatabase(Database):
|
|
|
48
48
|
self.conn = duckdb.connect(self.db_name, read_only=False)
|
|
49
49
|
except ErrInfo:
|
|
50
50
|
raise
|
|
51
|
-
except Exception:
|
|
51
|
+
except Exception as e:
|
|
52
52
|
raise ErrInfo(
|
|
53
53
|
type="exception",
|
|
54
54
|
exception_msg=exception_desc(),
|
|
55
55
|
other_msg=f"Can't open DuckDB database {self.db_name}",
|
|
56
|
-
)
|
|
56
|
+
) from e
|
|
57
57
|
|
|
58
58
|
def exec_cmd(self, querycommand: str) -> None:
|
|
59
59
|
# DuckDB does not support stored functions, so the querycommand
|
execsql/db/firebird.py
CHANGED
|
@@ -30,7 +30,7 @@ class FirebirdDatabase(Database):
|
|
|
30
30
|
import fdb as firebird_lib # noqa: F401
|
|
31
31
|
except Exception:
|
|
32
32
|
fatal_error(
|
|
33
|
-
"The fdb module is required to connect to
|
|
33
|
+
"The fdb module is required to connect to Firebird. See https://pypi.python.org/pypi/fdb/",
|
|
34
34
|
)
|
|
35
35
|
from execsql.types import dbt_firebird
|
|
36
36
|
|
|
@@ -104,9 +104,9 @@ class FirebirdDatabase(Database):
|
|
|
104
104
|
raise
|
|
105
105
|
except ErrInfo:
|
|
106
106
|
raise
|
|
107
|
-
except Exception:
|
|
107
|
+
except Exception as e:
|
|
108
108
|
msg = f"Failed to open Firebird database {self.db_name} on {self.server_name}"
|
|
109
|
-
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg)
|
|
109
|
+
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
|
|
110
110
|
|
|
111
111
|
def exec_cmd(self, querycommand: str) -> None:
|
|
112
112
|
# The querycommand must be a stored function (/procedure)
|
|
@@ -122,15 +122,15 @@ class FirebirdDatabase(Database):
|
|
|
122
122
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
123
123
|
curs = self.cursor()
|
|
124
124
|
sql = (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
"SELECT RDB$RELATION_NAME FROM RDB$RELATIONS "
|
|
126
|
+
"WHERE RDB$SYSTEM_FLAG=0 AND RDB$VIEW_BLR IS NULL "
|
|
127
|
+
"AND RDB$RELATION_NAME=?;"
|
|
128
128
|
)
|
|
129
129
|
try:
|
|
130
|
-
curs.execute(sql)
|
|
130
|
+
curs.execute(sql, (table_name.upper(),))
|
|
131
131
|
except ErrInfo:
|
|
132
132
|
raise
|
|
133
|
-
except Exception:
|
|
133
|
+
except Exception as e:
|
|
134
134
|
try:
|
|
135
135
|
self.rollback()
|
|
136
136
|
except Exception:
|
|
@@ -140,7 +140,7 @@ class FirebirdDatabase(Database):
|
|
|
140
140
|
command_text=sql,
|
|
141
141
|
exception_msg=exception_desc(),
|
|
142
142
|
other_msg=f"Failed test for existence of Firebird table {table_name}",
|
|
143
|
-
)
|
|
143
|
+
) from e
|
|
144
144
|
rows = curs.fetchall()
|
|
145
145
|
self.conn.commit()
|
|
146
146
|
curs.close()
|
|
@@ -153,7 +153,9 @@ class FirebirdDatabase(Database):
|
|
|
153
153
|
schema_name: str | None = None,
|
|
154
154
|
) -> bool:
|
|
155
155
|
curs = self.cursor()
|
|
156
|
-
|
|
156
|
+
quoted_col = self.quote_identifier(column_name)
|
|
157
|
+
quoted_tbl = self.quote_identifier(table_name)
|
|
158
|
+
sql = f"select first 1 {quoted_col} from {quoted_tbl};"
|
|
157
159
|
try:
|
|
158
160
|
curs.execute(sql)
|
|
159
161
|
except Exception:
|
|
@@ -162,36 +164,37 @@ class FirebirdDatabase(Database):
|
|
|
162
164
|
|
|
163
165
|
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
164
166
|
curs = self.cursor()
|
|
165
|
-
|
|
167
|
+
quoted_tbl = self.quote_identifier(table_name)
|
|
168
|
+
sql = f"select first 1 * from {quoted_tbl};"
|
|
166
169
|
try:
|
|
167
170
|
curs.execute(sql)
|
|
168
171
|
except ErrInfo:
|
|
169
172
|
raise
|
|
170
|
-
except Exception:
|
|
173
|
+
except Exception as e:
|
|
171
174
|
self.rollback()
|
|
172
175
|
raise ErrInfo(
|
|
173
176
|
type="db",
|
|
174
177
|
command_text=sql,
|
|
175
178
|
exception_msg=exception_desc(),
|
|
176
179
|
other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
|
|
177
|
-
)
|
|
180
|
+
) from e
|
|
178
181
|
return [d[0] for d in curs.description]
|
|
179
182
|
|
|
180
183
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
181
184
|
curs = self.cursor()
|
|
182
|
-
sql =
|
|
185
|
+
sql = "select distinct rdb$view_name from rdb$view_relations where rdb$view_name = ?;"
|
|
183
186
|
try:
|
|
184
|
-
curs.execute(sql)
|
|
187
|
+
curs.execute(sql, (view_name,))
|
|
185
188
|
except ErrInfo:
|
|
186
189
|
raise
|
|
187
|
-
except Exception:
|
|
190
|
+
except Exception as e:
|
|
188
191
|
self.rollback()
|
|
189
192
|
raise ErrInfo(
|
|
190
193
|
type="db",
|
|
191
194
|
command_text=sql,
|
|
192
195
|
exception_msg=exception_desc(),
|
|
193
196
|
other_msg=f"Failed test for existence of Firebird view {view_name}",
|
|
194
|
-
)
|
|
197
|
+
) from e
|
|
195
198
|
rows = curs.fetchall()
|
|
196
199
|
curs.close()
|
|
197
200
|
return len(rows) > 0
|
|
@@ -202,8 +205,9 @@ class FirebirdDatabase(Database):
|
|
|
202
205
|
def role_exists(self, rolename: str) -> bool:
|
|
203
206
|
curs = self.cursor()
|
|
204
207
|
curs.execute(
|
|
205
|
-
|
|
206
|
-
|
|
208
|
+
"SELECT DISTINCT USER FROM RDB$USER_PRIVILEGES WHERE USER = ? union "
|
|
209
|
+
" SELECT DISTINCT RDB$ROLE_NAME FROM RDB$ROLES WHERE RDB$ROLE_NAME = ?;",
|
|
210
|
+
(rolename, rolename),
|
|
207
211
|
)
|
|
208
212
|
rows = curs.fetchall()
|
|
209
213
|
curs.close()
|
execsql/db/mysql.py
CHANGED
|
@@ -109,9 +109,9 @@ class MySQLDatabase(Database):
|
|
|
109
109
|
raise
|
|
110
110
|
except ErrInfo:
|
|
111
111
|
raise
|
|
112
|
-
except Exception:
|
|
112
|
+
except Exception as e:
|
|
113
113
|
msg = f"Failed to open MySQL database {self.db_name} on {self.server_name}"
|
|
114
|
-
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg)
|
|
114
|
+
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
|
|
115
115
|
|
|
116
116
|
def exec_cmd(self, querycommand: str) -> None:
|
|
117
117
|
# The querycommand must be a stored function (/procedure)
|
|
@@ -130,9 +130,10 @@ class MySQLDatabase(Database):
|
|
|
130
130
|
def role_exists(self, rolename: str) -> bool:
|
|
131
131
|
curs = self.cursor()
|
|
132
132
|
curs.execute(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
"select distinct user as role from mysql.user where user = %s"
|
|
134
|
+
" union select distinct role_name as role from information_schema.applicable_roles"
|
|
135
|
+
" where role_name = %s",
|
|
136
|
+
(rolename, rolename),
|
|
136
137
|
)
|
|
137
138
|
rows = curs.fetchall()
|
|
138
139
|
curs.close()
|
|
@@ -274,14 +275,14 @@ class MySQLDatabase(Database):
|
|
|
274
275
|
curs.executemany(sql_template, b)
|
|
275
276
|
except ErrInfo:
|
|
276
277
|
raise
|
|
277
|
-
except Exception:
|
|
278
|
+
except Exception as e:
|
|
278
279
|
self.rollback()
|
|
279
280
|
raise ErrInfo(
|
|
280
281
|
type="db",
|
|
281
282
|
command_text=sql_template,
|
|
282
283
|
exception_msg=exception_desc(),
|
|
283
284
|
other_msg=f"Import from file into table {sq_name}, line {{{line}}}",
|
|
284
|
-
)
|
|
285
|
+
) from e
|
|
285
286
|
total_rows += len(b)
|
|
286
287
|
interval = _state.conf.import_progress_interval
|
|
287
288
|
if _state.exec_log and interval > 0 and total_rows % interval == 0:
|