execsql2 2.18.0__py3-none-any.whl → 2.19.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 (43) hide show
  1. execsql/cli/__init__.py +3 -5
  2. execsql/cli/lint.py +433 -18
  3. execsql/cli/run.py +46 -17
  4. execsql/data/execsql.conf.template +34 -2
  5. execsql/db/access.py +0 -6
  6. execsql/db/base.py +0 -13
  7. execsql/db/mysql.py +0 -6
  8. execsql/db/oracle.py +0 -6
  9. execsql/db/sqlserver.py +0 -6
  10. execsql/debug/repl.py +117 -35
  11. execsql/exporters/feather.py +10 -9
  12. execsql/format.py +23 -4
  13. execsql/importers/base.py +3 -4
  14. execsql/importers/xls.py +6 -1
  15. execsql/metacommands/__init__.py +2 -2
  16. execsql/metacommands/data.py +1 -0
  17. execsql/metacommands/dispatch.py +5 -10
  18. execsql/metacommands/script_ext.py +8 -7
  19. execsql/script/engine.py +1 -12
  20. execsql/script/executor.py +2 -6
  21. execsql/script/parser.py +49 -12
  22. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/example_config_prompt.sql +1 -1
  23. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/execsql.conf +34 -2
  24. {execsql2-2.18.0.dist-info → execsql2-2.19.0.dist-info}/METADATA +54 -43
  25. {execsql2-2.18.0.dist-info → execsql2-2.19.0.dist-info}/RECORD +42 -43
  26. {execsql2-2.18.0.dist-info → execsql2-2.19.0.dist-info}/WHEEL +1 -1
  27. execsql/cli/lint_ast.py +0 -439
  28. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/README.md +0 -0
  29. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  30. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  31. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  32. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  33. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  34. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  35. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  36. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  37. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/script_template.sql +0 -0
  38. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  39. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  40. {execsql2-2.18.0.data → execsql2-2.19.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  41. {execsql2-2.18.0.dist-info → execsql2-2.19.0.dist-info}/entry_points.txt +0 -0
  42. {execsql2-2.18.0.dist-info → execsql2-2.19.0.dist-info}/licenses/LICENSE.txt +0 -0
  43. {execsql2-2.18.0.dist-info → execsql2-2.19.0.dist-info}/licenses/NOTICE +0 -0
execsql/db/mysql.py CHANGED
@@ -73,12 +73,6 @@ class MySQLDatabase(Database):
73
73
  f"{self.need_passwd!r}, {self.port!r}, {self.encoding!r})"
74
74
  )
75
75
 
76
- def auto_commits_ddl(self) -> bool:
77
- """MySQL / MariaDB implicitly commit DDL — ``rollback()`` is a
78
- silent no-op for any transaction whose boundary the DDL
79
- crossed. See ``docs/about/divergence.md``."""
80
- return True
81
-
82
76
  def quote_identifier(self, identifier: str) -> str:
83
77
  """MySQL / MariaDB native identifier quoting uses backticks.
84
78
 
execsql/db/oracle.py CHANGED
@@ -61,12 +61,6 @@ class OracleDatabase(Database):
61
61
  f"{self.need_passwd!r}, {self.port!r}, {self.encoding!r})"
62
62
  )
63
63
 
64
- def auto_commits_ddl(self) -> bool:
65
- """Oracle implicitly commits DDL — ``rollback()`` is a silent
66
- no-op for any transaction whose boundary the DDL crossed.
67
- See ``docs/about/divergence.md`` for the per-DBMS matrix."""
68
- return True
69
-
70
64
  def open_db(self) -> None:
71
65
  """Open a connection to the Oracle database."""
72
66
  import cx_Oracle
execsql/db/sqlserver.py CHANGED
@@ -58,12 +58,6 @@ class SqlServerDatabase(Database):
58
58
  f"{self.need_passwd!r}, {self.port!r}, {self.encoding!r})"
59
59
  )
60
60
 
61
- def auto_commits_ddl(self) -> bool:
62
- """SQL Server implicitly commits DDL on Microsoft's pyodbc
63
- driver in autocommit mode — ``rollback()`` is a silent no-op
64
- for any transaction whose boundary the DDL crossed."""
65
- return True
66
-
67
61
  def quote_identifier(self, identifier: str) -> str:
68
62
  """SQL Server native identifier quoting uses square brackets.
69
63
 
execsql/debug/repl.py CHANGED
@@ -12,10 +12,26 @@ The REPL allows the user to:
12
12
  - Step through the script one statement at a time.
13
13
  - Resume or abort execution.
14
14
 
15
- All REPL commands are dot-prefixed (``.continue``, ``.vars``, ``.next``)
16
- to avoid ambiguity with variable names and SQL. Anything not starting
17
- with ``.`` is treated as either a variable lookup (if it matches a known
18
- variable) or SQL (if it ends with ``;``).
15
+ Dispatch is two-way (psql-style): input starting with ``.`` is a REPL
16
+ command (``.continue``, ``.vars [VAR]``, ``.next``, etc.), everything
17
+ else is SQL. Multi-line SQL is supported: any non-``.`` input opens a
18
+ buffer whose continuation prompt is `` ... > ``, accumulating
19
+ until a line ends with ``;``. ``.cancel`` (or Ctrl-C / EOF) discards
20
+ the partial buffer.
21
+
22
+ Variable lookup is explicit — ``.vars LOGFILE`` prints one variable;
23
+ ``.vars`` lists them all. There is no bare-identifier lookup, so any
24
+ SQL keyword you type starts a buffer the moment you press Enter.
25
+
26
+ The trailing ``;`` is the SQL terminator both within one line and across
27
+ multiple lines — the REPL has no read-only mode and DDL on most adapters
28
+ is irreversible, so requiring ``;`` is a small intent gate against
29
+ accidental DML/DDL on mistyped input. Use SQL ``BEGIN; … ROLLBACK;`` to
30
+ bracket exploratory DML if you need recoverability.
31
+
32
+ Errors raised by any REPL helper — bad SQL, malformed dot-commands, etc. —
33
+ are caught at the loop level so the session re-prompts instead of escaping
34
+ through ``x_breakpoint`` and being stamped as a "Metacommand error".
19
35
 
20
36
  In non-interactive environments (CI, piped input, ``sys.stdin.isatty()`` is
21
37
  ``False``) the metacommand is silently skipped so automated pipelines are not
@@ -104,21 +120,21 @@ def _c(code: str, text: str) -> str:
104
120
 
105
121
  _HELP_COMMANDS = [
106
122
  (".continue", ".c", "Resume script execution"),
107
- (".abort", ".q", "Halt the script (exit 1)"),
108
- (".vars", ".v", "List user, system, local, and counter variables"),
109
- (".vars all", ".v all", "Include environment variables (&) in the listing"),
123
+ (".quit", ".q", "Halt the script (exit 1)"),
124
+ (".vars", ".v", "List all execsql substitution variables"),
125
+ (".vars VAR", ".v VAR", "Print the value of a single variable (e.g. .vars logfile)"),
110
126
  (".next", ".n", "Execute the next statement then pause again (step mode)"),
111
127
  (".where", ".w", "Show the current script location and upcoming statement"),
112
128
  (".stack", "", "Show the command-list stack (script name, line, depth)"),
113
129
  (".set VAR VAL", ".s", "Set or update a substitution variable"),
114
130
  (".scripts", "", "List all registered SCRIPT definitions"),
115
131
  (".scripts NAME", "", "Show detail for a specific SCRIPT"),
132
+ (".cancel", "", "Discard a partial multi-line SQL buffer"),
116
133
  (".help", ".h", "Show this help text"),
117
134
  ]
118
135
 
119
136
  _HELP_OTHER = [
120
- ("varname", "Print a variable's value (e.g. logfile, $ARG_1, &HOME)"),
121
- ("SELECT ...;", "Run SQL ending with ';' (expects columns returned, e.g. SELECT)"),
137
+ ("SELECT ...;", "Run SQL multi-line accepted, terminate with ';' to execute"),
122
138
  ]
123
139
 
124
140
  _HELP_CMD_WIDTH = 13 # width of the command column
@@ -216,40 +232,71 @@ def _debug_repl(*, step: bool = False) -> None:
216
232
  _hint_c = _c(_DIM, "'.c'")
217
233
  _write(f" Type {_hint_help} for commands, {_hint_c} to resume.\n\n")
218
234
 
235
+ sql_buffer: list[str] = []
236
+
219
237
  while True:
238
+ prompt = " ... > " if sql_buffer else "execsql debug> "
220
239
  try:
221
- line = input("execsql debug> ").strip()
240
+ line = input(prompt).strip()
222
241
  except EOFError:
242
+ if sql_buffer:
243
+ sql_buffer.clear()
244
+ _write("\n (input discarded)\n")
245
+ continue
223
246
  _write("\n")
224
- return # Ctrl-D → continue
247
+ return
225
248
  except KeyboardInterrupt:
249
+ if sql_buffer:
250
+ sql_buffer.clear()
251
+ _write("\n (input discarded)\n")
252
+ continue
226
253
  _write("\n")
227
- return # Ctrl-C → continue
254
+ return
228
255
 
229
256
  if not line:
230
257
  continue
231
258
 
232
- # Dot-prefixed → REPL command
233
- if line.startswith("."):
234
- cmd = line[1:].strip().lower()
235
- _handle_dot_command(line)
236
- if cmd in ("continue", "c"):
237
- return
238
- if cmd in ("abort", "q", "quit"):
239
- # _handle_dot_command already raised SystemExit, but guard anyway
240
- return
241
- if cmd in ("next", "n"):
242
- return
259
+ try:
260
+ if line.startswith("."):
261
+ cmd = line[1:].strip().lower()
262
+ if cmd == "cancel":
263
+ if sql_buffer:
264
+ sql_buffer.clear()
265
+ _write(" (input discarded)\n")
266
+ continue
267
+ _handle_dot_command(line)
268
+ if cmd in ("continue", "c"):
269
+ return
270
+ if cmd in ("abort", "q", "quit"):
271
+ return
272
+ if cmd in ("next", "n"):
273
+ return
274
+ continue
275
+
276
+ if sql_buffer:
277
+ sql_buffer.append(line)
278
+ joined = " ".join(sql_buffer)
279
+ if joined.rstrip().endswith(";"):
280
+ _run_sql(joined)
281
+ sql_buffer.clear()
282
+ continue
283
+
284
+ if line.rstrip().endswith(";"):
285
+ _run_sql(line)
286
+ continue
287
+
288
+ sql_buffer.append(line)
289
+ except SystemExit:
290
+ raise
291
+ except KeyboardInterrupt:
292
+ sql_buffer.clear()
293
+ _write("\n (interrupted)\n")
243
294
  continue
244
-
245
- # SQL (ends with semicolon)
246
- if line.rstrip().endswith(";"):
247
- _run_sql(line)
295
+ except Exception as exc:
296
+ sql_buffer.clear()
297
+ _write(f" {_c(_RED, 'Error:')} {exc}\n")
248
298
  continue
249
299
 
250
- # Everything else → variable lookup
251
- _print_var(line)
252
-
253
300
 
254
301
  def _handle_dot_command(line: str) -> None:
255
302
  """Dispatch a dot-prefixed REPL command."""
@@ -262,10 +309,14 @@ def _handle_dot_command(line: str) -> None:
262
309
  raise SystemExit(1)
263
310
  elif cmd in ("help", "h"):
264
311
  _write(_format_help())
265
- elif cmd in ("vars all", "v all"):
266
- _print_all_vars(include_env=True)
267
312
  elif cmd in ("vars", "v"):
268
313
  _print_all_vars()
314
+ elif cmd.startswith("vars ") or cmd.startswith("v "):
315
+ rest = cmd.split(None, 1)[1].strip() if " " in cmd else ""
316
+ if rest:
317
+ _print_var(rest)
318
+ else:
319
+ _print_all_vars()
269
320
  elif cmd in ("where", "w"):
270
321
  _print_where()
271
322
  elif cmd == "stack":
@@ -383,7 +434,11 @@ def _print_all_vars(*, include_env: bool = False) -> None:
383
434
  _write(f" {_c(_BOLD, label)}:\n")
384
435
  max_name = max(len(n) for n, _ in group)
385
436
  for name, value in group:
386
- _write(f" {_c(_CYAN, name):<{max_name}} {_c(_DIM, '=')} {value}\n")
437
+ # Pre-compute padding from RAW name length — the format-spec ``:<{max_name}``
438
+ # form measures the colored string, which (when ANSI is on) is ~9 chars
439
+ # longer per wrap and breaks the column alignment.
440
+ pad = " " * (max_name - len(name))
441
+ _write(f" {_c(_CYAN, name)}{pad} {_c(_DIM, '=')} {value}\n")
387
442
 
388
443
  _print_group("User", user_vars)
389
444
  _print_group("System ($)", system_vars)
@@ -477,7 +532,7 @@ def _print_stack() -> None:
477
532
 
478
533
 
479
534
  def _run_sql(sql: str) -> None:
480
- """Execute ad-hoc SQL against the current database and pretty-print the results."""
535
+ """Execute ad-hoc SQL and pretty-print results or affected rowcount."""
481
536
  dbs = _state.dbs
482
537
  if dbs is None:
483
538
  _write(" (no database connection is active)\n")
@@ -486,8 +541,35 @@ def _run_sql(sql: str) -> None:
486
541
  if db is None:
487
542
  _write(" (no database connection is active)\n")
488
543
  return
544
+
489
545
  try:
490
- colnames, rows = db.select_data(sql)
546
+ with db._cursor() as curs:
547
+ try:
548
+ curs.execute(sql)
549
+ except Exception as exc:
550
+ try:
551
+ db.rollback()
552
+ except Exception:
553
+ pass
554
+ _write(f" {_c(_RED, 'SQL error:')} {exc}\n")
555
+ return
556
+
557
+ try:
558
+ _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
559
+ except Exception:
560
+ pass
561
+
562
+ if curs.description is None:
563
+ rowcount = curs.rowcount if curs.rowcount is not None else -1
564
+ if rowcount >= 0:
565
+ row_word = "row" if rowcount == 1 else "rows"
566
+ _write(f" {_c(_DIM, f'({rowcount} {row_word} affected)')}\n")
567
+ else:
568
+ _write(f" {_c(_DIM, '(statement executed)')}\n")
569
+ return
570
+
571
+ colnames = [d[0] for d in curs.description]
572
+ rows = curs.fetchall()
491
573
  except Exception as exc:
492
574
  _write(f" {_c(_RED, 'SQL error:')} {exc}\n")
493
575
  return
@@ -58,20 +58,26 @@ def write_query_to_hdf5(
58
58
  other_msg="The tables Python library must be installed to export data to the HDF5 format.",
59
59
  ) from e
60
60
  try:
61
- hdrs, rows = db.select_rowsource(select_stmt)
61
+ hdrs, row_iter = db.select_rowsource(select_stmt)
62
62
  except ErrInfo:
63
63
  raise
64
64
  except Exception as e:
65
65
  raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
66
+ # Materialize once so DataTable's type inference (below) and the write
67
+ # loop (further below) share the same rows without a second query.
68
+ rows = list(row_iter)
66
69
 
67
70
  def h5type(datatype, size):
68
- if datatype in (_state.DT_Varchar, _state.DT_Text):
71
+ if datatype == _state.DT_Varchar:
69
72
  t = tables.StringCol(size)
70
73
  do_cast = False
71
74
  elif datatype == _state.DT_Text:
72
75
  t = tables.StringCol(_state.conf.hdf5_text_len)
73
76
  do_cast = False
74
- elif datatype in (_state.DT_Integer, _state.DT_Long):
77
+ elif datatype == _state.DT_Long:
78
+ t = tables.Int64Col()
79
+ do_cast = False
80
+ elif datatype == _state.DT_Integer:
75
81
  t = tables.IntCol()
76
82
  do_cast = False
77
83
  elif datatype in (_state.DT_Float, _state.DT_Decimal):
@@ -87,25 +93,20 @@ def write_query_to_hdf5(
87
93
  raise ErrInfo("error", other_msg=f"Invalid data type for export to HDF5: {repr(datatype)}")
88
94
  return t, do_cast
89
95
 
90
- # Create a dictionary of column names with the HDF5 data types
91
96
  tbl_desc = DataTable(hdrs, rows)
92
97
  h5type_dict = {}
93
98
  cast_flags = []
94
- # Iterate over hdrs instead of tbl_desc.cols to preserve column order.
95
99
  for h in hdrs:
96
100
  dt = [col for col in tbl_desc.cols if col.name == h][0].dt
97
- # dt is a tuple of: 0: the column name; 1: the data type class; 2: the maximum length or None if NA; other info.
101
+ # dt is (name, data-type-class, max-length-or-None, ...)
98
102
  h5typ, as_str = h5type(dt[1], dt[2])
99
103
  h5type_dict[h] = h5typ
100
104
  cast_flags.append(as_str)
101
- # Open the HDF5 table
102
105
  filewriter_close(outfile)
103
106
  h5file_mode = "a" if append else "w"
104
107
  h5file = tables.open_file(outfile, mode=h5file_mode)
105
108
  h5grp = h5file.create_group("/", table_name, title=desc)
106
109
  h5tbl = h5file.create_table(h5grp, table_name, h5type_dict)
107
- # Write the data.
108
- hdrs, rows = db.select_rowsource(select_stmt)
109
110
  for datarow in rows:
110
111
  h5row = h5tbl.row
111
112
  for i, h in enumerate(hdrs):
execsql/format.py CHANGED
@@ -16,11 +16,27 @@ import io
16
16
  import re
17
17
  from pathlib import Path
18
18
 
19
- import sqlglot
20
- import sqlglot.errors
21
-
22
19
  __all__ = ["collect_paths", "format_file", "main", "parse_keyword"]
23
20
 
21
+
22
+ _SQLGLOT_MISSING_MSG = (
23
+ "execsql-format requires sqlglot for SQL reformatting.\n"
24
+ " Install with: pip install execsql2[formatter]\n"
25
+ " Or skip SQL reformatting with the --no-sql flag."
26
+ )
27
+
28
+
29
+ def _require_sqlglot():
30
+ """Lazy import of sqlglot, raising ImportError with an install hint if missing."""
31
+ try:
32
+ import sqlglot
33
+ import sqlglot.errors # noqa: F401
34
+
35
+ return sqlglot
36
+ except ImportError as e:
37
+ raise ImportError(_SQLGLOT_MISSING_MSG) from e
38
+
39
+
24
40
  # ---------------------------------------------------------------------------
25
41
  # Constants
26
42
  # ---------------------------------------------------------------------------
@@ -161,6 +177,9 @@ def _sqlglot_format(
161
177
  leading_comma: bool = False,
162
178
  ) -> list[str]:
163
179
  """Format a list of SQL-only lines (no comment-only lines) via sqlglot."""
180
+ sqlglot = _require_sqlglot()
181
+ import sqlglot.errors as sqlglot_errors
182
+
164
183
  text = "\n".join(sql_lines)
165
184
  protected, replacements = _protect_variables(text)
166
185
 
@@ -169,7 +188,7 @@ def _sqlglot_format(
169
188
 
170
189
  try:
171
190
  with contextlib.redirect_stderr(io.StringIO()):
172
- ast = sqlglot.parse(protected, read="postgres", error_level=sqlglot.errors.ErrorLevel.IGNORE)
191
+ ast = sqlglot.parse(protected, read="postgres", error_level=sqlglot_errors.ErrorLevel.IGNORE)
173
192
  statements: list[str] = []
174
193
  for node in ast:
175
194
  if node is None:
execsql/importers/base.py CHANGED
@@ -15,7 +15,6 @@ from typing import Any
15
15
  from execsql.exceptions import ErrInfo
16
16
  from execsql.db.base import Database
17
17
  import execsql.state as _state
18
- from execsql.types import dbt_firebird
19
18
 
20
19
  __all__ = ["import_data_table"]
21
20
 
@@ -84,9 +83,9 @@ def import_data_table(
84
83
  sql = get_ts().create_table(db.type, schemaname, tablename)
85
84
  try:
86
85
  db.execute(sql)
87
- # Don't commit here; commit will be done after populating the table
88
- # ...except for Firebird.
89
- if db.type == dbt_firebird:
86
+ # Most adapters delay commit until after populating the table; adapters
87
+ # that need DDL committed first (Firebird) must do it here.
88
+ if db.needs_explicit_commit_after_ddl():
90
89
  db.conn.commit()
91
90
  except Exception as e:
92
91
  raise ErrInfo(
execsql/importers/xls.py CHANGED
@@ -39,7 +39,12 @@ def xls_data(
39
39
 
40
40
  wbk = XlsFile()
41
41
  elif ext3 == "lsx":
42
- # openpyxl imported lazily
42
+ from execsql.utils.fileio import check_zip_decompression_ratio
43
+
44
+ try:
45
+ check_zip_decompression_ratio(filename)
46
+ except FileNotFoundError:
47
+ pass
43
48
  from execsql.exporters.xls import XlsxFile
44
49
 
45
50
  wbk = XlsxFile()
@@ -2,8 +2,8 @@
2
2
 
3
3
  Importing this module populates a ``MetaCommandList`` (``DISPATCH_TABLE``)
4
4
  with every metacommand regex and its handler function. The dispatch
5
- table is consumed by ``script.MetacommandStmt.run()`` via
6
- ``_state.metacommandlist``.
5
+ table is consumed by :func:`execsql.script.executor._exec_metacommand`
6
+ via ``_state.metacommandlist``.
7
7
 
8
8
  The table itself is built by ``build_dispatch_table()`` in
9
9
  :mod:`execsql.metacommands.dispatch`. Handler functions are organized
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from execsql.exceptions import ErrInfo
3
4
 
4
5
  """
@@ -1377,8 +1377,8 @@ def build_dispatch_table() -> MetaCommandList:
1377
1377
  # BEGIN / END BATCH / ROLLBACK
1378
1378
  # ------------------------------------------------------------------
1379
1379
  mcl.add(r"^\s*BEGIN\s+BATCH\s*$", x_begin_batch, description="BEGIN BATCH", category="block")
1380
- mcl.add(r"^\s*END\s+BATCH\s*$", x_end_batch, "END BATCH", run_in_batch=True, category="block")
1381
- mcl.add(r"^\s*ROLLBACK(:?\s+BATCH)?\s*$", x_rollback, "ROLLBACK BATCH", run_in_batch=True, category="block")
1380
+ mcl.add(r"^\s*END\s+BATCH\s*$", x_end_batch, "END BATCH", category="block")
1381
+ mcl.add(r"^\s*ROLLBACK(:?\s+BATCH)?\s*$", x_rollback, "ROLLBACK BATCH", category="block")
1382
1382
 
1383
1383
  # ------------------------------------------------------------------
1384
1384
  # ERROR_HALT / METACOMMAND_ERROR_HALT / CANCEL_HALT
@@ -1727,14 +1727,12 @@ def build_dispatch_table() -> MetaCommandList:
1727
1727
  x_assert,
1728
1728
  description="ASSERT",
1729
1729
  category="action",
1730
- run_when_false=False,
1731
1730
  )
1732
1731
  mcl.add(
1733
1732
  r"^\s*ASSERT\s+(?P<condtest>.+?)\s+(?P<message>(?:\"[^\"]*\"|'[^']*'))\s*$",
1734
1733
  x_assert,
1735
1734
  description="ASSERT",
1736
1735
  category="action",
1737
- run_when_false=False,
1738
1736
  )
1739
1737
 
1740
1738
  # ------------------------------------------------------------------
@@ -1745,7 +1743,6 @@ def build_dispatch_table() -> MetaCommandList:
1745
1743
  x_breakpoint,
1746
1744
  description="BREAKPOINT",
1747
1745
  category="action",
1748
- run_when_false=False,
1749
1746
  )
1750
1747
 
1751
1748
  # ------------------------------------------------------------------
@@ -1765,26 +1762,24 @@ def build_dispatch_table() -> MetaCommandList:
1765
1762
  r"^\s*ORIF\s*\(\s*(?P<condtest>.+)\s*\)\s*$",
1766
1763
  x_if_orif,
1767
1764
  description="ORIF",
1768
- run_when_false=True,
1769
1765
  category="control",
1770
1766
  )
1771
1767
  mcl.add(
1772
1768
  r"^\s*ELSEIF\s*\(\s*(?P<condtest>.+)\s*\)\s*$",
1773
1769
  x_if_elseif,
1774
1770
  description="ELSEIF",
1775
- run_when_false=True,
1776
1771
  category="control",
1777
1772
  )
1778
1773
  mcl.add(r"^\s*ANDIF\s*\(\s*(?P<condtest>.+)\s*\)\s*$", x_if_andif, description="ANDIF", category="control")
1779
- mcl.add(r"^\s*ELSE\s*$", x_if_else, description="ELSE", run_when_false=True, category="control")
1774
+ mcl.add(r"^\s*ELSE\s*$", x_if_else, description="ELSE", category="control")
1780
1775
  mcl.add(
1781
1776
  r"^\s*IF\s*\(\s*(?P<condtest>.+)\s*\)\s*{\s*(?P<condcmd>.+)\s*}\s*$",
1782
1777
  x_if,
1783
1778
  description="IF",
1784
1779
  category="control",
1785
1780
  )
1786
- mcl.add(r"^\s*IF\s*\(\s*(?P<condtest>.+)\s*\)\s*$", x_if_block, run_when_false=True, category="control")
1787
- mcl.add(r"^\s*ENDIF\s*$", x_if_end, description="ENDIF", run_when_false=True, category="control")
1781
+ mcl.add(r"^\s*IF\s*\(\s*(?P<condtest>.+)\s*\)\s*$", x_if_block, category="control")
1782
+ mcl.add(r"^\s*ENDIF\s*$", x_if_end, description="ENDIF", category="control")
1788
1783
 
1789
1784
  # ------------------------------------------------------------------
1790
1785
  # CONNECT — SQL Server
@@ -3,22 +3,23 @@ from __future__ import annotations
3
3
  """
4
4
  Script-block extension and dispatch handlers for execsql.
5
5
 
6
- Handlers for the named-script invocation and dynamic-extension
7
- metacommands. Used by both the AST executor and legacy command paths:
6
+ Handlers for the named-script extension metacommands invoked by the
7
+ AST executor:
8
8
 
9
- - ``x_executescript`` — ``EXECUTE SCRIPT <name>`` / ``RUN SCRIPT <name>``
10
- (look up a previously-registered ``BEGIN SCRIPT`` block and run it,
11
- optionally with parameter bindings and a WHILE / UNTIL loop).
12
9
  - ``x_extendscript`` — ``EXTEND SCRIPT <name> WITH SCRIPT|FILE …``
13
10
  (append additional commands to an existing named script block from
14
11
  an inline source).
15
12
  - ``x_extendscript_metacommand`` — ``EXTEND SCRIPT … WITH METACOMMAND …``.
16
13
  - ``x_extendscript_sql`` — ``EXTEND SCRIPT … WITH SQL …``.
14
+ - ``x_executescript`` — dispatch-table sentinel only. ``EXECUTE SCRIPT``
15
+ / ``RUN SCRIPT`` are handled natively by the AST executor; this stub
16
+ raises ``ErrInfo`` if the parser ever fails to recognize them as
17
+ structural nodes.
17
18
 
18
19
  Registration of ``BEGIN SCRIPT … END SCRIPT`` blocks themselves is
19
20
  handled by the AST parser (block boundaries) and executor (registering
20
- the block on ``ctx.ast_scripts``); this module is only the call-site /
21
- extension handlers.
21
+ the block on ``ctx.ast_scripts``); this module is only the
22
+ extension-site handlers.
22
23
  """
23
24
 
24
25
  import copy
execsql/script/engine.py CHANGED
@@ -67,25 +67,18 @@ class MetaCommand:
67
67
  rx: Any,
68
68
  exec_func: Any,
69
69
  description: str | None = None,
70
- run_in_batch: bool = False,
71
- run_when_false: bool = False,
72
70
  set_error_flag: bool = True,
73
71
  category: str | None = None,
74
72
  ) -> None:
75
73
  self.rx = rx
76
74
  self.exec_fn = exec_func
77
75
  self.description = description
78
- self.run_in_batch = run_in_batch
79
- self.run_when_false = run_when_false
80
76
  self.set_error_flag = set_error_flag
81
77
  self.category = category
82
78
  self.hitcount = 0
83
79
 
84
80
  def __repr__(self) -> str:
85
- return (
86
- f"MetaCommand({self.rx.pattern!r}, {self.exec_fn!r}, {self.description!r}, "
87
- f"{self.run_in_batch!r}, {self.run_when_false!r})"
88
- )
81
+ return f"MetaCommand({self.rx.pattern!r}, {self.exec_fn!r}, {self.description!r})"
89
82
 
90
83
  def run(self, cmd_str: str) -> tuple:
91
84
  """Match *cmd_str* against this entry's regex and, if it matches, invoke the handler.
@@ -171,8 +164,6 @@ class MetaCommandList:
171
164
  matching_regexes: Any,
172
165
  exec_func: Any,
173
166
  description: str | None = None,
174
- run_in_batch: bool = False,
175
- run_when_false: bool = False,
176
167
  set_error_flag: bool = True,
177
168
  category: str | None = None,
178
169
  ) -> None:
@@ -193,8 +184,6 @@ class MetaCommandList:
193
184
  rx,
194
185
  exec_func,
195
186
  description,
196
- run_in_batch,
197
- run_when_false,
198
187
  set_error_flag,
199
188
  category,
200
189
  )
@@ -83,10 +83,6 @@ _DEFER_RX = re.compile(r"!\{([$@&~#+]?\w+)\}!")
83
83
  _VARLIKE = re.compile(r"!![$@&~#]?\w+!!", re.I)
84
84
 
85
85
 
86
- # Legacy module-level alias — ``_ast_scripts`` is now ``ctx.ast_scripts``
87
- # on the RuntimeContext. Kept as a comment for grep-ability.
88
-
89
-
90
86
  def _stack_localvars(ctx: RuntimeContext) -> SubVarSet | None:
91
87
  """Build the merged ``~`` local + ``#`` param overlay for the current scope.
92
88
 
@@ -210,7 +206,7 @@ def _set_command_vars(ctx: RuntimeContext, source: str, line_no: int) -> None:
210
206
 
211
207
 
212
208
  # ---------------------------------------------------------------------------
213
- # SQL execution (bypasses SqlStmt.run's if_stack check)
209
+ # SQL execution
214
210
  # ---------------------------------------------------------------------------
215
211
 
216
212
 
@@ -264,7 +260,7 @@ def _exec_sql(
264
260
 
265
261
 
266
262
  # ---------------------------------------------------------------------------
267
- # Metacommand execution (bypasses MetacommandStmt.run's if_stack check)
263
+ # Metacommand execution
268
264
  # ---------------------------------------------------------------------------
269
265
 
270
266