execsql2 2.0.1__py3-none-any.whl → 2.1.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/cli.py +322 -108
- execsql/config.py +134 -114
- execsql/db/access.py +89 -65
- execsql/db/base.py +97 -68
- execsql/db/dsn.py +45 -29
- execsql/db/duckdb.py +4 -5
- execsql/db/factory.py +27 -27
- execsql/db/firebird.py +30 -18
- execsql/db/mysql.py +38 -14
- execsql/db/oracle.py +58 -33
- execsql/db/postgres.py +68 -28
- execsql/db/sqlite.py +36 -27
- execsql/db/sqlserver.py +45 -30
- execsql/exceptions.py +68 -64
- execsql/exporters/__init__.py +1 -1
- execsql/exporters/base.py +42 -17
- execsql/exporters/delimited.py +60 -59
- execsql/exporters/duckdb.py +8 -12
- execsql/exporters/feather.py +32 -24
- execsql/exporters/html.py +33 -30
- execsql/exporters/json.py +18 -17
- execsql/exporters/latex.py +11 -13
- execsql/exporters/ods.py +50 -46
- execsql/exporters/parquet.py +32 -0
- execsql/exporters/pretty.py +16 -15
- execsql/exporters/raw.py +9 -11
- execsql/exporters/sqlite.py +38 -38
- execsql/exporters/templates.py +15 -72
- execsql/exporters/values.py +13 -12
- execsql/exporters/xls.py +26 -26
- execsql/exporters/xml.py +12 -12
- execsql/exporters/zip.py +0 -3
- execsql/gui/__init__.py +2 -2
- execsql/gui/console.py +0 -1
- execsql/gui/desktop.py +6 -7
- execsql/gui/tui.py +8 -14
- execsql/importers/base.py +6 -9
- execsql/importers/csv.py +10 -17
- execsql/importers/feather.py +16 -22
- execsql/importers/ods.py +3 -4
- execsql/importers/xls.py +5 -6
- execsql/metacommands/__init__.py +8 -8
- execsql/metacommands/conditions.py +41 -33
- execsql/metacommands/connect.py +113 -99
- execsql/metacommands/control.py +38 -26
- execsql/metacommands/data.py +35 -33
- execsql/metacommands/debug.py +13 -9
- execsql/metacommands/io.py +288 -229
- execsql/metacommands/prompt.py +179 -157
- execsql/metacommands/script_ext.py +11 -9
- execsql/metacommands/system.py +44 -25
- execsql/models.py +9 -16
- execsql/parser.py +10 -10
- execsql/script.py +183 -157
- execsql/state.py +170 -208
- execsql/types.py +46 -81
- execsql/utils/auth.py +114 -14
- execsql/utils/crypto.py +31 -4
- execsql/utils/datetime.py +7 -7
- execsql/utils/errors.py +34 -29
- execsql/utils/fileio.py +90 -55
- execsql/utils/gui.py +22 -23
- execsql/utils/mail.py +15 -17
- execsql/utils/numeric.py +2 -3
- execsql/utils/regex.py +9 -12
- execsql/utils/strings.py +10 -12
- execsql/utils/timer.py +0 -2
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/execsql.conf +1 -1
- execsql2-2.1.2.dist-info/METADATA +300 -0
- execsql2-2.1.2.dist-info/RECORD +96 -0
- execsql2-2.0.1.dist-info/METADATA +0 -406
- execsql2-2.0.1.dist-info/RECORD +0 -95
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/READ_ME.rst +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/WHEEL +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/entry_points.txt +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/NOTICE +0 -0
execsql/db/firebird.py
CHANGED
|
@@ -7,12 +7,11 @@ Implements :class:`FirebirdDatabase`, which connects to Firebird databases
|
|
|
7
7
|
via the ``firebird-driver`` package. Corresponds to ``-t f`` on the CLI.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from typing import List, Optional
|
|
11
10
|
|
|
12
11
|
from execsql.db.base import Database
|
|
13
12
|
from execsql.exceptions import ErrInfo
|
|
14
13
|
from execsql.utils.errors import exception_desc, fatal_error
|
|
15
|
-
from execsql.utils.auth import get_password
|
|
14
|
+
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
16
15
|
import execsql.state as _state
|
|
17
16
|
|
|
18
17
|
|
|
@@ -21,11 +20,11 @@ class FirebirdDatabase(Database):
|
|
|
21
20
|
self,
|
|
22
21
|
server_name: str,
|
|
23
22
|
db_name: str,
|
|
24
|
-
user_name:
|
|
23
|
+
user_name: str | None,
|
|
25
24
|
need_passwd: bool = False,
|
|
26
|
-
port:
|
|
27
|
-
encoding:
|
|
28
|
-
password:
|
|
25
|
+
port: int | None = 3050,
|
|
26
|
+
encoding: str | None = "latin1",
|
|
27
|
+
password: str | None = None,
|
|
29
28
|
) -> None:
|
|
30
29
|
try:
|
|
31
30
|
import fdb as firebird_lib # noqa: F401
|
|
@@ -41,7 +40,7 @@ class FirebirdDatabase(Database):
|
|
|
41
40
|
self.user = str(user_name)
|
|
42
41
|
self.need_passwd = need_passwd
|
|
43
42
|
self.password = password
|
|
44
|
-
self.port =
|
|
43
|
+
self.port = port if port else 3050
|
|
45
44
|
self.encoding = encoding or "latin1"
|
|
46
45
|
self.encode_commands = True
|
|
47
46
|
self.paramstr = "?"
|
|
@@ -85,7 +84,21 @@ class FirebirdDatabase(Database):
|
|
|
85
84
|
self.user,
|
|
86
85
|
server_name=self.server_name,
|
|
87
86
|
)
|
|
88
|
-
|
|
87
|
+
try:
|
|
88
|
+
self.conn = db_conn()
|
|
89
|
+
except Exception:
|
|
90
|
+
if not password_from_keyring():
|
|
91
|
+
raise
|
|
92
|
+
clear_stored_password("Firebird", self.db_name, self.user, self.server_name)
|
|
93
|
+
self.password = get_password(
|
|
94
|
+
"Firebird",
|
|
95
|
+
self.db_name,
|
|
96
|
+
self.user,
|
|
97
|
+
server_name=self.server_name,
|
|
98
|
+
skip_keyring=True,
|
|
99
|
+
other_msg="(stored credential failed — enter current password)",
|
|
100
|
+
)
|
|
101
|
+
self.conn = db_conn()
|
|
89
102
|
except SystemExit:
|
|
90
103
|
# If the user canceled the password prompt.
|
|
91
104
|
raise
|
|
@@ -106,7 +119,7 @@ class FirebirdDatabase(Database):
|
|
|
106
119
|
raise
|
|
107
120
|
_state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
|
|
108
121
|
|
|
109
|
-
def table_exists(self, table_name: str, schema_name:
|
|
122
|
+
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
110
123
|
curs = self.cursor()
|
|
111
124
|
sql = (
|
|
112
125
|
f"SELECT RDB$RELATION_NAME FROM RDB$RELATIONS "
|
|
@@ -118,17 +131,16 @@ class FirebirdDatabase(Database):
|
|
|
118
131
|
except ErrInfo:
|
|
119
132
|
raise
|
|
120
133
|
except Exception:
|
|
121
|
-
|
|
134
|
+
try:
|
|
135
|
+
self.rollback()
|
|
136
|
+
except Exception:
|
|
137
|
+
pass # Rollback is best-effort after a failed query.
|
|
138
|
+
raise ErrInfo(
|
|
122
139
|
type="db",
|
|
123
140
|
command_text=sql,
|
|
124
141
|
exception_msg=exception_desc(),
|
|
125
142
|
other_msg=f"Failed test for existence of Firebird table {table_name}",
|
|
126
143
|
)
|
|
127
|
-
try:
|
|
128
|
-
self.rollback()
|
|
129
|
-
except Exception:
|
|
130
|
-
pass
|
|
131
|
-
raise e
|
|
132
144
|
rows = curs.fetchall()
|
|
133
145
|
self.conn.commit()
|
|
134
146
|
curs.close()
|
|
@@ -138,7 +150,7 @@ class FirebirdDatabase(Database):
|
|
|
138
150
|
self,
|
|
139
151
|
table_name: str,
|
|
140
152
|
column_name: str,
|
|
141
|
-
schema_name:
|
|
153
|
+
schema_name: str | None = None,
|
|
142
154
|
) -> bool:
|
|
143
155
|
curs = self.cursor()
|
|
144
156
|
sql = f"select first 1 {column_name} from {table_name};"
|
|
@@ -148,7 +160,7 @@ class FirebirdDatabase(Database):
|
|
|
148
160
|
return False
|
|
149
161
|
return True
|
|
150
162
|
|
|
151
|
-
def table_columns(self, table_name: str, schema_name:
|
|
163
|
+
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
152
164
|
curs = self.cursor()
|
|
153
165
|
sql = f"select first 1 * from {table_name};"
|
|
154
166
|
try:
|
|
@@ -165,7 +177,7 @@ class FirebirdDatabase(Database):
|
|
|
165
177
|
)
|
|
166
178
|
return [d[0] for d in curs.description]
|
|
167
179
|
|
|
168
|
-
def view_exists(self, view_name: str, schema_name:
|
|
180
|
+
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
169
181
|
curs = self.cursor()
|
|
170
182
|
sql = f"select distinct rdb$view_name from rdb$view_relations where rdb$view_name = '{view_name}';"
|
|
171
183
|
try:
|
execsql/db/mysql.py
CHANGED
|
@@ -8,12 +8,12 @@ servers via ``pymysql``. Corresponds to ``-t m`` on the CLI.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import re
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
12
12
|
|
|
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 get_password
|
|
16
|
+
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
17
17
|
import execsql.state as _state
|
|
18
18
|
|
|
19
19
|
|
|
@@ -22,11 +22,11 @@ class MySQLDatabase(Database):
|
|
|
22
22
|
self,
|
|
23
23
|
server_name: str,
|
|
24
24
|
db_name: str,
|
|
25
|
-
user_name:
|
|
25
|
+
user_name: str | None,
|
|
26
26
|
need_passwd: bool = False,
|
|
27
|
-
port:
|
|
28
|
-
encoding:
|
|
29
|
-
password:
|
|
27
|
+
port: int | None = 3306,
|
|
28
|
+
encoding: str | None = "latin1",
|
|
29
|
+
password: str | None = None,
|
|
30
30
|
) -> None:
|
|
31
31
|
try:
|
|
32
32
|
import pymysql as mysql_lib # noqa: F401
|
|
@@ -42,7 +42,7 @@ class MySQLDatabase(Database):
|
|
|
42
42
|
self.user = str(user_name)
|
|
43
43
|
self.need_passwd = need_passwd
|
|
44
44
|
self.password = password
|
|
45
|
-
self.port =
|
|
45
|
+
self.port = port if port else 3306
|
|
46
46
|
self.encoding = encoding or "latin1"
|
|
47
47
|
self.encode_commands = True
|
|
48
48
|
self.paramstr = "%s"
|
|
@@ -88,7 +88,21 @@ class MySQLDatabase(Database):
|
|
|
88
88
|
self.user,
|
|
89
89
|
server_name=self.server_name,
|
|
90
90
|
)
|
|
91
|
-
|
|
91
|
+
try:
|
|
92
|
+
self.conn = db_conn()
|
|
93
|
+
except Exception:
|
|
94
|
+
if not password_from_keyring():
|
|
95
|
+
raise
|
|
96
|
+
clear_stored_password("MySQL", self.db_name, self.user, self.server_name)
|
|
97
|
+
self.password = get_password(
|
|
98
|
+
"MySQL",
|
|
99
|
+
self.db_name,
|
|
100
|
+
self.user,
|
|
101
|
+
server_name=self.server_name,
|
|
102
|
+
skip_keyring=True,
|
|
103
|
+
other_msg="(stored credential failed — enter current password)",
|
|
104
|
+
)
|
|
105
|
+
self.conn = db_conn()
|
|
92
106
|
self.execute("set session sql_mode='ANSI';")
|
|
93
107
|
except SystemExit:
|
|
94
108
|
# If the user canceled the password prompt.
|
|
@@ -126,7 +140,7 @@ class MySQLDatabase(Database):
|
|
|
126
140
|
|
|
127
141
|
def import_tabular_file(
|
|
128
142
|
self,
|
|
129
|
-
schema_name:
|
|
143
|
+
schema_name: str | None,
|
|
130
144
|
table_name: str,
|
|
131
145
|
csv_file_obj: Any,
|
|
132
146
|
skipheader: bool,
|
|
@@ -193,9 +207,10 @@ class MySQLDatabase(Database):
|
|
|
193
207
|
next(f)
|
|
194
208
|
curs = self.cursor()
|
|
195
209
|
eof = False
|
|
210
|
+
total_rows = 0
|
|
196
211
|
while True:
|
|
197
212
|
b: list = []
|
|
198
|
-
for
|
|
213
|
+
for _j in range(_state.conf.import_row_buffer):
|
|
199
214
|
try:
|
|
200
215
|
line = next(f)
|
|
201
216
|
except StopIteration:
|
|
@@ -244,15 +259,14 @@ class MySQLDatabase(Database):
|
|
|
244
259
|
" ",
|
|
245
260
|
line[i],
|
|
246
261
|
)
|
|
247
|
-
if not _state.conf.empty_strings:
|
|
248
|
-
|
|
249
|
-
line[i] = None
|
|
262
|
+
if not _state.conf.empty_strings and line[i].strip() == "":
|
|
263
|
+
line[i] = None
|
|
250
264
|
# Pad short line with nulls
|
|
251
265
|
line.extend([None] * (len(import_cols) - len(line)))
|
|
252
266
|
linedata = [line[ix] for ix in data_indexes]
|
|
253
267
|
add_line = True
|
|
254
268
|
if not _state.conf.empty_rows:
|
|
255
|
-
add_line = not all(
|
|
269
|
+
add_line = not all(c is None for c in linedata)
|
|
256
270
|
if add_line:
|
|
257
271
|
b.append(linedata)
|
|
258
272
|
if len(b) > 0:
|
|
@@ -268,5 +282,15 @@ class MySQLDatabase(Database):
|
|
|
268
282
|
exception_msg=exception_desc(),
|
|
269
283
|
other_msg=f"Import from file into table {sq_name}, line {{{line}}}",
|
|
270
284
|
)
|
|
285
|
+
total_rows += len(b)
|
|
286
|
+
interval = _state.conf.import_progress_interval
|
|
287
|
+
if _state.exec_log and interval > 0 and total_rows % interval == 0:
|
|
288
|
+
_state.exec_log.log_status_info(
|
|
289
|
+
f"IMPORT into {sq_name}: {total_rows} rows imported so far.",
|
|
290
|
+
)
|
|
271
291
|
if eof:
|
|
272
292
|
break
|
|
293
|
+
if _state.exec_log:
|
|
294
|
+
_state.exec_log.log_status_info(
|
|
295
|
+
f"IMPORT into {sq_name} complete: {total_rows} rows imported.",
|
|
296
|
+
)
|
execsql/db/oracle.py
CHANGED
|
@@ -8,12 +8,12 @@ via the ``oracledb`` driver (python-oracledb). Corresponds to ``-t o``
|
|
|
8
8
|
on the CLI.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
12
12
|
|
|
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 get_password
|
|
16
|
+
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
17
17
|
import execsql.state as _state
|
|
18
18
|
|
|
19
19
|
|
|
@@ -22,11 +22,11 @@ class OracleDatabase(Database):
|
|
|
22
22
|
self,
|
|
23
23
|
server_name: str,
|
|
24
24
|
db_name: str,
|
|
25
|
-
user_name:
|
|
25
|
+
user_name: str | None,
|
|
26
26
|
need_passwd: bool = False,
|
|
27
|
-
port:
|
|
28
|
-
encoding:
|
|
29
|
-
password:
|
|
27
|
+
port: int | None = 5432,
|
|
28
|
+
encoding: str | None = "UTF8",
|
|
29
|
+
password: str | None = None,
|
|
30
30
|
) -> None:
|
|
31
31
|
try:
|
|
32
32
|
import cx_Oracle # noqa: F401
|
|
@@ -75,7 +75,21 @@ class OracleDatabase(Database):
|
|
|
75
75
|
self.user,
|
|
76
76
|
server_name=self.server_name,
|
|
77
77
|
)
|
|
78
|
-
|
|
78
|
+
try:
|
|
79
|
+
self.conn = db_conn(self, self.db_name)
|
|
80
|
+
except Exception:
|
|
81
|
+
if not password_from_keyring():
|
|
82
|
+
raise
|
|
83
|
+
clear_stored_password("Oracle", self.db_name, self.user, self.server_name)
|
|
84
|
+
self.password = get_password(
|
|
85
|
+
"Oracle",
|
|
86
|
+
self.db_name,
|
|
87
|
+
self.user,
|
|
88
|
+
server_name=self.server_name,
|
|
89
|
+
skip_keyring=True,
|
|
90
|
+
other_msg="(stored credential failed — enter current password)",
|
|
91
|
+
)
|
|
92
|
+
self.conn = db_conn(self, self.db_name)
|
|
79
93
|
except SystemExit:
|
|
80
94
|
# If the user canceled the password prompt.
|
|
81
95
|
raise
|
|
@@ -85,14 +99,14 @@ class OracleDatabase(Database):
|
|
|
85
99
|
msg = f"Failed to open Oracle database {self.db_name} on {self.server_name}"
|
|
86
100
|
raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg)
|
|
87
101
|
|
|
88
|
-
def execute(self, sql: Any, paramlist:
|
|
102
|
+
def execute(self, sql: Any, paramlist: list | None = None) -> None:
|
|
89
103
|
# Strip any semicolon off the end and pass to the parent method.
|
|
90
104
|
if sql[-1:] == ";":
|
|
91
105
|
super().execute(sql[:-1], paramlist)
|
|
92
106
|
else:
|
|
93
107
|
super().execute(sql, paramlist)
|
|
94
108
|
|
|
95
|
-
def select_data(self, sql: str) ->
|
|
109
|
+
def select_data(self, sql: str) -> tuple[list[str], list]:
|
|
96
110
|
if sql[-1:] == ";":
|
|
97
111
|
return super().select_data(sql[:-1])
|
|
98
112
|
else:
|
|
@@ -115,12 +129,16 @@ class OracleDatabase(Database):
|
|
|
115
129
|
|
|
116
130
|
raise DatabaseNotImplementedError(self.name(), "schema_exists")
|
|
117
131
|
|
|
118
|
-
def table_exists(self, table_name: str, schema_name:
|
|
132
|
+
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
119
133
|
curs = self.cursor()
|
|
120
|
-
|
|
121
|
-
|
|
134
|
+
params = {"tname": table_name}
|
|
135
|
+
owner_clause = ""
|
|
136
|
+
if schema_name:
|
|
137
|
+
owner_clause = " and owner = :owner"
|
|
138
|
+
params["owner"] = schema_name
|
|
139
|
+
sql = f"select table_name from sys.all_tables where table_name = :tname{owner_clause}"
|
|
122
140
|
try:
|
|
123
|
-
curs.execute(sql)
|
|
141
|
+
curs.execute(sql, params)
|
|
124
142
|
except ErrInfo:
|
|
125
143
|
raise
|
|
126
144
|
except Exception:
|
|
@@ -139,17 +157,17 @@ class OracleDatabase(Database):
|
|
|
139
157
|
self,
|
|
140
158
|
table_name: str,
|
|
141
159
|
column_name: str,
|
|
142
|
-
schema_name:
|
|
160
|
+
schema_name: str | None = None,
|
|
143
161
|
) -> bool:
|
|
144
162
|
curs = self.cursor()
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
params = {"tname": table_name, "cname": column_name}
|
|
164
|
+
owner_clause = ""
|
|
165
|
+
if schema_name:
|
|
166
|
+
owner_clause = " and owner = :owner"
|
|
167
|
+
params["owner"] = schema_name
|
|
168
|
+
sql = f"select column_name from all_tab_columns where table_name=:tname{owner_clause} and column_name=:cname"
|
|
151
169
|
try:
|
|
152
|
-
curs.execute(sql)
|
|
170
|
+
curs.execute(sql, params)
|
|
153
171
|
except ErrInfo:
|
|
154
172
|
raise
|
|
155
173
|
except Exception:
|
|
@@ -164,14 +182,16 @@ class OracleDatabase(Database):
|
|
|
164
182
|
curs.close()
|
|
165
183
|
return len(rows) > 0
|
|
166
184
|
|
|
167
|
-
def table_columns(self, table_name: str, schema_name:
|
|
185
|
+
def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
|
|
168
186
|
curs = self.cursor()
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
187
|
+
params = {"tname": table_name}
|
|
188
|
+
owner_clause = ""
|
|
189
|
+
if schema_name:
|
|
190
|
+
owner_clause = " and owner=:owner"
|
|
191
|
+
params["owner"] = schema_name
|
|
192
|
+
sql = f"select column_name from all_tab_columns where table_name=:tname{owner_clause} order by column_id"
|
|
173
193
|
try:
|
|
174
|
-
curs.execute(sql)
|
|
194
|
+
curs.execute(sql, params)
|
|
175
195
|
except ErrInfo:
|
|
176
196
|
raise
|
|
177
197
|
except Exception:
|
|
@@ -186,12 +206,16 @@ class OracleDatabase(Database):
|
|
|
186
206
|
curs.close()
|
|
187
207
|
return [row[0] for row in rows]
|
|
188
208
|
|
|
189
|
-
def view_exists(self, view_name: str, schema_name:
|
|
209
|
+
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
190
210
|
curs = self.cursor()
|
|
191
|
-
|
|
192
|
-
|
|
211
|
+
params = {"vname": view_name}
|
|
212
|
+
owner_clause = ""
|
|
213
|
+
if schema_name:
|
|
214
|
+
owner_clause = " and owner = :owner"
|
|
215
|
+
params["owner"] = schema_name
|
|
216
|
+
sql = f"select view_name from sys.all_views where view_name = :vname{owner_clause}"
|
|
193
217
|
try:
|
|
194
|
-
curs.execute(sql)
|
|
218
|
+
curs.execute(sql, params)
|
|
195
219
|
except ErrInfo:
|
|
196
220
|
raise
|
|
197
221
|
except Exception:
|
|
@@ -209,8 +233,9 @@ class OracleDatabase(Database):
|
|
|
209
233
|
def role_exists(self, rolename: str) -> bool:
|
|
210
234
|
curs = self.cursor()
|
|
211
235
|
curs.execute(
|
|
212
|
-
|
|
213
|
-
|
|
236
|
+
"select role from dba_roles where role = :rname union "
|
|
237
|
+
" select username from all_users where username = :rname",
|
|
238
|
+
{"rname": rolename},
|
|
214
239
|
)
|
|
215
240
|
rows = curs.fetchall()
|
|
216
241
|
curs.close()
|
execsql/db/postgres.py
CHANGED
|
@@ -9,29 +9,32 @@ supporting schema-qualified tables, server-side ``COPY``, ``LISTEN``/
|
|
|
9
9
|
``-t p`` on the CLI.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
import io
|
|
13
12
|
import re
|
|
14
|
-
from typing import Any
|
|
13
|
+
from typing import Any
|
|
15
14
|
|
|
16
15
|
from execsql.db.base import Database
|
|
17
16
|
from execsql.exceptions import ErrInfo
|
|
18
17
|
from execsql.utils.errors import exception_desc, fatal_error
|
|
19
|
-
from execsql.utils.auth import get_password
|
|
18
|
+
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
20
19
|
from execsql.utils.strings import encodings_match
|
|
21
20
|
import execsql.state as _state
|
|
22
21
|
|
|
23
22
|
|
|
23
|
+
DEFAULT_CONNECT_TIMEOUT = 30 # seconds
|
|
24
|
+
|
|
25
|
+
|
|
24
26
|
class PostgresDatabase(Database):
|
|
25
27
|
def __init__(
|
|
26
28
|
self,
|
|
27
29
|
server_name: str,
|
|
28
30
|
db_name: str,
|
|
29
|
-
user_name:
|
|
31
|
+
user_name: str | None,
|
|
30
32
|
need_passwd: bool = False,
|
|
31
|
-
port:
|
|
33
|
+
port: int | None = 5432,
|
|
32
34
|
new_db: bool = False,
|
|
33
|
-
encoding:
|
|
34
|
-
password:
|
|
35
|
+
encoding: str | None = "UTF8",
|
|
36
|
+
password: str | None = None,
|
|
37
|
+
connect_timeout: int = DEFAULT_CONNECT_TIMEOUT,
|
|
35
38
|
) -> None:
|
|
36
39
|
try:
|
|
37
40
|
import psycopg2 # noqa: F401
|
|
@@ -52,6 +55,7 @@ class PostgresDatabase(Database):
|
|
|
52
55
|
self.encoding = encoding or "UTF8"
|
|
53
56
|
self.encode_commands = False
|
|
54
57
|
self.paramstr = "%s"
|
|
58
|
+
self.connect_timeout = connect_timeout
|
|
55
59
|
self.conn = None
|
|
56
60
|
self.autocommit = True
|
|
57
61
|
self.open_db()
|
|
@@ -74,12 +78,14 @@ class PostgresDatabase(Database):
|
|
|
74
78
|
port=db.port,
|
|
75
79
|
user=db.user,
|
|
76
80
|
password=db.password,
|
|
81
|
+
connect_timeout=db.connect_timeout,
|
|
77
82
|
)
|
|
78
83
|
else:
|
|
79
84
|
return psycopg2.connect(
|
|
80
85
|
host=str(db.server_name),
|
|
81
86
|
database=db_name,
|
|
82
87
|
port=db.port,
|
|
88
|
+
connect_timeout=db.connect_timeout,
|
|
83
89
|
)
|
|
84
90
|
except Exception:
|
|
85
91
|
msg = (
|
|
@@ -106,7 +112,22 @@ class PostgresDatabase(Database):
|
|
|
106
112
|
)
|
|
107
113
|
if self.new_db:
|
|
108
114
|
create_db(self)
|
|
109
|
-
|
|
115
|
+
try:
|
|
116
|
+
self.conn = db_conn(self, self.db_name)
|
|
117
|
+
except (ErrInfo, Exception):
|
|
118
|
+
if not password_from_keyring():
|
|
119
|
+
raise
|
|
120
|
+
# Stored credential is stale — clear it and re-prompt.
|
|
121
|
+
clear_stored_password("PostgreSQL", self.db_name, self.user, self.server_name)
|
|
122
|
+
self.password = get_password(
|
|
123
|
+
"PostgreSQL",
|
|
124
|
+
self.db_name,
|
|
125
|
+
self.user,
|
|
126
|
+
server_name=self.server_name,
|
|
127
|
+
skip_keyring=True,
|
|
128
|
+
other_msg="(stored credential failed — enter current password)",
|
|
129
|
+
)
|
|
130
|
+
self.conn = db_conn(self, self.db_name)
|
|
110
131
|
except SystemExit:
|
|
111
132
|
# If the user canceled the password prompt.
|
|
112
133
|
raise
|
|
@@ -131,24 +152,29 @@ class PostgresDatabase(Database):
|
|
|
131
152
|
|
|
132
153
|
def role_exists(self, rolename: str) -> bool:
|
|
133
154
|
curs = self.cursor()
|
|
134
|
-
curs.execute(
|
|
155
|
+
curs.execute("select rolname from pg_roles where rolname = %s;", (rolename,))
|
|
135
156
|
rows = curs.fetchall()
|
|
136
157
|
curs.close()
|
|
137
158
|
return len(rows) > 0
|
|
138
159
|
|
|
139
|
-
def table_exists(self, table_name: str, schema_name:
|
|
160
|
+
def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
|
|
140
161
|
curs = self.cursor()
|
|
141
162
|
if schema_name is not None:
|
|
142
|
-
|
|
143
|
-
|
|
163
|
+
params: list = [table_name]
|
|
164
|
+
schema_clause = ""
|
|
165
|
+
if schema_name:
|
|
166
|
+
schema_clause = " and table_schema=%s"
|
|
167
|
+
params.append(schema_name)
|
|
168
|
+
sql = f"select table_name from information_schema.tables where table_name = %s{schema_clause};"
|
|
144
169
|
else:
|
|
145
|
-
|
|
170
|
+
params = [table_name]
|
|
171
|
+
sql = """select table_name from information_schema.tables where table_name = %s and
|
|
146
172
|
\t table_schema in (select nspname from pg_namespace where oid = pg_my_temp_schema()
|
|
147
173
|
union
|
|
148
174
|
select trim(unnest(string_to_array(replace(setting, '"$user"', CURRENT_USER), ',')))
|
|
149
175
|
from pg_settings where name = 'search_path');"""
|
|
150
176
|
try:
|
|
151
|
-
curs.execute(sql)
|
|
177
|
+
curs.execute(sql, params)
|
|
152
178
|
except ErrInfo:
|
|
153
179
|
raise
|
|
154
180
|
except Exception:
|
|
@@ -163,19 +189,24 @@ class PostgresDatabase(Database):
|
|
|
163
189
|
curs.close()
|
|
164
190
|
return len(rows) > 0
|
|
165
191
|
|
|
166
|
-
def view_exists(self, view_name: str, schema_name:
|
|
192
|
+
def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
|
|
167
193
|
curs = self.cursor()
|
|
168
194
|
if schema_name is not None:
|
|
169
|
-
|
|
170
|
-
|
|
195
|
+
params: list = [view_name]
|
|
196
|
+
schema_clause = ""
|
|
197
|
+
if schema_name:
|
|
198
|
+
schema_clause = " and table_schema=%s"
|
|
199
|
+
params.append(schema_name)
|
|
200
|
+
sql = f"select table_name from information_schema.views where table_name = %s{schema_clause};"
|
|
171
201
|
else:
|
|
172
|
-
|
|
202
|
+
params = [view_name]
|
|
203
|
+
sql = """select table_name from information_schema.views where table_name = %s and
|
|
173
204
|
\t table_schema in (select nspname from pg_namespace where oid = pg_my_temp_schema()
|
|
174
205
|
union
|
|
175
206
|
select trim(unnest(string_to_array(replace(setting, '"$user"', CURRENT_USER), ',')))
|
|
176
207
|
from pg_settings where name = 'search_path');"""
|
|
177
208
|
try:
|
|
178
|
-
curs.execute(sql)
|
|
209
|
+
curs.execute(sql, params)
|
|
179
210
|
except ErrInfo:
|
|
180
211
|
raise
|
|
181
212
|
except Exception:
|
|
@@ -198,7 +229,7 @@ class PostgresDatabase(Database):
|
|
|
198
229
|
|
|
199
230
|
def import_tabular_file(
|
|
200
231
|
self,
|
|
201
|
-
schema_name:
|
|
232
|
+
schema_name: str | None,
|
|
202
233
|
table_name: str,
|
|
203
234
|
csv_file_obj: Any,
|
|
204
235
|
skipheader: bool,
|
|
@@ -254,7 +285,6 @@ class PostgresDatabase(Database):
|
|
|
254
285
|
input_enc = csv_file_obj.encoding.lower()
|
|
255
286
|
if input_enc in enc_xlates:
|
|
256
287
|
input_enc = enc_xlates[input_enc]
|
|
257
|
-
enc_match = encodings_match(csv_file_obj.encoding, self.encoding)
|
|
258
288
|
if (
|
|
259
289
|
encodings_match(input_enc, self.encoding)
|
|
260
290
|
and data_table_cols == csv_file_cols
|
|
@@ -301,9 +331,10 @@ class PostgresDatabase(Database):
|
|
|
301
331
|
next(f)
|
|
302
332
|
curs = self.cursor()
|
|
303
333
|
eof = False
|
|
334
|
+
total_rows = 0
|
|
304
335
|
while True:
|
|
305
336
|
b: list = []
|
|
306
|
-
for
|
|
337
|
+
for _j in range(_state.conf.import_row_buffer):
|
|
307
338
|
try:
|
|
308
339
|
line = next(f)
|
|
309
340
|
except StopIteration:
|
|
@@ -352,15 +383,14 @@ class PostgresDatabase(Database):
|
|
|
352
383
|
" ",
|
|
353
384
|
line[i],
|
|
354
385
|
)
|
|
355
|
-
if not _state.conf.empty_strings:
|
|
356
|
-
|
|
357
|
-
line[i] = None
|
|
386
|
+
if not _state.conf.empty_strings and line[i].strip() == "":
|
|
387
|
+
line[i] = None
|
|
358
388
|
# Pad short line with nulls
|
|
359
389
|
line.extend([None] * (len(import_cols) - len(line)))
|
|
360
390
|
linedata = [line[ix] for ix in data_indexes]
|
|
361
391
|
add_line = True
|
|
362
392
|
if not _state.conf.empty_rows:
|
|
363
|
-
add_line = not all(
|
|
393
|
+
add_line = not all(c is None for c in linedata)
|
|
364
394
|
if add_line:
|
|
365
395
|
b.append(linedata)
|
|
366
396
|
if len(b) > 0:
|
|
@@ -376,19 +406,29 @@ class PostgresDatabase(Database):
|
|
|
376
406
|
exception_msg=exception_desc(),
|
|
377
407
|
other_msg=f"Can't load data into table {sq_name} of {self.name()} from line {{{line}}}",
|
|
378
408
|
)
|
|
409
|
+
total_rows += len(b)
|
|
410
|
+
interval = _state.conf.import_progress_interval
|
|
411
|
+
if _state.exec_log and interval > 0 and total_rows % interval == 0:
|
|
412
|
+
_state.exec_log.log_status_info(
|
|
413
|
+
f"IMPORT into {sq_name}: {total_rows} rows imported so far.",
|
|
414
|
+
)
|
|
379
415
|
if eof:
|
|
380
416
|
break
|
|
417
|
+
if _state.exec_log:
|
|
418
|
+
_state.exec_log.log_status_info(
|
|
419
|
+
f"IMPORT into {sq_name} complete: {total_rows} rows imported.",
|
|
420
|
+
)
|
|
381
421
|
|
|
382
422
|
def import_entire_file(
|
|
383
423
|
self,
|
|
384
|
-
schema_name:
|
|
424
|
+
schema_name: str | None,
|
|
385
425
|
table_name: str,
|
|
386
426
|
column_name: str,
|
|
387
427
|
file_name: str,
|
|
388
428
|
) -> None:
|
|
389
429
|
import psycopg2
|
|
390
430
|
|
|
391
|
-
with
|
|
431
|
+
with open(file_name, "rb") as f:
|
|
392
432
|
filedata = f.read()
|
|
393
433
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
394
434
|
sql = f"insert into {sq_name} ({column_name}) values ({self.paramsubs(1)});"
|