execsql2 2.23.1__py3-none-any.whl → 2.25.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/api.py +169 -160
- execsql/cli/__init__.py +19 -586
- execsql/cli/application.py +309 -0
- execsql/cli/commands/__init__.py +13 -0
- execsql/cli/commands/config.py +205 -0
- execsql/cli/commands/format.py +100 -0
- execsql/cli/commands/init.py +135 -0
- execsql/cli/commands/lint.py +225 -0
- execsql/cli/commands/listing.py +81 -0
- execsql/cli/commands/run.py +459 -0
- execsql/cli/dispatch.py +66 -0
- execsql/cli/dsn.py +11 -4
- execsql/cli/help.py +175 -24
- execsql/cli/lint.py +780 -100
- execsql/cli/options.py +108 -0
- execsql/cli/run.py +96 -32
- execsql/config.py +256 -173
- execsql/config_validation.py +282 -0
- execsql/data/execsql.conf.template +38 -5
- execsql/data/script_header.sql +23 -0
- execsql/db/access.py +29 -8
- execsql/db/base.py +40 -8
- execsql/db/dsn.py +13 -4
- execsql/db/duckdb.py +3 -1
- execsql/db/factory.py +13 -4
- execsql/db/firebird.py +21 -7
- execsql/db/mysql.py +37 -16
- execsql/db/oracle.py +49 -16
- execsql/db/postgres.py +30 -6
- execsql/db/sqlite.py +7 -9
- execsql/db/sqlserver.py +25 -8
- execsql/debug/repl.py +51 -193
- execsql/exceptions.py +9 -0
- execsql/exporters/delimited.py +87 -28
- execsql/exporters/xlsx.py +24 -9
- execsql/exporters/zip.py +1 -2
- execsql/format.py +115 -76
- execsql/gui/desktop.py +1 -3
- execsql/gui/tui.py +2 -2
- execsql/importers/base.py +35 -1
- execsql/importers/csv.py +3 -1
- execsql/interactive.py +232 -0
- execsql/manifest.py +200 -0
- execsql/metacommands/conditions.py +7 -8
- execsql/metacommands/control.py +2 -3
- execsql/metacommands/data.py +4 -1
- execsql/metacommands/debug.py +2 -4
- execsql/metacommands/effects.py +124 -0
- execsql/metacommands/io_fileops.py +62 -37
- execsql/metacommands/io_import.py +0 -1
- execsql/metacommands/prompt.py +2 -0
- execsql/metacommands/upsert.py +5 -2
- execsql/models.py +3 -2
- execsql/parser.py +14 -5
- execsql/plugins.py +24 -0
- execsql/script/__init__.py +0 -9
- execsql/script/ast.py +1 -1
- execsql/script/engine.py +46 -104
- execsql/script/executor.py +162 -39
- execsql/script/parser.py +167 -118
- execsql/state.py +34 -11
- execsql/state.pyi +11 -0
- execsql/types.py +22 -13
- execsql/utils/auth.py +169 -24
- execsql/utils/color.py +18 -0
- execsql/utils/datetime.py +55 -17
- execsql/utils/errors.py +82 -10
- execsql/utils/fileio.py +404 -117
- execsql/utils/gui.py +3 -0
- execsql/utils/mail.py +37 -12
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/README.md +1 -1
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/METADATA +122 -63
- execsql2-2.25.0.dist-info/RECORD +140 -0
- execsql2-2.25.0.dist-info/entry_points.txt +2 -0
- execsql2-2.23.1.dist-info/RECORD +0 -124
- execsql2-2.23.1.dist-info/entry_points.txt +0 -3
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/WHEEL +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/licenses/NOTICE +0 -0
execsql/db/factory.py
CHANGED
|
@@ -64,7 +64,16 @@ def db_Postgres(
|
|
|
64
64
|
password: str | None = None,
|
|
65
65
|
) -> PostgresDatabase:
|
|
66
66
|
"""Open a new PostgreSQL connection via psycopg (psycopg3)."""
|
|
67
|
-
return PostgresDatabase(
|
|
67
|
+
return PostgresDatabase(
|
|
68
|
+
server_name,
|
|
69
|
+
database_name,
|
|
70
|
+
user,
|
|
71
|
+
pw_needed,
|
|
72
|
+
port,
|
|
73
|
+
new_db=new_db,
|
|
74
|
+
encoding=encoding,
|
|
75
|
+
password=password,
|
|
76
|
+
)
|
|
68
77
|
|
|
69
78
|
|
|
70
79
|
def db_SQLite(
|
|
@@ -84,7 +93,7 @@ def db_SQLite(
|
|
|
84
93
|
if not Path(sqlite_fn).exists():
|
|
85
94
|
raise ErrInfo(
|
|
86
95
|
type="error",
|
|
87
|
-
other_msg=f'SQLite database file "{sqlite_fn}" does not exist.',
|
|
96
|
+
other_msg=f'SQLite database file "{sqlite_fn}" does not exist. Use -n (--new-db) to create it.',
|
|
88
97
|
)
|
|
89
98
|
return SQLiteDatabase(sqlite_fn)
|
|
90
99
|
|
|
@@ -129,7 +138,7 @@ def db_DuckDB(
|
|
|
129
138
|
if not Path(duckdb_fn).exists():
|
|
130
139
|
raise ErrInfo(
|
|
131
140
|
type="error",
|
|
132
|
-
other_msg=f'DuckDB database file "{duckdb_fn}" does not exist.',
|
|
141
|
+
other_msg=f'DuckDB database file "{duckdb_fn}" does not exist. Use -n (--new-db) to create it.',
|
|
133
142
|
)
|
|
134
143
|
return DuckDBDatabase(duckdb_fn)
|
|
135
144
|
|
|
@@ -143,7 +152,7 @@ def db_Oracle(
|
|
|
143
152
|
encoding: str | None = None,
|
|
144
153
|
password: str | None = None,
|
|
145
154
|
) -> OracleDatabase:
|
|
146
|
-
"""Open an Oracle database connection via
|
|
155
|
+
"""Open an Oracle database connection via python-oracledb."""
|
|
147
156
|
return OracleDatabase(server_name, database_name, user, pw_needed, port, encoding, password=password)
|
|
148
157
|
|
|
149
158
|
|
execsql/db/firebird.py
CHANGED
|
@@ -12,7 +12,14 @@ from execsql.db.base import Database
|
|
|
12
12
|
from execsql.db.tiers import SupportTier
|
|
13
13
|
from execsql.exceptions import ErrInfo
|
|
14
14
|
from execsql.utils.errors import exception_desc, fatal_error
|
|
15
|
-
from execsql.utils.auth import
|
|
15
|
+
from execsql.utils.auth import (
|
|
16
|
+
begin_login,
|
|
17
|
+
clear_stored_password,
|
|
18
|
+
get_password,
|
|
19
|
+
is_login_failure,
|
|
20
|
+
password_from_keyring,
|
|
21
|
+
remember_password,
|
|
22
|
+
)
|
|
16
23
|
import execsql.state as _state
|
|
17
24
|
|
|
18
25
|
__all__ = ["FirebirdDatabase"]
|
|
@@ -54,7 +61,6 @@ class FirebirdDatabase(Database):
|
|
|
54
61
|
self.type = dbt_firebird
|
|
55
62
|
self.password = password
|
|
56
63
|
self.encoding = encoding or "latin1"
|
|
57
|
-
self.encode_commands = True
|
|
58
64
|
self.paramstr = "?"
|
|
59
65
|
self.open_db()
|
|
60
66
|
self.password = None # Clear cleartext password after successful connection
|
|
@@ -100,28 +106,34 @@ class FirebirdDatabase(Database):
|
|
|
100
106
|
|
|
101
107
|
if self.conn is None:
|
|
102
108
|
try:
|
|
109
|
+
begin_login()
|
|
103
110
|
if self.user and self.need_passwd and not self.password:
|
|
104
111
|
self.password = get_password(
|
|
105
112
|
"Firebird",
|
|
106
113
|
self.db_name,
|
|
107
114
|
self.user,
|
|
108
115
|
server_name=self.server_name,
|
|
116
|
+
port=self.port,
|
|
109
117
|
)
|
|
110
118
|
try:
|
|
111
119
|
self.conn = db_conn()
|
|
112
|
-
except Exception:
|
|
113
|
-
|
|
120
|
+
except Exception as e:
|
|
121
|
+
# Only a rejected password means the stored one is stale;
|
|
122
|
+
# a timeout or a missing database leaves it alone.
|
|
123
|
+
if not (password_from_keyring() and is_login_failure(e)):
|
|
114
124
|
raise
|
|
115
|
-
clear_stored_password("Firebird", self.db_name, self.user, self.server_name)
|
|
125
|
+
clear_stored_password("Firebird", self.db_name, self.user, self.server_name, port=self.port)
|
|
116
126
|
self.password = get_password(
|
|
117
127
|
"Firebird",
|
|
118
128
|
self.db_name,
|
|
119
129
|
self.user,
|
|
120
130
|
server_name=self.server_name,
|
|
131
|
+
port=self.port,
|
|
121
132
|
skip_keyring=True,
|
|
122
133
|
other_msg="(stored credential failed — enter current password)",
|
|
123
134
|
)
|
|
124
135
|
self.conn = db_conn()
|
|
136
|
+
remember_password("Firebird", self.db_name, self.user, self.server_name, port=self.port)
|
|
125
137
|
except SystemExit:
|
|
126
138
|
# If the user canceled the password prompt.
|
|
127
139
|
raise
|
|
@@ -143,6 +155,9 @@ class FirebirdDatabase(Database):
|
|
|
143
155
|
raise
|
|
144
156
|
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
145
157
|
|
|
158
|
+
# table_exists() without a schema looks only where CREATE TABLE puts a table: one namespace.
|
|
159
|
+
unqualified_lookup_is_scoped = True
|
|
160
|
+
|
|
146
161
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
147
162
|
"""Return True if the named table exists in the Firebird database."""
|
|
148
163
|
sql = (
|
|
@@ -243,8 +258,7 @@ class FirebirdDatabase(Database):
|
|
|
243
258
|
return len(rows) > 0
|
|
244
259
|
|
|
245
260
|
def drop_table(self, tablename: str) -> None:
|
|
246
|
-
"""Drop
|
|
261
|
+
"""Drop *tablename*, schema-qualified and quoted by the caller."""
|
|
247
262
|
# Firebird will thrown an error if there are foreign keys into the table.
|
|
248
|
-
tablename = self.type.quoted(tablename)
|
|
249
263
|
self.execute(f"DROP TABLE {tablename};")
|
|
250
264
|
self.conn.commit()
|
execsql/db/mysql.py
CHANGED
|
@@ -13,7 +13,14 @@ from typing import Any, cast
|
|
|
13
13
|
from execsql.db.base import Database
|
|
14
14
|
from execsql.exceptions import ErrInfo
|
|
15
15
|
from execsql.utils.errors import exception_desc, fatal_error
|
|
16
|
-
from execsql.utils.auth import
|
|
16
|
+
from execsql.utils.auth import (
|
|
17
|
+
begin_login,
|
|
18
|
+
clear_stored_password,
|
|
19
|
+
get_password,
|
|
20
|
+
is_login_failure,
|
|
21
|
+
password_from_keyring,
|
|
22
|
+
remember_password,
|
|
23
|
+
)
|
|
17
24
|
import execsql.state as _state
|
|
18
25
|
|
|
19
26
|
__all__ = ["MySQLDatabase"]
|
|
@@ -93,7 +100,6 @@ class MySQLDatabase(Database):
|
|
|
93
100
|
# Greek, or Cyrillic at all — it raises UnicodeEncodeError before the
|
|
94
101
|
# server is reached — so it silently capped what execsql could move.
|
|
95
102
|
self.encoding = encoding or "utf8mb4"
|
|
96
|
-
self.encode_commands = True
|
|
97
103
|
self.paramstr = "%s"
|
|
98
104
|
self.open_db()
|
|
99
105
|
self.password = None # Clear cleartext password after successful connection
|
|
@@ -149,6 +155,9 @@ class MySQLDatabase(Database):
|
|
|
149
155
|
"""Lowercase *name* when the server is case-insensitive (LCTN 1/2)."""
|
|
150
156
|
return name.lower() if self._lower_case_table_names() in (1, 2) else name
|
|
151
157
|
|
|
158
|
+
def current_schema(self) -> str | None:
|
|
159
|
+
return self._query_current_schema("select database()")
|
|
160
|
+
|
|
152
161
|
# NB: schema_exists is overridden below to return False unconditionally
|
|
153
162
|
# — MySQL's pre-existing behavior. The case-folding only matters for
|
|
154
163
|
# adapters where schema lookups are meaningful, which MySQL skips.
|
|
@@ -209,28 +218,34 @@ class MySQLDatabase(Database):
|
|
|
209
218
|
|
|
210
219
|
if self.conn is None:
|
|
211
220
|
try:
|
|
221
|
+
begin_login()
|
|
212
222
|
if self.user and self.need_passwd and not self.password:
|
|
213
223
|
self.password = get_password(
|
|
214
224
|
"MySQL",
|
|
215
225
|
self.db_name,
|
|
216
226
|
self.user,
|
|
217
227
|
server_name=self.server_name,
|
|
228
|
+
port=self.port,
|
|
218
229
|
)
|
|
219
230
|
try:
|
|
220
231
|
self.conn = db_conn()
|
|
221
|
-
except Exception:
|
|
222
|
-
|
|
232
|
+
except Exception as e:
|
|
233
|
+
# Only a rejected password means the stored one is stale;
|
|
234
|
+
# a timeout or a missing database leaves it alone.
|
|
235
|
+
if not (password_from_keyring() and is_login_failure(e)):
|
|
223
236
|
raise
|
|
224
|
-
clear_stored_password("MySQL", self.db_name, self.user, self.server_name)
|
|
237
|
+
clear_stored_password("MySQL", self.db_name, self.user, self.server_name, port=self.port)
|
|
225
238
|
self.password = get_password(
|
|
226
239
|
"MySQL",
|
|
227
240
|
self.db_name,
|
|
228
241
|
self.user,
|
|
229
242
|
server_name=self.server_name,
|
|
243
|
+
port=self.port,
|
|
230
244
|
skip_keyring=True,
|
|
231
245
|
other_msg="(stored credential failed — enter current password)",
|
|
232
246
|
)
|
|
233
247
|
self.conn = db_conn()
|
|
248
|
+
remember_password("MySQL", self.db_name, self.user, self.server_name, port=self.port)
|
|
234
249
|
self.execute("set session sql_mode='ANSI';")
|
|
235
250
|
except SystemExit:
|
|
236
251
|
# If the user canceled the password prompt.
|
|
@@ -306,8 +321,12 @@ class MySQLDatabase(Database):
|
|
|
306
321
|
)
|
|
307
322
|
import_cols = csv_file_cols
|
|
308
323
|
input_col_list = ",".join(import_cols)
|
|
324
|
+
# LOAD DATA is told the file has no escape character (ESCAPED BY ''),
|
|
325
|
+
# so a file where one was detected takes the row-by-row path, which
|
|
326
|
+
# reads the format that was detected.
|
|
309
327
|
if (
|
|
310
|
-
|
|
328
|
+
csv_file_obj.escapechar is None
|
|
329
|
+
and data_table_cols == csv_file_cols
|
|
311
330
|
and _state.conf.empty_strings
|
|
312
331
|
and _state.conf.empty_rows
|
|
313
332
|
and not _state.conf.del_empty_cols
|
|
@@ -315,19 +334,21 @@ class MySQLDatabase(Database):
|
|
|
315
334
|
and not _state.conf.trim_strings
|
|
316
335
|
and not _state.conf.replace_newlines
|
|
317
336
|
):
|
|
318
|
-
|
|
319
|
-
|
|
337
|
+
# String literals in this session treat a backslash as an escape,
|
|
338
|
+
# so the file name, delimiter and quote character are quoted with
|
|
339
|
+
# quote_literal, which doubles it.
|
|
340
|
+
import_sql = f"load data local infile {self.quote_literal(csv_file_obj.csvfname)} into table {sq_name}"
|
|
320
341
|
if csv_file_obj.encoding:
|
|
321
342
|
charset = _PYTHON_TO_MYSQL_CHARSET.get(csv_file_obj.encoding.lower(), csv_file_obj.encoding)
|
|
322
343
|
import_sql = f"{import_sql} character set {charset}"
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
344
|
+
import_sql = import_sql + " columns"
|
|
345
|
+
if csv_file_obj.delimiter:
|
|
346
|
+
import_sql = f"{import_sql} terminated by {self.quote_literal(csv_file_obj.delimiter)}"
|
|
347
|
+
if csv_file_obj.quotechar:
|
|
348
|
+
import_sql = f"{import_sql} optionally enclosed by {self.quote_literal(csv_file_obj.quotechar)}"
|
|
349
|
+
# MySQL's default ESCAPED BY '\\' turned C:\new into a newline and
|
|
350
|
+
# \N into NULL; every other backend keeps the text as written.
|
|
351
|
+
import_sql = f"{import_sql} escaped by ''"
|
|
331
352
|
# Tell LOAD DATA what ends a line. Its default is "\n", so a CRLF
|
|
332
353
|
# file left a carriage return on the last field of every row: the
|
|
333
354
|
# value was not empty, NULLIF below could not see it, and a date
|
execsql/db/oracle.py
CHANGED
|
@@ -15,14 +15,42 @@ from execsql.db.base import Database
|
|
|
15
15
|
from execsql.db.tiers import SupportTier
|
|
16
16
|
from execsql.exceptions import ErrInfo
|
|
17
17
|
from execsql.utils.errors import exception_desc, fatal_error
|
|
18
|
-
from execsql.utils.auth import
|
|
18
|
+
from execsql.utils.auth import (
|
|
19
|
+
begin_login,
|
|
20
|
+
clear_stored_password,
|
|
21
|
+
get_password,
|
|
22
|
+
is_login_failure,
|
|
23
|
+
password_from_keyring,
|
|
24
|
+
remember_password,
|
|
25
|
+
)
|
|
19
26
|
import execsql.state as _state
|
|
20
27
|
|
|
21
28
|
__all__ = ["OracleDatabase"]
|
|
22
29
|
|
|
23
30
|
|
|
31
|
+
def _oracle_driver() -> Any:
|
|
32
|
+
"""The Oracle driver module: python-oracledb, or else the legacy cx_Oracle.
|
|
33
|
+
|
|
34
|
+
``execsql2[oracle]`` installs oracledb. cx_Oracle, the driver it replaced
|
|
35
|
+
(same ``makedsn`` / ``connect`` calls), is used only when it is the one
|
|
36
|
+
installed. ``None`` if neither is.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
import oracledb
|
|
40
|
+
|
|
41
|
+
return oracledb
|
|
42
|
+
except ImportError:
|
|
43
|
+
pass
|
|
44
|
+
try:
|
|
45
|
+
import cx_Oracle
|
|
46
|
+
|
|
47
|
+
return cx_Oracle
|
|
48
|
+
except ImportError:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
24
52
|
class OracleDatabase(Database):
|
|
25
|
-
"""Oracle adapter using the
|
|
53
|
+
"""Oracle adapter using the python-oracledb driver."""
|
|
26
54
|
|
|
27
55
|
#: Oracle commits every DDL statement at once, together with everything
|
|
28
56
|
#: before it, including CREATE GLOBAL TEMPORARY TABLE.
|
|
@@ -46,11 +74,9 @@ class OracleDatabase(Database):
|
|
|
46
74
|
encoding: str | None = "UTF8",
|
|
47
75
|
password: str | None = None,
|
|
48
76
|
) -> None:
|
|
49
|
-
|
|
50
|
-
import cx_Oracle # noqa: F401
|
|
51
|
-
except Exception:
|
|
77
|
+
if _oracle_driver() is None:
|
|
52
78
|
fatal_error(
|
|
53
|
-
|
|
79
|
+
'The oracledb module is required to connect to Oracle. Install it with: pip install "execsql2[oracle]"',
|
|
54
80
|
)
|
|
55
81
|
from execsql.types import dbt_oracle
|
|
56
82
|
|
|
@@ -64,7 +90,6 @@ class OracleDatabase(Database):
|
|
|
64
90
|
self.type = dbt_oracle
|
|
65
91
|
self.password = password
|
|
66
92
|
self.encoding = encoding or "UTF8"
|
|
67
|
-
self.encode_commands = False
|
|
68
93
|
self.paramstr = ":1"
|
|
69
94
|
self.open_db()
|
|
70
95
|
self.password = None # Clear cleartext password after successful connection
|
|
@@ -77,39 +102,45 @@ class OracleDatabase(Database):
|
|
|
77
102
|
|
|
78
103
|
def open_db(self) -> None:
|
|
79
104
|
"""Open a connection to the Oracle database."""
|
|
80
|
-
|
|
105
|
+
driver = _oracle_driver()
|
|
81
106
|
|
|
82
107
|
def db_conn(db: OracleDatabase, db_name: str):
|
|
83
|
-
dsn =
|
|
108
|
+
dsn = driver.makedsn(db.server_name, db.port, service_name=db_name)
|
|
84
109
|
if db.user and db.password:
|
|
85
|
-
return
|
|
110
|
+
return driver.connect(user=db.user, password=db.password, dsn=dsn)
|
|
86
111
|
else:
|
|
87
|
-
return
|
|
112
|
+
return driver.connect(dsn=dsn)
|
|
88
113
|
|
|
89
114
|
if self.conn is None:
|
|
90
115
|
try:
|
|
116
|
+
begin_login()
|
|
91
117
|
if self.user and self.need_passwd and not self.password:
|
|
92
118
|
self.password = get_password(
|
|
93
119
|
"Oracle",
|
|
94
120
|
self.db_name,
|
|
95
121
|
self.user,
|
|
96
122
|
server_name=self.server_name,
|
|
123
|
+
port=self.port,
|
|
97
124
|
)
|
|
98
125
|
try:
|
|
99
126
|
self.conn = db_conn(self, self.db_name)
|
|
100
|
-
except Exception:
|
|
101
|
-
|
|
127
|
+
except Exception as e:
|
|
128
|
+
# Only a rejected password means the stored one is stale;
|
|
129
|
+
# a timeout or a missing database leaves it alone.
|
|
130
|
+
if not (password_from_keyring() and is_login_failure(e)):
|
|
102
131
|
raise
|
|
103
|
-
clear_stored_password("Oracle", self.db_name, self.user, self.server_name)
|
|
132
|
+
clear_stored_password("Oracle", self.db_name, self.user, self.server_name, port=self.port)
|
|
104
133
|
self.password = get_password(
|
|
105
134
|
"Oracle",
|
|
106
135
|
self.db_name,
|
|
107
136
|
self.user,
|
|
108
137
|
server_name=self.server_name,
|
|
138
|
+
port=self.port,
|
|
109
139
|
skip_keyring=True,
|
|
110
140
|
other_msg="(stored credential failed — enter current password)",
|
|
111
141
|
)
|
|
112
142
|
self.conn = db_conn(self, self.db_name)
|
|
143
|
+
remember_password("Oracle", self.db_name, self.user, self.server_name, port=self.port)
|
|
113
144
|
except SystemExit:
|
|
114
145
|
# If the user canceled the password prompt.
|
|
115
146
|
raise
|
|
@@ -153,6 +184,9 @@ class OracleDatabase(Database):
|
|
|
153
184
|
|
|
154
185
|
raise DatabaseNotImplementedError(self.name(), "schema_exists")
|
|
155
186
|
|
|
187
|
+
def current_schema(self) -> str | None:
|
|
188
|
+
return self._query_current_schema("select sys_context('USERENV', 'CURRENT_SCHEMA') from dual")
|
|
189
|
+
|
|
156
190
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
157
191
|
"""Return True if the named table exists in the Oracle database."""
|
|
158
192
|
# Oracle folds unquoted identifiers to uppercase; normalise so a lookup
|
|
@@ -288,8 +322,7 @@ class OracleDatabase(Database):
|
|
|
288
322
|
return len(rows) > 0
|
|
289
323
|
|
|
290
324
|
def drop_table(self, tablename: str) -> None:
|
|
291
|
-
"""Drop the
|
|
292
|
-
tablename = self.type.quoted(tablename)
|
|
325
|
+
"""Drop *tablename* (schema-qualified and quoted by the caller) with cascade constraints."""
|
|
293
326
|
self.execute(f"drop table {tablename} cascade constraints")
|
|
294
327
|
|
|
295
328
|
def paramsubs(self, paramcount: int) -> str:
|
execsql/db/postgres.py
CHANGED
|
@@ -16,7 +16,14 @@ from typing import Any
|
|
|
16
16
|
from execsql.db.base import Database
|
|
17
17
|
from execsql.exceptions import ErrInfo
|
|
18
18
|
from execsql.utils.errors import exception_desc, fatal_error
|
|
19
|
-
from execsql.utils.auth import
|
|
19
|
+
from execsql.utils.auth import (
|
|
20
|
+
begin_login,
|
|
21
|
+
clear_stored_password,
|
|
22
|
+
get_password,
|
|
23
|
+
is_login_failure,
|
|
24
|
+
password_from_keyring,
|
|
25
|
+
remember_password,
|
|
26
|
+
)
|
|
20
27
|
from execsql.utils.strings import encodings_match
|
|
21
28
|
import execsql.state as _state
|
|
22
29
|
|
|
@@ -60,7 +67,6 @@ class PostgresDatabase(Database):
|
|
|
60
67
|
self.type = dbt_postgres
|
|
61
68
|
self.password = password
|
|
62
69
|
self.new_db = new_db
|
|
63
|
-
self.encode_commands = False
|
|
64
70
|
self.paramstr = "%s"
|
|
65
71
|
self.connect_timeout = connect_timeout
|
|
66
72
|
self.open_db()
|
|
@@ -123,31 +129,37 @@ class PostgresDatabase(Database):
|
|
|
123
129
|
|
|
124
130
|
if self.conn is None:
|
|
125
131
|
try:
|
|
132
|
+
begin_login()
|
|
126
133
|
if self.user and self.need_passwd and not self.password:
|
|
127
134
|
self.password = get_password(
|
|
128
135
|
"PostgreSQL",
|
|
129
136
|
self.db_name,
|
|
130
137
|
self.user,
|
|
131
138
|
server_name=self.server_name,
|
|
139
|
+
port=self.port,
|
|
132
140
|
)
|
|
133
141
|
if self.new_db:
|
|
134
142
|
create_db(self)
|
|
135
143
|
try:
|
|
136
144
|
self.conn = db_conn(self, self.db_name)
|
|
137
|
-
except (ErrInfo, Exception):
|
|
138
|
-
|
|
145
|
+
except (ErrInfo, Exception) as e:
|
|
146
|
+
# Only a rejected password means the stored one is stale;
|
|
147
|
+
# a timeout or a missing database leaves it alone.
|
|
148
|
+
if not (password_from_keyring() and is_login_failure(e)):
|
|
139
149
|
raise
|
|
140
150
|
# Stored credential is stale — clear it and re-prompt.
|
|
141
|
-
clear_stored_password("PostgreSQL", self.db_name, self.user, self.server_name)
|
|
151
|
+
clear_stored_password("PostgreSQL", self.db_name, self.user, self.server_name, port=self.port)
|
|
142
152
|
self.password = get_password(
|
|
143
153
|
"PostgreSQL",
|
|
144
154
|
self.db_name,
|
|
145
155
|
self.user,
|
|
146
156
|
server_name=self.server_name,
|
|
157
|
+
port=self.port,
|
|
147
158
|
skip_keyring=True,
|
|
148
159
|
other_msg="(stored credential failed — enter current password)",
|
|
149
160
|
)
|
|
150
161
|
self.conn = db_conn(self, self.db_name)
|
|
162
|
+
remember_password("PostgreSQL", self.db_name, self.user, self.server_name, port=self.port)
|
|
151
163
|
except SystemExit:
|
|
152
164
|
# If the user canceled the password prompt.
|
|
153
165
|
raise
|
|
@@ -178,6 +190,9 @@ class PostgresDatabase(Database):
|
|
|
178
190
|
rows = curs.fetchall()
|
|
179
191
|
return len(rows) > 0
|
|
180
192
|
|
|
193
|
+
# table_exists() without a schema looks only where CREATE TABLE puts a table: it searches the temp schema and the search path.
|
|
194
|
+
unqualified_lookup_is_scoped = True
|
|
195
|
+
|
|
181
196
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
182
197
|
"""Return True if the named table exists in the PostgreSQL database."""
|
|
183
198
|
if schema_name is not None:
|
|
@@ -336,6 +351,8 @@ class PostgresDatabase(Database):
|
|
|
336
351
|
and not _state.conf.trim_strings
|
|
337
352
|
and not _state.conf.replace_newlines
|
|
338
353
|
):
|
|
354
|
+
from execsql.exporters.delimited import blank_lines_as_empty_rows
|
|
355
|
+
|
|
339
356
|
# Use Postgres' COPY FROM method via psycopg3's cursor.copy() context manager.
|
|
340
357
|
rf = csv_file_obj.open("rt")
|
|
341
358
|
if skipheader:
|
|
@@ -364,7 +381,14 @@ class PostgresDatabase(Database):
|
|
|
364
381
|
with self._cursor() as curs:
|
|
365
382
|
try:
|
|
366
383
|
with curs.copy(copy_cmd) as copy:
|
|
367
|
-
|
|
384
|
+
# COPY rejects a blank line; the row path reads it as an empty row.
|
|
385
|
+
for chunk in blank_lines_as_empty_rows(
|
|
386
|
+
rf,
|
|
387
|
+
delim,
|
|
388
|
+
csv_file_obj.quotechar,
|
|
389
|
+
len(csv_file_cols),
|
|
390
|
+
_state.conf.import_buffer,
|
|
391
|
+
):
|
|
368
392
|
copy.write(chunk)
|
|
369
393
|
except ErrInfo:
|
|
370
394
|
raise
|
execsql/db/sqlite.py
CHANGED
|
@@ -45,7 +45,6 @@ class SQLiteDatabase(Database):
|
|
|
45
45
|
encoding="UTF-8",
|
|
46
46
|
)
|
|
47
47
|
self.type = dbt_sqlite
|
|
48
|
-
self.encode_commands = False
|
|
49
48
|
self.timeout = timeout
|
|
50
49
|
self.open_db()
|
|
51
50
|
|
|
@@ -95,6 +94,9 @@ class SQLiteDatabase(Database):
|
|
|
95
94
|
self.rollback()
|
|
96
95
|
raise
|
|
97
96
|
|
|
97
|
+
# table_exists() without a schema looks only where CREATE TABLE puts a table: one namespace.
|
|
98
|
+
unqualified_lookup_is_scoped = True
|
|
99
|
+
|
|
98
100
|
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
99
101
|
"""Return True if the named table exists in the SQLite database."""
|
|
100
102
|
# SQLite stores names as-created; match case-insensitively so a lookup
|
|
@@ -170,8 +172,7 @@ class SQLiteDatabase(Database):
|
|
|
170
172
|
return False
|
|
171
173
|
|
|
172
174
|
def drop_table(self, tablename: str) -> None:
|
|
173
|
-
"""Drop
|
|
174
|
-
tablename = self.type.quoted(tablename)
|
|
175
|
+
"""Drop *tablename* (schema-qualified and quoted by the caller) if it exists."""
|
|
175
176
|
self.execute(f"drop table if exists {tablename};")
|
|
176
177
|
|
|
177
178
|
def populate_table(
|
|
@@ -237,14 +238,11 @@ class SQLiteDatabase(Database):
|
|
|
237
238
|
|
|
238
239
|
last_line: list[Any] = []
|
|
239
240
|
try:
|
|
240
|
-
for
|
|
241
|
+
for line in rowsource:
|
|
241
242
|
# Skip empty rows.
|
|
242
243
|
if not (len(line) == 1 and line[0] is None):
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
type="error",
|
|
246
|
-
other_msg=f"Too few values on data line {datalineno} of input.",
|
|
247
|
-
)
|
|
244
|
+
# Trailing values left off the line are NULL.
|
|
245
|
+
line = list(line) + [None] * (len(ts_colnames) - len(line))
|
|
248
246
|
if _state.conf.trim_strings or _state.conf.replace_newlines or not _state.conf.empty_strings:
|
|
249
247
|
for i in range(len(line)):
|
|
250
248
|
if line[i] is not None and isinstance(line[i], _state.stringtypes):
|
execsql/db/sqlserver.py
CHANGED
|
@@ -12,7 +12,14 @@ import re
|
|
|
12
12
|
from execsql.db.base import Database
|
|
13
13
|
from execsql.exceptions import ErrInfo
|
|
14
14
|
from execsql.utils.errors import fatal_error
|
|
15
|
-
from execsql.utils.auth import
|
|
15
|
+
from execsql.utils.auth import (
|
|
16
|
+
begin_login,
|
|
17
|
+
clear_stored_password,
|
|
18
|
+
get_password,
|
|
19
|
+
is_login_failure,
|
|
20
|
+
password_from_keyring,
|
|
21
|
+
remember_password,
|
|
22
|
+
)
|
|
16
23
|
import execsql.state as _state
|
|
17
24
|
|
|
18
25
|
__all__ = ["SqlServerDatabase"]
|
|
@@ -47,11 +54,13 @@ class SqlServerDatabase(Database):
|
|
|
47
54
|
self.type = dbt_sqlserver
|
|
48
55
|
self.password = password
|
|
49
56
|
self.encoding = encoding or "latin1" # Default on installation of SQL Server
|
|
50
|
-
self.encode_commands = True
|
|
51
57
|
self.paramstr = "?"
|
|
52
58
|
self.open_db()
|
|
53
59
|
self.password = None # Clear cleartext password after successful connection
|
|
54
60
|
|
|
61
|
+
def current_schema(self) -> str | None:
|
|
62
|
+
return self._query_current_schema("select schema_name()")
|
|
63
|
+
|
|
55
64
|
def __repr__(self) -> str:
|
|
56
65
|
return (
|
|
57
66
|
f"SqlServerDatabase({self.server_name!r}, {self.db_name!r}, {self.user!r}, "
|
|
@@ -75,12 +84,14 @@ class SqlServerDatabase(Database):
|
|
|
75
84
|
import pyodbc
|
|
76
85
|
|
|
77
86
|
if self.conn is None:
|
|
87
|
+
begin_login()
|
|
78
88
|
if self.user and self.need_passwd and not self.password:
|
|
79
89
|
self.password = get_password(
|
|
80
90
|
"SQL Server",
|
|
81
91
|
self.db_name,
|
|
82
92
|
self.user,
|
|
83
93
|
server_name=self.server_name,
|
|
94
|
+
port=self.port,
|
|
84
95
|
)
|
|
85
96
|
# Use pyodbc to connect. Try different driver versions from newest to oldest.
|
|
86
97
|
ssdrivers = (
|
|
@@ -94,6 +105,8 @@ class SqlServerDatabase(Database):
|
|
|
94
105
|
"SQL Server",
|
|
95
106
|
)
|
|
96
107
|
|
|
108
|
+
errors: list[BaseException] = []
|
|
109
|
+
|
|
97
110
|
def _try_drivers():
|
|
98
111
|
for drv in ssdrivers:
|
|
99
112
|
if self.user:
|
|
@@ -117,7 +130,8 @@ class SqlServerDatabase(Database):
|
|
|
117
130
|
# default; pyodbc treats `timeout` as the login-and-
|
|
118
131
|
# query timeout on the connection.
|
|
119
132
|
self.conn = pyodbc.connect(connstr, timeout=30)
|
|
120
|
-
except Exception:
|
|
133
|
+
except Exception as e:
|
|
134
|
+
errors.append(e)
|
|
121
135
|
if _state.exec_log is not None:
|
|
122
136
|
_state.exec_log.log_status_info(
|
|
123
137
|
f"Could not connect using: {re.sub(r'Pwd=[^;]*', 'Pwd=***', connstr)}",
|
|
@@ -130,18 +144,22 @@ class SqlServerDatabase(Database):
|
|
|
130
144
|
return True
|
|
131
145
|
return False
|
|
132
146
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
147
|
+
# Only a rejected password means the stored one is stale; a
|
|
148
|
+
# timeout or a missing database leaves it alone.
|
|
149
|
+
if not _try_drivers() and password_from_keyring() and any(is_login_failure(e) for e in errors):
|
|
150
|
+
clear_stored_password("SQL Server", self.db_name, self.user, self.server_name, port=self.port)
|
|
136
151
|
self.password = get_password(
|
|
137
152
|
"SQL Server",
|
|
138
153
|
self.db_name,
|
|
139
154
|
self.user,
|
|
140
155
|
server_name=self.server_name,
|
|
156
|
+
port=self.port,
|
|
141
157
|
skip_keyring=True,
|
|
142
158
|
other_msg="(stored credential failed — enter current password)",
|
|
143
159
|
)
|
|
144
160
|
_try_drivers()
|
|
161
|
+
if self.conn:
|
|
162
|
+
remember_password("SQL Server", self.db_name, self.user, self.server_name, port=self.port)
|
|
145
163
|
|
|
146
164
|
if not self.conn:
|
|
147
165
|
raise ErrInfo(
|
|
@@ -193,9 +211,8 @@ class SqlServerDatabase(Database):
|
|
|
193
211
|
return len(rows) > 0
|
|
194
212
|
|
|
195
213
|
def drop_table(self, tablename: str) -> None:
|
|
196
|
-
"""Drop
|
|
214
|
+
"""Drop *tablename*, schema-qualified and quoted by the caller."""
|
|
197
215
|
# SQL Server and Firebird will throw an error if there are foreign keys to the table.
|
|
198
|
-
tablename = self.type.quoted(tablename)
|
|
199
216
|
self.execute(f"drop table {tablename};")
|
|
200
217
|
|
|
201
218
|
def import_entire_file(
|