execsql2 2.21.2__py3-none-any.whl → 2.22.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.
Files changed (81) hide show
  1. execsql/api.py +21 -4
  2. execsql/cli/run.py +37 -17
  3. execsql/config.py +37 -22
  4. execsql/data/execsql.conf.template +6 -0
  5. execsql/db/access.py +6 -5
  6. execsql/db/base.py +23 -5
  7. execsql/db/dsn.py +2 -0
  8. execsql/db/duckdb.py +1 -1
  9. execsql/db/mysql.py +6 -8
  10. execsql/db/sqlite.py +1 -1
  11. execsql/exceptions.py +14 -11
  12. execsql/exporters/base.py +1 -1
  13. execsql/exporters/delimited.py +22 -16
  14. execsql/exporters/duckdb.py +6 -1
  15. execsql/exporters/feather.py +1 -1
  16. execsql/exporters/json.py +5 -3
  17. execsql/exporters/latex.py +2 -2
  18. execsql/exporters/markdown.py +1 -0
  19. execsql/exporters/ods.py +7 -3
  20. execsql/exporters/raw.py +8 -8
  21. execsql/exporters/sqlite.py +11 -1
  22. execsql/exporters/templates.py +2 -1
  23. execsql/exporters/xls.py +10 -10
  24. execsql/exporters/xml.py +1 -0
  25. execsql/exporters/yaml.py +1 -0
  26. execsql/exporters/zip.py +9 -7
  27. execsql/format.py +6 -6
  28. execsql/gui/console.py +3 -3
  29. execsql/gui/desktop.py +28 -10
  30. execsql/gui/tui.py +8 -6
  31. execsql/importers/base.py +14 -8
  32. execsql/importers/csv.py +8 -3
  33. execsql/importers/xls.py +1 -0
  34. execsql/metacommands/conditions.py +14 -14
  35. execsql/metacommands/connect.py +2 -2
  36. execsql/metacommands/control.py +1 -1
  37. execsql/metacommands/data.py +2 -1
  38. execsql/metacommands/io_export.py +4 -0
  39. execsql/metacommands/io_fileops.py +85 -39
  40. execsql/metacommands/io_import.py +1 -0
  41. execsql/metacommands/io_write.py +1 -1
  42. execsql/metacommands/prompt.py +24 -22
  43. execsql/metacommands/system.py +14 -1
  44. execsql/models.py +56 -17
  45. execsql/parser.py +8 -8
  46. execsql/plugins.py +1 -1
  47. execsql/script/engine.py +4 -4
  48. execsql/script/executor.py +67 -28
  49. execsql/script/parser.py +13 -12
  50. execsql/script/variables.py +4 -2
  51. execsql/state.pyi +122 -0
  52. execsql/types.py +31 -26
  53. execsql/utils/auth.py +6 -2
  54. execsql/utils/datetime.py +3 -3
  55. execsql/utils/errors.py +7 -5
  56. execsql/utils/fileio.py +77 -31
  57. execsql/utils/gui.py +15 -11
  58. execsql/utils/mail.py +9 -2
  59. execsql/utils/numeric.py +1 -1
  60. execsql/utils/regex.py +9 -9
  61. {execsql2-2.21.2.dist-info → execsql2-2.22.0.dist-info}/METADATA +3 -3
  62. execsql2-2.22.0.dist-info/RECORD +123 -0
  63. execsql2-2.21.2.dist-info/RECORD +0 -122
  64. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/README.md +0 -0
  65. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  66. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  67. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  68. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  69. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  70. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  71. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  72. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  73. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  74. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/script_template.sql +0 -0
  75. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  76. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  77. {execsql2-2.21.2.data → execsql2-2.22.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  78. {execsql2-2.21.2.dist-info → execsql2-2.22.0.dist-info}/WHEEL +0 -0
  79. {execsql2-2.21.2.dist-info → execsql2-2.22.0.dist-info}/entry_points.txt +0 -0
  80. {execsql2-2.21.2.dist-info → execsql2-2.22.0.dist-info}/licenses/LICENSE.txt +0 -0
  81. {execsql2-2.21.2.dist-info → execsql2-2.22.0.dist-info}/licenses/NOTICE +0 -0
execsql/gui/desktop.py CHANGED
@@ -12,7 +12,8 @@ from __future__ import annotations
12
12
  import os
13
13
  import threading
14
14
  import time
15
- from typing import Any
15
+ from typing import Any, cast
16
+ from collections.abc import Callable
16
17
 
17
18
  from execsql.gui.base import GuiBackend
18
19
 
@@ -53,7 +54,7 @@ def _center_window(win: tk.Tk | tk.Toplevel, width: int = 600, height: int = 400
53
54
  from execsql.gui.base import DIFF_MARKER, compare_stats as _compare_stats, compute_row_diffs
54
55
 
55
56
 
56
- def _add_help_button(frame: tk.Frame, url: str | None) -> None:
57
+ def _add_help_button(frame: Any, url: str | None) -> None:
57
58
  """Add a Help button to the top-right of *frame* that opens *url* in the system browser.
58
59
 
59
60
  Uses ``place()`` so the button overlays the top-right corner without
@@ -82,19 +83,31 @@ def _populate_treeview(tree: ttk.Treeview, headers: list, rows: list) -> None:
82
83
  tree.insert("", "end", values=[str(c) if c is not None else "" for c in row])
83
84
 
84
85
 
85
- def _add_buttons(frame: tk.Frame, button_list: list, callback) -> None:
86
+ def _add_buttons(frame: Any, button_list: list, callback: Callable[[Any], Any]) -> None:
86
87
  """Add buttons from button_list = [(label, value, key?), ...] to frame."""
87
88
  for _i, btn in enumerate(button_list):
88
89
  label = btn[0]
89
90
  value = btn[1]
90
- b = tk.Button(frame, text=label, command=lambda v=value: callback(v), padx=8)
91
+
92
+ def _command(v: Any = value) -> None:
93
+ callback(v)
94
+
95
+ b = tk.Button(frame, text=label, command=_command, padx=8)
91
96
  b.pack(side=tk.LEFT, padx=4)
92
97
  if len(btn) > 2 and btn[2]:
93
98
  key_seq = btn[2]
94
99
  if key_seq == "<Return>":
95
- frame.winfo_toplevel().bind("<Return>", lambda e, v=value: callback(v))
100
+
101
+ def _return_handler(_event: Any, v: Any = value) -> None:
102
+ callback(v)
103
+
104
+ frame.winfo_toplevel().bind("<Return>", _return_handler)
96
105
  elif len(key_seq) == 1:
97
- frame.winfo_toplevel().bind(key_seq, lambda e, v=value: callback(v))
106
+
107
+ def _key_handler(_event: Any, v: Any = value) -> None:
108
+ callback(v)
109
+
110
+ frame.winfo_toplevel().bind(key_seq, _key_handler)
98
111
 
99
112
 
100
113
  # ---------------------------------------------------------------------------
@@ -318,6 +331,7 @@ class EntryFormDialog:
318
331
 
319
332
  for i, spec in enumerate(specs):
320
333
  etype = (spec.entry_type or "text").lower()
334
+ var: Any
321
335
  # For radiobuttons, the label is semicolon-delimited: first part is the label
322
336
  if etype == "radiobuttons":
323
337
  parts = (spec.label or "").split(";")
@@ -369,7 +383,7 @@ class EntryFormDialog:
369
383
  var = tk.StringVar(value=spec.initial_value or "")
370
384
  ttk.Entry(file_frame, textvariable=var, width=30).pack(side=tk.LEFT, fill=tk.X, expand=True)
371
385
 
372
- def _browse(sv=var, mode=etype):
386
+ def _browse(sv: Any = var, mode: str = etype) -> None:
373
387
  from tkinter import filedialog
374
388
 
375
389
  if mode == "inputfile":
@@ -597,7 +611,7 @@ class CompareDialog:
597
611
  tree.item(iid, tags=("diff_match",))
598
612
  elif state == "changed":
599
613
  tree.item(iid, tags=("diff_changed",))
600
- originals[iid] = tree.item(iid, "values")
614
+ originals[iid] = cast(tuple[Any, ...], tree.item(iid, "values"))
601
615
  vals = list(tree.item(iid, "values"))
602
616
  diff_set = changed_cols[ridx]
603
617
  for ci, col_name in enumerate(headers_str):
@@ -828,10 +842,14 @@ class ActionDialog:
828
842
  button_specs = args.get("button_specs", [])
829
843
  for i, spec in enumerate(button_specs):
830
844
  btn_text = f"{spec.label}\n{spec.prompt}"
845
+
846
+ def _button_command(v: int = i + 1) -> None:
847
+ self._close(win, v)
848
+
831
849
  ttk.Button(
832
850
  frame,
833
851
  text=btn_text,
834
- command=lambda v=i + 1: self._close(win, v),
852
+ command=_button_command,
835
853
  ).pack(fill=tk.X, pady=2)
836
854
 
837
855
  include_continue = args.get("include_continue_button")
@@ -1380,7 +1398,7 @@ class TkinterBackend(GuiBackend):
1380
1398
  root = self._root_or_raise()
1381
1399
  dlg = dialog_class(root, args)
1382
1400
  root.update()
1383
- return dlg.result
1401
+ return cast(dict, dlg.result)
1384
1402
 
1385
1403
  def show_halt(self, args: dict) -> dict:
1386
1404
  return self._run_dialog(MsgDialog, args)
execsql/gui/tui.py CHANGED
@@ -21,7 +21,7 @@ import ctypes
21
21
  import os
22
22
  import queue as _stdlib_queue
23
23
  import threading
24
- from typing import Any
24
+ from typing import Any, Literal, cast
25
25
 
26
26
  from execsql.gui.base import GuiBackend
27
27
 
@@ -61,7 +61,7 @@ from textual.widgets import (
61
61
 
62
62
  __all__ = ["TextualBackend"]
63
63
 
64
- from execsql.gui.base import DIFF_MARKER, compare_stats as _compare_stats, compute_row_diffs
64
+ from execsql.gui.base import DIFF_MARKER, DiffResult, compare_stats as _compare_stats, compute_row_diffs
65
65
 
66
66
  # ---------------------------------------------------------------------------
67
67
  # Helpers
@@ -105,7 +105,7 @@ def _button_row(button_list: list) -> list[Button]:
105
105
  if not value and value != 1: # falsy but not accidentally 1
106
106
  cancel_label = label
107
107
  else:
108
- variant = "primary" if i == 0 else "default"
108
+ variant: Literal["primary", "default"] = "primary" if i == 0 else "default"
109
109
  primary_buttons.append(Button(label, id=f"btn_{i}", variant=variant))
110
110
  return [Button(cancel_label, id="btn_cancel_exit", variant="warning")] + primary_buttons
111
111
 
@@ -695,7 +695,7 @@ class CompareScreen(_BaseDialog):
695
695
  self._col_keys1: list = []
696
696
  self._col_keys2: list = []
697
697
  self._diff_on = False
698
- self._diff_result = None
698
+ self._diff_result: DiffResult | None = None
699
699
  self._original_cells1: dict = {}
700
700
  self._original_cells2: dict = {}
701
701
 
@@ -1407,7 +1407,9 @@ class _TextualSyncQueue:
1407
1407
  return
1408
1408
  if self._screen_map is None:
1409
1409
  self.__class__._screen_map = _build_screen_map()
1410
- screen_class = self._screen_map.get(spec.gui_type)
1410
+ screen_map = self._screen_map
1411
+ assert screen_map is not None
1412
+ screen_class = screen_map.get(spec.gui_type)
1411
1413
  if screen_class is None:
1412
1414
  spec.return_queue.put({"error": f"Unknown GUI type: {spec.gui_type}", "button": None})
1413
1415
  return
@@ -1578,7 +1580,7 @@ class ConsoleApp(App):
1578
1580
  else:
1579
1581
  spec.return_queue.put(result if result is not None else {})
1580
1582
 
1581
- self.run_worker(_push, exclusive=False)
1583
+ self.run_worker(cast(Any, _push), exclusive=False)
1582
1584
 
1583
1585
  def write_console(self, text: str) -> None:
1584
1586
  """Thread-safe method to append text to the RichLog widget."""
execsql/importers/base.py CHANGED
@@ -63,23 +63,29 @@ def import_data_table(
63
63
 
64
64
  hdrs = dedup_words(hdrs)
65
65
 
66
+ tablespec_cache: Any = None
67
+
66
68
  def get_ts():
67
- if not get_ts.tablespec:
69
+ nonlocal tablespec_cache
70
+ if tablespec_cache is None:
68
71
  from execsql.models import DataTable
69
72
 
70
- get_ts.tablespec = DataTable(hdrs, data)
71
- return get_ts.tablespec
72
-
73
- get_ts.tablespec = None
73
+ tablespec_cache = DataTable(hdrs, data)
74
+ return tablespec_cache
74
75
 
75
- exec_log = _state.exec_log
76
76
  if is_new:
77
77
  if is_new == 2:
78
78
  tblspec = db.schema_qualified_table_name(schemaname, tablename)
79
79
  try:
80
80
  db.drop_table(tblspec)
81
- except Exception:
82
- exec_log.log_status_info(f"Could not drop existing table ({tblspec}) for IMPORT metacommand")
81
+ except ErrInfo:
82
+ raise
83
+ except Exception as e:
84
+ raise ErrInfo(
85
+ type="db",
86
+ other_msg=f"Could not drop existing table ({tblspec}) for IMPORT metacommand",
87
+ exception_msg=exception_desc(),
88
+ ) from e
83
89
  sql = get_ts().create_table(db.type, schemaname, tablename)
84
90
  try:
85
91
  db.execute(sql)
execsql/importers/csv.py CHANGED
@@ -54,9 +54,14 @@ def importtable(
54
54
  if is_new == 2:
55
55
  try:
56
56
  db.drop_table(db.schema_qualified_table_name(schemaname, tablename))
57
- except Exception:
58
- _state.exec_log.log_status_info(f"Could not drop existing table ({tablename}) for IMPORT metacommand")
59
- # Don't raise an exception; this may not be a problem because the table may not already exist.
57
+ except ErrInfo:
58
+ raise
59
+ except Exception as e:
60
+ raise ErrInfo(
61
+ type="db",
62
+ other_msg=f"Could not drop existing table ({tablename}) for IMPORT metacommand",
63
+ exception_msg=exception_desc(),
64
+ ) from e
60
65
  try:
61
66
  db.execute(sql)
62
67
  # Don't commit table creation here; the commit will be done after data import
execsql/importers/xls.py CHANGED
@@ -33,6 +33,7 @@ def xls_data(
33
33
  if len(filename) < 4:
34
34
  raise ErrInfo(type="cmd", other_msg=f"{filename} is not a recognizable Excel spreadsheet name.")
35
35
  ext3 = filename[-3:].lower()
36
+ wbk: Any
36
37
  if ext3 == "xls":
37
38
  # xlrd imported lazily
38
39
  from execsql.exporters.xls import XlsFile
@@ -61,8 +61,8 @@ def _quote_table_name(name: str) -> str:
61
61
 
62
62
 
63
63
  def xf_contains(**kwargs: Any) -> bool:
64
- s1 = kwargs["string1"]
65
- s2 = kwargs["string2"]
64
+ s1 = str(kwargs["string1"])
65
+ s2 = str(kwargs["string2"])
66
66
  if kwargs["ignorecase"] and kwargs["ignorecase"].lower() == "i":
67
67
  s1 = s1.lower()
68
68
  s2 = s2.lower()
@@ -70,8 +70,8 @@ def xf_contains(**kwargs: Any) -> bool:
70
70
 
71
71
 
72
72
  def xf_startswith(**kwargs: Any) -> bool:
73
- s1 = kwargs["string1"]
74
- s2 = kwargs["string2"]
73
+ s1 = str(kwargs["string1"])
74
+ s2 = str(kwargs["string2"])
75
75
  if kwargs["ignorecase"] and kwargs["ignorecase"].lower() == "i":
76
76
  s1 = s1.lower()
77
77
  s2 = s2.lower()
@@ -79,8 +79,8 @@ def xf_startswith(**kwargs: Any) -> bool:
79
79
 
80
80
 
81
81
  def xf_endswith(**kwargs: Any) -> bool:
82
- s1 = kwargs["string1"]
83
- s2 = kwargs["string2"]
82
+ s1 = str(kwargs["string1"])
83
+ s2 = str(kwargs["string2"])
84
84
  if kwargs["ignorecase"] and kwargs["ignorecase"].lower() == "i":
85
85
  s1 = s1.lower()
86
86
  s2 = s2.lower()
@@ -97,7 +97,7 @@ def xf_hasrows(**kwargs: Any) -> bool:
97
97
  except Exception as e:
98
98
  raise ErrInfo("db", sql, exception_msg=exception_desc()) from e
99
99
  nrows = rec[0][0]
100
- return nrows > 0
100
+ return bool(nrows > 0)
101
101
 
102
102
 
103
103
  def _row_count(queryname: str, sql_context: str, metacommandline: str) -> int:
@@ -313,13 +313,13 @@ def xf_equals(**kwargs: Any) -> bool:
313
313
 
314
314
 
315
315
  def xf_identical(**kwargs: Any) -> bool:
316
- s1 = kwargs["string1"].strip('"')
317
- s2 = kwargs["string2"].strip('"')
316
+ s1 = str(kwargs["string1"]).strip('"')
317
+ s2 = str(kwargs["string2"]).strip('"')
318
318
  return s1 == s2
319
319
 
320
320
 
321
321
  def xf_isnull(**kwargs: Any) -> bool:
322
- item = kwargs["item"].strip().strip('"')
322
+ item = str(kwargs["item"]).strip().strip('"')
323
323
  return item == ""
324
324
 
325
325
 
@@ -379,12 +379,12 @@ def xf_isfalse(**kwargs: Any) -> bool:
379
379
 
380
380
 
381
381
  def xf_dbms(**kwargs: Any) -> bool:
382
- dbms = kwargs["dbms"]
383
- return _state.dbs.current().type.dbms_id.lower() == dbms.strip().lower()
382
+ dbms = str(kwargs["dbms"])
383
+ return bool(_state.dbs.current().type.dbms_id.lower() == dbms.strip().lower())
384
384
 
385
385
 
386
386
  def xf_dbname(**kwargs: Any) -> bool:
387
- dbname = kwargs["dbname"]
387
+ dbname = str(kwargs["dbname"])
388
388
  return _state.dbs.current().name().lower() == dbname.strip().lower()
389
389
 
390
390
 
@@ -875,6 +875,6 @@ CONDITIONAL_TABLE = build_conditional_table()
875
875
  def xcmd_test(teststr: str) -> bool:
876
876
  result = CondParser(teststr).parse().eval()
877
877
  if result is not None:
878
- return result
878
+ return bool(result)
879
879
  else:
880
880
  raise ErrInfo(type="cmd", command_text=teststr, other_msg="Unrecognized conditional")
@@ -27,7 +27,7 @@ Plus the connection-management handlers:
27
27
  """
28
28
 
29
29
  from pathlib import Path
30
- from typing import Any
30
+ from typing import Any, cast
31
31
 
32
32
  import execsql.state as _state
33
33
  from execsql.db.access import AccessDatabase # noqa: F401 — used in x_connect_access; module-level for test patchability
@@ -498,7 +498,7 @@ def x_pg_vacuum(**kwargs: Any) -> None:
498
498
  db = _state.dbs.current()
499
499
  if db.type == dbt_postgres:
500
500
  args = kwargs["vacuum_args"]
501
- db.vacuum(args)
501
+ cast(Any, db).vacuum(args)
502
502
 
503
503
 
504
504
  def x_daoflushdelay(**kwargs: Any) -> None:
@@ -243,7 +243,7 @@ def x_halt_msg(**kwargs: Any) -> None:
243
243
  else:
244
244
  headers, rows = None, None
245
245
  enable_gui()
246
- return_queue = _queue.Queue()
246
+ return_queue: _queue.Queue[Any] = _queue.Queue()
247
247
  gui_args = {
248
248
  "title": "HALT",
249
249
  "message": errmsg,
@@ -227,7 +227,7 @@ def x_prompt_selectsub(**kwargs: Any) -> None:
227
227
  if cont:
228
228
  btns.append(("Continue", 2, "<Return>"))
229
229
  enable_gui()
230
- return_queue = _queue.Queue()
230
+ return_queue: _queue.Queue[Any] = _queue.Queue()
231
231
  gui_args = {
232
232
  "title": "Select data",
233
233
  "message": msg,
@@ -254,6 +254,7 @@ def x_prompt_selectsub(**kwargs: Any) -> None:
254
254
  exit_now(2, None)
255
255
  else:
256
256
  if btn_val == 1:
257
+ assert selected_row is not None
257
258
  for i, item in enumerate(selected_row):
258
259
  if item is None:
259
260
  item = ""
@@ -113,10 +113,13 @@ def _dispatch_format(
113
113
  elif filefmt == "xlsx":
114
114
  write_query_to_xlsx(select_stmt, db, outfile, append, sheetname=sheetname, desc=description)
115
115
  elif filefmt == "duckdb":
116
+ assert tablename is not None
116
117
  write_query_to_duckdb(select_stmt, db, outfile, append, tablename=tablename)
117
118
  elif filefmt == "sqlite":
119
+ assert tablename is not None
118
120
  write_query_to_sqlite(select_stmt, db, outfile, append, tablename=tablename)
119
121
  elif filefmt == "xml":
122
+ assert xml_table is not None
120
123
  write_query_to_xml(select_stmt, xml_table, db, outfile, append, desc=description, zipfile=zipfilename)
121
124
  elif filefmt == "json":
122
125
  write_query_to_json(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
@@ -131,6 +134,7 @@ def _dispatch_format(
131
134
  elif filefmt == "latex":
132
135
  write_query_to_latex(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
133
136
  elif filefmt == "hdf5":
137
+ assert hdf5_table is not None
134
138
  write_query_to_hdf5(hdf5_table, select_stmt, db, outfile, append, desc=description)
135
139
  elif filefmt == "yaml":
136
140
  write_query_to_yaml(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
@@ -9,9 +9,10 @@ from __future__ import annotations
9
9
 
10
10
  import os
11
11
  import sys
12
+ from itertools import tee
12
13
  from pathlib import Path
13
14
  from shutil import copyfileobj
14
- from typing import Any
15
+ from typing import Any, Literal, cast
15
16
 
16
17
  import execsql.state as _state
17
18
  from execsql.exceptions import ErrInfo
@@ -22,6 +23,12 @@ from execsql.utils.fileio import filewriter_close
22
23
  from execsql.utils.strings import unquoted
23
24
 
24
25
 
26
+ def _close_rowsource(rows: Any) -> None:
27
+ close = getattr(rows, "close", None)
28
+ if close:
29
+ close()
30
+
31
+
25
32
  def x_include(**kwargs: Any) -> None:
26
33
  # INCLUDE is now handled natively by the AST executor (_execute_include_native).
27
34
  # This handler exists only for dispatch table registration compatibility.
@@ -77,36 +84,59 @@ def x_copy(**kwargs: Any) -> None:
77
84
  pass # Best-effort check; some adapters lack information_schema.
78
85
  select_stmt = f"select * from {tbl1};"
79
86
 
80
- def get_ts() -> DataTable:
81
- if get_ts.tablespec is None:
82
- hdrs, rows = db1.select_rowsource(select_stmt)
83
- get_ts.tablespec = DataTable(hdrs, rows)
84
- return get_ts.tablespec
85
-
86
- get_ts.tablespec = None
87
+ get_ts_tablespec = None
88
+ rows_to_close = None
89
+ rows_for_insert: Any
87
90
 
88
91
  if new_tbl2:
89
- tbl_desc = get_ts()
92
+ try:
93
+ hdrs, rows = db1.select_rowsource(select_stmt)
94
+ except ErrInfo:
95
+ raise
96
+ except Exception as e:
97
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
98
+ rows_to_close = rows
99
+ rows = cast(Any, rows)
100
+ rows_for_schema, rows_for_insert = tee(rows)
101
+ get_ts_tablespec = DataTable(hdrs, rows_for_schema)
102
+ tbl_desc = get_ts_tablespec
90
103
  create_tbl = tbl_desc.create_table(db2.type, schema2, table2)
91
104
  if new_tbl2 == "replacement":
92
105
  try:
93
106
  db2.drop_table(tbl2)
94
- except Exception:
95
- _state.exec_log.log_status_info(f"Could not drop existing table ({tbl2}) for COPY metacommand")
107
+ except ErrInfo:
108
+ raise
109
+ except Exception as e:
110
+ raise ErrInfo(
111
+ type="db",
112
+ other_msg=f"Could not drop existing table ({tbl2}) for COPY metacommand",
113
+ exception_msg=exception_desc(),
114
+ ) from e
96
115
  db2.execute(create_tbl)
97
116
  if db2.needs_explicit_commit_after_ddl():
98
117
  db2.execute("COMMIT;")
118
+ else:
119
+ try:
120
+ hdrs, rows = db1.select_rowsource(select_stmt)
121
+ except ErrInfo:
122
+ raise
123
+ except Exception as e:
124
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
125
+ rows_to_close = rows
126
+ rows_for_insert = rows
127
+
128
+ def get_ts() -> DataTable:
129
+ nonlocal get_ts_tablespec
130
+ if get_ts_tablespec is None:
131
+ ts_hdrs, ts_rows = db1.select_rowsource(select_stmt)
132
+ get_ts_tablespec = DataTable(ts_hdrs, ts_rows)
133
+ return get_ts_tablespec
134
+
99
135
  try:
100
- hdrs, rows = db1.select_rowsource(select_stmt)
101
- except ErrInfo:
102
- raise
103
- except Exception as e:
104
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
105
- try:
106
- db2.populate_table(schema2, table2, rows, hdrs, get_ts)
136
+ db2.populate_table(schema2, table2, rows_for_insert, hdrs, get_ts)
107
137
  db2.commit()
108
138
  except BaseException:
109
- rows.close()
139
+ _close_rowsource(rows_to_close)
110
140
  raise
111
141
 
112
142
 
@@ -144,13 +174,9 @@ def x_copy_query(**kwargs: Any) -> None:
144
174
  except Exception:
145
175
  pass # Best-effort check; some adapters lack information_schema.
146
176
 
147
- def get_ts() -> DataTable:
148
- if not get_ts.tablespec:
149
- hdrs, rows = db1.select_rowsource(select_stmt)
150
- get_ts.tablespec = DataTable(hdrs, rows)
151
- return get_ts.tablespec
152
-
153
- get_ts.tablespec = None
177
+ get_ts_tablespec = None
178
+ rows_to_close = None
179
+ rows_for_insert: Any
154
180
 
155
181
  if new_tbl2:
156
182
  try:
@@ -159,28 +185,48 @@ def x_copy_query(**kwargs: Any) -> None:
159
185
  raise
160
186
  except Exception as e:
161
187
  raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
162
- get_ts.tablespec = DataTable(hdrs, rows)
163
- tbl_desc = get_ts.tablespec
188
+ rows_to_close = rows
189
+ rows = cast(Any, rows)
190
+ rows_for_schema, rows_for_insert = tee(rows)
191
+ get_ts_tablespec = DataTable(hdrs, rows_for_schema)
192
+ tbl_desc = get_ts_tablespec
164
193
  create_tbl = tbl_desc.create_table(db2.type, schema2, table2)
165
194
  if new_tbl2 == "replacement":
166
195
  try:
167
196
  db2.drop_table(tbl2)
168
- except Exception:
169
- _state.exec_log.log_status_info(f"Could not drop existing table ({tbl2}) for COPY metacommand")
197
+ except ErrInfo:
198
+ raise
199
+ except Exception as e:
200
+ raise ErrInfo(
201
+ type="db",
202
+ other_msg=f"Could not drop existing table ({tbl2}) for COPY metacommand",
203
+ exception_msg=exception_desc(),
204
+ ) from e
170
205
  db2.execute(create_tbl)
171
206
  if db2.needs_explicit_commit_after_ddl():
172
207
  db2.execute("COMMIT;")
208
+ else:
209
+ try:
210
+ hdrs, rows = db1.select_rowsource(select_stmt)
211
+ except ErrInfo:
212
+ raise
213
+ except Exception as e:
214
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
215
+ rows_to_close = rows
216
+ rows_for_insert = rows
217
+
218
+ def get_ts() -> DataTable:
219
+ nonlocal get_ts_tablespec
220
+ if get_ts_tablespec is None:
221
+ ts_hdrs, ts_rows = db1.select_rowsource(select_stmt)
222
+ get_ts_tablespec = DataTable(ts_hdrs, ts_rows)
223
+ return get_ts_tablespec
224
+
173
225
  try:
174
- hdrs, rows = db1.select_rowsource(select_stmt)
175
- except ErrInfo:
176
- raise
177
- except Exception as e:
178
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
179
- try:
180
- db2.populate_table(schema2, table2, rows, hdrs, get_ts)
226
+ db2.populate_table(schema2, table2, rows_for_insert, hdrs, get_ts)
181
227
  db2.commit()
182
228
  except BaseException:
183
- rows.close()
229
+ _close_rowsource(rows_to_close)
184
230
  raise
185
231
 
186
232
 
@@ -191,7 +237,7 @@ def x_zip(**kwargs: Any) -> None:
191
237
  files = kwargs["filename"].strip(' "')
192
238
  zipfile_name = kwargs["zipfilename"].strip(' "')
193
239
  append = kwargs["append"]
194
- zmode = "a" if append is not None else "w"
240
+ zmode: Literal["a", "w"] = "a" if append is not None else "w"
195
241
  zf = _zipfile.ZipFile(zipfile_name, mode=zmode, compression=_zipfile.ZIP_BZIP2, compresslevel=9)
196
242
  fnlist = _glob.glob(files)
197
243
  for f in fnlist:
@@ -296,6 +296,7 @@ def x_import_xls_pattern(**kwargs: Any) -> None:
296
296
  if len(filename) < 4:
297
297
  raise ErrInfo(type="cmd", other_msg=f"{filename} is not a recognizable Excel spreadsheet name.")
298
298
  ext3 = filename[-3:].lower()
299
+ wbk: Any
299
300
  if ext3 == "xls":
300
301
  wbk = XlsFile()
301
302
  elif ext3 == "lsx":
@@ -51,7 +51,7 @@ def x_write(**kwargs: Any) -> None:
51
51
  ) from e
52
52
  except ConsoleUIError as e:
53
53
  _state.output.reset()
54
- _state.exec_log.log_status_info(f"Console UI write failed (message {{{e.value}}}); output reset to stdout.")
54
+ _state.exec_log.log_status_info(f"Console UI write failed (message {{{e}}}); output reset to stdout.")
55
55
  _state.output.write(msg)
56
56
  if _state.conf.tee_write_log:
57
57
  _state.exec_log.log_user_msg(msg)