execsql2 2.2.1__py3-none-any.whl → 2.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- execsql/cli/run.py +11 -5
- execsql/config.py +52 -0
- execsql/db/access.py +11 -3
- execsql/db/base.py +180 -135
- execsql/db/dsn.py +4 -0
- execsql/db/duckdb.py +4 -0
- execsql/db/factory.py +31 -5
- execsql/db/firebird.py +4 -0
- execsql/db/mysql.py +18 -1
- execsql/db/oracle.py +4 -0
- execsql/db/postgres.py +3 -0
- execsql/db/sqlite.py +3 -0
- execsql/db/sqlserver.py +11 -2
- execsql/exceptions.py +18 -0
- execsql/exporters/base.py +6 -0
- execsql/exporters/delimited.py +36 -0
- execsql/exporters/duckdb.py +4 -0
- execsql/exporters/feather.py +4 -0
- execsql/exporters/html.py +6 -0
- execsql/exporters/json.py +5 -6
- execsql/exporters/latex.py +4 -0
- execsql/exporters/ods.py +28 -7
- execsql/exporters/parquet.py +3 -0
- execsql/exporters/pretty.py +5 -0
- execsql/exporters/raw.py +5 -3
- execsql/exporters/sqlite.py +4 -0
- execsql/exporters/templates.py +16 -6
- execsql/exporters/values.py +4 -0
- execsql/exporters/xls.py +26 -7
- execsql/exporters/xml.py +3 -0
- execsql/exporters/zip.py +15 -0
- execsql/importers/base.py +5 -3
- execsql/importers/csv.py +7 -5
- execsql/importers/feather.py +6 -4
- execsql/importers/ods.py +2 -0
- execsql/importers/xls.py +2 -0
- execsql/metacommands/__init__.py +177 -1968
- execsql/metacommands/dispatch.py +2011 -0
- execsql/models.py +7 -0
- execsql/parser.py +10 -0
- execsql/script/__init__.py +95 -0
- execsql/script/control.py +162 -0
- execsql/{script.py → script/engine.py} +144 -406
- execsql/script/variables.py +281 -0
- execsql/types.py +29 -0
- execsql/utils/auth.py +2 -0
- execsql/utils/crypto.py +4 -6
- execsql/utils/datetime.py +1 -0
- execsql/utils/errors.py +11 -0
- execsql/utils/fileio.py +18 -0
- execsql/utils/gui.py +46 -0
- execsql/utils/mail.py +7 -17
- execsql/utils/numeric.py +2 -0
- execsql/utils/regex.py +9 -0
- execsql/utils/strings.py +16 -0
- execsql/utils/timer.py +2 -0
- execsql2-2.4.1.data/data/execsql2_extras/README.md +65 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/execsql.conf +1 -1
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/METADATA +8 -1
- execsql2-2.4.1.dist-info/RECORD +108 -0
- execsql2-2.2.1.data/data/execsql2_extras/READ_ME.rst +0 -127
- execsql2-2.2.1.dist-info/RECORD +0 -104
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/WHEEL +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/licenses/NOTICE +0 -0
execsql/models.py
CHANGED
|
@@ -40,6 +40,13 @@ from execsql.types import (
|
|
|
40
40
|
DT_Varchar,
|
|
41
41
|
)
|
|
42
42
|
|
|
43
|
+
__all__ = [
|
|
44
|
+
"Column",
|
|
45
|
+
"DataTable",
|
|
46
|
+
"JsonDatatype",
|
|
47
|
+
"to_json_type",
|
|
48
|
+
]
|
|
49
|
+
|
|
43
50
|
|
|
44
51
|
class Column:
|
|
45
52
|
# Column objects are used to compile information about the data types that a set of data
|
execsql/parser.py
CHANGED
|
@@ -27,6 +27,16 @@ from typing import Any
|
|
|
27
27
|
|
|
28
28
|
from execsql.exceptions import CondParserError, NumericParserError
|
|
29
29
|
|
|
30
|
+
__all__ = [
|
|
31
|
+
"SourceString",
|
|
32
|
+
"CondTokens",
|
|
33
|
+
"NumTokens",
|
|
34
|
+
"CondAstNode",
|
|
35
|
+
"NumericAstNode",
|
|
36
|
+
"CondParser",
|
|
37
|
+
"NumericParser",
|
|
38
|
+
]
|
|
39
|
+
|
|
30
40
|
|
|
31
41
|
class SourceString:
|
|
32
42
|
def __init__(self, source_string: str) -> None:
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Core script-execution engine for execsql.
|
|
5
|
+
|
|
6
|
+
This module contains the data structures and functions that load, parse, and
|
|
7
|
+
drive execution of execsql ``.sql`` script files. It is the heart of the
|
|
8
|
+
runtime.
|
|
9
|
+
|
|
10
|
+
Key classes:
|
|
11
|
+
|
|
12
|
+
- :class:`BatchLevels` — tracks which databases are used in nested BEGIN/END
|
|
13
|
+
BATCH blocks for commit/rollback handling.
|
|
14
|
+
- :class:`IfItem` / :class:`IfLevels` — stack-based IF/ELSE/ENDIF nesting.
|
|
15
|
+
- :class:`CounterVars` — named integer counters (``@NAME``).
|
|
16
|
+
- :class:`SubVarSet` — global ``!!$VAR!!`` substitution-variable store, plus
|
|
17
|
+
``&ENV``, ``@COUNTER``, ``~LOCAL``, and ``#ARG`` prefixes.
|
|
18
|
+
- :class:`LocalSubVarSet` / :class:`ScriptArgSubVarSet` — per-script-scope
|
|
19
|
+
variable overlays.
|
|
20
|
+
- :class:`MetaCommand` — one entry in the metacommand dispatch table (regex +
|
|
21
|
+
handler function + flags).
|
|
22
|
+
- :class:`MetaCommandList` — ordered list of :class:`MetaCommand` entries;
|
|
23
|
+
``get_match()`` finds the first matching entry for a given line.
|
|
24
|
+
- :class:`SqlStmt` — wraps a single SQL string; ``run()`` executes it via the
|
|
25
|
+
active database connection.
|
|
26
|
+
- :class:`MetacommandStmt` — wraps a metacommand line; ``run()`` dispatches
|
|
27
|
+
through :attr:`execsql.state.metacommandlist`.
|
|
28
|
+
- :class:`ScriptCmd` — pairs a statement with its source-file location.
|
|
29
|
+
- :class:`CommandList` — ordered list of :class:`ScriptCmd` objects plus an
|
|
30
|
+
execution cursor; ``run_next()`` drives one step of execution.
|
|
31
|
+
- :class:`CommandListWhileLoop` / :class:`CommandListUntilLoop` — loop
|
|
32
|
+
variants of :class:`CommandList` that re-evaluate a condition each pass.
|
|
33
|
+
- :class:`ScriptFile` — reads and tokenises a ``.sql`` file into
|
|
34
|
+
:class:`ScriptCmd` objects.
|
|
35
|
+
- :class:`ScriptExecSpec` — specification for deferred script execution.
|
|
36
|
+
|
|
37
|
+
Key functions:
|
|
38
|
+
|
|
39
|
+
- :func:`set_system_vars` — populates built-in ``$VARNAME`` system variables.
|
|
40
|
+
- :func:`substitute_vars` — performs ``!!$VAR!!`` and ``!{$var}!`` expansion.
|
|
41
|
+
- :func:`runscripts` — central execution loop; pops the top
|
|
42
|
+
:class:`CommandList` from ``_state.commandliststack`` and drives
|
|
43
|
+
``run_next()`` until the stack is empty.
|
|
44
|
+
- :func:`current_script_line` — returns the source location of the currently
|
|
45
|
+
executing command.
|
|
46
|
+
- :func:`read_sqlfile` — parses a SQL script file into a new
|
|
47
|
+
:class:`CommandList` and pushes it onto ``_state.commandliststack``.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from execsql.script.control import BatchLevels, IfItem, IfLevels
|
|
51
|
+
from execsql.script.engine import (
|
|
52
|
+
CommandList,
|
|
53
|
+
CommandListUntilLoop,
|
|
54
|
+
CommandListWhileLoop,
|
|
55
|
+
MetaCommand,
|
|
56
|
+
MetaCommandList,
|
|
57
|
+
MetacommandStmt,
|
|
58
|
+
ScriptCmd,
|
|
59
|
+
ScriptExecSpec,
|
|
60
|
+
ScriptFile,
|
|
61
|
+
SqlStmt,
|
|
62
|
+
current_script_line,
|
|
63
|
+
read_sqlfile,
|
|
64
|
+
read_sqlstring,
|
|
65
|
+
runscripts,
|
|
66
|
+
set_system_vars,
|
|
67
|
+
substitute_vars,
|
|
68
|
+
)
|
|
69
|
+
from execsql.script.variables import CounterVars, LocalSubVarSet, ScriptArgSubVarSet, SubVarSet
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"BatchLevels",
|
|
73
|
+
"IfItem",
|
|
74
|
+
"IfLevels",
|
|
75
|
+
"CounterVars",
|
|
76
|
+
"SubVarSet",
|
|
77
|
+
"LocalSubVarSet",
|
|
78
|
+
"ScriptArgSubVarSet",
|
|
79
|
+
"MetaCommand",
|
|
80
|
+
"MetaCommandList",
|
|
81
|
+
"SqlStmt",
|
|
82
|
+
"MetacommandStmt",
|
|
83
|
+
"ScriptCmd",
|
|
84
|
+
"CommandList",
|
|
85
|
+
"CommandListWhileLoop",
|
|
86
|
+
"CommandListUntilLoop",
|
|
87
|
+
"ScriptFile",
|
|
88
|
+
"ScriptExecSpec",
|
|
89
|
+
"set_system_vars",
|
|
90
|
+
"substitute_vars",
|
|
91
|
+
"runscripts",
|
|
92
|
+
"current_script_line",
|
|
93
|
+
"read_sqlfile",
|
|
94
|
+
"read_sqlstring",
|
|
95
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Control-flow stack structures for execsql script execution.
|
|
4
|
+
|
|
5
|
+
Classes:
|
|
6
|
+
- :class:`BatchLevels` — tracks which databases are used in nested BEGIN/END BATCH blocks.
|
|
7
|
+
- :class:`IfItem` — one level of a nested IF/ELSE/ENDIF condition.
|
|
8
|
+
- :class:`IfLevels` — stack of boolean IF-level states.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from execsql.exceptions import ErrInfo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# BatchLevels
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BatchLevels:
|
|
22
|
+
"""Track the databases used within nested BEGIN/END BATCH blocks.
|
|
23
|
+
|
|
24
|
+
Maintains a stack of :class:`Batch` objects so that each nesting level
|
|
25
|
+
records its own set of active database connections for commit/rollback.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# A stack to keep a record of the databases used in nested batches.
|
|
29
|
+
class Batch:
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
self.dbs_used: list[Any] = []
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self.batchlevels: list[BatchLevels.Batch] = []
|
|
35
|
+
|
|
36
|
+
def in_batch(self) -> bool:
|
|
37
|
+
"""Return True if execution is currently inside at least one BATCH block."""
|
|
38
|
+
return len(self.batchlevels) > 0
|
|
39
|
+
|
|
40
|
+
def new_batch(self) -> None:
|
|
41
|
+
"""Push a new empty batch level onto the stack."""
|
|
42
|
+
self.batchlevels.append(self.Batch())
|
|
43
|
+
|
|
44
|
+
def using_db(self, db: Any) -> None:
|
|
45
|
+
"""Register *db* as used within the innermost active batch."""
|
|
46
|
+
if len(self.batchlevels) > 0 and db not in self.batchlevels[-1].dbs_used:
|
|
47
|
+
self.batchlevels[-1].dbs_used.append(db)
|
|
48
|
+
|
|
49
|
+
def uses_db(self, db: Any) -> bool:
|
|
50
|
+
"""Return True if *db* is registered in any active batch level."""
|
|
51
|
+
if len(self.batchlevels) == 0:
|
|
52
|
+
return False
|
|
53
|
+
return any(db in batch.dbs_used for batch in self.batchlevels)
|
|
54
|
+
|
|
55
|
+
def rollback_batch(self) -> None:
|
|
56
|
+
"""Roll back all databases registered in the innermost batch level."""
|
|
57
|
+
if len(self.batchlevels) > 0:
|
|
58
|
+
b = self.batchlevels[-1]
|
|
59
|
+
for db in b.dbs_used:
|
|
60
|
+
db.rollback()
|
|
61
|
+
|
|
62
|
+
def end_batch(self) -> None:
|
|
63
|
+
"""Commit all databases in the innermost batch level and pop the stack."""
|
|
64
|
+
b = self.batchlevels.pop()
|
|
65
|
+
for db in b.dbs_used:
|
|
66
|
+
db.commit()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# IfItem / IfLevels
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class IfItem:
|
|
75
|
+
"""One level of a nested IF/ELSE/ENDIF condition, paired with its source location."""
|
|
76
|
+
|
|
77
|
+
# An object representing an 'if' level, with context data.
|
|
78
|
+
def __init__(self, tf_value: bool) -> None:
|
|
79
|
+
self.tf_value = tf_value
|
|
80
|
+
# Import from the package (not engine directly) so that test patches on
|
|
81
|
+
# execsql.script.current_script_line are effective.
|
|
82
|
+
from execsql.script import current_script_line
|
|
83
|
+
|
|
84
|
+
self.scriptname, self.scriptline = current_script_line()
|
|
85
|
+
|
|
86
|
+
def value(self) -> bool:
|
|
87
|
+
return self.tf_value
|
|
88
|
+
|
|
89
|
+
def invert(self) -> None:
|
|
90
|
+
self.tf_value = not self.tf_value
|
|
91
|
+
|
|
92
|
+
def change_to(self, tf_value: bool) -> None:
|
|
93
|
+
self.tf_value = tf_value
|
|
94
|
+
|
|
95
|
+
def script_line(self) -> tuple:
|
|
96
|
+
return (self.scriptname, self.scriptline)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class IfLevels:
|
|
100
|
+
"""Stack of boolean IF-level states for nested conditional execution.
|
|
101
|
+
|
|
102
|
+
Each :meth:`nest` call corresponds to an IF statement; each
|
|
103
|
+
:meth:`unnest` call corresponds to an ENDIF. :meth:`all_true` drives
|
|
104
|
+
the execution gate — commands are skipped unless every level is ``True``.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
# A stack of True/False values corresponding to a nested set of conditionals,
|
|
108
|
+
# with methods to manipulate and query the set of conditional states.
|
|
109
|
+
def __init__(self) -> None:
|
|
110
|
+
self.if_levels: list[IfItem] = []
|
|
111
|
+
|
|
112
|
+
def nest(self, tf_value: bool) -> None:
|
|
113
|
+
"""Push a new IF level onto the stack with the given boolean value."""
|
|
114
|
+
self.if_levels.append(IfItem(tf_value))
|
|
115
|
+
|
|
116
|
+
def unnest(self) -> None:
|
|
117
|
+
"""Pop the innermost IF level; raise ErrInfo if the stack is empty."""
|
|
118
|
+
if len(self.if_levels) == 0:
|
|
119
|
+
raise ErrInfo(type="error", other_msg="Can't exit an IF block; no IF block is active.")
|
|
120
|
+
else:
|
|
121
|
+
self.if_levels.pop()
|
|
122
|
+
|
|
123
|
+
def invert(self) -> None:
|
|
124
|
+
if len(self.if_levels) == 0:
|
|
125
|
+
raise ErrInfo(type="error", other_msg="Can't change the IF state; no IF block is active.")
|
|
126
|
+
else:
|
|
127
|
+
self.if_levels[-1].invert()
|
|
128
|
+
|
|
129
|
+
def replace(self, tf_value: bool) -> None:
|
|
130
|
+
if len(self.if_levels) == 0:
|
|
131
|
+
raise ErrInfo(type="error", other_msg="Can't change the IF state; no IF block is active.")
|
|
132
|
+
else:
|
|
133
|
+
self.if_levels[-1].change_to(tf_value)
|
|
134
|
+
|
|
135
|
+
def current(self) -> bool:
|
|
136
|
+
if len(self.if_levels) == 0:
|
|
137
|
+
raise ErrInfo(type="error", other_msg="No IF block is active.")
|
|
138
|
+
else:
|
|
139
|
+
return self.if_levels[-1].value()
|
|
140
|
+
|
|
141
|
+
def all_true(self) -> bool:
|
|
142
|
+
"""Return True if every active IF level is true (or the stack is empty)."""
|
|
143
|
+
if self.if_levels == []:
|
|
144
|
+
return True
|
|
145
|
+
return all(tf.value() for tf in self.if_levels)
|
|
146
|
+
|
|
147
|
+
def only_current_false(self) -> bool:
|
|
148
|
+
# Returns True if the current if level is false and all higher levels are True.
|
|
149
|
+
if len(self.if_levels) == 0:
|
|
150
|
+
return False
|
|
151
|
+
elif len(self.if_levels) == 1:
|
|
152
|
+
return not self.if_levels[-1].value()
|
|
153
|
+
else:
|
|
154
|
+
return not self.if_levels[-1].value() and all(tf.value() for tf in self.if_levels[:-1])
|
|
155
|
+
|
|
156
|
+
def script_lines(self, top_n: int) -> list[tuple]:
|
|
157
|
+
# Returns a list of tuples containing the script name and line number
|
|
158
|
+
# for the topmost 'top_n' if levels, in bottom-up order.
|
|
159
|
+
if len(self.if_levels) < top_n:
|
|
160
|
+
raise ErrInfo(type="error", other_msg="Invalid IF stack depth reference.")
|
|
161
|
+
levels = self.if_levels[len(self.if_levels) - top_n :]
|
|
162
|
+
return [lvl.script_line() for lvl in levels]
|