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.
Files changed (75) hide show
  1. execsql/cli/__init__.py +436 -0
  2. execsql/cli/dsn.py +86 -0
  3. execsql/cli/help.py +140 -0
  4. execsql/{cli.py → cli/run.py} +14 -589
  5. execsql/config.py +13 -1
  6. execsql/db/access.py +16 -12
  7. execsql/db/base.py +158 -90
  8. execsql/db/dsn.py +6 -5
  9. execsql/db/duckdb.py +2 -2
  10. execsql/db/firebird.py +23 -19
  11. execsql/db/mysql.py +8 -7
  12. execsql/db/oracle.py +11 -11
  13. execsql/db/postgres.py +28 -16
  14. execsql/db/sqlite.py +12 -11
  15. execsql/db/sqlserver.py +5 -3
  16. execsql/exceptions.py +7 -7
  17. execsql/exporters/base.py +6 -1
  18. execsql/exporters/delimited.py +44 -35
  19. execsql/exporters/duckdb.py +2 -2
  20. execsql/exporters/feather.py +6 -6
  21. execsql/exporters/html.py +83 -69
  22. execsql/exporters/json.py +50 -42
  23. execsql/exporters/latex.py +33 -27
  24. execsql/exporters/ods.py +4 -4
  25. execsql/exporters/parquet.py +2 -2
  26. execsql/exporters/pretty.py +11 -9
  27. execsql/exporters/raw.py +17 -13
  28. execsql/exporters/sqlite.py +2 -2
  29. execsql/exporters/templates.py +23 -15
  30. execsql/exporters/values.py +22 -20
  31. execsql/exporters/xls.py +4 -4
  32. execsql/exporters/xml.py +28 -13
  33. execsql/importers/base.py +4 -4
  34. execsql/importers/csv.py +6 -6
  35. execsql/importers/feather.py +4 -4
  36. execsql/importers/ods.py +4 -4
  37. execsql/importers/xls.py +4 -4
  38. execsql/metacommands/__init__.py +518 -67
  39. execsql/metacommands/conditions.py +101 -27
  40. execsql/metacommands/control.py +8 -4
  41. execsql/metacommands/data.py +6 -6
  42. execsql/metacommands/debug.py +6 -2
  43. execsql/metacommands/io.py +67 -1310
  44. execsql/metacommands/io_export.py +442 -0
  45. execsql/metacommands/io_fileops.py +287 -0
  46. execsql/metacommands/io_import.py +398 -0
  47. execsql/metacommands/io_write.py +248 -0
  48. execsql/metacommands/prompt.py +22 -66
  49. execsql/metacommands/system.py +7 -2
  50. execsql/py.typed +0 -0
  51. execsql/script.py +49 -5
  52. execsql/types.py +20 -20
  53. execsql/utils/fileio.py +15 -8
  54. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/METADATA +6 -6
  55. execsql2-2.2.1.dist-info/RECORD +104 -0
  56. execsql2-2.1.2.dist-info/RECORD +0 -96
  57. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/READ_ME.rst +0 -0
  58. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  59. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  60. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/execsql.conf +0 -0
  61. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
  62. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_compare.sql +0 -0
  63. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
  64. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
  65. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
  66. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  67. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  68. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/script_template.sql +0 -0
  69. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
  70. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  71. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  72. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/WHEEL +0 -0
  73. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/entry_points.txt +0 -0
  74. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/licenses/LICENSE.txt +0 -0
  75. {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(Database.dt_cast)
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 = f"select Name from MSysObjects where Name='{table_name}' And Type In (1,4,6);"
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
- sql = f"select top 1 {column_name} from {table_name};"
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
- curs.execute(f"select top 1 * from {table_name};")
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 = f"select Name from MSysObjects where Name='{view_name}' And Type = 5;"
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
- sql = f"insert into {sq_name} ({column_name}) values ({self.paramsubs(1)});"
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
- dt_cast: dict[type, Callable] = {} # populated per-subclass or in __init__
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
- while True:
386
- b = []
387
- for _j in range(_state.conf.import_row_buffer):
388
- try:
389
- line = next(rows)
390
- except StopIteration:
391
- eof = True
392
- else:
393
- if len(line) > len(ts_colnames):
394
- extra_err = True
395
- if _state.conf.del_empty_cols:
396
- any_non_empty = False
397
- for cno in range(len(ts_colnames), len(line)):
398
- if not (
399
- line[cno] is None
400
- or (
401
- not _state.conf.empty_strings
402
- and isinstance(line[cno], _state.stringtypes)
403
- and len(line[cno].strip()) == 0
404
- )
405
- and _state.conf.del_empty_cols
406
- ):
407
- any_non_empty = True
408
- break
409
- extra_err = any_non_empty
410
- if extra_err:
411
- raise ErrInfo(
412
- type="error",
413
- other_msg=f"Too many data columns on line {{{line}}}",
414
- )
415
- else:
416
- line = line[: len(ts_colnames)]
417
- if not (len(line) == 1 and line[0] is None):
418
- if _state.conf.trim_strings or _state.conf.replace_newlines or not _state.conf.empty_strings:
419
- for i in range(len(line)):
420
- if line[i] is not None and isinstance(
421
- line[i],
422
- _state.stringtypes,
423
- ):
424
- if _state.conf.trim_strings:
425
- line[i] = line[i].strip()
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
- if not _state.conf.empty_strings and line[i].strip() == "":
433
- line[i] = None
434
- lt = [type_objs[i].from_data(val) if val is not None else None for i, val in enumerate(line)]
435
- lt = [type_mod_fn[i](v) if type_mod_fn[i] else v for i, v in enumerate(lt)]
436
- row = []
437
- for i, v in enumerate(lt):
438
- if incl_col[i]:
439
- row.append(v)
440
- add_line = True
441
- if not _state.conf.empty_rows:
442
- add_line = not all(c is None for c in row)
443
- if add_line:
444
- b.append(row)
445
- if len(b) > 0:
446
- try:
447
- curs.executemany(sql, b)
448
- except ErrInfo:
449
- raise
450
- except Exception:
451
- self.rollback()
452
- raise ErrInfo(
453
- type="db",
454
- command_text=sql,
455
- exception_msg=exception_desc(),
456
- other_msg=f"Can't load data into table {sq_name} of {self.name()} from line {{{line}}}",
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
- )
464
- if eof:
465
- break
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
- sql = f"insert into {sq_name} ({column_name}) values ({self.paramsubs(1)});"
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
- sql = f"insert into {sq_name} ({column_name}) values ({self.paramsubs(1)});"
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 MySQL. See https://pypi.python.org/pypi/fdb/",
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
- f"SELECT RDB$RELATION_NAME FROM RDB$RELATIONS "
126
- f"WHERE RDB$SYSTEM_FLAG=0 AND RDB$VIEW_BLR IS NULL "
127
- f"AND RDB$RELATION_NAME='{table_name.upper()}';"
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
- sql = f"select first 1 {column_name} from {table_name};"
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
- sql = f"select first 1 * from {table_name};"
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 = f"select distinct rdb$view_name from rdb$view_relations where rdb$view_name = '{view_name}';"
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
- f"SELECT DISTINCT USER FROM RDB$USER_PRIVILEGES WHERE USER = '{rolename}' union "
206
- f" SELECT DISTINCT RDB$ROLE_NAME FROM RDB$ROLES WHERE RDB$ROLE_NAME = '{rolename}';",
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
- f"select distinct user as role from mysql.user where user = '{rolename}'"
134
- f" union select distinct role_name as role from information_schema.applicable_roles"
135
- f" where role_name = '{rolename}'",
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: