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.
- execsql/__init__.py +6 -2
- execsql/api.py +25 -6
- execsql/cli/__init__.py +5 -3
- execsql/cli/lint.py +30 -34
- execsql/cli/run.py +10 -0
- execsql/config.py +145 -92
- execsql/db/access.py +54 -40
- execsql/db/base.py +33 -6
- execsql/db/firebird.py +3 -1
- execsql/db/mysql.py +4 -3
- execsql/db/oracle.py +36 -14
- execsql/db/postgres.py +8 -6
- execsql/db/sqlite.py +5 -2
- execsql/db/sqlserver.py +8 -6
- execsql/debug/repl.py +59 -21
- execsql/exceptions.py +19 -4
- execsql/exporters/base.py +3 -2
- execsql/exporters/delimited.py +2 -3
- execsql/exporters/feather.py +3 -3
- execsql/exporters/ods.py +1 -1
- execsql/exporters/xls.py +12 -4
- execsql/exporters/xlsx.py +1 -1
- execsql/gui/desktop.py +129 -15
- execsql/importers/__init__.py +1 -1
- execsql/importers/ods.py +1 -1
- execsql/importers/xls.py +1 -1
- execsql/metacommands/__init__.py +34 -5
- execsql/metacommands/conditions.py +26 -14
- execsql/metacommands/connect.py +21 -14
- execsql/metacommands/control.py +55 -68
- execsql/metacommands/data.py +25 -9
- execsql/metacommands/debug.py +132 -77
- execsql/metacommands/io_export.py +14 -2
- execsql/metacommands/io_import.py +11 -2
- execsql/metacommands/io_write.py +113 -11
- execsql/metacommands/prompt.py +46 -32
- execsql/metacommands/script_ext.py +63 -34
- execsql/metacommands/system.py +4 -3
- execsql/script/__init__.py +28 -37
- execsql/script/ast.py +7 -7
- execsql/script/control.py +4 -101
- execsql/script/engine.py +37 -251
- execsql/script/executor.py +181 -222
- execsql/script/parser.py +1 -3
- execsql/script/variables.py +8 -3
- execsql/state.py +125 -37
- execsql/utils/errors.py +0 -2
- execsql/utils/fileio.py +47 -3
- execsql/utils/mail.py +3 -2
- execsql/utils/strings.py +5 -5
- {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/METADATA +42 -36
- execsql2-2.17.2.dist-info/RECORD +124 -0
- execsql2-2.17.2.dist-info/licenses/NOTICE +11 -0
- execsql2-2.17.0.dist-info/RECORD +0 -124
- execsql2-2.17.0.dist-info/licenses/NOTICE +0 -10
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.17.0.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/WHEEL +0 -0
- {execsql2-2.17.0.dist-info → execsql2-2.17.2.dist-info}/entry_points.txt +0 -0
- {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
|
|
377
|
-
"""
|
|
378
|
-
|
|
457
|
+
def current_scope() -> ExecFrame | None:
|
|
458
|
+
"""Module-level wrapper for :meth:`RuntimeContext.current_scope`."""
|
|
459
|
+
return _get_ctx().current_scope()
|
|
379
460
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
|
254
|
-
if
|
|
255
|
-
subvarset =
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
| `--
|
|
254
|
-
| `--
|
|
255
|
-
| `--
|
|
256
|
-
| `--
|
|
257
|
-
| `--
|
|
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
|
|