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.
Files changed (90) hide show
  1. execsql/cli.py +322 -108
  2. execsql/config.py +134 -114
  3. execsql/db/access.py +89 -65
  4. execsql/db/base.py +97 -68
  5. execsql/db/dsn.py +45 -29
  6. execsql/db/duckdb.py +4 -5
  7. execsql/db/factory.py +27 -27
  8. execsql/db/firebird.py +30 -18
  9. execsql/db/mysql.py +38 -14
  10. execsql/db/oracle.py +58 -33
  11. execsql/db/postgres.py +68 -28
  12. execsql/db/sqlite.py +36 -27
  13. execsql/db/sqlserver.py +45 -30
  14. execsql/exceptions.py +68 -64
  15. execsql/exporters/__init__.py +1 -1
  16. execsql/exporters/base.py +42 -17
  17. execsql/exporters/delimited.py +60 -59
  18. execsql/exporters/duckdb.py +8 -12
  19. execsql/exporters/feather.py +32 -24
  20. execsql/exporters/html.py +33 -30
  21. execsql/exporters/json.py +18 -17
  22. execsql/exporters/latex.py +11 -13
  23. execsql/exporters/ods.py +50 -46
  24. execsql/exporters/parquet.py +32 -0
  25. execsql/exporters/pretty.py +16 -15
  26. execsql/exporters/raw.py +9 -11
  27. execsql/exporters/sqlite.py +38 -38
  28. execsql/exporters/templates.py +15 -72
  29. execsql/exporters/values.py +13 -12
  30. execsql/exporters/xls.py +26 -26
  31. execsql/exporters/xml.py +12 -12
  32. execsql/exporters/zip.py +0 -3
  33. execsql/gui/__init__.py +2 -2
  34. execsql/gui/console.py +0 -1
  35. execsql/gui/desktop.py +6 -7
  36. execsql/gui/tui.py +8 -14
  37. execsql/importers/base.py +6 -9
  38. execsql/importers/csv.py +10 -17
  39. execsql/importers/feather.py +16 -22
  40. execsql/importers/ods.py +3 -4
  41. execsql/importers/xls.py +5 -6
  42. execsql/metacommands/__init__.py +8 -8
  43. execsql/metacommands/conditions.py +41 -33
  44. execsql/metacommands/connect.py +113 -99
  45. execsql/metacommands/control.py +38 -26
  46. execsql/metacommands/data.py +35 -33
  47. execsql/metacommands/debug.py +13 -9
  48. execsql/metacommands/io.py +288 -229
  49. execsql/metacommands/prompt.py +179 -157
  50. execsql/metacommands/script_ext.py +11 -9
  51. execsql/metacommands/system.py +44 -25
  52. execsql/models.py +9 -16
  53. execsql/parser.py +10 -10
  54. execsql/script.py +183 -157
  55. execsql/state.py +170 -208
  56. execsql/types.py +46 -81
  57. execsql/utils/auth.py +114 -14
  58. execsql/utils/crypto.py +31 -4
  59. execsql/utils/datetime.py +7 -7
  60. execsql/utils/errors.py +34 -29
  61. execsql/utils/fileio.py +90 -55
  62. execsql/utils/gui.py +22 -23
  63. execsql/utils/mail.py +15 -17
  64. execsql/utils/numeric.py +2 -3
  65. execsql/utils/regex.py +9 -12
  66. execsql/utils/strings.py +10 -12
  67. execsql/utils/timer.py +0 -2
  68. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/execsql.conf +1 -1
  69. execsql2-2.1.2.dist-info/METADATA +300 -0
  70. execsql2-2.1.2.dist-info/RECORD +96 -0
  71. execsql2-2.0.1.dist-info/METADATA +0 -406
  72. execsql2-2.0.1.dist-info/RECORD +0 -95
  73. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/READ_ME.rst +0 -0
  74. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  75. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  76. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
  77. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_compare.sql +0 -0
  78. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
  79. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
  80. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
  81. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  82. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  83. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/script_template.sql +0 -0
  84. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
  85. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  86. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  87. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/WHEEL +0 -0
  88. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/entry_points.txt +0 -0
  89. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/LICENSE.txt +0 -0
  90. {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: Optional[str],
23
+ user_name: str | None,
25
24
  need_passwd: bool = False,
26
- port: Optional[int] = 3050,
27
- encoding: Optional[str] = "latin1",
28
- password: Optional[str] = None,
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 = 3050 if not port else 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
- self.conn = db_conn()
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: Optional[str] = None) -> bool:
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
- e = ErrInfo(
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: Optional[str] = None,
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: Optional[str] = None) -> List[str]:
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: Optional[str] = None) -> bool:
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, List, Optional
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: Optional[str],
25
+ user_name: str | None,
26
26
  need_passwd: bool = False,
27
- port: Optional[int] = 3306,
28
- encoding: Optional[str] = "latin1",
29
- password: Optional[str] = None,
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 = 3306 if not port else 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
- self.conn = db_conn()
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: Optional[str],
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 j in range(_state.conf.import_row_buffer):
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
- if line[i].strip() == "":
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([c is None for c in linedata])
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, List, Optional, Tuple
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: Optional[str],
25
+ user_name: str | None,
26
26
  need_passwd: bool = False,
27
- port: Optional[int] = 5432,
28
- encoding: Optional[str] = "UTF8",
29
- password: Optional[str] = None,
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
- self.conn = db_conn(self, self.db_name)
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: Optional[list] = None) -> None:
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) -> Tuple[List[str], list]:
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: Optional[str] = None) -> bool:
132
+ def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
119
133
  curs = self.cursor()
120
- owner_clause = "" if not schema_name else f" and owner ='{schema_name}'"
121
- sql = f"select table_name from sys.all_tables where table_name = '{table_name}'{owner_clause}"
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: Optional[str] = None,
160
+ schema_name: str | None = None,
143
161
  ) -> bool:
144
162
  curs = self.cursor()
145
- owner_clause = "" if not schema_name else f" and owner ='{schema_name}'"
146
- sql = (
147
- f"select column_name from all_tab_columns "
148
- f"where table_name='{table_name}'{owner_clause} "
149
- f"and column_name='{column_name}'"
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: Optional[str] = None) -> List[str]:
185
+ def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
168
186
  curs = self.cursor()
169
- owner_clause = "" if not schema_name else f" and owner='{schema_name}'"
170
- sql = (
171
- f"select column_name from all_tab_columns where table_name='{table_name}'{owner_clause} order by column_id"
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: Optional[str] = None) -> bool:
209
+ def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
190
210
  curs = self.cursor()
191
- owner_clause = "" if not schema_name else f" and owner ='{schema_name}'"
192
- sql = f"select view_name from sys.all_views where view_name = '{view_name}'{owner_clause}"
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
- f"select role from dba_roles where role = '{rolename}' union "
213
- f" select username from all_users where username = '{rolename}';",
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, List, Optional, Tuple
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: Optional[str],
31
+ user_name: str | None,
30
32
  need_passwd: bool = False,
31
- port: Optional[int] = 5432,
33
+ port: int | None = 5432,
32
34
  new_db: bool = False,
33
- encoding: Optional[str] = "UTF8",
34
- password: Optional[str] = None,
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
- self.conn = db_conn(self, self.db_name)
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(f"select rolname from pg_roles where rolname = '{rolename}';")
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: Optional[str] = None) -> bool:
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
- schema_clause = "" if not schema_name else f" and table_schema='{schema_name}'"
143
- sql = f"select table_name from information_schema.tables where table_name = '{table_name}'{schema_clause};"
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
- sql = f"""select table_name from information_schema.tables where table_name = '{table_name}' and
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: Optional[str] = None) -> bool:
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
- schema_clause = "" if not schema_name else f" and table_schema='{schema_name}'"
170
- sql = f"select table_name from information_schema.views where table_name = '{view_name}'{schema_clause};"
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
- sql = f"""select table_name from information_schema.views where table_name = '{view_name}' and
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: Optional[str],
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 j in range(_state.conf.import_row_buffer):
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
- if line[i].strip() == "":
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([c is None for c in linedata])
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: Optional[str],
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 io.open(file_name, "rb") as f:
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)});"