execsql2 2.17.0__py3-none-any.whl → 2.17.2__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 (73) hide show
  1. execsql/__init__.py +6 -2
  2. execsql/api.py +25 -6
  3. execsql/cli/__init__.py +5 -3
  4. execsql/cli/lint.py +30 -34
  5. execsql/cli/run.py +10 -0
  6. execsql/config.py +145 -92
  7. execsql/db/access.py +54 -40
  8. execsql/db/base.py +33 -6
  9. execsql/db/firebird.py +3 -1
  10. execsql/db/mysql.py +4 -3
  11. execsql/db/oracle.py +36 -14
  12. execsql/db/postgres.py +8 -6
  13. execsql/db/sqlite.py +5 -2
  14. execsql/db/sqlserver.py +8 -6
  15. execsql/debug/repl.py +59 -21
  16. execsql/exceptions.py +19 -4
  17. execsql/exporters/base.py +3 -2
  18. execsql/exporters/delimited.py +2 -3
  19. execsql/exporters/feather.py +3 -3
  20. execsql/exporters/ods.py +1 -1
  21. execsql/exporters/xls.py +12 -4
  22. execsql/exporters/xlsx.py +1 -1
  23. execsql/gui/desktop.py +129 -15
  24. execsql/importers/__init__.py +1 -1
  25. execsql/importers/ods.py +1 -1
  26. execsql/importers/xls.py +1 -1
  27. execsql/metacommands/__init__.py +34 -5
  28. execsql/metacommands/conditions.py +26 -14
  29. execsql/metacommands/connect.py +21 -14
  30. execsql/metacommands/control.py +55 -68
  31. execsql/metacommands/data.py +25 -9
  32. execsql/metacommands/debug.py +132 -77
  33. execsql/metacommands/io_export.py +14 -2
  34. execsql/metacommands/io_import.py +11 -2
  35. execsql/metacommands/io_write.py +113 -11
  36. execsql/metacommands/prompt.py +46 -32
  37. execsql/metacommands/script_ext.py +63 -34
  38. execsql/metacommands/system.py +4 -3
  39. execsql/script/__init__.py +28 -37
  40. execsql/script/ast.py +7 -7
  41. execsql/script/control.py +4 -101
  42. execsql/script/engine.py +37 -251
  43. execsql/script/executor.py +181 -222
  44. execsql/script/parser.py +1 -3
  45. execsql/script/variables.py +8 -3
  46. execsql/state.py +125 -37
  47. execsql/utils/errors.py +0 -2
  48. execsql/utils/fileio.py +47 -3
  49. execsql/utils/mail.py +3 -2
  50. execsql/utils/strings.py +5 -5
  51. {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/METADATA +42 -36
  52. execsql2-2.17.2.dist-info/RECORD +124 -0
  53. execsql2-2.17.2.dist-info/licenses/NOTICE +11 -0
  54. execsql2-2.17.0.dist-info/RECORD +0 -124
  55. execsql2-2.17.0.dist-info/licenses/NOTICE +0 -10
  56. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/README.md +0 -0
  57. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  58. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  59. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/execsql.conf +0 -0
  60. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
  61. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_compare.sql +0 -0
  62. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
  63. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
  64. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
  65. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  66. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  67. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/script_template.sql +0 -0
  68. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
  69. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  70. {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  71. {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/WHEEL +0 -0
  72. {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/entry_points.txt +0 -0
  73. {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/licenses/LICENSE.txt +0 -0
execsql/state.py CHANGED
@@ -31,8 +31,62 @@ import re
31
31
  import sys
32
32
  import threading
33
33
  import types
34
+ from dataclasses import dataclass
34
35
  from typing import TYPE_CHECKING, Any
35
36
 
37
+
38
+ @dataclass
39
+ class ExecFrame:
40
+ """One frame on the AST executor's unified execution stack.
41
+
42
+ Every nesting construct the AST executor enters (IF/ELSEIF/ELSE branches,
43
+ LOOP iterations, BATCH blocks, INCLUDE'd files, EXECUTE SCRIPT calls,
44
+ plus the top-level ``<main>`` script) pushes one of these via try/finally
45
+ so the debug REPL ``.stack`` command and ``DEBUG WRITE COMMANDLISTSTACK``
46
+ can show real execution context.
47
+
48
+ Frames with ``kind in ("main", "script")`` are **scope frames** — they
49
+ own a ``LocalSubVarSet`` (``localvars``) for ``~``-prefixed variables and
50
+ a ``ScriptArgSubVarSet`` (``paramvals``) for ``#``-prefixed parameters.
51
+ All other kinds (if/elseif/else/loop_*/batch/include) are non-scope and
52
+ cache a reference to their enclosing scope in ``scope_ref`` so that
53
+ ``current_localvars()`` lookups stay O(1) regardless of nesting depth.
54
+
55
+ Attributes:
56
+ kind: One of ``"main"`` / ``"script"`` / ``"include"`` / ``"if"`` /
57
+ ``"elseif"`` / ``"else"`` / ``"loop_while"`` / ``"loop_until"`` /
58
+ ``"batch"``.
59
+ label: Human-readable summary — condition text for IF, file basename
60
+ for INCLUDE, script name + params for SCRIPT, etc.
61
+ source: Path of the source file the block lives in.
62
+ line: Source line where the block opens, or ``None`` for ``main``.
63
+ iteration: Current 1-based iteration count for LOOP frames; 0
64
+ otherwise.
65
+ params: Bound parameter values for SCRIPT frames (display-only dict
66
+ of ``name -> str(value)``); ``None`` otherwise.
67
+ localvars: ``LocalSubVarSet`` for ``~`` variables; only populated for
68
+ ``main`` / ``script`` frames.
69
+ paramvals: ``ScriptArgSubVarSet`` for ``#`` script parameters; only
70
+ populated for ``script`` frames.
71
+ paramnames: Formal parameter names declared on BEGIN SCRIPT, used for
72
+ display purposes; only populated for ``script`` frames.
73
+ scope_ref: Cached pointer to the enclosing scope frame
74
+ (``main``/``script``) for non-scope frames; ``None`` for scope
75
+ frames themselves.
76
+ """
77
+
78
+ kind: str
79
+ label: str = ""
80
+ source: str = ""
81
+ line: int | None = None
82
+ iteration: int = 0
83
+ params: dict[str, str] | None = None
84
+ localvars: Any = None # LocalSubVarSet — TYPE_CHECKING import would be circular
85
+ paramvals: Any = None # ScriptArgSubVarSet
86
+ paramnames: list[str] | None = None
87
+ scope_ref: ExecFrame | None = None
88
+
89
+
36
90
  if TYPE_CHECKING:
37
91
  import multiprocessing as _mp
38
92
 
@@ -40,9 +94,7 @@ if TYPE_CHECKING:
40
94
  from execsql.db.base import DatabasePool
41
95
  from execsql.exporters.base import ExportMetadata, WriteSpec
42
96
  from execsql.script import (
43
- CommandList,
44
97
  CounterVars,
45
- IfLevels,
46
98
  MetaCommandList,
47
99
  ScriptCmd,
48
100
  ScriptExecSpec,
@@ -66,13 +118,8 @@ __all__ = [
66
118
  "cancel_halt_writespec",
67
119
  "cancel_halt_mailspec",
68
120
  "cancel_halt_exec",
69
- "commandliststack",
70
- "savedscripts",
71
- "loopcommandstack",
72
- "compiling_loop",
73
121
  "endloop_rx",
74
122
  "loop_rx",
75
- "loop_nest_level",
76
123
  "cmds_run",
77
124
  "defer_rx",
78
125
  "stringtypes",
@@ -80,7 +127,6 @@ __all__ = [
80
127
  "subvars",
81
128
  "status",
82
129
  # Lazy singletons
83
- "if_stack",
84
130
  "counters",
85
131
  "timer",
86
132
  "output",
@@ -103,7 +149,6 @@ __all__ = [
103
149
  "tertiary_vno",
104
150
  # Functions
105
151
  "xcmd_test",
106
- "endloop",
107
152
  "reset",
108
153
  "initialize",
109
154
  # New public API
@@ -170,11 +215,6 @@ _CONTEXT_ATTRS: frozenset[str] = frozenset(
170
215
  "cancel_halt_mailspec",
171
216
  "cancel_halt_exec",
172
217
  # Execution stack
173
- "commandliststack",
174
- "savedscripts",
175
- "loopcommandstack",
176
- "compiling_loop",
177
- "loop_nest_level",
178
218
  "cmds_run",
179
219
  # I/O
180
220
  "exec_log",
@@ -183,7 +223,6 @@ _CONTEXT_ATTRS: frozenset[str] = frozenset(
183
223
  "output",
184
224
  "filewriter",
185
225
  # Lazy singletons
186
- "if_stack",
187
226
  "counters",
188
227
  "timer",
189
228
  "dbs",
@@ -202,6 +241,7 @@ _CONTEXT_ATTRS: frozenset[str] = frozenset(
202
241
  # AST executor
203
242
  "ast_scripts",
204
243
  "include_chain",
244
+ "ast_exec_stack",
205
245
  },
206
246
  )
207
247
 
@@ -228,11 +268,6 @@ class RuntimeContext:
228
268
  "cancel_halt_mailspec",
229
269
  "cancel_halt_exec",
230
270
  # Execution stack
231
- "commandliststack",
232
- "savedscripts",
233
- "loopcommandstack",
234
- "compiling_loop",
235
- "loop_nest_level",
236
271
  "cmds_run",
237
272
  # I/O
238
273
  "exec_log",
@@ -241,7 +276,6 @@ class RuntimeContext:
241
276
  "output",
242
277
  "filewriter",
243
278
  # Lazy singletons
244
- "if_stack",
245
279
  "counters",
246
280
  "timer",
247
281
  "dbs",
@@ -260,6 +294,7 @@ class RuntimeContext:
260
294
  # AST executor
261
295
  "ast_scripts",
262
296
  "include_chain",
297
+ "ast_exec_stack",
263
298
  )
264
299
 
265
300
  def __init__(self) -> None:
@@ -278,11 +313,6 @@ class RuntimeContext:
278
313
  self.cancel_halt_exec: ScriptExecSpec | None = None
279
314
 
280
315
  # Execution stack
281
- self.commandliststack: list[CommandList] = []
282
- self.savedscripts: dict[str, CommandList] = {}
283
- self.loopcommandstack: list[CommandList] = []
284
- self.compiling_loop: bool = False
285
- self.loop_nest_level: int = 0
286
316
  self.cmds_run: int = 0
287
317
 
288
318
  # I/O
@@ -293,7 +323,6 @@ class RuntimeContext:
293
323
  self.filewriter: FileWriter | None = None
294
324
 
295
325
  # Lazy singletons
296
- self.if_stack: IfLevels | None = None
297
326
  self.counters: CounterVars | None = None
298
327
  self.timer: Timer | None = None
299
328
  self.dbs: DatabasePool | None = None
@@ -317,6 +346,58 @@ class RuntimeContext:
317
346
  # AST executor — script block registry and include-chain tracking.
318
347
  self.ast_scripts: dict = {}
319
348
  self.include_chain: list[str] = []
349
+ # Unified execution stack maintained by the AST executor for every
350
+ # nesting construct: top-level script, EXECUTE SCRIPT calls, INCLUDE'd
351
+ # files, IF/ELSEIF/ELSE branches, LOOP iterations, BATCH blocks. See
352
+ # :class:`ExecFrame` for frame structure. Read by the debug REPL's
353
+ # ``.stack`` command and ``DEBUG WRITE COMMANDLISTSTACK`` for genuine
354
+ # execution context — the legacy ``commandliststack`` only records
355
+ # SCRIPT call frames and is therefore insufficient for the debugger.
356
+ self.ast_exec_stack: list[ExecFrame] = []
357
+
358
+ # -----------------------------------------------------------------
359
+ # Unified-stack scope accessors
360
+ # -----------------------------------------------------------------
361
+
362
+ def current_scope(self) -> ExecFrame | None:
363
+ """Return the innermost SCRIPT or ``<main>`` frame, or ``None`` if empty.
364
+
365
+ Non-scope frames (if/elseif/else/loop_*/batch/include) cache a
366
+ ``scope_ref`` to their enclosing scope at push time, so this lookup
367
+ is O(1) regardless of nesting depth.
368
+ """
369
+ if not self.ast_exec_stack:
370
+ return None
371
+ top = self.ast_exec_stack[-1]
372
+ if top.kind in ("main", "script"):
373
+ return top
374
+ return top.scope_ref
375
+
376
+ def current_localvars(self) -> Any: # LocalSubVarSet | None
377
+ """Return the current scope's ``~`` variable container, or ``None``."""
378
+ scope = self.current_scope()
379
+ return scope.localvars if scope is not None else None
380
+
381
+ def current_paramvals(self) -> Any: # ScriptArgSubVarSet | None
382
+ """Return the current SCRIPT frame's ``#`` parameter container.
383
+
384
+ Returns ``None`` when the current scope is ``<main>`` (parameters
385
+ only exist for named SCRIPT blocks).
386
+ """
387
+ scope = self.current_scope()
388
+ if scope is None or scope.kind != "script":
389
+ return None
390
+ return scope.paramvals
391
+
392
+ def outer_script_scopes(self) -> list[ExecFrame]:
393
+ """Return the list of enclosing scope frames excluding the innermost.
394
+
395
+ Used by ``utils/strings.get_subvarset`` for ``+``-prefixed outer-scope
396
+ variable lookup — the caller iterates this list in reverse order to
397
+ find the most recently entered outer scope that defines the variable.
398
+ """
399
+ scopes = [f for f in self.ast_exec_stack if f.kind in ("main", "script")]
400
+ return scopes[:-1]
320
401
 
321
402
 
322
403
  # ---------------------------------------------------------------------------
@@ -373,16 +454,24 @@ def xcmd_test(teststr: str) -> bool:
373
454
  raise _exc.ErrInfo(type="cmd", command_text=teststr, other_msg="Unrecognized conditional")
374
455
 
375
456
 
376
- def endloop() -> None:
377
- """Complete the current loop being compiled and push it onto the command stack."""
378
- import execsql.exceptions as _exc
457
+ def current_scope() -> ExecFrame | None:
458
+ """Module-level wrapper for :meth:`RuntimeContext.current_scope`."""
459
+ return _get_ctx().current_scope()
379
460
 
380
- ctx = _get_ctx()
381
- if len(ctx.loopcommandstack) == 0:
382
- raise _exc.ErrInfo("error", other_msg="END LOOP metacommand without a matching preceding LOOP metacommand.")
383
- ctx.compiling_loop = False
384
- ctx.commandliststack.append(ctx.loopcommandstack[-1])
385
- ctx.loopcommandstack.pop()
461
+
462
+ def current_localvars() -> Any:
463
+ """Module-level wrapper for :meth:`RuntimeContext.current_localvars`."""
464
+ return _get_ctx().current_localvars()
465
+
466
+
467
+ def current_paramvals() -> Any:
468
+ """Module-level wrapper for :meth:`RuntimeContext.current_paramvals`."""
469
+ return _get_ctx().current_paramvals()
470
+
471
+
472
+ def outer_script_scopes() -> list[ExecFrame]:
473
+ """Module-level wrapper for :meth:`RuntimeContext.outer_script_scopes`."""
474
+ return _get_ctx().outer_script_scopes()
386
475
 
387
476
 
388
477
  # ---------------------------------------------------------------------------
@@ -502,7 +591,6 @@ def initialize(
502
591
 
503
592
  ctx = _get_ctx()
504
593
  ctx.conf = config
505
- ctx.if_stack = _script.IfLevels()
506
594
  ctx.counters = _script.CounterVars()
507
595
  ctx.timer = _timer_mod.Timer()
508
596
  ctx.dbs = _db_base.DatabasePool()
execsql/utils/errors.py CHANGED
@@ -164,7 +164,6 @@ def exit_now(exit_status: int, errinfo: ErrInfo | None, logmsg: str | None = Non
164
164
  if errinfo is not None and _state.err_halt_exec is not None:
165
165
  errexec = _state.err_halt_exec
166
166
  _state.err_halt_exec = None
167
- _state.commandliststack = []
168
167
  _run_deferred_script(errexec)
169
168
  if exit_status == 2 and _state.cancel_halt_mailspec is not None:
170
169
  try:
@@ -175,7 +174,6 @@ def exit_now(exit_status: int, errinfo: ErrInfo | None, logmsg: str | None = Non
175
174
  if exit_status == 2 and _state.cancel_halt_exec is not None:
176
175
  cancelexec = _state.cancel_halt_exec
177
176
  _state.cancel_halt_exec = None
178
- _state.commandliststack = []
179
177
  _run_deferred_script(cancelexec)
180
178
  if exit_status > 0 and _state.exec_log:
181
179
  if logmsg:
execsql/utils/fileio.py CHANGED
@@ -311,51 +311,95 @@ fw_input: multiprocessing.Queue = multiprocessing.Queue()
311
311
  fw_output: multiprocessing.Queue = multiprocessing.Queue()
312
312
 
313
313
 
314
+ def _writer_alive() -> bool:
315
+ """True if the FileWriter subprocess is running and consuming fw_input.
316
+
317
+ Every entry-point that puts a command on ``fw_input`` should guard on this:
318
+ if the subprocess isn't running (test contexts that bypass
319
+ ``_state.initialize()``, or a subprocess that crashed), unbounded ``put()``
320
+ calls eventually fill the OS pipe buffer (smaller on macOS than Linux) and
321
+ deadlock the caller. ``fw_output.get()`` calls would block forever on a
322
+ dead writer for the same reason.
323
+ """
324
+ return filewriter is not None and filewriter.is_alive()
325
+
326
+
314
327
  def filewriter_filestatus(filename: str) -> int:
328
+ if not _writer_alive():
329
+ return FileWriter.FileControl.STATUS_CLOSED
315
330
  fw_input.put((FileWriter.CMD_GET_STATUS, (filename,)))
316
331
  return fw_output.get()
317
332
 
318
333
 
319
334
  def filewriter_write(filename: str, message: str) -> None:
335
+ if not _writer_alive():
336
+ return
320
337
  fw_input.put((FileWriter.CMD_WRITE, (filename, message)))
321
338
 
322
339
 
323
340
  def filewriter_open_as_new(filename: str) -> None:
324
341
  # FileWriter opens files in append mode ("a") by default. This ensures that it
325
342
  # will be opened in write mode ("w") instead. If the file is open, it will be closed.
343
+ if not _writer_alive():
344
+ return
326
345
  fw_input.put((FileWriter.CMD_OPEN_AS_NEW, (filename,)))
327
346
 
328
347
 
329
348
  def filewriter_close(filename: str) -> None:
330
349
  # This is intended to be used by the main process to ensure that a file
331
350
  # is closed before that process writes to it.
351
+ if not _writer_alive():
352
+ return
332
353
  fw_input.put((FileWriter.CMD_CLOSE_IF_OPEN, (filename,)))
333
354
  while filewriter_filestatus(filename) == FileWriter.FileControl.STATUS_OPEN:
334
355
  time.sleep(0.05)
335
356
 
336
357
 
337
358
  def filewriter_close_all_after_write() -> None:
359
+ if not _writer_alive():
360
+ return
338
361
  fw_input.put((FileWriter.CMD_CLOSE_ALL_AFTER_WRITE, ()))
339
362
  all_closed = False
340
363
  while not all_closed:
364
+ # Re-check liveness on every iteration: if the subprocess dies
365
+ # mid-loop, fw_output.get() would block forever otherwise.
366
+ if not _writer_alive():
367
+ return
341
368
  fw_input.put((FileWriter.CMD_CLOSED_STATUS, ()))
342
- close_status = fw_output.get()
369
+ try:
370
+ close_status = fw_output.get(timeout=2.0)
371
+ except queue.Empty:
372
+ # Either the writer is too slow or it died after the alive
373
+ # check above — recheck on the next iteration.
374
+ continue
343
375
  all_closed = close_status == FileWriter.FileControl.STATUS_CLOSED
344
376
  time.sleep(0.05)
345
377
 
346
378
 
347
379
  def filewriter_closeall() -> None:
380
+ if not _writer_alive():
381
+ return
348
382
  fw_input.put((FileWriter.CMD_CLOSE_ALL, ()))
349
383
 
350
384
 
351
385
  def filewriter_shutdown() -> None:
386
+ if not _writer_alive():
387
+ return
352
388
  fw_input.put((FileWriter.CMD_SHUTDOWN, ()))
353
389
 
354
390
 
355
391
  def filewriter_end() -> None:
392
+ # join() with no timeout blocks forever if the subprocess is stuck;
393
+ # cap it so atexit handlers can't wedge Python shutdown.
394
+ if filewriter is None:
395
+ return
356
396
  try:
357
- filewriter_shutdown()
358
- filewriter.join()
397
+ if filewriter.is_alive():
398
+ filewriter_shutdown()
399
+ filewriter.join(timeout=5.0)
400
+ if filewriter.is_alive():
401
+ filewriter.terminate()
402
+ filewriter.join(timeout=2.0)
359
403
  except Exception:
360
404
  pass # Best-effort cleanup at interpreter shutdown.
361
405
 
execsql/utils/mail.py CHANGED
@@ -138,8 +138,9 @@ class MailSpec:
138
138
  def _expand(text: str) -> str:
139
139
  """Expand local and global substitution variables in *text*."""
140
140
  result = text
141
- if _state.commandliststack:
142
- result, _ = _state.commandliststack[-1].localvars.substitute_all(result)
141
+ localvars = _state.current_localvars()
142
+ if localvars is not None:
143
+ result, _ = localvars.substitute_all(result)
143
144
  result, _ = _state.subvars.substitute_all(result)
144
145
  return result
145
146
 
execsql/utils/strings.py CHANGED
@@ -250,11 +250,11 @@ def get_subvarset(varname: str, metacommandline: str) -> tuple:
250
250
  # Outer scope variable
251
251
  if varname[0] == "+":
252
252
  varname = re.sub("^[+]", "~", varname)
253
- for cl in reversed(_state.commandliststack[0:-1]):
254
- if cl.localvars.sub_exists(varname):
255
- subvarset = cl.localvars
253
+ for frame in reversed(_state.outer_script_scopes()):
254
+ if frame.localvars is not None and frame.localvars.sub_exists(varname):
255
+ subvarset = frame.localvars
256
256
  break
257
- # Raise error if local variable not found anywhere down in commandliststack
257
+ # Raise error if local variable not found anywhere in the enclosing scopes
258
258
  if not subvarset:
259
259
  raise ErrInfo(
260
260
  type="cmd",
@@ -266,5 +266,5 @@ def get_subvarset(varname: str, metacommandline: str) -> tuple:
266
266
  )
267
267
  # Global or local variable
268
268
  else:
269
- subvarset = _state.subvars if varname[0] != "~" else _state.commandliststack[-1].localvars
269
+ subvarset = _state.subvars if varname[0] != "~" else _state.current_localvars()
270
270
  return subvarset, varname
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.17.0
3
+ Version: 2.17.2
4
4
  Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
5
5
  Project-URL: Homepage, https://execsql2.readthedocs.io
6
6
  Project-URL: Repository, https://github.com/geocoug/execsql
@@ -59,6 +59,7 @@ Requires-Dist: pymysql; extra == 'all'
59
59
  Requires-Dist: pyodbc; extra == 'all'
60
60
  Requires-Dist: pyyaml; extra == 'all'
61
61
  Requires-Dist: tables; extra == 'all'
62
+ Requires-Dist: tkintermapview>=1.29; extra == 'all'
62
63
  Requires-Dist: xlrd; extra == 'all'
63
64
  Provides-Extra: all-db
64
65
  Requires-Dist: duckdb; extra == 'all-db'
@@ -106,6 +107,8 @@ Requires-Dist: polars; extra == 'formats'
106
107
  Requires-Dist: pyyaml; extra == 'formats'
107
108
  Requires-Dist: tables; extra == 'formats'
108
109
  Requires-Dist: xlrd; extra == 'formats'
110
+ Provides-Extra: map
111
+ Requires-Dist: tkintermapview>=1.29; extra == 'map'
109
112
  Provides-Extra: mssql
110
113
  Requires-Dist: pyodbc; extra == 'mssql'
111
114
  Provides-Extra: mysql
@@ -220,41 +223,44 @@ execsql script.sql # read connection from config file
220
223
 
221
224
  ## Options
222
225
 
223
- | Flag | Description |
224
- | ------------------------------------- | ---------------------------------------------------------------- |
225
- | `-t {p,m,s,l,k,a,f,o,d}` | Database type |
226
- | `-u USER` | Database username |
227
- | `-p PORT` | Server port |
228
- | `-a VALUE` | Set substitution variable `$ARG_x` |
229
- | `-b` / `--boolean-int` | Treat integers 0 and 1 as boolean values |
230
- | `-c SCRIPT` | Execute inline SQL or metacommand string |
231
- | `-d` | Auto-create export directories |
232
- | `-e ENCODING` / `--database-encoding` | Character encoding used in the database |
233
- | `-f ENCODING` | Script file encoding (default: UTF-8) |
234
- | `-g ENCODING` / `--output-encoding` | Encoding for WRITE and EXPORT output |
235
- | `-i ENCODING` / `--import-encoding` | Encoding for data files used with IMPORT |
236
- | `-l` | Write run log to `~/execsql.log` |
237
- | `-m` | List metacommands and exit |
238
- | `-n` | Create a new SQLite or PostgreSQL database if it does not exist |
239
- | `-o` / `--online-help` | Open the online documentation in the default browser |
240
- | `-s N` / `--scan-lines` | Lines to scan for IMPORT format detection (0 = scan entire file) |
241
- | `-v {0,1,2,3}` | GUI level (0=none, 1=password, 2=selection, 3=full) |
242
- | `-w` | Skip password prompt when a username is supplied |
243
- | `-y` / `--encodings` | List available encoding names and exit |
244
- | `-z KB` / `--import-buffer` | Import buffer size in KB (default: 32) |
245
- | `--dsn URL` | Connection string (e.g. `postgresql://user:pass@host/db`) |
246
- | `--output-dir DIR` | Default base directory for EXPORT output files |
247
- | `--dry-run` | Parse the script and report commands without executing |
248
- | `--lint` | Static analysis: check structure and warn on issues (no DB) |
249
- | `--parse-tree` | Print the script's AST structure and exit (no DB) |
250
- | `--list-plugins` | List discovered plugins and exit |
251
- | `--ping` | Test database connectivity and exit |
252
- | `--profile` | Show per-statement timing summary after execution |
253
- | `--progress` | Show a progress bar for long-running IMPORT operations |
254
- | `--config FILE` | Load an explicit config file (highest priority after CLI args) |
255
- | `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
256
- | `--dump-keywords` | Print metacommand keywords as JSON and exit |
257
- | `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
226
+ | Flag | Description |
227
+ | ------------------------------------- | ----------------------------------------------------------------- |
228
+ | `-t {p,m,s,l,k,a,f,o,d}` | Database type |
229
+ | `-u USER` | Database username |
230
+ | `-p PORT` | Server port |
231
+ | `-a VALUE` | Set substitution variable `$ARG_x` |
232
+ | `-b` / `--boolean-int` | Treat integers 0 and 1 as boolean values |
233
+ | `-c SCRIPT` | Execute inline SQL or metacommand string |
234
+ | `-d` | Auto-create export directories |
235
+ | `-e ENCODING` / `--database-encoding` | Character encoding used in the database |
236
+ | `-f ENCODING` | Script file encoding (default: UTF-8) |
237
+ | `-g ENCODING` / `--output-encoding` | Encoding for WRITE and EXPORT output |
238
+ | `-i ENCODING` / `--import-encoding` | Encoding for data files used with IMPORT |
239
+ | `-l` | Write run log to `~/execsql.log` |
240
+ | `-m` | List metacommands and exit |
241
+ | `-n` | Create a new SQLite or PostgreSQL database if it does not exist |
242
+ | `-o` / `--online-help` | Open the online documentation in the default browser |
243
+ | `-s N` / `--scan-lines` | Lines to scan for IMPORT format detection (0 = scan entire file) |
244
+ | `-v {0,1,2,3}` | GUI level (0=none, 1=password, 2=selection, 3=full) |
245
+ | `-w` | Skip password prompt when a username is supplied |
246
+ | `-y` / `--encodings` | List available encoding names and exit |
247
+ | `-z KB` / `--import-buffer` | Import buffer size in KB (default: 32) |
248
+ | `--dsn URL` | Connection string (e.g. `postgresql://user:pass@host/db`) |
249
+ | `--output-dir DIR` | Default base directory for EXPORT output files |
250
+ | `--dry-run` | Parse the script and report commands without executing |
251
+ | `--lint` | Static analysis: check structure and warn on issues (no DB) |
252
+ | `--parse-tree` | Print the script's AST structure and exit (no DB) |
253
+ | `--list-plugins` | List discovered plugins and exit |
254
+ | `--ping` | Test database connectivity and exit |
255
+ | `--profile` | Show per-statement timing summary after execution |
256
+ | `--profile-limit N` | Top N statements to display in `--profile` summary (default: 20) |
257
+ | `--progress` | Show a progress bar for long-running IMPORT operations |
258
+ | `--config FILE` | Load an explicit config file (highest priority after CLI args) |
259
+ | `--no-system-cmd` | Disable the `SYSTEM_CMD` metacommand (safer for CI / shared envs) |
260
+ | `--init-config` | Print a default `execsql.conf` template to stdout and exit |
261
+ | `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
262
+ | `--dump-keywords` | Print metacommand keywords as JSON and exit |
263
+ | `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
258
264
 
259
265
  Run `execsql --help` for the full option list, or `execsql -m` to list all metacommands.
260
266