execsql2 2.1.2__py3-none-any.whl → 2.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- execsql/cli/__init__.py +436 -0
- execsql/cli/dsn.py +86 -0
- execsql/cli/help.py +140 -0
- execsql/{cli.py → cli/run.py} +14 -589
- execsql/config.py +65 -1
- execsql/db/access.py +27 -15
- execsql/db/base.py +328 -215
- execsql/db/dsn.py +10 -5
- execsql/db/duckdb.py +6 -2
- execsql/db/factory.py +21 -0
- execsql/db/firebird.py +27 -19
- execsql/db/mysql.py +12 -7
- execsql/db/oracle.py +15 -11
- execsql/db/postgres.py +31 -16
- execsql/db/sqlite.py +15 -11
- execsql/db/sqlserver.py +16 -5
- execsql/exceptions.py +25 -7
- execsql/exporters/base.py +12 -1
- execsql/exporters/delimited.py +80 -35
- execsql/exporters/duckdb.py +6 -2
- execsql/exporters/feather.py +10 -6
- execsql/exporters/html.py +89 -69
- execsql/exporters/json.py +52 -45
- execsql/exporters/latex.py +37 -27
- execsql/exporters/ods.py +32 -11
- execsql/exporters/parquet.py +5 -2
- execsql/exporters/pretty.py +16 -9
- execsql/exporters/raw.py +22 -16
- execsql/exporters/sqlite.py +6 -2
- execsql/exporters/templates.py +39 -21
- execsql/exporters/values.py +26 -20
- execsql/exporters/xls.py +30 -11
- execsql/exporters/xml.py +31 -13
- execsql/exporters/zip.py +15 -0
- execsql/importers/base.py +6 -4
- execsql/importers/csv.py +8 -6
- execsql/importers/feather.py +6 -4
- execsql/importers/ods.py +6 -4
- execsql/importers/xls.py +6 -4
- execsql/metacommands/__init__.py +208 -1548
- execsql/metacommands/conditions.py +101 -27
- execsql/metacommands/control.py +8 -4
- execsql/metacommands/data.py +6 -6
- execsql/metacommands/debug.py +6 -2
- execsql/metacommands/dispatch.py +2011 -0
- execsql/metacommands/io.py +67 -1310
- execsql/metacommands/io_export.py +442 -0
- execsql/metacommands/io_fileops.py +287 -0
- execsql/metacommands/io_import.py +398 -0
- execsql/metacommands/io_write.py +248 -0
- execsql/metacommands/prompt.py +22 -66
- execsql/metacommands/system.py +7 -2
- execsql/models.py +7 -0
- execsql/parser.py +10 -0
- execsql/py.typed +0 -0
- execsql/script/__init__.py +95 -0
- execsql/script/control.py +162 -0
- execsql/{script.py → script/engine.py} +184 -402
- execsql/script/variables.py +281 -0
- execsql/types.py +49 -20
- 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 +33 -8
- 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.0.data/data/execsql2_extras/README.md +65 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/METADATA +13 -6
- execsql2-2.4.0.dist-info/RECORD +108 -0
- execsql2-2.1.2.data/data/execsql2_extras/READ_ME.rst +0 -127
- execsql2-2.1.2.dist-info/RECORD +0 -96
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/NOTICE +0 -0
execsql/db/dsn.py
CHANGED
|
@@ -15,8 +15,12 @@ from execsql.utils.errors import exception_desc, fatal_error
|
|
|
15
15
|
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
16
16
|
import execsql.state as _state
|
|
17
17
|
|
|
18
|
+
__all__ = ["DsnDatabase"]
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
class DsnDatabase(Database):
|
|
22
|
+
"""Generic ODBC adapter that connects to any data source registered as an ODBC DSN via pyodbc."""
|
|
23
|
+
|
|
20
24
|
# There's no telling what is actually connected to a DSN, so this uses
|
|
21
25
|
# generic Database methods almost exclusively. Only 'exec_cmd()' is
|
|
22
26
|
# overridden, and that uses the method for SQL Server because the DAO
|
|
@@ -79,23 +83,23 @@ class DsnDatabase(Database):
|
|
|
79
83
|
def _try_connect():
|
|
80
84
|
try:
|
|
81
85
|
_dsn_connect()
|
|
82
|
-
except Exception:
|
|
86
|
+
except Exception as e:
|
|
83
87
|
excdesc = exception_desc()
|
|
84
88
|
if "Optional feature not implemented" in excdesc:
|
|
85
89
|
try:
|
|
86
90
|
_dsn_connect(autocommit=True)
|
|
87
|
-
except Exception:
|
|
91
|
+
except Exception as e:
|
|
88
92
|
raise ErrInfo(
|
|
89
93
|
type="exception",
|
|
90
94
|
exception_msg=exception_desc(),
|
|
91
95
|
other_msg=f"Can't open DSN database {self.db_name} using ODBC",
|
|
92
|
-
)
|
|
96
|
+
) from e
|
|
93
97
|
else:
|
|
94
98
|
raise ErrInfo(
|
|
95
99
|
type="exception",
|
|
96
100
|
exception_msg=excdesc,
|
|
97
101
|
other_msg=f"Can't open DSN database {self.db_name} using ODBC",
|
|
98
|
-
)
|
|
102
|
+
) from e
|
|
99
103
|
|
|
100
104
|
try:
|
|
101
105
|
_try_connect()
|
|
@@ -136,5 +140,6 @@ class DsnDatabase(Database):
|
|
|
136
140
|
with open(file_name, "rb") as f:
|
|
137
141
|
filedata = f.read()
|
|
138
142
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
139
|
-
|
|
143
|
+
quoted_col = self.quote_identifier(column_name)
|
|
144
|
+
sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
|
|
140
145
|
self.cursor().execute(sql, (pyodbc.Binary(filedata),))
|
execsql/db/duckdb.py
CHANGED
|
@@ -15,8 +15,12 @@ from execsql.exceptions import ErrInfo
|
|
|
15
15
|
from execsql.utils.errors import exception_desc, fatal_error
|
|
16
16
|
import execsql.state as _state
|
|
17
17
|
|
|
18
|
+
__all__ = ["DuckDBDatabase"]
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
class DuckDBDatabase(Database):
|
|
22
|
+
"""DuckDB in-process analytics adapter using the duckdb package."""
|
|
23
|
+
|
|
20
24
|
def __init__(self, DuckDB_fn: str) -> None:
|
|
21
25
|
try:
|
|
22
26
|
import duckdb # noqa: F401
|
|
@@ -48,12 +52,12 @@ class DuckDBDatabase(Database):
|
|
|
48
52
|
self.conn = duckdb.connect(self.db_name, read_only=False)
|
|
49
53
|
except ErrInfo:
|
|
50
54
|
raise
|
|
51
|
-
except Exception:
|
|
55
|
+
except Exception as e:
|
|
52
56
|
raise ErrInfo(
|
|
53
57
|
type="exception",
|
|
54
58
|
exception_msg=exception_desc(),
|
|
55
59
|
other_msg=f"Can't open DuckDB database {self.db_name}",
|
|
56
|
-
)
|
|
60
|
+
) from e
|
|
57
61
|
|
|
58
62
|
def exec_cmd(self, querycommand: str) -> None:
|
|
59
63
|
# DuckDB does not support stored functions, so the querycommand
|
execsql/db/factory.py
CHANGED
|
@@ -25,6 +25,18 @@ from execsql.db.duckdb import DuckDBDatabase
|
|
|
25
25
|
from execsql.db.mysql import MySQLDatabase
|
|
26
26
|
from execsql.db.firebird import FirebirdDatabase
|
|
27
27
|
|
|
28
|
+
__all__ = [
|
|
29
|
+
"db_Access",
|
|
30
|
+
"db_Dsn",
|
|
31
|
+
"db_DuckDB",
|
|
32
|
+
"db_Firebird",
|
|
33
|
+
"db_MySQL",
|
|
34
|
+
"db_Oracle",
|
|
35
|
+
"db_Postgres",
|
|
36
|
+
"db_SQLite",
|
|
37
|
+
"db_SqlServer",
|
|
38
|
+
]
|
|
39
|
+
|
|
28
40
|
|
|
29
41
|
def db_Access(
|
|
30
42
|
Access_fn: str,
|
|
@@ -32,6 +44,7 @@ def db_Access(
|
|
|
32
44
|
user: str | None = None,
|
|
33
45
|
encoding: str | None = None,
|
|
34
46
|
) -> AccessDatabase:
|
|
47
|
+
"""Open an MS Access database file (.mdb or .accdb) via DAO/ODBC."""
|
|
35
48
|
if not Path(Access_fn).exists():
|
|
36
49
|
raise ErrInfo(
|
|
37
50
|
type="error",
|
|
@@ -50,6 +63,7 @@ def db_Postgres(
|
|
|
50
63
|
new_db: bool = False,
|
|
51
64
|
password: str | None = None,
|
|
52
65
|
) -> PostgresDatabase:
|
|
66
|
+
"""Open a new PostgreSQL connection via psycopg2."""
|
|
53
67
|
return PostgresDatabase(server_name, database_name, user, pw_needed, port, new_db=new_db, password=password)
|
|
54
68
|
|
|
55
69
|
|
|
@@ -58,6 +72,7 @@ def db_SQLite(
|
|
|
58
72
|
new_db: bool = False,
|
|
59
73
|
encoding: str | None = None,
|
|
60
74
|
) -> SQLiteDatabase:
|
|
75
|
+
"""Open a SQLite database file via the standard-library sqlite3 module."""
|
|
61
76
|
if new_db:
|
|
62
77
|
from execsql.utils.fileio import check_dir
|
|
63
78
|
|
|
@@ -79,6 +94,7 @@ def db_SqlServer(
|
|
|
79
94
|
port: int | None = None,
|
|
80
95
|
encoding: str | None = None,
|
|
81
96
|
) -> SqlServerDatabase:
|
|
97
|
+
"""Open a Microsoft SQL Server connection via pyodbc."""
|
|
82
98
|
return SqlServerDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
83
99
|
|
|
84
100
|
|
|
@@ -90,6 +106,7 @@ def db_MySQL(
|
|
|
90
106
|
port: int | None = None,
|
|
91
107
|
encoding: str | None = None,
|
|
92
108
|
) -> MySQLDatabase:
|
|
109
|
+
"""Open a MySQL or MariaDB connection via pymysql."""
|
|
93
110
|
return MySQLDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
94
111
|
|
|
95
112
|
|
|
@@ -98,6 +115,7 @@ def db_DuckDB(
|
|
|
98
115
|
new_db: bool = False,
|
|
99
116
|
encoding: str | None = None,
|
|
100
117
|
) -> DuckDBDatabase:
|
|
118
|
+
"""Open a DuckDB in-process analytics database file via the duckdb package."""
|
|
101
119
|
if new_db:
|
|
102
120
|
from execsql.utils.fileio import check_dir
|
|
103
121
|
|
|
@@ -119,6 +137,7 @@ def db_Oracle(
|
|
|
119
137
|
port: int | None = None,
|
|
120
138
|
encoding: str | None = None,
|
|
121
139
|
) -> OracleDatabase:
|
|
140
|
+
"""Open an Oracle database connection via cx_Oracle (python-oracledb)."""
|
|
122
141
|
return OracleDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
123
142
|
|
|
124
143
|
|
|
@@ -130,6 +149,7 @@ def db_Firebird(
|
|
|
130
149
|
port: int | None = None,
|
|
131
150
|
encoding: str | None = None,
|
|
132
151
|
) -> FirebirdDatabase:
|
|
152
|
+
"""Open a Firebird database connection via the firebird-driver package."""
|
|
133
153
|
return FirebirdDatabase(server_name, database_name, user, pw_needed, port, encoding)
|
|
134
154
|
|
|
135
155
|
|
|
@@ -139,4 +159,5 @@ def db_Dsn(
|
|
|
139
159
|
pw_needed: bool = True,
|
|
140
160
|
encoding: str | None = None,
|
|
141
161
|
) -> DsnDatabase:
|
|
162
|
+
"""Open a connection to any ODBC data source registered under *dsn_name*."""
|
|
142
163
|
return DsnDatabase(dsn_name=dsn_name, user_name=user, need_passwd=pw_needed, encoding=encoding)
|
execsql/db/firebird.py
CHANGED
|
@@ -14,8 +14,12 @@ from execsql.utils.errors import exception_desc, fatal_error
|
|
|
14
14
|
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
15
15
|
import execsql.state as _state
|
|
16
16
|
|
|
17
|
+
__all__ = ["FirebirdDatabase"]
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
class FirebirdDatabase(Database):
|
|
21
|
+
"""Firebird adapter using the firebird-driver (fdb) package."""
|
|
22
|
+
|
|
19
23
|
def __init__(
|
|
20
24
|
self,
|
|
21
25
|
server_name: str,
|
|
@@ -30,7 +34,7 @@ class FirebirdDatabase(Database):
|
|
|
30
34
|
import fdb as firebird_lib # noqa: F401
|
|
31
35
|
except Exception:
|
|
32
36
|
fatal_error(
|
|
33
|
-
"The fdb module is required to connect to
|
|
37
|
+
"The fdb module is required to connect to Firebird. See https://pypi.python.org/pypi/fdb/",
|
|
34
38
|
)
|
|
35
39
|
from execsql.types import dbt_firebird
|
|
36
40
|
|
|
@@ -104,9 +108,9 @@ class FirebirdDatabase(Database):
|
|
|
104
108
|
raise
|
|
105
109
|
except ErrInfo:
|
|
106
110
|
raise
|
|
107
|
-
except Exception:
|
|
111
|
+
except Exception as e:
|
|
108
112
|
msg = f"Failed to open Firebird database {self.db_name} on {self.server_name}"
|
|
109
|
-
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg)
|
|
113
|
+
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
|
|
110
114
|
|
|
111
115
|
def exec_cmd(self, querycommand: str) -> None:
|
|
112
116
|
# The querycommand must be a stored function (/procedure)
|
|
@@ -122,15 +126,15 @@ class FirebirdDatabase(Database):
|
|
|
122
126
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
123
127
|
curs = self.cursor()
|
|
124
128
|
sql = (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
"SELECT RDB$RELATION_NAME FROM RDB$RELATIONS "
|
|
130
|
+
"WHERE RDB$SYSTEM_FLAG=0 AND RDB$VIEW_BLR IS NULL "
|
|
131
|
+
"AND RDB$RELATION_NAME=?;"
|
|
128
132
|
)
|
|
129
133
|
try:
|
|
130
|
-
curs.execute(sql)
|
|
134
|
+
curs.execute(sql, (table_name.upper(),))
|
|
131
135
|
except ErrInfo:
|
|
132
136
|
raise
|
|
133
|
-
except Exception:
|
|
137
|
+
except Exception as e:
|
|
134
138
|
try:
|
|
135
139
|
self.rollback()
|
|
136
140
|
except Exception:
|
|
@@ -140,7 +144,7 @@ class FirebirdDatabase(Database):
|
|
|
140
144
|
command_text=sql,
|
|
141
145
|
exception_msg=exception_desc(),
|
|
142
146
|
other_msg=f"Failed test for existence of Firebird table {table_name}",
|
|
143
|
-
)
|
|
147
|
+
) from e
|
|
144
148
|
rows = curs.fetchall()
|
|
145
149
|
self.conn.commit()
|
|
146
150
|
curs.close()
|
|
@@ -153,7 +157,9 @@ class FirebirdDatabase(Database):
|
|
|
153
157
|
schema_name: str | None = None,
|
|
154
158
|
) -> bool:
|
|
155
159
|
curs = self.cursor()
|
|
156
|
-
|
|
160
|
+
quoted_col = self.quote_identifier(column_name)
|
|
161
|
+
quoted_tbl = self.quote_identifier(table_name)
|
|
162
|
+
sql = f"select first 1 {quoted_col} from {quoted_tbl};"
|
|
157
163
|
try:
|
|
158
164
|
curs.execute(sql)
|
|
159
165
|
except Exception:
|
|
@@ -162,36 +168,37 @@ class FirebirdDatabase(Database):
|
|
|
162
168
|
|
|
163
169
|
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
164
170
|
curs = self.cursor()
|
|
165
|
-
|
|
171
|
+
quoted_tbl = self.quote_identifier(table_name)
|
|
172
|
+
sql = f"select first 1 * from {quoted_tbl};"
|
|
166
173
|
try:
|
|
167
174
|
curs.execute(sql)
|
|
168
175
|
except ErrInfo:
|
|
169
176
|
raise
|
|
170
|
-
except Exception:
|
|
177
|
+
except Exception as e:
|
|
171
178
|
self.rollback()
|
|
172
179
|
raise ErrInfo(
|
|
173
180
|
type="db",
|
|
174
181
|
command_text=sql,
|
|
175
182
|
exception_msg=exception_desc(),
|
|
176
183
|
other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
|
|
177
|
-
)
|
|
184
|
+
) from e
|
|
178
185
|
return [d[0] for d in curs.description]
|
|
179
186
|
|
|
180
187
|
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
181
188
|
curs = self.cursor()
|
|
182
|
-
sql =
|
|
189
|
+
sql = "select distinct rdb$view_name from rdb$view_relations where rdb$view_name = ?;"
|
|
183
190
|
try:
|
|
184
|
-
curs.execute(sql)
|
|
191
|
+
curs.execute(sql, (view_name,))
|
|
185
192
|
except ErrInfo:
|
|
186
193
|
raise
|
|
187
|
-
except Exception:
|
|
194
|
+
except Exception as e:
|
|
188
195
|
self.rollback()
|
|
189
196
|
raise ErrInfo(
|
|
190
197
|
type="db",
|
|
191
198
|
command_text=sql,
|
|
192
199
|
exception_msg=exception_desc(),
|
|
193
200
|
other_msg=f"Failed test for existence of Firebird view {view_name}",
|
|
194
|
-
)
|
|
201
|
+
) from e
|
|
195
202
|
rows = curs.fetchall()
|
|
196
203
|
curs.close()
|
|
197
204
|
return len(rows) > 0
|
|
@@ -202,8 +209,9 @@ class FirebirdDatabase(Database):
|
|
|
202
209
|
def role_exists(self, rolename: str) -> bool:
|
|
203
210
|
curs = self.cursor()
|
|
204
211
|
curs.execute(
|
|
205
|
-
|
|
206
|
-
|
|
212
|
+
"SELECT DISTINCT USER FROM RDB$USER_PRIVILEGES WHERE USER = ? union "
|
|
213
|
+
" SELECT DISTINCT RDB$ROLE_NAME FROM RDB$ROLES WHERE RDB$ROLE_NAME = ?;",
|
|
214
|
+
(rolename, rolename),
|
|
207
215
|
)
|
|
208
216
|
rows = curs.fetchall()
|
|
209
217
|
curs.close()
|
execsql/db/mysql.py
CHANGED
|
@@ -16,8 +16,12 @@ from execsql.utils.errors import exception_desc, fatal_error
|
|
|
16
16
|
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
17
17
|
import execsql.state as _state
|
|
18
18
|
|
|
19
|
+
__all__ = ["MySQLDatabase"]
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
class MySQLDatabase(Database):
|
|
23
|
+
"""MySQL and MariaDB adapter using the pymysql package."""
|
|
24
|
+
|
|
21
25
|
def __init__(
|
|
22
26
|
self,
|
|
23
27
|
server_name: str,
|
|
@@ -109,9 +113,9 @@ class MySQLDatabase(Database):
|
|
|
109
113
|
raise
|
|
110
114
|
except ErrInfo:
|
|
111
115
|
raise
|
|
112
|
-
except Exception:
|
|
116
|
+
except Exception as e:
|
|
113
117
|
msg = f"Failed to open MySQL database {self.db_name} on {self.server_name}"
|
|
114
|
-
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg)
|
|
118
|
+
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
|
|
115
119
|
|
|
116
120
|
def exec_cmd(self, querycommand: str) -> None:
|
|
117
121
|
# The querycommand must be a stored function (/procedure)
|
|
@@ -130,9 +134,10 @@ class MySQLDatabase(Database):
|
|
|
130
134
|
def role_exists(self, rolename: str) -> bool:
|
|
131
135
|
curs = self.cursor()
|
|
132
136
|
curs.execute(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
"select distinct user as role from mysql.user where user = %s"
|
|
138
|
+
" union select distinct role_name as role from information_schema.applicable_roles"
|
|
139
|
+
" where role_name = %s",
|
|
140
|
+
(rolename, rolename),
|
|
136
141
|
)
|
|
137
142
|
rows = curs.fetchall()
|
|
138
143
|
curs.close()
|
|
@@ -274,14 +279,14 @@ class MySQLDatabase(Database):
|
|
|
274
279
|
curs.executemany(sql_template, b)
|
|
275
280
|
except ErrInfo:
|
|
276
281
|
raise
|
|
277
|
-
except Exception:
|
|
282
|
+
except Exception as e:
|
|
278
283
|
self.rollback()
|
|
279
284
|
raise ErrInfo(
|
|
280
285
|
type="db",
|
|
281
286
|
command_text=sql_template,
|
|
282
287
|
exception_msg=exception_desc(),
|
|
283
288
|
other_msg=f"Import from file into table {sq_name}, line {{{line}}}",
|
|
284
|
-
)
|
|
289
|
+
) from e
|
|
285
290
|
total_rows += len(b)
|
|
286
291
|
interval = _state.conf.import_progress_interval
|
|
287
292
|
if _state.exec_log and interval > 0 and total_rows % interval == 0:
|
execsql/db/oracle.py
CHANGED
|
@@ -16,15 +16,19 @@ from execsql.utils.errors import exception_desc, fatal_error
|
|
|
16
16
|
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
17
17
|
import execsql.state as _state
|
|
18
18
|
|
|
19
|
+
__all__ = ["OracleDatabase"]
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
class OracleDatabase(Database):
|
|
23
|
+
"""Oracle adapter using the cx_Oracle (python-oracledb) driver."""
|
|
24
|
+
|
|
21
25
|
def __init__(
|
|
22
26
|
self,
|
|
23
27
|
server_name: str,
|
|
24
28
|
db_name: str,
|
|
25
29
|
user_name: str | None,
|
|
26
30
|
need_passwd: bool = False,
|
|
27
|
-
port: int | None =
|
|
31
|
+
port: int | None = 1521,
|
|
28
32
|
encoding: str | None = "UTF8",
|
|
29
33
|
password: str | None = None,
|
|
30
34
|
) -> None:
|
|
@@ -95,9 +99,9 @@ class OracleDatabase(Database):
|
|
|
95
99
|
raise
|
|
96
100
|
except ErrInfo:
|
|
97
101
|
raise
|
|
98
|
-
except Exception:
|
|
102
|
+
except Exception as e:
|
|
99
103
|
msg = f"Failed to open Oracle database {self.db_name} on {self.server_name}"
|
|
100
|
-
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg)
|
|
104
|
+
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
|
|
101
105
|
|
|
102
106
|
def execute(self, sql: Any, paramlist: list | None = None) -> None:
|
|
103
107
|
# Strip any semicolon off the end and pass to the parent method.
|
|
@@ -141,14 +145,14 @@ class OracleDatabase(Database):
|
|
|
141
145
|
curs.execute(sql, params)
|
|
142
146
|
except ErrInfo:
|
|
143
147
|
raise
|
|
144
|
-
except Exception:
|
|
148
|
+
except Exception as e:
|
|
145
149
|
self.rollback()
|
|
146
150
|
raise ErrInfo(
|
|
147
151
|
type="db",
|
|
148
152
|
command_text=sql,
|
|
149
153
|
exception_msg=exception_desc(),
|
|
150
154
|
other_msg=f"Failed test for existence of table {table_name} in {self.name()}",
|
|
151
|
-
)
|
|
155
|
+
) from e
|
|
152
156
|
rows = curs.fetchall()
|
|
153
157
|
curs.close()
|
|
154
158
|
return len(rows) > 0
|
|
@@ -170,14 +174,14 @@ class OracleDatabase(Database):
|
|
|
170
174
|
curs.execute(sql, params)
|
|
171
175
|
except ErrInfo:
|
|
172
176
|
raise
|
|
173
|
-
except Exception:
|
|
177
|
+
except Exception as e:
|
|
174
178
|
self.rollback()
|
|
175
179
|
raise ErrInfo(
|
|
176
180
|
type="db",
|
|
177
181
|
command_text=sql,
|
|
178
182
|
exception_msg=exception_desc(),
|
|
179
183
|
other_msg=f"Failed test for existence of column {column_name} in table {table_name} of {self.name()}",
|
|
180
|
-
)
|
|
184
|
+
) from e
|
|
181
185
|
rows = curs.fetchall()
|
|
182
186
|
curs.close()
|
|
183
187
|
return len(rows) > 0
|
|
@@ -194,14 +198,14 @@ class OracleDatabase(Database):
|
|
|
194
198
|
curs.execute(sql, params)
|
|
195
199
|
except ErrInfo:
|
|
196
200
|
raise
|
|
197
|
-
except Exception:
|
|
201
|
+
except Exception as e:
|
|
198
202
|
self.rollback()
|
|
199
203
|
raise ErrInfo(
|
|
200
204
|
type="db",
|
|
201
205
|
command_text=sql,
|
|
202
206
|
exception_msg=exception_desc(),
|
|
203
207
|
other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
|
|
204
|
-
)
|
|
208
|
+
) from e
|
|
205
209
|
rows = curs.fetchall()
|
|
206
210
|
curs.close()
|
|
207
211
|
return [row[0] for row in rows]
|
|
@@ -218,14 +222,14 @@ class OracleDatabase(Database):
|
|
|
218
222
|
curs.execute(sql, params)
|
|
219
223
|
except ErrInfo:
|
|
220
224
|
raise
|
|
221
|
-
except Exception:
|
|
225
|
+
except Exception as e:
|
|
222
226
|
self.rollback()
|
|
223
227
|
raise ErrInfo(
|
|
224
228
|
type="db",
|
|
225
229
|
command_text=sql,
|
|
226
230
|
exception_msg=exception_desc(),
|
|
227
231
|
other_msg=f"Failed test for existence of view {view_name} in {self.name()}",
|
|
228
|
-
)
|
|
232
|
+
) from e
|
|
229
233
|
rows = curs.fetchall()
|
|
230
234
|
curs.close()
|
|
231
235
|
return len(rows) > 0
|
execsql/db/postgres.py
CHANGED
|
@@ -19,11 +19,14 @@ from execsql.utils.auth import clear_stored_password, get_password, password_fro
|
|
|
19
19
|
from execsql.utils.strings import encodings_match
|
|
20
20
|
import execsql.state as _state
|
|
21
21
|
|
|
22
|
+
__all__ = ["PostgresDatabase"]
|
|
22
23
|
|
|
23
24
|
DEFAULT_CONNECT_TIMEOUT = 30 # seconds
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class PostgresDatabase(Database):
|
|
28
|
+
"""PostgreSQL adapter using psycopg2, with schema support, server-side COPY, and keyring auth."""
|
|
29
|
+
|
|
27
30
|
def __init__(
|
|
28
31
|
self,
|
|
29
32
|
server_name: str,
|
|
@@ -87,18 +90,20 @@ class PostgresDatabase(Database):
|
|
|
87
90
|
port=db.port,
|
|
88
91
|
connect_timeout=db.connect_timeout,
|
|
89
92
|
)
|
|
90
|
-
except Exception:
|
|
93
|
+
except Exception as e:
|
|
91
94
|
msg = (
|
|
92
95
|
f"Failed to open PostgreSQL database {self.db_name} on {self.server_name}; "
|
|
93
96
|
"check server and database name, and validity of credentials"
|
|
94
97
|
)
|
|
95
|
-
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg)
|
|
98
|
+
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
|
|
96
99
|
|
|
97
100
|
def create_db(db: PostgresDatabase) -> None:
|
|
98
101
|
conn = db_conn(db, "postgres")
|
|
99
102
|
conn.autocommit = True
|
|
100
103
|
curs = conn.cursor()
|
|
101
|
-
|
|
104
|
+
quoted_name = db.quote_identifier(db.db_name)
|
|
105
|
+
quoted_enc = db.quote_identifier(db.encoding)
|
|
106
|
+
curs.execute(f"create database {quoted_name} encoding {quoted_enc};")
|
|
102
107
|
conn.close()
|
|
103
108
|
|
|
104
109
|
if self.conn is None:
|
|
@@ -133,9 +138,9 @@ class PostgresDatabase(Database):
|
|
|
133
138
|
raise
|
|
134
139
|
except ErrInfo:
|
|
135
140
|
raise
|
|
136
|
-
except Exception:
|
|
141
|
+
except Exception as e:
|
|
137
142
|
msg = f"Failed to open PostgreSQL database {self.db_name} on {self.server_name}"
|
|
138
|
-
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg)
|
|
143
|
+
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
|
|
139
144
|
# (Re)set the encoding to match the database.
|
|
140
145
|
self.encoding = self.conn.encoding
|
|
141
146
|
|
|
@@ -177,14 +182,14 @@ class PostgresDatabase(Database):
|
|
|
177
182
|
curs.execute(sql, params)
|
|
178
183
|
except ErrInfo:
|
|
179
184
|
raise
|
|
180
|
-
except Exception:
|
|
185
|
+
except Exception as e:
|
|
181
186
|
self.rollback()
|
|
182
187
|
raise ErrInfo(
|
|
183
188
|
type="db",
|
|
184
189
|
command_text=sql,
|
|
185
190
|
exception_msg=exception_desc(),
|
|
186
191
|
other_msg=f"Failed test for existence of table {table_name} in {self.name()}",
|
|
187
|
-
)
|
|
192
|
+
) from e
|
|
188
193
|
rows = curs.fetchall()
|
|
189
194
|
curs.close()
|
|
190
195
|
return len(rows) > 0
|
|
@@ -209,14 +214,14 @@ class PostgresDatabase(Database):
|
|
|
209
214
|
curs.execute(sql, params)
|
|
210
215
|
except ErrInfo:
|
|
211
216
|
raise
|
|
212
|
-
except Exception:
|
|
217
|
+
except Exception as e:
|
|
213
218
|
self.rollback()
|
|
214
219
|
raise ErrInfo(
|
|
215
220
|
type="db",
|
|
216
221
|
command_text=sql,
|
|
217
222
|
exception_msg=exception_desc(),
|
|
218
223
|
other_msg=f"Failed test for existence of view {view_name} in {self.name()}",
|
|
219
|
-
)
|
|
224
|
+
) from e
|
|
220
225
|
rows = curs.fetchall()
|
|
221
226
|
curs.close()
|
|
222
227
|
return len(rows) > 0
|
|
@@ -304,9 +309,18 @@ class PostgresDatabase(Database):
|
|
|
304
309
|
# ASCII unit separator, which, if it had been used for its intended purpose,
|
|
305
310
|
# should have been identified as the delimiter, so presumably it has not been used.
|
|
306
311
|
delim = csv_file_obj.delimiter if csv_file_obj.delimiter else chr(31)
|
|
307
|
-
|
|
312
|
+
if len(delim) != 1:
|
|
313
|
+
raise ErrInfo(
|
|
314
|
+
type="error",
|
|
315
|
+
other_msg=f"Invalid delimiter for COPY: expected single character, got {len(delim)} characters",
|
|
316
|
+
)
|
|
317
|
+
safe_delim = delim.replace("'", "''")
|
|
318
|
+
copy_cmd = (
|
|
319
|
+
f"copy {sq_name} ({input_col_list}) from stdin with (format csv, null '', delimiter '{safe_delim}'"
|
|
320
|
+
)
|
|
308
321
|
if csv_file_obj.quotechar:
|
|
309
|
-
|
|
322
|
+
safe_quote = csv_file_obj.quotechar.replace("'", "''")
|
|
323
|
+
copy_cmd = copy_cmd + f", quote '{safe_quote}'"
|
|
310
324
|
copy_cmd = copy_cmd + ")"
|
|
311
325
|
_state.exec_log.log_status_info(
|
|
312
326
|
f"IMPORTing {csv_file_obj.csvfname} using Postgres' fast file reading routine",
|
|
@@ -315,13 +329,13 @@ class PostgresDatabase(Database):
|
|
|
315
329
|
curs.copy_expert(copy_cmd, rf, _state.conf.import_buffer)
|
|
316
330
|
except ErrInfo:
|
|
317
331
|
raise
|
|
318
|
-
except Exception:
|
|
332
|
+
except Exception as e:
|
|
319
333
|
self.rollback()
|
|
320
334
|
raise ErrInfo(
|
|
321
335
|
type="exception",
|
|
322
336
|
exception_msg=exception_desc(),
|
|
323
337
|
other_msg=f"Can't import from file to table {sq_name}",
|
|
324
|
-
)
|
|
338
|
+
) from e
|
|
325
339
|
else:
|
|
326
340
|
data_indexes = [csv_file_cols_q.index(col) for col in import_cols]
|
|
327
341
|
paramspec = ",".join(["%s"] * len(import_cols))
|
|
@@ -398,14 +412,14 @@ class PostgresDatabase(Database):
|
|
|
398
412
|
curs.executemany(sql_template, b)
|
|
399
413
|
except ErrInfo:
|
|
400
414
|
raise
|
|
401
|
-
except Exception:
|
|
415
|
+
except Exception as e:
|
|
402
416
|
self.rollback()
|
|
403
417
|
raise ErrInfo(
|
|
404
418
|
type="db",
|
|
405
419
|
command_text=sql_template,
|
|
406
420
|
exception_msg=exception_desc(),
|
|
407
421
|
other_msg=f"Can't load data into table {sq_name} of {self.name()} from line {{{line}}}",
|
|
408
|
-
)
|
|
422
|
+
) from e
|
|
409
423
|
total_rows += len(b)
|
|
410
424
|
interval = _state.conf.import_progress_interval
|
|
411
425
|
if _state.exec_log and interval > 0 and total_rows % interval == 0:
|
|
@@ -431,5 +445,6 @@ class PostgresDatabase(Database):
|
|
|
431
445
|
with open(file_name, "rb") as f:
|
|
432
446
|
filedata = f.read()
|
|
433
447
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
434
|
-
|
|
448
|
+
quoted_col = self.quote_identifier(column_name)
|
|
449
|
+
sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
|
|
435
450
|
self.cursor().execute(sql, (psycopg2.Binary(filedata),))
|