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
@@ -0,0 +1,248 @@
1
+ """WRITE metacommand handlers.
2
+
3
+ Implements ``x_write``, ``x_write_create_table`` (CSV, ODS, XLS, alias),
4
+ ``x_write_prefix``, ``x_write_suffix``, and ``x_writescript``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import execsql.state as _state
13
+ from execsql.exceptions import ErrInfo
14
+ from execsql.exporters.delimited import CsvFile
15
+ from execsql.importers.ods import ods_data
16
+ from execsql.importers.xls import xls_data
17
+ from execsql.models import DataTable
18
+ from execsql.script import substitute_vars
19
+ from execsql.utils.errors import exception_desc
20
+ from execsql.utils.fileio import check_dir, filewriter_close, filewriter_open_as_new, filewriter_write
21
+ from execsql.utils.gui import ConsoleUIError
22
+
23
+
24
+ def x_write(**kwargs: Any) -> None:
25
+ msg = f"{kwargs['text']}\n"
26
+ tee = kwargs["tee"]
27
+ tee = bool(tee)
28
+ outf = kwargs["filename"]
29
+ if _state.conf.write_prefix is not None:
30
+ msg = substitute_vars(_state.conf.write_prefix) + " " + msg
31
+ if _state.conf.write_suffix is not None:
32
+ msg = msg[:-1] + " " + substitute_vars(_state.conf.write_suffix) + "\n"
33
+ if outf:
34
+ check_dir(outf)
35
+ filewriter_write(outf, msg)
36
+ if (not outf) or tee:
37
+ try:
38
+ _state.output.write(msg)
39
+ except TypeError as e:
40
+ raise ErrInfo(
41
+ type="other",
42
+ command_text=kwargs["metacommandline"],
43
+ other_msg="TypeError in 'write' metacommand.",
44
+ ) from e
45
+ except ConsoleUIError as e:
46
+ _state.output.reset()
47
+ _state.exec_log.log_status_info(f"Console UI write failed (message {{{e.value}}}); output reset to stdout.")
48
+ _state.output.write(msg.encode(_state.conf.output_encoding))
49
+ if _state.conf.tee_write_log:
50
+ _state.exec_log.log_user_msg(msg)
51
+ return None
52
+
53
+
54
+ def x_write_create_table(**kwargs: Any) -> None:
55
+ filename = kwargs["filename"]
56
+ if not Path(filename).exists():
57
+ raise ErrInfo(
58
+ type="cmd",
59
+ command_text=kwargs["metacommandline"],
60
+ other_msg="Input file does not exist",
61
+ )
62
+ quotechar = kwargs["quotechar"]
63
+ delimchar = kwargs["delimchar"]
64
+ encoding = kwargs["encoding"]
65
+ if delimchar:
66
+ if delimchar.lower() == "tab":
67
+ delimchar = chr(9)
68
+ elif delimchar.lower() in ("unitsep", "us"):
69
+ delimchar = chr(31)
70
+ junk_hdrs = kwargs["skip"]
71
+ if not junk_hdrs:
72
+ junk_hdrs = 0
73
+ else:
74
+ junk_hdrs = int(junk_hdrs)
75
+ enc = encoding if encoding else _state.conf.import_encoding
76
+ inf = CsvFile(filename, enc, junk_header_lines=junk_hdrs)
77
+ if quotechar and delimchar:
78
+ inf.lineformat(delimchar, quotechar, None)
79
+ inf.evaluate_column_types()
80
+ sql = inf.create_table(_state.dbs.current().type, kwargs["schema"], kwargs["table"], pretty=True)
81
+ inf.close()
82
+ comment = kwargs["comment"]
83
+ outfile = kwargs["outfile"]
84
+
85
+ def write(txt: str) -> None:
86
+ if outfile is None or outfile == "stdout":
87
+ _state.output.write(txt)
88
+ else:
89
+ filewriter_write(outfile, txt)
90
+
91
+ if outfile:
92
+ check_dir(outfile)
93
+ if comment:
94
+ write(f"-- {comment}\n")
95
+ write(f"{sql}\n")
96
+
97
+
98
+ def x_write_create_table_ods(**kwargs: Any) -> None:
99
+ schemaname = kwargs["schema"]
100
+ tablename = kwargs["table"]
101
+ filename = kwargs["filename"]
102
+ sheetname = kwargs["sheet"]
103
+ hdr_rows = kwargs["skip"]
104
+ if not hdr_rows:
105
+ hdr_rows = 0
106
+ else:
107
+ hdr_rows = int(hdr_rows)
108
+ comment = kwargs["comment"]
109
+ outfile = kwargs["outfile"]
110
+ if not Path(filename).exists():
111
+ raise ErrInfo(
112
+ type="cmd",
113
+ command_text=kwargs["metacommandline"],
114
+ other_msg="Input file does not exist",
115
+ )
116
+ hdrs, data = ods_data(filename, sheetname, hdr_rows)
117
+ tablespec = DataTable(hdrs, data)
118
+ sql = tablespec.create_table(_state.dbs.current().type, schemaname, tablename, pretty=True)
119
+ if outfile:
120
+ if comment:
121
+ filewriter_write(outfile, f"-- {comment}\n")
122
+ filewriter_write(outfile, sql)
123
+ filewriter_close(outfile)
124
+ else:
125
+ if comment:
126
+ _state.output.write(f"-- {comment}\n")
127
+ _state.output.write(f"{sql}\n")
128
+
129
+
130
+ def x_write_create_table_xls(**kwargs: Any) -> None:
131
+ schemaname = kwargs["schema"]
132
+ tablename = kwargs["table"]
133
+ filename = kwargs["filename"]
134
+ sheetname = kwargs["sheet"]
135
+ junk_hdrs = kwargs["skip"]
136
+ encoding = kwargs["encoding"]
137
+ enc = encoding if encoding else _state.conf.import_encoding
138
+ if not junk_hdrs:
139
+ junk_hdrs = 0
140
+ else:
141
+ junk_hdrs = int(junk_hdrs)
142
+ comment = kwargs["comment"]
143
+ outfile = kwargs["outfile"]
144
+ if not Path(filename).exists():
145
+ raise ErrInfo(
146
+ type="cmd",
147
+ command_text=kwargs["metacommandline"],
148
+ other_msg="Input file does not exist",
149
+ )
150
+ hdrs, data = xls_data(filename, sheetname, junk_hdrs, enc)
151
+ tablespec = DataTable(hdrs, data)
152
+ sql = tablespec.create_table(_state.dbs.current().type, schemaname, tablename, pretty=True)
153
+ if outfile:
154
+ if comment:
155
+ filewriter_write(outfile, f"-- {comment}\n")
156
+ filewriter_write(outfile, sql)
157
+ filewriter_close(outfile)
158
+ else:
159
+ if comment:
160
+ _state.output.write(f"-- {comment}\n")
161
+ _state.output.write(f"{sql}\n")
162
+
163
+
164
+ def x_write_create_table_alias(**kwargs: Any) -> None:
165
+ alias = kwargs["alias"].lower()
166
+ schema = kwargs["schema"]
167
+ table = kwargs["table"]
168
+ comment = kwargs["comment"]
169
+ outfile = kwargs["filename"]
170
+ if alias not in _state.dbs.aliases():
171
+ raise ErrInfo(
172
+ type="cmd",
173
+ command_text=kwargs["metacommandline"],
174
+ other_msg=f"Unrecognized database alias: {alias}.",
175
+ )
176
+ db = _state.dbs.aliased_as(alias)
177
+ tbl = db.schema_qualified_table_name(schema, table)
178
+ try:
179
+ if not db.table_exists(table, schema):
180
+ raise ErrInfo(
181
+ type="cmd",
182
+ command_text=kwargs["metacommandline"],
183
+ other_msg=f"Table {tbl} does not exist",
184
+ )
185
+ except Exception:
186
+ pass # Best-effort check; some adapters lack information_schema.
187
+ select_stmt = f"select * from {tbl};"
188
+ try:
189
+ hdrs, rows = db.select_rowsource(select_stmt)
190
+ except ErrInfo:
191
+ raise
192
+ except Exception as e:
193
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
194
+ tablespec = DataTable(hdrs, rows)
195
+ sql = tablespec.create_table(_state.dbs.current().type, kwargs["schema1"], kwargs["table1"], pretty=True)
196
+ if outfile:
197
+ if comment:
198
+ filewriter_write(outfile, f"-- {comment}\n")
199
+ filewriter_write(outfile, sql)
200
+ filewriter_close(outfile)
201
+ else:
202
+ if comment:
203
+ _state.output.write(f"-- {comment}\n")
204
+ _state.output.write(f"{sql}\n")
205
+
206
+
207
+ def x_write_prefix(**kwargs: Any) -> None:
208
+ pf = kwargs["prefix"]
209
+ if pf.lower() == "clear":
210
+ _state.conf.write_prefix = None
211
+ else:
212
+ _state.conf.write_prefix = pf
213
+ return None
214
+
215
+
216
+ def x_write_suffix(**kwargs: Any) -> None:
217
+ sf = kwargs["suffix"]
218
+ if sf.lower() == "clear":
219
+ _state.conf.write_suffix = None
220
+ else:
221
+ _state.conf.write_suffix = sf
222
+ return None
223
+
224
+
225
+ def x_writescript(**kwargs: Any) -> None:
226
+ script_id = kwargs["script_id"]
227
+ output_dest = kwargs["filename"]
228
+ append = kwargs["append"]
229
+
230
+ def write(txt: str) -> None:
231
+ if output_dest is None or output_dest == "stdout":
232
+ _state.output.write(txt)
233
+ else:
234
+ filewriter_write(output_dest, txt)
235
+
236
+ if output_dest is not None and output_dest != "stdout":
237
+ check_dir(output_dest)
238
+ if not append:
239
+ filewriter_open_as_new(output_dest)
240
+ script = _state.savedscripts[script_id]
241
+ if script.paramnames is not None and len(script.paramnames) > 0:
242
+ write(f"BEGIN SCRIPT {script_id} ({', '.join(script.paramnames)})\n")
243
+ else:
244
+ write(f"BEGIN SCRIPT {script_id}\n")
245
+ lines = [c.commandline() for c in script.cmdlist]
246
+ for line in lines:
247
+ write(f"{line}\n")
248
+ write(f"END SCRIPT {script_id}\n")
@@ -29,7 +29,6 @@ from typing import Any
29
29
  import execsql.state as _state
30
30
  from execsql.script import current_script_line
31
31
  from execsql.utils.errors import exception_desc, exit_now
32
- from execsql.utils.fileio import EncodedFile, check_dir
33
32
  from execsql.utils.gui import (
34
33
  ActionSpec,
35
34
  EntrySpec,
@@ -38,7 +37,6 @@ from execsql.utils.gui import (
38
37
  GUI_DIRECTORY,
39
38
  GUI_DISPLAY,
40
39
  GUI_ENTRY,
41
- GUI_HALT,
42
40
  GUI_MAP,
43
41
  GUI_MSG,
44
42
  GUI_OPENFILE,
@@ -210,12 +208,12 @@ def x_prompt_entryform(**kwargs: Any) -> None:
210
208
  initial_value = str(str(v["initial_value"]).lower() in ("yes", "true", "on", "1"))
211
209
  else:
212
210
  initial_value = str(v["initial_value"])
213
- except Exception:
211
+ except Exception as e:
214
212
  raise ErrInfo(
215
213
  type="cmd",
216
214
  command_text=kwargs["metacommandline"],
217
215
  other_msg=f"The initial value of {v['initial_value']} can't be used.",
218
- )
216
+ ) from e
219
217
  if "lookup_table" in colhdrs:
220
218
  lt = v["lookup_table"]
221
219
  if lt:
@@ -226,23 +224,23 @@ def x_prompt_entryform(**kwargs: Any) -> None:
226
224
  if entry_width:
227
225
  try:
228
226
  entry_width = int(entry_width)
229
- except Exception:
227
+ except Exception as e:
230
228
  raise ErrInfo(
231
229
  type="cmd",
232
230
  command_text=kwargs["metacommandline"],
233
231
  other_msg=f"Entry width {entry_width} is not an integer",
234
- )
232
+ ) from e
235
233
  if "height" in colhdrs:
236
234
  entry_height = v.get("height")
237
235
  if entry_height:
238
236
  try:
239
237
  entry_height = int(entry_height)
240
- except Exception:
238
+ except Exception as e:
241
239
  raise ErrInfo(
242
240
  type="cmd",
243
241
  command_text=kwargs["metacommandline"],
244
242
  other_msg=f"Entry height {entry_height} is not an integer",
245
- )
243
+ ) from e
246
244
  if entry_height < 1:
247
245
  entry_height = 1
248
246
  if "form_column" in colhdrs:
@@ -250,12 +248,12 @@ def x_prompt_entryform(**kwargs: Any) -> None:
250
248
  if entry_col:
251
249
  try:
252
250
  entry_col = int(entry_col)
253
- except Exception:
251
+ except Exception as e:
254
252
  raise ErrInfo(
255
253
  type="cmd",
256
254
  command_text=kwargs["metacommandline"],
257
255
  other_msg=f"Entry column {entry_col} is not an integer",
258
- )
256
+ ) from e
259
257
  if entry_col < 1:
260
258
  entry_col = 1
261
259
  subvarset = _state.subvars if subvar[0] != "~" else _state.commandliststack[-1].localvars
@@ -363,15 +361,15 @@ def prompt_compare(button_list: list, **kwargs: Any) -> Any:
363
361
  if alias1 is not None:
364
362
  try:
365
363
  db1 = _state.dbs.aliased_as(alias1)
366
- except Exception:
367
- raise ErrInfo(type="error", other_msg=badaliasmsg % alias1)
364
+ except Exception as e:
365
+ raise ErrInfo(type="error", other_msg=badaliasmsg % alias1) from e
368
366
  else:
369
367
  db1 = _state.dbs.current()
370
368
  if alias2 is not None:
371
369
  try:
372
370
  db2 = _state.dbs.aliased_as(alias2)
373
- except Exception:
374
- raise ErrInfo(type="error", other_msg=badaliasmsg % alias2)
371
+ except Exception as e:
372
+ raise ErrInfo(type="error", other_msg=badaliasmsg % alias2) from e
375
373
  else:
376
374
  db2 = _state.dbs.current()
377
375
  sq_name1 = db1.schema_qualified_table_name(schema1, table1)
@@ -666,8 +664,8 @@ def x_prompt_savefile(**kwargs: Any) -> None:
666
664
  subvarset5.add_substitution(sub_name5, Path(fn).stem)
667
665
  except (ErrInfo, SystemExit):
668
666
  raise
669
- except Exception:
670
- raise ErrInfo(type="exception", exception_msg=exception_desc())
667
+ except Exception as e:
668
+ raise ErrInfo(type="exception", exception_msg=exception_desc()) from e
671
669
  return None
672
670
 
673
671
 
@@ -733,8 +731,8 @@ def x_prompt_openfile(**kwargs: Any) -> None:
733
731
  subvarset5.add_substitution(sub_name5, Path(fn).stem)
734
732
  except (ErrInfo, SystemExit):
735
733
  raise
736
- except Exception:
737
- raise ErrInfo(type="exception", exception_msg=exception_desc())
734
+ except Exception as e:
735
+ raise ErrInfo(type="exception", exception_msg=exception_desc()) from e
738
736
  return None
739
737
 
740
738
 
@@ -769,8 +767,8 @@ def x_prompt_directory(**kwargs: Any) -> None:
769
767
  )
770
768
  except (ErrInfo, SystemExit):
771
769
  raise
772
- except Exception:
773
- raise ErrInfo(type="exception", exception_msg=exception_desc())
770
+ except Exception as e:
771
+ raise ErrInfo(type="exception", exception_msg=exception_desc()) from e
774
772
  return None
775
773
 
776
774
 
@@ -787,15 +785,15 @@ def prompt_select_rows(button_list: list, **kwargs: Any) -> Any:
787
785
  if alias1 is not None:
788
786
  try:
789
787
  db1 = _state.dbs.aliased_as(alias1)
790
- except Exception:
791
- raise ErrInfo(type="error", other_msg=badaliasmsg % alias1)
788
+ except Exception as e:
789
+ raise ErrInfo(type="error", other_msg=badaliasmsg % alias1) from e
792
790
  else:
793
791
  db1 = _state.dbs.current()
794
792
  if alias2 is not None:
795
793
  try:
796
794
  db2 = _state.dbs.aliased_as(alias2)
797
- except Exception:
798
- raise ErrInfo(type="error", other_msg=badaliasmsg % alias2)
795
+ except Exception as e:
796
+ raise ErrInfo(type="error", other_msg=badaliasmsg % alias2) from e
799
797
  else:
800
798
  db2 = _state.dbs.current()
801
799
  sq_name1 = db1.schema_qualified_table_name(schema1, table1)
@@ -942,48 +940,6 @@ def x_pause(**kwargs: Any) -> None:
942
940
  return None
943
941
 
944
942
 
945
- def x_halt_msg(**kwargs: Any) -> None:
946
- errmsg = kwargs["errmsg"]
947
- tee = kwargs["tee"]
948
- tee = bool(tee)
949
- outf = kwargs["filename"]
950
- errlevel = kwargs["errorlevel"]
951
- if errlevel:
952
- errlevel = int(errlevel)
953
- else:
954
- errlevel = 3
955
- conf = _state.conf
956
- if outf:
957
- check_dir(outf)
958
- of = EncodedFile(outf, conf.output_encoding).open("a")
959
- of.write(f"{errmsg}\n")
960
- of.close()
961
- schema = kwargs.get("schema")
962
- table = kwargs.get("table")
963
- if table:
964
- db = _state.dbs.current()
965
- db_obj = db.schema_qualified_table_name(schema, table)
966
- sql = f"select * from {db_obj};"
967
- headers, rows = db.select_data(sql)
968
- else:
969
- headers, rows = None, None
970
- enable_gui()
971
- return_queue = _queue.Queue()
972
- gui_args = {
973
- "title": "HALT",
974
- "message": errmsg,
975
- "button_list": [("OK", 1, "<Return>")],
976
- "no_cancel": True,
977
- "column_headers": headers,
978
- "rowset": rows,
979
- "help_url": None,
980
- }
981
- _state.gui_manager_queue.put(GuiSpec(GUI_HALT, gui_args, return_queue))
982
- return_queue.get(block=True)
983
- _state.exec_log.log_exit_halt(*current_script_line(), msg=errmsg)
984
- exit_now(errlevel, None)
985
-
986
-
987
943
  def x_msg(**kwargs: Any) -> None:
988
944
  message = kwargs["message"]
989
945
  current_script_line()
@@ -89,6 +89,11 @@ def x_log_datavars(**kwargs: Any) -> None:
89
89
  _state.conf.log_datavars = setting in ("yes", "on", "true", "1")
90
90
 
91
91
 
92
+ def x_log_sql(**kwargs: Any) -> None:
93
+ setting = kwargs["setting"].lower()
94
+ _state.conf.log_sql = setting in ("yes", "on", "true", "1")
95
+
96
+
92
97
  def x_console(**kwargs: Any) -> None:
93
98
  onoff = kwargs["onoff"].lower()
94
99
  if onoff == "on":
@@ -251,6 +256,6 @@ def x_execute(**kwargs: Any) -> None:
251
256
  db.commit()
252
257
  except ErrInfo:
253
258
  raise
254
- except Exception:
255
- raise ErrInfo("db", command_text=sql, exception_msg=exception_desc())
259
+ except Exception as e:
260
+ raise ErrInfo("db", command_text=sql, exception_msg=exception_desc()) from e
256
261
  return None
execsql/py.typed ADDED
File without changes
execsql/script.py CHANGED
@@ -419,6 +419,7 @@ class MetaCommand:
419
419
  run_in_batch: bool = False,
420
420
  run_when_false: bool = False,
421
421
  set_error_flag: bool = True,
422
+ category: str | None = None,
422
423
  ) -> None:
423
424
  self.rx = rx
424
425
  self.exec_fn = exec_func
@@ -426,6 +427,7 @@ class MetaCommand:
426
427
  self.run_in_batch = run_in_batch
427
428
  self.run_when_false = run_when_false
428
429
  self.set_error_flag = set_error_flag
430
+ self.category = category
429
431
  self.hitcount = 0
430
432
 
431
433
  def __repr__(self) -> str:
@@ -488,6 +490,7 @@ class MetaCommandList:
488
490
  run_in_batch: bool = False,
489
491
  run_when_false: bool = False,
490
492
  set_error_flag: bool = True,
493
+ category: str | None = None,
491
494
  ) -> None:
492
495
  if type(matching_regexes) in (tuple, list):
493
496
  regexes = [re.compile(rx, re.I) for rx in tuple(matching_regexes)]
@@ -497,9 +500,30 @@ class MetaCommandList:
497
500
  # Prepend to preserve "last registered, first checked" ordering.
498
501
  self._commands.insert(
499
502
  0,
500
- MetaCommand(rx, exec_func, description, run_in_batch, run_when_false, set_error_flag),
503
+ MetaCommand(
504
+ rx,
505
+ exec_func,
506
+ description,
507
+ run_in_batch,
508
+ run_when_false,
509
+ set_error_flag,
510
+ category,
511
+ ),
501
512
  )
502
513
 
514
+ def keywords_by_category(self) -> dict[str, list[str]]:
515
+ """Return ``{category: [keyword, ...]}`` from entries that have both.
516
+
517
+ Used by ``--dump-keywords`` to introspect the dispatch table.
518
+ """
519
+ result: dict[str, list[str]] = {}
520
+ for mc in self._commands:
521
+ if mc.category and mc.description:
522
+ kw_list = result.setdefault(mc.category, [])
523
+ if mc.description not in kw_list:
524
+ kw_list.append(mc.description)
525
+ return result
526
+
503
527
  def eval(self, cmd_str: str) -> tuple:
504
528
  """Evaluate *cmd_str* against the registered metacommands.
505
529
 
@@ -549,6 +573,10 @@ class SqlStmt:
549
573
  )
550
574
  try:
551
575
  db = _state.dbs.current()
576
+ if _state.conf.log_sql and _state.exec_log:
577
+ lno = getattr(_state, "last_command", None)
578
+ lno = lno.line_no if lno and hasattr(lno, "line_no") else None
579
+ _state.exec_log.log_sql_query(cmd, db.name(), lno)
552
580
  db.execute(cmd)
553
581
  if commit:
554
582
  db.commit()
@@ -915,6 +943,9 @@ def set_system_vars() -> None:
915
943
  _state.subvars.add_substitution("$VERSION3", str(_state.tertiary_vno))
916
944
 
917
945
 
946
+ _MAX_SUBSTITUTION_DEPTH = 100
947
+
948
+
918
949
  def substitute_vars(command_str: str, localvars: SubVarSet | None = None) -> str:
919
950
  # Substitutes global variables, global counters, and local variables.
920
951
  if localvars is not None:
@@ -923,11 +954,21 @@ def substitute_vars(command_str: str, localvars: SubVarSet | None = None) -> str
923
954
  subs = _state.subvars
924
955
  cmdstr = copy.copy(command_str)
925
956
  subs_made = True
957
+ iterations = 0
926
958
  while subs_made:
927
959
  subs_made = False
928
960
  cmdstr, subs_made = subs.substitute_all(cmdstr)
929
961
  cmdstr, any_subbed = _state.counters.substitute_all(cmdstr)
930
962
  subs_made = subs_made or any_subbed
963
+ iterations += 1
964
+ if iterations >= _MAX_SUBSTITUTION_DEPTH:
965
+ raise ErrInfo(
966
+ type="error",
967
+ other_msg=(
968
+ f"Substitution variable cycle detected: exceeded {_MAX_SUBSTITUTION_DEPTH} "
969
+ f"iterations while expanding variables in: {command_str[:200]}"
970
+ ),
971
+ )
931
972
  m = _state.defer_rx.findall(cmdstr)
932
973
  # Substitute any deferred substitution variables with regular substitution var flags.
933
974
  if m is not None:
@@ -952,8 +993,8 @@ def runscripts() -> None:
952
993
  raise
953
994
  except ErrInfo:
954
995
  raise
955
- except Exception:
956
- raise ErrInfo(type="exception", exception_msg=exception_desc())
996
+ except Exception as e:
997
+ raise ErrInfo(type="exception", exception_msg=exception_desc()) from e
957
998
  _state.cmds_run += 1
958
999
 
959
1000
 
@@ -1126,8 +1167,11 @@ def read_sqlfile(sql_file_name: str) -> None:
1126
1167
 
1127
1168
  sz, dt = file_size_date(sql_file_name)
1128
1169
  _state.exec_log.log_status_info(f"Reading script file {sql_file_name} (size: {sz}; date: {dt})")
1129
- scriptfile_obj = ScriptFile(sql_file_name, _state.conf.script_encoding).open("r")
1130
- sqllist = _parse_script_lines(scriptfile_obj, sql_file_name)
1170
+ scriptfile_obj = ScriptFile(sql_file_name, _state.conf.script_encoding)
1171
+ try:
1172
+ sqllist = _parse_script_lines(scriptfile_obj, sql_file_name)
1173
+ finally:
1174
+ scriptfile_obj.close()
1131
1175
  if sqllist:
1132
1176
  _state.commandliststack.append(CommandList(sqllist, Path(sql_file_name).name))
1133
1177