execsql2 2.15.8__py3-none-any.whl → 2.16.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 (66) hide show
  1. execsql/__init__.py +8 -3
  2. execsql/api.py +580 -0
  3. execsql/cli/__init__.py +123 -0
  4. execsql/cli/lint_ast.py +439 -0
  5. execsql/cli/run.py +113 -102
  6. execsql/config.py +29 -4
  7. execsql/db/access.py +1 -0
  8. execsql/db/base.py +4 -1
  9. execsql/db/dsn.py +3 -2
  10. execsql/db/duckdb.py +1 -1
  11. execsql/db/factory.py +3 -0
  12. execsql/db/firebird.py +2 -1
  13. execsql/db/mysql.py +2 -1
  14. execsql/db/oracle.py +2 -1
  15. execsql/db/postgres.py +2 -1
  16. execsql/db/sqlite.py +1 -1
  17. execsql/db/sqlserver.py +3 -2
  18. execsql/debug/repl.py +27 -10
  19. execsql/exporters/base.py +6 -4
  20. execsql/exporters/delimited.py +11 -3
  21. execsql/exporters/pretty.py +9 -12
  22. execsql/gui/tui.py +59 -2
  23. execsql/metacommands/__init__.py +3 -0
  24. execsql/metacommands/conditions.py +20 -2
  25. execsql/metacommands/connect.py +1 -1
  26. execsql/metacommands/control.py +8 -14
  27. execsql/metacommands/debug.py +6 -4
  28. execsql/metacommands/io_export.py +117 -315
  29. execsql/metacommands/io_fileops.py +7 -13
  30. execsql/metacommands/io_write.py +1 -1
  31. execsql/metacommands/script_ext.py +8 -5
  32. execsql/metacommands/upsert.py +40 -0
  33. execsql/models.py +8 -12
  34. execsql/plugins.py +414 -0
  35. execsql/script/__init__.py +36 -12
  36. execsql/script/ast.py +562 -0
  37. execsql/script/engine.py +59 -368
  38. execsql/script/executor.py +833 -0
  39. execsql/script/parser.py +663 -0
  40. execsql/script/variables.py +11 -0
  41. execsql/state.py +55 -2
  42. execsql/utils/crypto.py +14 -10
  43. execsql/utils/errors.py +31 -8
  44. execsql/utils/gui.py +139 -17
  45. execsql/utils/mail.py +15 -12
  46. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/METADATA +59 -1
  47. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/RECORD +66 -60
  48. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/README.md +0 -0
  49. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  50. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  51. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/execsql.conf +0 -0
  52. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  53. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  54. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  55. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  56. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  57. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  58. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  59. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/script_template.sql +0 -0
  60. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  61. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  62. {execsql2-2.15.8.data → execsql2-2.16.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  63. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/WHEEL +0 -0
  64. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/entry_points.txt +0 -0
  65. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/licenses/LICENSE.txt +0 -0
  66. {execsql2-2.15.8.dist-info → execsql2-2.16.0.dist-info}/licenses/NOTICE +0 -0
@@ -56,190 +56,132 @@ def _apply_output_dir(path: str) -> str:
56
56
  return str(Path(output_dir) / path)
57
57
 
58
58
 
59
- def x_export(**kwargs: Any) -> None:
60
- schema = kwargs["schema"]
61
- table = kwargs["table"]
62
- queryname = _state.dbs.current().schema_qualified_table_name(schema, table)
63
- select_stmt = f"select * from {queryname};"
64
- outfile = _apply_output_dir(kwargs["filename"])
65
- description = kwargs["description"]
66
- tee = kwargs["tee"]
67
- tee = bool(tee)
68
- append = kwargs["append"]
69
- append = bool(append)
70
- filefmt = kwargs["format"].lower()
71
- zipfilename = _apply_output_dir(kwargs["zipfilename"]) if kwargs["zipfilename"] else None
72
- if zipfilename is not None:
73
- if outfile.lower() == "stdout":
74
- raise ErrInfo("error", other_msg="Cannot write stdout to a zipfile.")
75
- elif len(outfile) > 1 and outfile[1] == ":":
76
- raise ErrInfo("error", other_msg="Cannot use a drive letter for a file path within a zipfile.")
77
- if filefmt == "duckdb":
78
- raise ErrInfo("error", other_msg="Cannot export to the DuckDB format within a zipfile.")
79
- if filefmt == "sqlite":
80
- raise ErrInfo("error", other_msg="Cannot export to the SQLite format within a zipfile.")
81
- if filefmt == "latex":
82
- raise ErrInfo("error", other_msg="Cannot export to the LaTeX format within a zipfile.")
83
- if filefmt == "feather":
84
- raise ErrInfo("error", other_msg="Cannot export to the feather format within a zipfile.")
85
- if filefmt == "parquet":
86
- raise ErrInfo("error", other_msg="Cannot export to the parquet format within a zipfile.")
87
- if filefmt == "hdf5":
88
- raise ErrInfo("error", other_msg="Cannot export to the HDF5 format within a zipfile.")
89
- if filefmt == "ods":
90
- raise ErrInfo("error", other_msg="Cannot export to an ODS workbook within a zipfile.")
91
- if filefmt == "xlsx":
92
- raise ErrInfo("error", other_msg="Cannot export to an XLSX workbook within a zipfile.")
93
- notype = bool(kwargs.get("notype"))
94
- if zipfilename is not None:
95
- check_dir(zipfilename)
96
- else:
97
- check_dir(outfile)
98
- if tee and outfile.lower() != "stdout":
99
- prettyprint_query(select_stmt, _state.dbs.current(), "stdout", False, desc=description)
59
+ # ---------------------------------------------------------------------------
60
+ # Shared format-dispatch logic
61
+ # ---------------------------------------------------------------------------
62
+
63
+ # Formats that cannot be written into a zipfile.
64
+ _NO_ZIP_FORMATS = frozenset({"duckdb", "sqlite", "latex", "feather", "parquet", "hdf5", "ods", "xlsx"})
65
+
66
+
67
+ def _check_zip_compat(outfile: str, filefmt: str, zipfilename: str | None) -> None:
68
+ """Raise if the format/outfile combination is incompatible with zip output."""
69
+ if zipfilename is None:
70
+ return
71
+ if outfile.lower() == "stdout":
72
+ raise ErrInfo("error", other_msg="Cannot write stdout to a zipfile.")
73
+ if len(outfile) > 1 and outfile[1] == ":":
74
+ raise ErrInfo("error", other_msg="Cannot use a drive letter for a file path within a zipfile.")
75
+ if filefmt in _NO_ZIP_FORMATS:
76
+ raise ErrInfo("error", other_msg=f"Cannot export to the {filefmt} format within a zipfile.")
77
+
78
+
79
+ def _dispatch_format(
80
+ select_stmt: str,
81
+ outfile: str,
82
+ filefmt: str,
83
+ append: bool,
84
+ *,
85
+ description: str | None = None,
86
+ zipfilename: str | None = None,
87
+ notype: bool = False,
88
+ sheetname: str | None = None,
89
+ tablename: str | None = None,
90
+ xml_table: str | None = None,
91
+ hdf5_table: str | None = None,
92
+ ) -> None:
93
+ """Execute the appropriate exporter for *filefmt*.
94
+
95
+ All format-specific parameters that differ between ``x_export`` and
96
+ ``x_export_query`` are passed explicitly rather than duplicating the
97
+ dispatch chain.
98
+ """
99
+ db = _state.dbs.current()
100
100
  if filefmt in ("txt", "text"):
101
- prettyprint_query(
102
- select_stmt,
103
- _state.dbs.current(),
104
- outfile,
105
- append,
106
- desc=description,
107
- zipfile=zipfilename,
108
- )
109
- elif filefmt in ("txt-and", "text-and", "txt-and", "text-and"):
110
- prettyprint_query(
111
- select_stmt,
112
- _state.dbs.current(),
113
- outfile,
114
- append,
115
- and_val="AND",
116
- desc=description,
117
- zipfile=zipfilename,
118
- )
101
+ prettyprint_query(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
102
+ elif filefmt in ("txt-and", "text-and"):
103
+ prettyprint_query(select_stmt, db, outfile, append, and_val="AND", desc=description, zipfile=zipfilename)
119
104
  elif filefmt == "ods":
120
- write_query_to_ods(
121
- select_stmt,
122
- _state.dbs.current(),
123
- outfile,
124
- append,
125
- sheetname=queryname,
126
- desc=description,
127
- )
105
+ write_query_to_ods(select_stmt, db, outfile, append, sheetname=sheetname, desc=description)
128
106
  elif filefmt == "xlsx":
129
- write_query_to_xlsx(
130
- select_stmt,
131
- _state.dbs.current(),
132
- outfile,
133
- append,
134
- sheetname=queryname,
135
- desc=description,
136
- )
107
+ write_query_to_xlsx(select_stmt, db, outfile, append, sheetname=sheetname, desc=description)
137
108
  elif filefmt == "duckdb":
138
- write_query_to_duckdb(select_stmt, _state.dbs.current(), outfile, append, tablename=queryname)
109
+ write_query_to_duckdb(select_stmt, db, outfile, append, tablename=tablename)
139
110
  elif filefmt == "sqlite":
140
- write_query_to_sqlite(select_stmt, _state.dbs.current(), outfile, append, tablename=queryname)
111
+ write_query_to_sqlite(select_stmt, db, outfile, append, tablename=tablename)
141
112
  elif filefmt == "xml":
142
- write_query_to_xml(
143
- select_stmt,
144
- table,
145
- _state.dbs.current(),
146
- outfile,
147
- append,
148
- desc=description,
149
- zipfile=zipfilename,
150
- )
113
+ write_query_to_xml(select_stmt, xml_table, db, outfile, append, desc=description, zipfile=zipfilename)
151
114
  elif filefmt == "json":
152
- write_query_to_json(
153
- select_stmt,
154
- _state.dbs.current(),
155
- outfile,
156
- append,
157
- desc=description,
158
- zipfile=zipfilename,
159
- )
115
+ write_query_to_json(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
160
116
  elif filefmt in ("json_ts", "json_tableschema"):
161
- write_query_to_json_ts(
162
- select_stmt,
163
- _state.dbs.current(),
164
- outfile,
165
- append,
166
- not notype,
167
- desc=description,
168
- zipfile=zipfilename,
169
- )
117
+ write_query_to_json_ts(select_stmt, db, outfile, append, not notype, desc=description, zipfile=zipfilename)
170
118
  elif filefmt == "values":
171
- write_query_to_values(
172
- select_stmt,
173
- _state.dbs.current(),
174
- outfile,
175
- append,
176
- desc=description,
177
- zipfile=zipfilename,
178
- )
119
+ write_query_to_values(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
179
120
  elif filefmt == "html":
180
- write_query_to_html(
181
- select_stmt,
182
- _state.dbs.current(),
183
- outfile,
184
- append,
185
- desc=description,
186
- zipfile=zipfilename,
187
- )
121
+ write_query_to_html(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
188
122
  elif filefmt == "cgi-html":
189
- write_query_to_cgi_html(
190
- select_stmt,
191
- _state.dbs.current(),
192
- outfile,
193
- append,
194
- desc=description,
195
- zipfile=zipfilename,
196
- )
123
+ write_query_to_cgi_html(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
197
124
  elif filefmt == "latex":
198
- write_query_to_latex(
199
- select_stmt,
200
- _state.dbs.current(),
201
- outfile,
202
- append,
203
- desc=description,
204
- zipfile=zipfilename,
205
- )
125
+ write_query_to_latex(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
206
126
  elif filefmt == "hdf5":
207
- write_query_to_hdf5(table, select_stmt, _state.dbs.current(), outfile, append, desc=description)
127
+ write_query_to_hdf5(hdf5_table, select_stmt, db, outfile, append, desc=description)
208
128
  elif filefmt == "yaml":
209
- write_query_to_yaml(
210
- select_stmt,
211
- _state.dbs.current(),
212
- outfile,
213
- append,
214
- desc=description,
215
- zipfile=zipfilename,
216
- )
129
+ write_query_to_yaml(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
217
130
  elif filefmt in ("markdown", "md"):
218
- write_query_to_markdown(
219
- select_stmt,
220
- _state.dbs.current(),
221
- outfile,
222
- append,
223
- desc=description,
224
- zipfile=zipfilename,
225
- )
131
+ write_query_to_markdown(select_stmt, db, outfile, append, desc=description, zipfile=zipfilename)
226
132
  else:
227
133
  try:
228
- hdrs, rows = _state.dbs.current().select_rowsource(select_stmt)
134
+ hdrs, rows = db.select_rowsource(select_stmt)
229
135
  except ErrInfo:
230
136
  raise
231
137
  except Exception as e:
232
138
  raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
233
139
  if filefmt == "raw":
234
- write_query_raw(outfile, rows, _state.dbs.current().encoding, append, zipfile=zipfilename)
140
+ write_query_raw(outfile, rows, db.encoding, append, zipfile=zipfilename)
235
141
  elif filefmt == "b64":
236
- write_query_b64(outfile, rows, append)
142
+ write_query_b64(outfile, rows, append, zipfile=zipfilename)
237
143
  elif filefmt == "feather":
238
144
  write_query_to_feather(outfile, hdrs, rows)
239
145
  elif filefmt == "parquet":
240
146
  write_query_to_parquet(outfile, hdrs, rows)
241
147
  else:
242
148
  write_delimited_file(outfile, filefmt, hdrs, rows, _state.conf.output_encoding, append, zipfilename)
149
+
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # EXPORT <table> TO <format> <file>
153
+ # ---------------------------------------------------------------------------
154
+
155
+
156
+ def x_export(**kwargs: Any) -> None:
157
+ schema = kwargs["schema"]
158
+ table = kwargs["table"]
159
+ queryname = _state.dbs.current().schema_qualified_table_name(schema, table)
160
+ select_stmt = f"select * from {queryname};"
161
+ outfile = _apply_output_dir(kwargs["filename"])
162
+ description = kwargs["description"]
163
+ tee = bool(kwargs["tee"])
164
+ append = bool(kwargs["append"])
165
+ filefmt = kwargs["format"].lower()
166
+ zipfilename = _apply_output_dir(kwargs["zipfilename"]) if kwargs["zipfilename"] else None
167
+ notype = bool(kwargs.get("notype"))
168
+ _check_zip_compat(outfile, filefmt, zipfilename)
169
+ check_dir(zipfilename if zipfilename is not None else outfile)
170
+ if tee and outfile.lower() != "stdout":
171
+ prettyprint_query(select_stmt, _state.dbs.current(), "stdout", False, desc=description)
172
+ _dispatch_format(
173
+ select_stmt,
174
+ outfile,
175
+ filefmt,
176
+ append,
177
+ description=description,
178
+ zipfilename=zipfilename,
179
+ notype=notype,
180
+ sheetname=queryname,
181
+ tablename=queryname,
182
+ xml_table=table,
183
+ hdf5_table=table,
184
+ )
243
185
  _state.export_metadata.add(ExportRecord(queryname, outfile, zipfilename, description))
244
186
  if _state.exec_log:
245
187
  _, line_no = current_script_line()
@@ -247,178 +189,38 @@ def x_export(**kwargs: Any) -> None:
247
189
  return None
248
190
 
249
191
 
192
+ # ---------------------------------------------------------------------------
193
+ # EXPORT QUERY <sql> TO <format> <file>
194
+ # ---------------------------------------------------------------------------
195
+
196
+
250
197
  def x_export_query(**kwargs: Any) -> None:
251
198
  select_stmt = kwargs["query"]
252
199
  outfile = kwargs["filename"]
253
200
  description = kwargs["description"]
254
- tee = kwargs["tee"]
255
- tee = bool(tee)
256
- append = kwargs["append"]
257
- append = bool(append)
201
+ tee = bool(kwargs["tee"])
202
+ append = bool(kwargs["append"])
258
203
  filefmt = kwargs["format"].lower()
259
204
  zipfilename = kwargs["zipfilename"]
260
- if zipfilename is not None:
261
- if outfile == "stdout":
262
- raise ErrInfo("error", other_msg="Cannot write stdout to a zipfile.")
263
- elif len(outfile) > 1 and outfile[1] == ":":
264
- raise ErrInfo("error", other_msg="Cannot use a drive letter for a file path within a zipfile.")
265
- if filefmt == "latex":
266
- raise ErrInfo("error", other_msg="Cannot export to the LaTeX format within a zipfile.")
267
- if filefmt == "feather":
268
- raise ErrInfo("error", other_msg="Cannot export to the feather format within a zipfile.")
269
- if filefmt == "parquet":
270
- raise ErrInfo("error", other_msg="Cannot export to the parquet format within a zipfile.")
271
- if filefmt == "hdf5":
272
- raise ErrInfo("error", other_msg="Cannot export to the HDF5 format within a zipfile.")
273
- if filefmt == "ods":
274
- raise ErrInfo("error", other_msg="Cannot export to an ODS workbook within a zipfile.")
275
- if filefmt == "xlsx":
276
- raise ErrInfo("error", other_msg="Cannot export to an XLSX workbook within a zipfile.")
277
205
  notype = bool(kwargs.get("notype"))
206
+ _check_zip_compat(outfile, filefmt, zipfilename)
278
207
  check_dir(outfile)
279
208
  if tee and outfile.lower() != "stdout":
280
209
  prettyprint_query(select_stmt, _state.dbs.current(), "stdout", False, desc=description)
281
- if filefmt in ("txt", "text"):
282
- prettyprint_query(
283
- select_stmt,
284
- _state.dbs.current(),
285
- outfile,
286
- append,
287
- desc=description,
288
- zipfile=zipfilename,
289
- )
290
- elif filefmt in ("txt-and", "text-and", "txt-and", "text-and"):
291
- prettyprint_query(
292
- select_stmt,
293
- _state.dbs.current(),
294
- outfile,
295
- append,
296
- and_val="AND",
297
- desc=description,
298
- zipfile=zipfilename,
299
- )
300
- elif filefmt == "ods":
301
- script_name, lno = current_script_line()
302
- write_query_to_ods(
303
- select_stmt,
304
- _state.dbs.current(),
305
- outfile,
306
- append,
307
- sheetname=f"Query_{lno}",
308
- desc=description,
309
- )
310
- elif filefmt == "xlsx":
311
- script_name, lno = current_script_line()
312
- write_query_to_xlsx(
313
- select_stmt,
314
- _state.dbs.current(),
315
- outfile,
316
- append,
317
- sheetname=f"Query_{lno}",
318
- desc=description,
319
- )
320
- elif filefmt == "json":
321
- write_query_to_json(
322
- select_stmt,
323
- _state.dbs.current(),
324
- outfile,
325
- append,
326
- desc=description,
327
- zipfile=zipfilename,
328
- )
329
- elif filefmt in ("json_ts", "json_tableschema"):
330
- write_query_to_json_ts(
331
- select_stmt,
332
- _state.dbs.current(),
333
- outfile,
334
- append,
335
- not notype,
336
- desc=description,
337
- zipfile=zipfilename,
338
- )
339
- elif filefmt == "values":
340
- write_query_to_values(
341
- select_stmt,
342
- _state.dbs.current(),
343
- outfile,
344
- append,
345
- desc=description,
346
- zipfile=zipfilename,
347
- )
348
- elif filefmt == "html":
349
- write_query_to_html(
350
- select_stmt,
351
- _state.dbs.current(),
352
- outfile,
353
- append,
354
- desc=description,
355
- zipfile=zipfilename,
356
- )
357
- elif filefmt == "cgi-html":
358
- write_query_to_cgi_html(
359
- select_stmt,
360
- _state.dbs.current(),
361
- outfile,
362
- append,
363
- desc=description,
364
- zipfile=zipfilename,
365
- )
366
- elif filefmt == "latex":
367
- write_query_to_latex(
368
- select_stmt,
369
- _state.dbs.current(),
370
- outfile,
371
- append,
372
- desc=description,
373
- zipfile=zipfilename,
374
- )
375
- elif filefmt == "yaml":
376
- write_query_to_yaml(
377
- select_stmt,
378
- _state.dbs.current(),
379
- outfile,
380
- append,
381
- desc=description,
382
- zipfile=zipfilename,
383
- )
384
- elif filefmt in ("markdown", "md"):
385
- write_query_to_markdown(
386
- select_stmt,
387
- _state.dbs.current(),
388
- outfile,
389
- append,
390
- desc=description,
391
- zipfile=zipfilename,
392
- )
393
- else:
394
- try:
395
- hdrs, rows = _state.dbs.current().select_rowsource(select_stmt)
396
- except ErrInfo:
397
- raise
398
- except Exception as e:
399
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
400
- if filefmt == "raw":
401
- write_query_raw(outfile, rows, _state.dbs.current().encoding, append, zipfile=zipfilename)
402
- elif filefmt == "b64":
403
- write_query_b64(outfile, rows, append, zipfile=zipfilename)
404
- elif filefmt == "feather":
405
- write_query_to_feather(outfile, hdrs, rows)
406
- elif filefmt == "parquet":
407
- write_query_to_parquet(outfile, hdrs, rows)
408
- else:
409
- write_delimited_file(
410
- outfile,
411
- filefmt,
412
- hdrs,
413
- rows,
414
- _state.conf.output_encoding,
415
- append,
416
- zipfile=zipfilename,
417
- )
210
+ _, lno = current_script_line()
211
+ _dispatch_format(
212
+ select_stmt,
213
+ outfile,
214
+ filefmt,
215
+ append,
216
+ description=description,
217
+ zipfilename=zipfilename,
218
+ notype=notype,
219
+ sheetname=f"Query_{lno}",
220
+ )
418
221
  _state.export_metadata.add(ExportRecord(select_stmt, outfile, zipfilename, description))
419
222
  if _state.exec_log:
420
- _, line_no = current_script_line()
421
- _state.exec_log.log_action_export(line_no, select_stmt[:80], outfile)
223
+ _state.exec_log.log_action_export(lno, select_stmt[:80], outfile)
422
224
  return None
423
225
 
424
226
 
@@ -16,7 +16,7 @@ from typing import Any
16
16
  import execsql.state as _state
17
17
  from execsql.exceptions import ErrInfo
18
18
  from execsql.models import DataTable
19
- from execsql.script import current_script_line, read_sqlfile
19
+ from execsql.script import current_script_line
20
20
  from execsql.types import dbt_firebird
21
21
  from execsql.utils.errors import exception_desc
22
22
  from execsql.utils.fileio import filewriter_close
@@ -24,18 +24,12 @@ from execsql.utils.strings import unquoted
24
24
 
25
25
 
26
26
  def x_include(**kwargs: Any) -> None:
27
- filename = kwargs["filename"]
28
- if len(filename) > 1 and filename[0] == "~" and filename[1] == os.sep:
29
- filename = str(Path.home() / filename[2:])
30
- exists = kwargs["exists"]
31
- if exists is not None:
32
- if Path(filename).is_file():
33
- read_sqlfile(filename)
34
- else:
35
- if not Path(filename).is_file():
36
- raise ErrInfo(type="error", other_msg=f"File {filename} does not exist.")
37
- read_sqlfile(filename)
38
- return None
27
+ # INCLUDE is now handled natively by the AST executor (_execute_include_native).
28
+ # This handler exists only for dispatch table registration compatibility.
29
+ raise ErrInfo(
30
+ type="cmd",
31
+ other_msg="INCLUDE should be handled by the AST executor, not the dispatch table.",
32
+ )
39
33
 
40
34
 
41
35
  def x_copy(**kwargs: Any) -> None:
@@ -45,7 +45,7 @@ def x_write(**kwargs: Any) -> None:
45
45
  except ConsoleUIError as e:
46
46
  _state.output.reset()
47
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))
48
+ _state.output.write(msg)
49
49
  if _state.conf.tee_write_log:
50
50
  _state.exec_log.log_user_msg(msg)
51
51
  return None
@@ -14,7 +14,7 @@ from typing import Any
14
14
 
15
15
  import execsql.state as _state
16
16
  from execsql.exceptions import ErrInfo
17
- from execsql.script import MetacommandStmt, ScriptCmd, ScriptExecSpec, SqlStmt, current_script_line
17
+ from execsql.script import MetacommandStmt, ScriptCmd, SqlStmt, current_script_line
18
18
 
19
19
 
20
20
  def x_extendscript(**kwargs: Any) -> None:
@@ -57,7 +57,10 @@ def x_extendscript_sql(**kwargs: Any) -> None:
57
57
 
58
58
 
59
59
  def x_executescript(**kwargs: Any) -> None:
60
- exists = kwargs["exists"]
61
- script_id = kwargs["script_id"].lower()
62
- if exists is None or (exists is not None and script_id in _state.savedscripts):
63
- ScriptExecSpec(**kwargs).execute()
60
+ # EXECUTE SCRIPT is now handled natively by the AST executor
61
+ # (_execute_include / _execute_script_native). This handler exists only
62
+ # for dispatch table registration compatibility.
63
+ raise ErrInfo(
64
+ "cmd",
65
+ other_msg="EXECUTE SCRIPT should be handled by the AST executor, not the dispatch table.",
66
+ )
@@ -573,3 +573,43 @@ def x_pg_upsert_check(**kwargs: Any) -> None:
573
573
  command_text=metacommandline,
574
574
  other_msg=_qa_failure_msg(result),
575
575
  )
576
+
577
+
578
+ # ---------------------------------------------------------------------------
579
+ # Plugin registration
580
+ # ---------------------------------------------------------------------------
581
+
582
+ _PG_UPSERT_RX = r"^\s*PG_UPSERT\s+FROM\s+(?P<staging_schema>\S+)\s+TO\s+(?P<base_schema>\S+)\s+TABLES\s+(?P<tail>.+)$"
583
+ _PG_UPSERT_CHECK_RX = (
584
+ r"^\s*PG_UPSERT\s+CHECK\s+FROM\s+(?P<staging_schema>\S+)\s+TO\s+(?P<base_schema>\S+)\s+TABLES\s+(?P<tail>.+)$"
585
+ )
586
+ _PG_UPSERT_QA_RX = (
587
+ r"^\s*PG_UPSERT\s+QA\s+FROM\s+(?P<staging_schema>\S+)\s+TO\s+(?P<base_schema>\S+)\s+TABLES\s+(?P<tail>.+)$"
588
+ )
589
+
590
+
591
+ def register(mcl: Any) -> None:
592
+ """Register PG_UPSERT metacommands as a plugin.
593
+
594
+ Called by execsql's plugin discovery via the ``execsql.metacommands``
595
+ entry point. The CHECK and QA variants are registered first so their
596
+ more-specific regexes take priority over the general form.
597
+ """
598
+ mcl.add(
599
+ _PG_UPSERT_CHECK_RX,
600
+ x_pg_upsert_check,
601
+ description="PG_UPSERT CHECK",
602
+ category="action",
603
+ )
604
+ mcl.add(
605
+ _PG_UPSERT_QA_RX,
606
+ x_pg_upsert_qa,
607
+ description="PG_UPSERT QA",
608
+ category="action",
609
+ )
610
+ mcl.add(
611
+ _PG_UPSERT_RX,
612
+ x_pg_upsert,
613
+ description="PG_UPSERT",
614
+ category="action",
615
+ )
execsql/models.py CHANGED
@@ -307,20 +307,16 @@ class DataTable:
307
307
  class JsonDatatype:
308
308
  """Namespace mapping Python DataType subclasses to JSON Schema type strings."""
309
309
 
310
- def __init__(self) -> None:
311
- """Create an empty JsonDatatype namespace instance."""
312
- pass
310
+ any = "any"
311
+ integer = "integer"
312
+ string = "string"
313
+ date = "date"
314
+ datetime = "datetime"
315
+ time = "time"
316
+ number = "number"
317
+ boolean = "boolean"
313
318
 
314
319
 
315
- JsonDatatype.any = "any"
316
- JsonDatatype.integer = "integer"
317
- JsonDatatype.string = "string"
318
- JsonDatatype.date = "date"
319
- JsonDatatype.datetime = "datetime"
320
- JsonDatatype.time = "time"
321
- JsonDatatype.number = "number"
322
- JsonDatatype.boolean = "boolean"
323
-
324
320
  # Types without a JSON type equivalent are converted
325
321
  # to strings via the "default=str" argument of 'json.dumps()'.
326
322
  to_json_type = {