execsql2 2.16.18__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/metacommands/upsert.py +0 -29
- 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 +193 -230
- 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.16.18.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.16.18.dist-info/RECORD +0 -124
- execsql2-2.16.18.dist-info/licenses/NOTICE +0 -10
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.16.18.dist-info → execsql2-2.17.2.dist-info}/WHEEL +0 -0
- {execsql2-2.16.18.dist-info → execsql2-2.17.2.dist-info}/entry_points.txt +0 -0
- {execsql2-2.16.18.dist-info → execsql2-2.17.2.dist-info}/licenses/LICENSE.txt +0 -0
execsql/db/base.py
CHANGED
|
@@ -46,7 +46,26 @@ def _default_dt_cast() -> dict[type, Callable]:
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
class Database(ABC):
|
|
49
|
-
"""Abstract base class for
|
|
49
|
+
"""Abstract base class for every DBMS adapter.
|
|
50
|
+
|
|
51
|
+
Concrete adapters in sibling modules (``postgres.py``, ``sqlite.py``,
|
|
52
|
+
``duckdb.py``, etc.) subclass ``Database`` and override the two
|
|
53
|
+
abstract methods (:meth:`open_db`, :meth:`exec_cmd`) plus any
|
|
54
|
+
introspection or data-handling method whose default ANSI
|
|
55
|
+
``information_schema`` implementation does not work for the target
|
|
56
|
+
DBMS (``schema_exists``, ``table_exists``, ``column_exists``,
|
|
57
|
+
``table_columns``, ``view_exists``, ``role_exists``, ``drop_table``,
|
|
58
|
+
``populate_table``).
|
|
59
|
+
|
|
60
|
+
The base class provides shared implementations of :meth:`execute`,
|
|
61
|
+
:meth:`select_data`, :meth:`select_rowsource`, :meth:`select_rowdict`,
|
|
62
|
+
:meth:`commit`, :meth:`rollback`, :meth:`quote_identifier`, and
|
|
63
|
+
:meth:`paramsubs` that work for any DB-API 2.0 driver.
|
|
64
|
+
|
|
65
|
+
Adapter instances are owned by :class:`DatabasePool` (accessed at
|
|
66
|
+
runtime via ``_state.dbs``); metacommand handlers call
|
|
67
|
+
``_state.dbs.current()`` rather than constructing adapters directly.
|
|
68
|
+
"""
|
|
50
69
|
|
|
51
70
|
_dt_cast: dict[type, Callable] | None = None
|
|
52
71
|
|
|
@@ -125,7 +144,7 @@ class Database(ABC):
|
|
|
125
144
|
def close(self) -> None:
|
|
126
145
|
"""Close the database connection, logging a warning if autocommit is off."""
|
|
127
146
|
if self.conn:
|
|
128
|
-
if not self.autocommit:
|
|
147
|
+
if not self.autocommit and _state.exec_log is not None:
|
|
129
148
|
_state.exec_log.log_status_info(
|
|
130
149
|
f"Closing {self.name()} when AUTOCOMMIT is OFF; transactions may not have completed.",
|
|
131
150
|
)
|
|
@@ -412,7 +431,14 @@ class Database(ABC):
|
|
|
412
431
|
return len(rows) > 0
|
|
413
432
|
|
|
414
433
|
def role_exists(self, rolename: str) -> bool:
|
|
415
|
-
"""Return ``True`` if *rolename* exists
|
|
434
|
+
"""Return ``True`` if *rolename* exists in this database.
|
|
435
|
+
|
|
436
|
+
The default implementation raises :class:`~execsql.exceptions.DatabaseNotImplementedError`;
|
|
437
|
+
adapters for DBMSes that have a concept of roles (PostgreSQL,
|
|
438
|
+
SQL Server, etc.) override this to query the appropriate
|
|
439
|
+
catalog. Calling ``ROLE_EXISTS()`` from a script against a DBMS
|
|
440
|
+
without role support will surface the raised error.
|
|
441
|
+
"""
|
|
416
442
|
from execsql.exceptions import DatabaseNotImplementedError
|
|
417
443
|
|
|
418
444
|
raise DatabaseNotImplementedError(self.name(), "role_exists")
|
|
@@ -682,9 +708,10 @@ class DatabasePool:
|
|
|
682
708
|
type="error",
|
|
683
709
|
other_msg="You may not reassign the alias of a database that is currently used in a batch.",
|
|
684
710
|
)
|
|
685
|
-
_state.exec_log
|
|
686
|
-
|
|
687
|
-
|
|
711
|
+
if _state.exec_log is not None:
|
|
712
|
+
_state.exec_log.log_status_info(
|
|
713
|
+
f"Reassigning database alias '{db_alias}' from {self.pool[db_alias].name()} to {db_obj.name()}.",
|
|
714
|
+
)
|
|
688
715
|
self.pool[db_alias].close()
|
|
689
716
|
self.pool[db_alias] = db_obj
|
|
690
717
|
# Refresh static system vars so $DB_NAME, $DB_USER, etc. reflect the new connection.
|
execsql/db/firebird.py
CHANGED
|
@@ -191,10 +191,12 @@ class FirebirdDatabase(Database):
|
|
|
191
191
|
|
|
192
192
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
193
193
|
"""Return True if the named view exists in the Firebird database."""
|
|
194
|
+
# Firebird folds unquoted identifiers to uppercase in the system catalog;
|
|
195
|
+
# match the case of the table_exists() handling above.
|
|
194
196
|
sql = "select distinct rdb$view_name from rdb$view_relations where rdb$view_name = ?;"
|
|
195
197
|
with self._cursor() as curs:
|
|
196
198
|
try:
|
|
197
|
-
curs.execute(sql, (view_name,))
|
|
199
|
+
curs.execute(sql, (view_name.upper(),))
|
|
198
200
|
except ErrInfo:
|
|
199
201
|
raise
|
|
200
202
|
except Exception as e:
|
execsql/db/mysql.py
CHANGED
|
@@ -220,9 +220,10 @@ class MySQLDatabase(Database):
|
|
|
220
220
|
import_sql = f"{import_sql} optionally enclosed by '{safe_quote}'"
|
|
221
221
|
import_sql = f"{import_sql} ignore {1 + csv_file_obj.junk_header_lines} lines"
|
|
222
222
|
import_sql = f"{import_sql} ({input_col_list});"
|
|
223
|
-
_state.exec_log
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
if _state.exec_log is not None:
|
|
224
|
+
_state.exec_log.log_status_info(
|
|
225
|
+
f"IMPORTing {csv_file_obj.csvfname} using the DBMS' fast file reading routine",
|
|
226
|
+
)
|
|
226
227
|
self.execute(import_sql)
|
|
227
228
|
else:
|
|
228
229
|
data_indexes = [csv_file_cols.index(col) for col in import_cols]
|
execsql/db/oracle.py
CHANGED
|
@@ -142,11 +142,13 @@ class OracleDatabase(Database):
|
|
|
142
142
|
|
|
143
143
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
144
144
|
"""Return True if the named table exists in the Oracle database."""
|
|
145
|
-
|
|
145
|
+
# Oracle folds unquoted identifiers to uppercase; normalise so a lookup
|
|
146
|
+
# of "mytable" finds the catalog entry for MYTABLE.
|
|
147
|
+
params = {"tname": table_name.upper()}
|
|
146
148
|
owner_clause = ""
|
|
147
149
|
if schema_name:
|
|
148
150
|
owner_clause = " and owner = :owner"
|
|
149
|
-
params["owner"] = schema_name
|
|
151
|
+
params["owner"] = schema_name.upper()
|
|
150
152
|
sql = f"select table_name from sys.all_tables where table_name = :tname{owner_clause}"
|
|
151
153
|
with self._cursor() as curs:
|
|
152
154
|
try:
|
|
@@ -171,11 +173,12 @@ class OracleDatabase(Database):
|
|
|
171
173
|
schema_name: str | None = None,
|
|
172
174
|
) -> bool:
|
|
173
175
|
"""Return True if the named column exists in the given Oracle table."""
|
|
174
|
-
|
|
176
|
+
# Oracle folds unquoted identifiers to uppercase.
|
|
177
|
+
params = {"tname": table_name.upper(), "cname": column_name.upper()}
|
|
175
178
|
owner_clause = ""
|
|
176
179
|
if schema_name:
|
|
177
180
|
owner_clause = " and owner = :owner"
|
|
178
|
-
params["owner"] = schema_name
|
|
181
|
+
params["owner"] = schema_name.upper()
|
|
179
182
|
sql = f"select column_name from all_tab_columns where table_name=:tname{owner_clause} and column_name=:cname"
|
|
180
183
|
with self._cursor() as curs:
|
|
181
184
|
try:
|
|
@@ -195,11 +198,12 @@ class OracleDatabase(Database):
|
|
|
195
198
|
|
|
196
199
|
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
197
200
|
"""Return a list of column names for the given Oracle table."""
|
|
198
|
-
|
|
201
|
+
# Oracle folds unquoted identifiers to uppercase.
|
|
202
|
+
params = {"tname": table_name.upper()}
|
|
199
203
|
owner_clause = ""
|
|
200
204
|
if schema_name:
|
|
201
205
|
owner_clause = " and owner=:owner"
|
|
202
|
-
params["owner"] = schema_name
|
|
206
|
+
params["owner"] = schema_name.upper()
|
|
203
207
|
sql = f"select column_name from all_tab_columns where table_name=:tname{owner_clause} order by column_id"
|
|
204
208
|
with self._cursor() as curs:
|
|
205
209
|
try:
|
|
@@ -219,11 +223,12 @@ class OracleDatabase(Database):
|
|
|
219
223
|
|
|
220
224
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
221
225
|
"""Return True if the named view exists in the Oracle database."""
|
|
222
|
-
|
|
226
|
+
# Oracle folds unquoted identifiers to uppercase.
|
|
227
|
+
params = {"vname": view_name.upper()}
|
|
223
228
|
owner_clause = ""
|
|
224
229
|
if schema_name:
|
|
225
230
|
owner_clause = " and owner = :owner"
|
|
226
|
-
params["owner"] = schema_name
|
|
231
|
+
params["owner"] = schema_name.upper()
|
|
227
232
|
sql = f"select view_name from sys.all_views where view_name = :vname{owner_clause}"
|
|
228
233
|
with self._cursor() as curs:
|
|
229
234
|
try:
|
|
@@ -242,13 +247,30 @@ class OracleDatabase(Database):
|
|
|
242
247
|
return len(rows) > 0
|
|
243
248
|
|
|
244
249
|
def role_exists(self, rolename: str) -> bool:
|
|
245
|
-
"""Return True if the named role or user exists in the Oracle database.
|
|
250
|
+
"""Return True if the named role or user exists in the Oracle database.
|
|
251
|
+
|
|
252
|
+
``dba_roles`` is restricted to DBA accounts and would raise
|
|
253
|
+
``ORA-00942: table or view does not exist`` for non-DBA users; we
|
|
254
|
+
try it first and fall back to ``session_roles`` (always readable
|
|
255
|
+
for the current session) if the catalog isn't accessible.
|
|
256
|
+
"""
|
|
257
|
+
# Oracle folds unquoted identifiers to uppercase in the catalog.
|
|
258
|
+
params = {"rname": rolename.upper()}
|
|
246
259
|
with self._cursor() as curs:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
260
|
+
try:
|
|
261
|
+
curs.execute(
|
|
262
|
+
"select role from dba_roles where role = :rname union "
|
|
263
|
+
" select username from all_users where username = :rname",
|
|
264
|
+
params,
|
|
265
|
+
)
|
|
266
|
+
except Exception:
|
|
267
|
+
# Non-DBA fallback: enumerate session roles + users we can see.
|
|
268
|
+
self.rollback()
|
|
269
|
+
curs.execute(
|
|
270
|
+
"select role from session_roles where role = :rname union "
|
|
271
|
+
" select username from all_users where username = :rname",
|
|
272
|
+
params,
|
|
273
|
+
)
|
|
252
274
|
rows = curs.fetchall()
|
|
253
275
|
return len(rows) > 0
|
|
254
276
|
|
execsql/db/postgres.py
CHANGED
|
@@ -3,9 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
"""
|
|
4
4
|
PostgreSQL database adapter for execsql.
|
|
5
5
|
|
|
6
|
-
Implements :class:`PostgresDatabase
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
Implements :class:`PostgresDatabase`. Uses ``psycopg2`` for the
|
|
7
|
+
connection, supports schema-qualified tables, server-side ``COPY`` for
|
|
8
|
+
fast IMPORT, ``CREATE DATABASE`` when ``new_db=True``, ``ROLE_EXISTS``,
|
|
9
|
+
and the ``PG_VACUUM`` metacommand (``vacuum()`` method). Corresponds to
|
|
9
10
|
``-t p`` on the CLI.
|
|
10
11
|
"""
|
|
11
12
|
|
|
@@ -339,9 +340,10 @@ class PostgresDatabase(Database):
|
|
|
339
340
|
safe_quote = csv_file_obj.quotechar.replace("'", "''")
|
|
340
341
|
copy_cmd = copy_cmd + f", quote '{safe_quote}'"
|
|
341
342
|
copy_cmd = copy_cmd + ")"
|
|
342
|
-
_state.exec_log
|
|
343
|
-
|
|
344
|
-
|
|
343
|
+
if _state.exec_log is not None:
|
|
344
|
+
_state.exec_log.log_status_info(
|
|
345
|
+
f"IMPORTing {csv_file_obj.csvfname} using Postgres' fast file reading routine",
|
|
346
|
+
)
|
|
345
347
|
with self._cursor() as curs:
|
|
346
348
|
try:
|
|
347
349
|
curs.copy_expert(copy_cmd, rf, _state.conf.import_buffer)
|
execsql/db/sqlite.py
CHANGED
|
@@ -82,8 +82,10 @@ class SQLiteDatabase(Database):
|
|
|
82
82
|
|
|
83
83
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
84
84
|
"""Return True if the named table exists in the SQLite database."""
|
|
85
|
+
# SQLite stores names as-created; match case-insensitively so a lookup
|
|
86
|
+
# of "mytable" finds the catalog entry for MyTable.
|
|
85
87
|
with self._cursor() as curs:
|
|
86
|
-
sql = "select name from sqlite_master where type='table' and name
|
|
88
|
+
sql = "select name from sqlite_master where type='table' and lower(name)=lower(?);"
|
|
87
89
|
try:
|
|
88
90
|
curs.execute(sql, (table_name,))
|
|
89
91
|
except ErrInfo:
|
|
@@ -130,8 +132,9 @@ class SQLiteDatabase(Database):
|
|
|
130
132
|
|
|
131
133
|
def view_exists(self, view_name: str) -> bool:
|
|
132
134
|
"""Return True if the named view exists in the SQLite database."""
|
|
135
|
+
# Match case-insensitively — see table_exists().
|
|
133
136
|
with self._cursor() as curs:
|
|
134
|
-
sql = "select name from sqlite_master where type='view' and name
|
|
137
|
+
sql = "select name from sqlite_master where type='view' and lower(name)=lower(?);"
|
|
135
138
|
try:
|
|
136
139
|
curs.execute(sql, (view_name,))
|
|
137
140
|
except ErrInfo:
|
execsql/db/sqlserver.py
CHANGED
|
@@ -103,13 +103,15 @@ class SqlServerDatabase(Database):
|
|
|
103
103
|
try:
|
|
104
104
|
self.conn = pyodbc.connect(connstr)
|
|
105
105
|
except Exception:
|
|
106
|
-
_state.exec_log
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
if _state.exec_log is not None:
|
|
107
|
+
_state.exec_log.log_status_info(
|
|
108
|
+
f"Could not connect using: {re.sub(r'Pwd=[^;]*', 'Pwd=***', connstr)}",
|
|
109
|
+
)
|
|
109
110
|
else:
|
|
110
|
-
_state.exec_log
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
if _state.exec_log is not None:
|
|
112
|
+
_state.exec_log.log_status_info(
|
|
113
|
+
f"Connected using: {re.sub(r'Pwd=[^;]*', 'Pwd=***', connstr)}",
|
|
114
|
+
)
|
|
113
115
|
return True
|
|
114
116
|
return False
|
|
115
117
|
|
execsql/debug/repl.py
CHANGED
|
@@ -345,12 +345,13 @@ def _print_all_vars(*, include_env: bool = False) -> None:
|
|
|
345
345
|
_write(" (no substitution variables defined)\n\n")
|
|
346
346
|
return
|
|
347
347
|
items = list(subvars.substitutions) # list of (name, value) tuples
|
|
348
|
-
# Include ~local and #param variables from the current
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
items.extend(
|
|
352
|
-
|
|
353
|
-
|
|
348
|
+
# Include ~local and #param variables from the current scope frame.
|
|
349
|
+
localvars = _state.current_localvars()
|
|
350
|
+
if localvars is not None:
|
|
351
|
+
items.extend(localvars.substitutions)
|
|
352
|
+
paramvals = _state.current_paramvals()
|
|
353
|
+
if paramvals is not None:
|
|
354
|
+
items.extend(paramvals.substitutions)
|
|
354
355
|
if not items:
|
|
355
356
|
_write(" (no substitution variables defined)\n\n")
|
|
356
357
|
return
|
|
@@ -415,12 +416,15 @@ def _print_var(varname: str) -> None:
|
|
|
415
416
|
value = subvars.varvalue(varname)
|
|
416
417
|
if value is None and len(varname) > 1 and varname[0] in "$&@#~":
|
|
417
418
|
value = subvars.varvalue(varname[1:])
|
|
418
|
-
# Check
|
|
419
|
-
if value is None
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
419
|
+
# Check current scope frame for ~local and #param variables.
|
|
420
|
+
if value is None:
|
|
421
|
+
localvars = _state.current_localvars()
|
|
422
|
+
if localvars is not None:
|
|
423
|
+
value = localvars.varvalue(varname)
|
|
424
|
+
if value is None:
|
|
425
|
+
paramvals = _state.current_paramvals()
|
|
426
|
+
if paramvals is not None:
|
|
427
|
+
value = paramvals.varvalue(varname)
|
|
424
428
|
if value is None:
|
|
425
429
|
_write(f" {_c(_CYAN, varname)}: {_c(_DIM, '(undefined)')}\n")
|
|
426
430
|
else:
|
|
@@ -428,17 +432,47 @@ def _print_var(varname: str) -> None:
|
|
|
428
432
|
|
|
429
433
|
|
|
430
434
|
def _print_stack() -> None:
|
|
431
|
-
"""Print the
|
|
432
|
-
|
|
435
|
+
"""Print the unified AST execution stack.
|
|
436
|
+
|
|
437
|
+
Shows every nesting construct the executor is currently inside:
|
|
438
|
+
``<main>`` script, ``EXECUTE SCRIPT`` calls, ``INCLUDE``'d files,
|
|
439
|
+
``IF``/``ELSEIF``/``ELSE`` branches, ``LOOP`` iterations (with iteration
|
|
440
|
+
count), and ``BATCH`` blocks. Each frame shows source file and line.
|
|
441
|
+
"""
|
|
442
|
+
stack = _state.ast_exec_stack
|
|
433
443
|
if not stack:
|
|
434
|
-
_write(" (
|
|
444
|
+
_write(" (execution stack is empty)\n\n")
|
|
435
445
|
return
|
|
436
446
|
_write_rule(f" {_c(_BOLD + _YELLOW, 'Stack')} ")
|
|
437
447
|
_write(f" {_c(_DIM, 'depth:')} {len(stack)}\n")
|
|
438
|
-
for depth,
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
448
|
+
for depth, frame in enumerate(stack):
|
|
449
|
+
kind_label = frame.kind.upper().replace("LOOP_", "LOOP ")
|
|
450
|
+
# Build the right-hand description per kind
|
|
451
|
+
if frame.kind in ("if", "elseif"):
|
|
452
|
+
desc = f"{kind_label} {frame.label}"
|
|
453
|
+
elif frame.kind == "else":
|
|
454
|
+
desc = kind_label
|
|
455
|
+
elif frame.kind in ("loop_while", "loop_until"):
|
|
456
|
+
desc = f"{kind_label} {frame.label} {_c(_DIM, f'iter={frame.iteration}')}"
|
|
457
|
+
elif frame.kind == "script":
|
|
458
|
+
params = ""
|
|
459
|
+
if frame.params:
|
|
460
|
+
params = "(" + ", ".join(f"{k}={v!r}" for k, v in frame.params.items()) + ")"
|
|
461
|
+
iter_suffix = f" {_c(_DIM, f'iter={frame.iteration}')}" if frame.iteration else ""
|
|
462
|
+
desc = f"SCRIPT {frame.label}{params}{iter_suffix}"
|
|
463
|
+
elif frame.kind == "include":
|
|
464
|
+
desc = f"INCLUDE {frame.label}"
|
|
465
|
+
elif frame.kind == "batch":
|
|
466
|
+
desc = "BATCH"
|
|
467
|
+
elif frame.kind == "main":
|
|
468
|
+
desc = frame.label or "<main>"
|
|
469
|
+
else:
|
|
470
|
+
desc = f"{kind_label} {frame.label}"
|
|
471
|
+
src = ""
|
|
472
|
+
if frame.source:
|
|
473
|
+
src_name = frame.source.rsplit("/", 1)[-1].rsplit("\\", 1)[-1]
|
|
474
|
+
src = _c(_DIM, f" {src_name}:{frame.line}" if frame.line else f" {src_name}")
|
|
475
|
+
_write(f" [{depth}] {desc}{src}\n")
|
|
442
476
|
_write("\n")
|
|
443
477
|
|
|
444
478
|
|
|
@@ -510,8 +544,12 @@ def _set_var(varname: str, value: str) -> None:
|
|
|
510
544
|
if subvars is None:
|
|
511
545
|
_write(" Error: substitution variables are not initialised.\n")
|
|
512
546
|
return
|
|
513
|
-
if varname.startswith("~")
|
|
514
|
-
_state.
|
|
547
|
+
if varname.startswith("~"):
|
|
548
|
+
localvars = _state.current_localvars()
|
|
549
|
+
if localvars is not None:
|
|
550
|
+
localvars.add_substitution(varname, value)
|
|
551
|
+
else:
|
|
552
|
+
subvars.add_substitution(varname, value)
|
|
515
553
|
else:
|
|
516
554
|
subvars.add_substitution(varname, value)
|
|
517
555
|
_write(f" {_c(_CYAN, varname)} {_c(_DIM, '=')} {value}\n")
|
execsql/exceptions.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
"""
|
|
4
|
-
Custom exception hierarchy for execsql.
|
|
4
|
+
Custom exception hierarchy for execsql (internal use).
|
|
5
5
|
|
|
6
|
-
All domain-specific exceptions are defined here so that callers
|
|
7
|
-
from a single location.
|
|
6
|
+
All domain-specific exceptions are defined here so that internal callers
|
|
7
|
+
can import from a single location. Notable exceptions:
|
|
8
8
|
|
|
9
|
-
- :class:`ExecSqlError` — common base for all
|
|
9
|
+
- :class:`ExecSqlError` — common base for all internal execsql exceptions.
|
|
10
10
|
- :class:`ConfigError` — invalid or missing ``execsql.conf`` values.
|
|
11
11
|
- :class:`ErrInfo` — rich exception carrying type, command text, exception
|
|
12
12
|
message, and script location; used as both a raised exception and an error
|
|
@@ -19,6 +19,15 @@ from a single location. Notable exceptions:
|
|
|
19
19
|
— spreadsheet I/O failures.
|
|
20
20
|
- :class:`ConsoleUIError` — GUI console errors.
|
|
21
21
|
- :class:`CondParserError` / :class:`NumericParserError` — parser failures.
|
|
22
|
+
|
|
23
|
+
Note:
|
|
24
|
+
The ``execsql`` top-level package re-exports a *separate*
|
|
25
|
+
:class:`execsql.api.ExecSqlError` for library-API consumers. That
|
|
26
|
+
class is unrelated to this internal hierarchy; it is raised only by
|
|
27
|
+
:meth:`execsql.api.ScriptResult.raise_on_error`. Library callers
|
|
28
|
+
using ``from execsql import ExecSqlError`` get the public one;
|
|
29
|
+
internal modules using ``from execsql.exceptions import ExecSqlError``
|
|
30
|
+
get the hierarchy base defined here.
|
|
22
31
|
"""
|
|
23
32
|
|
|
24
33
|
__all__ = [
|
|
@@ -173,6 +182,8 @@ class ErrInfo(ExecSqlError):
|
|
|
173
182
|
|
|
174
183
|
|
|
175
184
|
class DataTypeError(ExecSqlError):
|
|
185
|
+
"""Raised when a value cannot be cast to or rendered as a given DataType."""
|
|
186
|
+
|
|
176
187
|
def __init__(self, data_type_name: str, error_msg: str) -> None:
|
|
177
188
|
self.data_type_name = data_type_name or "Unspecified data type"
|
|
178
189
|
self.error_msg = error_msg or "Unspecified error"
|
|
@@ -186,6 +197,8 @@ class DataTypeError(ExecSqlError):
|
|
|
186
197
|
|
|
187
198
|
|
|
188
199
|
class DbTypeError(ExecSqlError):
|
|
200
|
+
"""Raised when a DataType has no DBMS-specific mapping for the active database."""
|
|
201
|
+
|
|
189
202
|
def __init__(self, dbms_id: str, data_type: object, error_msg: str) -> None:
|
|
190
203
|
self.dbms_id = dbms_id
|
|
191
204
|
self.data_type = data_type
|
|
@@ -211,6 +224,8 @@ class DataTableError(ExecSqlError):
|
|
|
211
224
|
|
|
212
225
|
|
|
213
226
|
class DatabaseNotImplementedError(ExecSqlError):
|
|
227
|
+
"""Raised when a Database subclass does not implement a required method for the current DBMS."""
|
|
228
|
+
|
|
214
229
|
def __init__(self, db_name: str, method: str) -> None:
|
|
215
230
|
self.db_name = db_name
|
|
216
231
|
self.method = method
|
execsql/exporters/base.py
CHANGED
|
@@ -147,8 +147,9 @@ class WriteSpec:
|
|
|
147
147
|
if self.repeatable or not self.written:
|
|
148
148
|
self.written = True
|
|
149
149
|
msg = self.msg
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
localvars = _state.current_localvars()
|
|
151
|
+
if localvars is not None:
|
|
152
|
+
msg, _ = localvars.substitute_all(msg)
|
|
152
153
|
msg, _ = subvars.substitute_all(msg)
|
|
153
154
|
if self.outfile:
|
|
154
155
|
from execsql.utils.fileio import EncodedFile
|
execsql/exporters/delimited.py
CHANGED
|
@@ -8,9 +8,8 @@ Provides:
|
|
|
8
8
|
- :class:`LineDelimiter` — configurable line-ending constant.
|
|
9
9
|
- :class:`DelimitedWriter` / :class:`CsvWriter` — low-level writers used
|
|
10
10
|
by the export logic.
|
|
11
|
-
- :class:`CsvFile` — full delimited-file reader/writer
|
|
12
|
-
|
|
13
|
-
ZIP output.
|
|
11
|
+
- :class:`CsvFile` — full delimited-file reader/writer supporting custom
|
|
12
|
+
delimiters, quoting, encoding, and ZIP output.
|
|
14
13
|
- :func:`write_delimited_file` — writes a query result set to a
|
|
15
14
|
CSV/TSV/delimited text file.
|
|
16
15
|
"""
|
execsql/exporters/feather.py
CHANGED
|
@@ -5,9 +5,9 @@ from execsql.exceptions import ErrInfo
|
|
|
5
5
|
Apache Feather and HDF5 export for execsql.
|
|
6
6
|
|
|
7
7
|
Provides :func:`write_query_to_feather` (Apache Arrow Feather v2 format
|
|
8
|
-
via ``
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
via ``polars``) and :func:`write_query_to_hdf5` (HDF5 via the ``tables``
|
|
9
|
+
library). Used by ``EXPORT … FORMAT feather`` and ``FORMAT hdf5``.
|
|
10
|
+
Both back-ends are optional; install via ``execsql2[formats]``.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from typing import Any
|
execsql/exporters/ods.py
CHANGED
|
@@ -6,7 +6,7 @@ ODS (OpenDocument Spreadsheet) export for execsql.
|
|
|
6
6
|
Provides :func:`write_query_to_ods` (single-sheet export),
|
|
7
7
|
:func:`write_queries_to_ods` (multi-sheet export), and :class:`OdsFile`
|
|
8
8
|
(wrapper around ``odfpy`` for writing ``.ods`` files). Requires the
|
|
9
|
-
``odfpy`` package (``execsql2[
|
|
9
|
+
``odfpy`` package (``execsql2[formats]``).
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import datetime
|
execsql/exporters/xls.py
CHANGED
|
@@ -2,11 +2,19 @@ from __future__ import annotations
|
|
|
2
2
|
from execsql.types import DT_TimestampTZ
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
|
-
|
|
5
|
+
Spreadsheet file wrappers used by the EXPORT and IMPORT metacommands.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
:
|
|
9
|
-
|
|
7
|
+
Despite living in the ``exporters`` package, both classes here are
|
|
8
|
+
mixed-use:
|
|
9
|
+
|
|
10
|
+
- :class:`XlsFile` — **read-only** wrapper around ``xlrd`` for importing
|
|
11
|
+
legacy ``.xls`` spreadsheets. (Writing ``.xls`` is not supported; new
|
|
12
|
+
exports should target ``.xlsx``.)
|
|
13
|
+
- :class:`XlsxFile` — read/write wrapper around ``openpyxl`` for
|
|
14
|
+
``.xlsx`` spreadsheets.
|
|
15
|
+
|
|
16
|
+
Both back-ends are optional dependencies; install via
|
|
17
|
+
``execsql2[formats]``.
|
|
10
18
|
"""
|
|
11
19
|
|
|
12
20
|
import datetime
|
execsql/exporters/xlsx.py
CHANGED
|
@@ -5,7 +5,7 @@ XLSX (Excel Open XML) export for execsql.
|
|
|
5
5
|
|
|
6
6
|
Provides :func:`write_query_to_xlsx` (single-sheet export) and
|
|
7
7
|
:func:`write_queries_to_xlsx` (multi-sheet export). Requires the
|
|
8
|
-
``openpyxl`` package (``execsql2[
|
|
8
|
+
``openpyxl`` package (``execsql2[formats]``).
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import datetime
|