execsql2 2.4.5__py3-none-any.whl → 2.6.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.
Files changed (62) hide show
  1. execsql/cli/__init__.py +14 -0
  2. execsql/cli/dsn.py +2 -0
  3. execsql/cli/help.py +2 -0
  4. execsql/cli/run.py +4 -2
  5. execsql/constants.py +11 -0
  6. execsql/db/access.py +20 -0
  7. execsql/db/base.py +4 -0
  8. execsql/db/dsn.py +11 -8
  9. execsql/db/duckdb.py +12 -8
  10. execsql/db/firebird.py +17 -8
  11. execsql/db/mysql.py +13 -8
  12. execsql/db/oracle.py +22 -8
  13. execsql/db/postgres.py +21 -9
  14. execsql/db/sqlite.py +18 -8
  15. execsql/db/sqlserver.py +14 -8
  16. execsql/exporters/__init__.py +6 -1
  17. execsql/exporters/base.py +2 -0
  18. execsql/exporters/delimited.py +10 -0
  19. execsql/exporters/protocol.py +128 -0
  20. execsql/exporters/xls.py +8 -0
  21. execsql/format.py +3 -1
  22. execsql/gui/__init__.py +2 -0
  23. execsql/gui/base.py +2 -0
  24. execsql/gui/console.py +2 -0
  25. execsql/gui/desktop.py +1 -0
  26. execsql/gui/tui.py +134 -0
  27. execsql/importers/base.py +1 -0
  28. execsql/importers/csv.py +2 -0
  29. execsql/importers/feather.py +2 -0
  30. execsql/importers/ods.py +1 -0
  31. execsql/importers/xls.py +1 -0
  32. execsql/metacommands/__init__.py +386 -180
  33. execsql/metacommands/dispatch.py +2 -0
  34. execsql/metacommands/io.py +41 -0
  35. execsql/models.py +17 -0
  36. execsql/parser.py +41 -0
  37. execsql/script/control.py +2 -0
  38. execsql/script/engine.py +19 -0
  39. execsql/script/variables.py +9 -5
  40. execsql/state.py +312 -199
  41. execsql/types.py +46 -0
  42. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/METADATA +2 -2
  43. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/RECORD +62 -61
  44. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/README.md +0 -0
  45. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  46. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  47. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/execsql.conf +0 -0
  48. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  49. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  50. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  51. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  52. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  53. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  54. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  55. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/script_template.sql +0 -0
  56. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  57. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  58. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  59. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/WHEEL +0 -0
  60. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/entry_points.txt +0 -0
  61. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/licenses/LICENSE.txt +0 -0
  62. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/licenses/NOTICE +0 -0
execsql/cli/__init__.py CHANGED
@@ -24,6 +24,20 @@ from execsql.cli.help import _console, _err_console, _print_encodings, _print_me
24
24
  from execsql.cli.run import _connect_initial_db, _run # noqa: F401 — re-export
25
25
  from execsql.exceptions import ConfigError, ErrInfo
26
26
 
27
+ __all__ = [
28
+ "_SCHEME_TO_DBTYPE",
29
+ "_connect_initial_db",
30
+ "_console",
31
+ "_err_console",
32
+ "_legacy_main",
33
+ "_parse_connection_string",
34
+ "_print_encodings",
35
+ "_print_metacommands",
36
+ "_run",
37
+ "app",
38
+ "main",
39
+ ]
40
+
27
41
 
28
42
  # ---------------------------------------------------------------------------
29
43
  # Typer app
execsql/cli/dsn.py CHANGED
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  from execsql.exceptions import ConfigError
6
6
 
7
+ __all__ = ["_SCHEME_TO_DBTYPE", "_parse_connection_string"]
8
+
7
9
  #: Mapping from URL scheme to execsql db_type code.
8
10
  _SCHEME_TO_DBTYPE: dict[str, str] = {
9
11
  "postgresql": "p",
execsql/cli/help.py CHANGED
@@ -11,6 +11,8 @@ from encodings.aliases import aliases as codec_dict
11
11
  from rich.console import Console
12
12
  from rich.table import Table
13
13
 
14
+ __all__ = ["_console", "_err_console", "_print_encodings", "_print_metacommands"]
15
+
14
16
  _console = Console()
15
17
  _err_console = Console(stderr=True)
16
18
 
execsql/cli/run.py CHANGED
@@ -24,6 +24,8 @@ from execsql.script import SubVarSet, current_script_line, read_sqlfile, read_sq
24
24
  from execsql.utils.fileio import FileWriter, Logger, filewriter_end
25
25
  from execsql.utils.gui import gui_connect, gui_console_isrunning, gui_console_off, gui_console_on, gui_console_wait_user
26
26
 
27
+ __all__ = ["_connect_initial_db", "_print_dry_run", "_run"]
28
+
27
29
 
28
30
  # ---------------------------------------------------------------------------
29
31
  # Dry-run helper
@@ -92,7 +94,7 @@ def _run(
92
94
  # Security note: ALL environment variables are exposed as &-prefixed
93
95
  # substitution variables. Sensitive values (API keys, tokens) in the
94
96
  # process environment will be accessible to scripts. See the
95
- # "Environment Variables" section in docs/substitution_vars.md.
97
+ # "Environment Variables" section in docs/reference/substitution_vars.md.
96
98
  for k in os.environ:
97
99
  try:
98
100
  _state.subvars.add_substitution("&" + k, os.environ[k])
@@ -138,7 +140,7 @@ def _run(
138
140
  parsed_dsn = _parse_connection_string(dsn)
139
141
  except ConfigError as exc:
140
142
  _err_console.print(f"[bold red]Error:[/bold red] {exc}")
141
- raise SystemExit(1)
143
+ raise SystemExit(1) from None
142
144
  db_type = db_type or parsed_dsn["db_type"]
143
145
  conf.db_type = db_type
144
146
  # DSN values override conf-file values — the CLI flag is explicit.
execsql/constants.py CHANGED
@@ -17,6 +17,17 @@ Contains:
17
17
  colour+icon combinations.
18
18
  """
19
19
 
20
+ __all__ = [
21
+ "bm_servers",
22
+ "cancel_xbm",
23
+ "color_names",
24
+ "custom_icons",
25
+ "expand_xbm",
26
+ "icon_xbm",
27
+ "wedge_sm_xbm",
28
+ "wedges_3_xbm",
29
+ ]
30
+
20
31
  # Tile servers for map basemap layers
21
32
  bm_servers: dict[str, str] = {
22
33
  "OpenStreetMap": "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
execsql/db/access.py CHANGED
@@ -88,6 +88,7 @@ class AccessDatabase(Database):
88
88
  return f"AccessDatabase({self.db_name}, {self.encoding})"
89
89
 
90
90
  def open_db(self) -> None:
91
+ """Open an ODBC connection to the Access database."""
91
92
  # Open an ODBC connection.
92
93
  import pyodbc
93
94
 
@@ -136,6 +137,7 @@ class AccessDatabase(Database):
136
137
  )
137
138
 
138
139
  def open_dao(self) -> None:
140
+ """Open a DAO connection to the Access database."""
139
141
  import win32com.client
140
142
 
141
143
  if self.dao_conn is not None:
@@ -186,6 +188,7 @@ class AccessDatabase(Database):
186
188
  )
187
189
 
188
190
  def exec_dao(self, querystring: str) -> None:
191
+ """Execute a query using the DAO connection."""
189
192
  # Execute a query using DAO.
190
193
  if self.dao_conn is None:
191
194
  self.open_dao()
@@ -193,6 +196,7 @@ class AccessDatabase(Database):
193
196
  self.last_dao_time = time.time()
194
197
 
195
198
  def close(self) -> None:
199
+ """Close both the DAO and ODBC connections."""
196
200
  if self.dao_conn:
197
201
  for qn in self.temp_query_names:
198
202
  try:
@@ -206,10 +210,13 @@ class AccessDatabase(Database):
206
210
  self.conn = None
207
211
 
208
212
  def dao_flush_check(self) -> None:
213
+ """Wait if needed for Jet's read buffer to flush after a DAO command."""
209
214
  if time.time() - self.last_dao_time < 5.0:
210
215
  time.sleep(5 - (time.time() - self.last_dao_time))
211
216
 
212
217
  def execute(self, sqlcmd: Any, paramlist: list | None = None) -> None:
218
+ """Execute a SQL command, handling encoding, DAO flush, and temporary queries."""
219
+
213
220
  # A shortcut to self.cursor().execute() that handles encoding and that
214
221
  # ensures that at least 5 seconds have passed since the last DAO command,
215
222
  # to allow Jet's read buffer to be flushed (see https://support.microsoft.com/en-us/kb/225048).
@@ -254,9 +261,11 @@ class AccessDatabase(Database):
254
261
  exec1(sqlcmd, paramlist)
255
262
 
256
263
  def exec_cmd(self, querycommand: str) -> None:
264
+ """Execute a stored query command via DAO."""
257
265
  self.exec_dao(querycommand)
258
266
 
259
267
  def select_data(self, sql: str) -> tuple[list[str], list]:
268
+ """Return column names and all rows from a SELECT statement."""
260
269
  # Returns the results of the sql select statement.
261
270
  # The Access driver returns data as unicode, so no decoding is necessary.
262
271
  self.dao_flush_check()
@@ -266,6 +275,7 @@ class AccessDatabase(Database):
266
275
  return [d[0] for d in curs.description], rows
267
276
 
268
277
  def select_rowsource(self, sql: str) -> tuple[list[str], Any]:
278
+ """Return column names and an iterable that yields rows one at a time."""
269
279
  # Return 1) a list of column names, and 2) an iterable that yields rows.
270
280
  self.dao_flush_check()
271
281
  curs = self.cursor()
@@ -274,6 +284,7 @@ class AccessDatabase(Database):
274
284
  return [d[0] for d in curs.description], iter(curs.fetchone, None)
275
285
 
276
286
  def select_rowdict(self, sql: str) -> tuple[list[str], Any]:
287
+ """Return column names and an iterable that yields rows as dictionaries."""
277
288
  # Return an iterable that yields dictionaries of row data.
278
289
  self.dao_flush_check()
279
290
  curs = self.cursor()
@@ -295,6 +306,7 @@ class AccessDatabase(Database):
295
306
  return headers, iter(dict_row, None)
296
307
 
297
308
  def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
309
+ """Return True if the named table exists in the Access database."""
298
310
  self.dao_flush_check()
299
311
  curs = self.cursor()
300
312
  try:
@@ -318,6 +330,7 @@ class AccessDatabase(Database):
318
330
  column_name: str,
319
331
  schema_name: str | None = None,
320
332
  ) -> bool:
333
+ """Return True if the named column exists in the given Access table."""
321
334
  self.dao_flush_check()
322
335
  curs = self.cursor()
323
336
  quoted_col = self.quote_identifier(column_name)
@@ -330,6 +343,7 @@ class AccessDatabase(Database):
330
343
  return True
331
344
 
332
345
  def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
346
+ """Return a list of column names for the given Access table."""
333
347
  self.dao_flush_check()
334
348
  curs = self.cursor()
335
349
  quoted_tbl = self.quote_identifier(table_name)
@@ -337,6 +351,7 @@ class AccessDatabase(Database):
337
351
  return [d[0] for d in curs.description]
338
352
 
339
353
  def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
354
+ """Return True if the named view or query exists in the Access database."""
340
355
  self.dao_flush_check()
341
356
  curs = self.cursor()
342
357
  try:
@@ -355,14 +370,17 @@ class AccessDatabase(Database):
355
370
  return len(rows) > 0
356
371
 
357
372
  def schema_exists(self, schema_name: str) -> bool:
373
+ """Return False; Access does not support schemas."""
358
374
  return False
359
375
 
360
376
  def drop_table(self, tablename: str) -> None:
377
+ """Drop the named table from the Access database."""
361
378
  self.dao_flush_check()
362
379
  tablename = self.type.quoted(tablename)
363
380
  self.execute(f"drop table {tablename};")
364
381
 
365
382
  def as_datetime(self, val: Any) -> datetime.datetime | None:
383
+ """Convert a value to a datetime object suitable for Access."""
366
384
  from execsql.types import DT_Timestamp, DT_Date, DT_Time, DataTypeError
367
385
 
368
386
  if val is None or (isinstance(val, _state.stringtypes) and len(val) == 0):
@@ -393,6 +411,7 @@ class AccessDatabase(Database):
393
411
  return v
394
412
 
395
413
  def int_or_bool(self, val: Any) -> int | None:
414
+ """Convert a value to an integer, recognizing Access boolean values."""
396
415
  # Because Booleans are stored as integers in Access (at least, if execsql
397
416
  # creates the table), we have to recognize Boolean values as legitimate
398
417
  # integers.
@@ -420,6 +439,7 @@ class AccessDatabase(Database):
420
439
  column_name: str,
421
440
  file_name: str,
422
441
  ) -> None:
442
+ """Import an entire binary file into a single column of a table."""
423
443
  import pyodbc
424
444
 
425
445
  with open(file_name, "rb") as f:
execsql/db/base.py CHANGED
@@ -70,6 +70,7 @@ class Database(ABC):
70
70
  port: int | None = None,
71
71
  encoding: str | None = None,
72
72
  ) -> None:
73
+ """Initialize common connection attributes for a database backend."""
73
74
  self.type = None
74
75
  self.server_name = server_name
75
76
  self.db_name = db_name
@@ -84,6 +85,7 @@ class Database(ABC):
84
85
  self.autocommit = True
85
86
 
86
87
  def __repr__(self) -> str:
88
+ """Return a developer-friendly string representation of this connection."""
87
89
  return (
88
90
  f"Database({self.server_name!r}, {self.db_name!r}, {self.user!r}, "
89
91
  f"{self.need_passwd!r}, {self.port!r}, {self.encoding!r})"
@@ -637,12 +639,14 @@ class DatabasePool:
637
639
  and with the current and initial databases identified."""
638
640
 
639
641
  def __init__(self) -> None:
642
+ """Initialize an empty connection pool with no active database."""
640
643
  self.pool: dict[str, Database] = {}
641
644
  self.initial_db: str | None = None
642
645
  self.current_db: str | None = None
643
646
  self.do_rollback: bool = True
644
647
 
645
648
  def __repr__(self) -> str:
649
+ """Return a string representation of the pool."""
646
650
  return "DatabasePool()"
647
651
 
648
652
  def add(self, db_alias: str, db_obj: Database) -> None:
execsql/db/dsn.py CHANGED
@@ -59,6 +59,7 @@ class DsnDatabase(Database):
59
59
  return f"DsnDatabase({self.db_name!r}, {self.user!r}, {self.need_passwd!r}, {self.port!r}, {self.encoding!r})"
60
60
 
61
61
  def open_db(self) -> None:
62
+ """Open an ODBC connection using the configured DSN."""
62
63
  # Open an ODBC connection using a DSN.
63
64
  import pyodbc
64
65
 
@@ -118,15 +119,16 @@ class DsnDatabase(Database):
118
119
  _try_connect()
119
120
 
120
121
  def exec_cmd(self, querycommand: str) -> None:
122
+ """Execute a stored procedure by name."""
121
123
  # The querycommand must be a stored procedure
122
- curs = self.cursor()
123
- cmd = f"execute {querycommand};"
124
- try:
125
- curs.execute(cmd.encode(self.encoding))
126
- _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
127
- except Exception:
128
- self.rollback()
129
- raise
124
+ with self._cursor() as curs:
125
+ cmd = f"execute {querycommand};"
126
+ try:
127
+ curs.execute(cmd.encode(self.encoding))
128
+ _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
129
+ except Exception:
130
+ self.rollback()
131
+ raise
130
132
 
131
133
  def import_entire_file(
132
134
  self,
@@ -135,6 +137,7 @@ class DsnDatabase(Database):
135
137
  column_name: str,
136
138
  file_name: str,
137
139
  ) -> None:
140
+ """Import an entire binary file into a single column of a table."""
138
141
  import pyodbc
139
142
 
140
143
  with open(file_name, "rb") as f:
execsql/db/duckdb.py CHANGED
@@ -45,6 +45,7 @@ class DuckDBDatabase(Database):
45
45
  return f"DuckDBDatabase({self.db_name!r})"
46
46
 
47
47
  def open_db(self) -> None:
48
+ """Open a connection to the DuckDB database file."""
48
49
  import duckdb
49
50
 
50
51
  if self.conn is None:
@@ -60,22 +61,25 @@ class DuckDBDatabase(Database):
60
61
  ) from e
61
62
 
62
63
  def exec_cmd(self, querycommand: str) -> None:
64
+ """Execute a query command as a view selection, since DuckDB lacks stored procedures."""
63
65
  # DuckDB does not support stored functions, so the querycommand
64
66
  # is treated as (and therefore must be) a view.
65
- curs = self.cursor()
66
- cmd = f"select * from {querycommand};"
67
- try:
68
- curs.execute(cmd.encode(self.encoding))
69
- _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
70
- except Exception:
71
- self.rollback()
72
- raise
67
+ with self._cursor() as curs:
68
+ cmd = f"select * from {querycommand};"
69
+ try:
70
+ curs.execute(cmd.encode(self.encoding))
71
+ _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
72
+ except Exception:
73
+ self.rollback()
74
+ raise
73
75
 
74
76
  def view_exists(self, view_name: str) -> bool:
77
+ """Return True if the named view exists in the DuckDB database."""
75
78
  # DuckDB information_schema has no 'views' table; views are listed in 'tables'
76
79
  return self.table_exists(view_name)
77
80
 
78
81
  def schema_exists(self, schema_name: str) -> bool:
82
+ """Return True if the named schema exists in the current DuckDB catalog."""
79
83
  # In DuckDB, the 'schemata' view is not limited to the current database.
80
84
  curs = self.cursor()
81
85
  curs.execute(
execsql/db/firebird.py CHANGED
@@ -59,6 +59,7 @@ class FirebirdDatabase(Database):
59
59
  )
60
60
 
61
61
  def open_db(self) -> None:
62
+ """Open a connection to the Firebird database."""
62
63
  import fdb as firebird_lib
63
64
 
64
65
  def db_conn():
@@ -113,17 +114,19 @@ class FirebirdDatabase(Database):
113
114
  raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
114
115
 
115
116
  def exec_cmd(self, querycommand: str) -> None:
117
+ """Execute a stored procedure by name."""
116
118
  # The querycommand must be a stored function (/procedure)
117
- curs = self.cursor()
118
- cmd = f"execute procedure {querycommand};"
119
- try:
120
- curs.execute(cmd)
121
- except Exception:
122
- self.rollback()
123
- raise
124
- _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
119
+ with self._cursor() as curs:
120
+ cmd = f"execute procedure {querycommand};"
121
+ try:
122
+ curs.execute(cmd)
123
+ except Exception:
124
+ self.rollback()
125
+ raise
126
+ _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
125
127
 
126
128
  def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
129
+ """Return True if the named table exists in the Firebird database."""
127
130
  curs = self.cursor()
128
131
  sql = (
129
132
  "SELECT RDB$RELATION_NAME FROM RDB$RELATIONS "
@@ -156,6 +159,7 @@ class FirebirdDatabase(Database):
156
159
  column_name: str,
157
160
  schema_name: str | None = None,
158
161
  ) -> bool:
162
+ """Return True if the named column exists in the given Firebird table."""
159
163
  curs = self.cursor()
160
164
  quoted_col = self.quote_identifier(column_name)
161
165
  quoted_tbl = self.quote_identifier(table_name)
@@ -167,6 +171,7 @@ class FirebirdDatabase(Database):
167
171
  return True
168
172
 
169
173
  def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
174
+ """Return a list of column names for the given Firebird table."""
170
175
  curs = self.cursor()
171
176
  quoted_tbl = self.quote_identifier(table_name)
172
177
  sql = f"select first 1 * from {quoted_tbl};"
@@ -185,6 +190,7 @@ class FirebirdDatabase(Database):
185
190
  return [d[0] for d in curs.description]
186
191
 
187
192
  def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
193
+ """Return True if the named view exists in the Firebird database."""
188
194
  curs = self.cursor()
189
195
  sql = "select distinct rdb$view_name from rdb$view_relations where rdb$view_name = ?;"
190
196
  try:
@@ -204,9 +210,11 @@ class FirebirdDatabase(Database):
204
210
  return len(rows) > 0
205
211
 
206
212
  def schema_exists(self, schema_name: str) -> bool:
213
+ """Return False; Firebird does not support schemas."""
207
214
  return False
208
215
 
209
216
  def role_exists(self, rolename: str) -> bool:
217
+ """Return True if the named role or user exists in the Firebird database."""
210
218
  curs = self.cursor()
211
219
  curs.execute(
212
220
  "SELECT DISTINCT USER FROM RDB$USER_PRIVILEGES WHERE USER = ? union "
@@ -218,6 +226,7 @@ class FirebirdDatabase(Database):
218
226
  return len(rows) > 0
219
227
 
220
228
  def drop_table(self, tablename: str) -> None:
229
+ """Drop the named table from the Firebird database."""
221
230
  # Firebird will thrown an error if there are foreign keys into the table.
222
231
  tablename = self.type.quoted(tablename)
223
232
  self.execute(f"DROP TABLE {tablename};")
execsql/db/mysql.py CHANGED
@@ -73,6 +73,7 @@ class MySQLDatabase(Database):
73
73
  )
74
74
 
75
75
  def open_db(self) -> None:
76
+ """Open a connection to the MySQL or MariaDB server."""
76
77
  import pymysql as mysql_lib
77
78
 
78
79
  def db_conn():
@@ -130,20 +131,23 @@ class MySQLDatabase(Database):
130
131
  raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
131
132
 
132
133
  def exec_cmd(self, querycommand: str) -> None:
134
+ """Execute a stored procedure by name."""
133
135
  # The querycommand must be a stored function (/procedure)
134
- curs = self.cursor()
135
- cmd = f"call {querycommand}();"
136
- try:
137
- curs.execute(cmd)
138
- _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
139
- except Exception:
140
- self.rollback()
141
- raise
136
+ with self._cursor() as curs:
137
+ cmd = f"call {querycommand}();"
138
+ try:
139
+ curs.execute(cmd)
140
+ _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
141
+ except Exception:
142
+ self.rollback()
143
+ raise
142
144
 
143
145
  def schema_exists(self, schema_name: str) -> bool:
146
+ """Return False; MySQL does not support schemas in the execsql sense."""
144
147
  return False
145
148
 
146
149
  def role_exists(self, rolename: str) -> bool:
150
+ """Return True if the named role or user exists in the MySQL server."""
147
151
  curs = self.cursor()
148
152
  curs.execute(
149
153
  "select distinct user as role from mysql.user where user = %s"
@@ -162,6 +166,7 @@ class MySQLDatabase(Database):
162
166
  csv_file_obj: Any,
163
167
  skipheader: bool,
164
168
  ) -> None:
169
+ """Import a delimited file into a MySQL table."""
165
170
  # Import a file to a table. Columns must be compatible.
166
171
  sq_name = self.schema_qualified_table_name(schema_name, table_name)
167
172
  if not self.table_exists(table_name, schema_name):
execsql/db/oracle.py CHANGED
@@ -61,6 +61,7 @@ class OracleDatabase(Database):
61
61
  )
62
62
 
63
63
  def open_db(self) -> None:
64
+ """Open a connection to the Oracle database."""
64
65
  import cx_Oracle
65
66
 
66
67
  def db_conn(db: OracleDatabase, db_name: str):
@@ -104,6 +105,7 @@ class OracleDatabase(Database):
104
105
  raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
105
106
 
106
107
  def execute(self, sql: Any, paramlist: list | None = None) -> None:
108
+ """Execute a SQL command, stripping any trailing semicolon for Oracle."""
107
109
  # Strip any semicolon off the end and pass to the parent method.
108
110
  if sql[-1:] == ";":
109
111
  super().execute(sql[:-1], paramlist)
@@ -111,29 +113,34 @@ class OracleDatabase(Database):
111
113
  super().execute(sql, paramlist)
112
114
 
113
115
  def select_data(self, sql: str) -> tuple[list[str], list]:
116
+ """Return column names and all rows from a SELECT statement."""
114
117
  if sql[-1:] == ";":
115
118
  return super().select_data(sql[:-1])
116
119
  else:
117
120
  return super().select_data(sql)
118
121
 
119
122
  def select_rowsource(self, sql: str) -> Any:
123
+ """Return column names and an iterable that yields rows one at a time."""
120
124
  if sql[-1:] == ";":
121
125
  return super().select_rowsource(sql[:-1])
122
126
  else:
123
127
  return super().select_rowsource(sql)
124
128
 
125
129
  def select_rowdict(self, sql: str) -> Any:
130
+ """Return column names and an iterable that yields rows as dictionaries."""
126
131
  if sql[-1:] == ";":
127
132
  return super().select_rowdict(sql[:-1])
128
133
  else:
129
134
  return super().select_rowdict(sql)
130
135
 
131
136
  def schema_exists(self, schema_name: str) -> bool:
137
+ """Raise DatabaseNotImplementedError; schema_exists is not supported for Oracle."""
132
138
  from execsql.exceptions import DatabaseNotImplementedError
133
139
 
134
140
  raise DatabaseNotImplementedError(self.name(), "schema_exists")
135
141
 
136
142
  def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
143
+ """Return True if the named table exists in the Oracle database."""
137
144
  curs = self.cursor()
138
145
  params = {"tname": table_name}
139
146
  owner_clause = ""
@@ -163,6 +170,7 @@ class OracleDatabase(Database):
163
170
  column_name: str,
164
171
  schema_name: str | None = None,
165
172
  ) -> bool:
173
+ """Return True if the named column exists in the given Oracle table."""
166
174
  curs = self.cursor()
167
175
  params = {"tname": table_name, "cname": column_name}
168
176
  owner_clause = ""
@@ -187,6 +195,7 @@ class OracleDatabase(Database):
187
195
  return len(rows) > 0
188
196
 
189
197
  def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
198
+ """Return a list of column names for the given Oracle table."""
190
199
  curs = self.cursor()
191
200
  params = {"tname": table_name}
192
201
  owner_clause = ""
@@ -211,6 +220,7 @@ class OracleDatabase(Database):
211
220
  return [row[0] for row in rows]
212
221
 
213
222
  def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
223
+ """Return True if the named view exists in the Oracle database."""
214
224
  curs = self.cursor()
215
225
  params = {"vname": view_name}
216
226
  owner_clause = ""
@@ -235,6 +245,7 @@ class OracleDatabase(Database):
235
245
  return len(rows) > 0
236
246
 
237
247
  def role_exists(self, rolename: str) -> bool:
248
+ """Return True if the named role or user exists in the Oracle database."""
238
249
  curs = self.cursor()
239
250
  curs.execute(
240
251
  "select role from dba_roles where role = :rname union "
@@ -246,19 +257,22 @@ class OracleDatabase(Database):
246
257
  return len(rows) > 0
247
258
 
248
259
  def drop_table(self, tablename: str) -> None:
260
+ """Drop the named table with cascade constraints."""
249
261
  tablename = self.type.quoted(tablename)
250
262
  self.execute(f"drop table {tablename} cascade constraints")
251
263
 
252
264
  def paramsubs(self, paramcount: int) -> str:
265
+ """Return Oracle-style positional parameter placeholders (:1, :2, ...)."""
253
266
  return ",".join(":" + str(d) for d in range(1, paramcount + 1))
254
267
 
255
268
  def exec_cmd(self, querycommand: str) -> None:
269
+ """Execute a stored function by name."""
256
270
  # The querycommand must be a stored function (/procedure)
257
- curs = self.cursor()
258
- cmd = f"select {querycommand}()"
259
- try:
260
- curs.execute(cmd)
261
- _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
262
- except Exception:
263
- self.rollback()
264
- raise
271
+ with self._cursor() as curs:
272
+ cmd = f"select {querycommand}()"
273
+ try:
274
+ curs.execute(cmd)
275
+ _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
276
+ except Exception:
277
+ self.rollback()
278
+ raise
execsql/db/postgres.py CHANGED
@@ -70,6 +70,7 @@ class PostgresDatabase(Database):
70
70
  )
71
71
 
72
72
  def open_db(self) -> None:
73
+ """Open a connection to the PostgreSQL database."""
73
74
  import psycopg2
74
75
 
75
76
  def db_conn(db: PostgresDatabase, db_name: str):
@@ -145,17 +146,19 @@ class PostgresDatabase(Database):
145
146
  self.encoding = self.conn.encoding
146
147
 
147
148
  def exec_cmd(self, querycommand: str) -> None:
149
+ """Execute a stored function by name."""
148
150
  # The querycommand must be a stored function (/procedure)
149
- curs = self.cursor()
150
- cmd = f"select {querycommand}()"
151
- try:
152
- curs.execute(cmd)
153
- _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
154
- except Exception:
155
- self.rollback()
156
- raise
151
+ with self._cursor() as curs:
152
+ cmd = f"select {querycommand}()"
153
+ try:
154
+ curs.execute(cmd)
155
+ _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
156
+ except Exception:
157
+ self.rollback()
158
+ raise
157
159
 
158
160
  def role_exists(self, rolename: str) -> bool:
161
+ """Return True if the named role exists in the PostgreSQL cluster."""
159
162
  curs = self.cursor()
160
163
  curs.execute("select rolname from pg_roles where rolname = %s;", (rolename,))
161
164
  rows = curs.fetchall()
@@ -163,6 +166,7 @@ class PostgresDatabase(Database):
163
166
  return len(rows) > 0
164
167
 
165
168
  def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
169
+ """Return True if the named table exists in the PostgreSQL database."""
166
170
  curs = self.cursor()
167
171
  if schema_name is not None:
168
172
  params: list = [table_name]
@@ -195,6 +199,7 @@ class PostgresDatabase(Database):
195
199
  return len(rows) > 0
196
200
 
197
201
  def view_exists(self, view_name: str, schema_name: str | None = None) -> bool:
202
+ """Return True if the named view exists in the PostgreSQL database."""
198
203
  curs = self.cursor()
199
204
  if schema_name is not None:
200
205
  params: list = [view_name]
@@ -227,9 +232,14 @@ class PostgresDatabase(Database):
227
232
  return len(rows) > 0
228
233
 
229
234
  def vacuum(self, argstring: str) -> None:
235
+ """Run VACUUM with the given arguments in autocommit mode."""
230
236
  self.commit()
231
237
  self.conn.set_session(autocommit=True)
232
- self.conn.cursor().execute(f"VACUUM {argstring};")
238
+ curs = self.conn.cursor()
239
+ try:
240
+ curs.execute(f"VACUUM {argstring};")
241
+ finally:
242
+ curs.close()
233
243
  self.conn.set_session(autocommit=False)
234
244
 
235
245
  def import_tabular_file(
@@ -239,6 +249,7 @@ class PostgresDatabase(Database):
239
249
  csv_file_obj: Any,
240
250
  skipheader: bool,
241
251
  ) -> None:
252
+ """Import a delimited file into a PostgreSQL table, using COPY when possible."""
242
253
  # Import a file to a table. Columns must be compatible.
243
254
  sq_name = self.schema_qualified_table_name(schema_name, table_name)
244
255
  if not self.table_exists(table_name, schema_name):
@@ -440,6 +451,7 @@ class PostgresDatabase(Database):
440
451
  column_name: str,
441
452
  file_name: str,
442
453
  ) -> None:
454
+ """Import an entire binary file into a single column of a table."""
443
455
  import psycopg2
444
456
 
445
457
  with open(file_name, "rb") as f: