execsql2 2.23.0__py3-none-any.whl → 2.23.1__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 (27) hide show
  1. execsql/db/access.py +22 -6
  2. execsql/db/base.py +105 -1
  3. execsql/db/dsn.py +2 -0
  4. execsql/db/duckdb.py +95 -2
  5. execsql/db/mysql.py +11 -0
  6. execsql/db/oracle.py +12 -4
  7. execsql/db/sqlite.py +16 -0
  8. {execsql2-2.23.0.dist-info → execsql2-2.23.1.dist-info}/METADATA +2 -2
  9. {execsql2-2.23.0.dist-info → execsql2-2.23.1.dist-info}/RECORD +27 -27
  10. {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/README.md +0 -0
  11. {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  12. {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  13. {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
  14. {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/md_compare.sql +0 -0
  15. {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
  16. {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
  17. {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
  18. {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  19. {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  20. {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/script_template.sql +0 -0
  21. {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
  22. {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  23. {execsql2-2.23.0.data → execsql2-2.23.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  24. {execsql2-2.23.0.dist-info → execsql2-2.23.1.dist-info}/WHEEL +0 -0
  25. {execsql2-2.23.0.dist-info → execsql2-2.23.1.dist-info}/entry_points.txt +0 -0
  26. {execsql2-2.23.0.dist-info → execsql2-2.23.1.dist-info}/licenses/LICENSE.txt +0 -0
  27. {execsql2-2.23.0.dist-info → execsql2-2.23.1.dist-info}/licenses/NOTICE +0 -0
execsql/db/access.py CHANGED
@@ -14,7 +14,7 @@ import time
14
14
  from pathlib import Path
15
15
  from typing import Any, cast
16
16
 
17
- from execsql.db.base import Database
17
+ from execsql.db.base import Database, _fetched
18
18
  from execsql.db.tiers import SupportTier
19
19
  from execsql.exceptions import ErrInfo
20
20
  from execsql.utils.errors import exception_desc, fatal_error
@@ -231,7 +231,13 @@ class AccessDatabase(Database):
231
231
  if time.time() - self.last_dao_time < 5.0:
232
232
  time.sleep(5 - (time.time() - self.last_dao_time))
233
233
 
234
- def execute(self, sqlcmd: Any, paramlist: list | None = None) -> None:
234
+ def execute(
235
+ self,
236
+ sqlcmd: Any,
237
+ paramlist: list | None = None,
238
+ *,
239
+ fetch: bool = False,
240
+ ) -> tuple[list[str], list] | None:
235
241
  """Execute a SQL command, handling encoding, DAO flush, and temporary queries."""
236
242
 
237
243
  # A shortcut to self.cursor().execute() that handles encoding and that
@@ -239,9 +245,15 @@ class AccessDatabase(Database):
239
245
  # to allow Jet's read buffer to be flushed (see https://support.microsoft.com/en-us/kb/225048).
240
246
  # This also handles the 'CREATE TEMPORARY QUERY' extension to Access.
241
247
  # For Access, commands in a tuple (batch) are executed singly.
242
- def exec1(sql: str, paramlist: list | None) -> None:
248
+ def exec1(sql: str, paramlist: list | None) -> tuple[list[str], list] | None:
243
249
  tqd = self.temp_rx.match(sql)
244
250
  if tqd:
251
+ if self.conn is not None and self.holding():
252
+ self.warn_once(
253
+ "temporary query",
254
+ "CREATE TEMPORARY QUERY reopens the Access connection, which rolls back statements "
255
+ "not yet committed under AUTOCOMMIT OFF or BEGIN BATCH.",
256
+ )
245
257
  qn = tqd.group(3)
246
258
  qsql = sql[tqd.end() :]
247
259
  if self.dao_conn is None:
@@ -258,7 +270,9 @@ class AccessDatabase(Database):
258
270
  self.conn = None
259
271
  if tqd.group(1) and tqd.group(1).strip().lower()[:4] == "temp" and qn not in self.temp_query_names:
260
272
  self.temp_query_names.append(qn)
273
+ return None
261
274
  else:
275
+ self.before_statement(sql)
262
276
  self.dao_flush_check()
263
277
  with self._cursor() as curs:
264
278
  encoded_sql: Any
@@ -271,12 +285,14 @@ class AccessDatabase(Database):
271
285
  else:
272
286
  curs.execute(encoded_sql, paramlist)
273
287
  _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
288
+ return _fetched(curs) if fetch else None
274
289
 
275
290
  if type(sqlcmd) in (list, tuple):
291
+ result = None
276
292
  for sql in sqlcmd:
277
- exec1(sql, paramlist)
278
- else:
279
- exec1(sqlcmd, paramlist)
293
+ result = exec1(sql, paramlist)
294
+ return result
295
+ return exec1(sqlcmd, paramlist)
280
296
 
281
297
  def exec_cmd(self, querycommand: str) -> None:
282
298
  """Execute a stored query command via DAO."""
execsql/db/base.py CHANGED
@@ -29,6 +29,21 @@ import execsql.state as _state
29
29
 
30
30
  __all__ = ["Database", "DatabasePool"]
31
31
 
32
+ # Leading whitespace and comments, skipped to find a statement's first words.
33
+ _LEADING_NOISE_RX = re.compile(r"(?:\s+|--[^\n]*(?:\n|$)|/\*.*?\*/)*", re.S)
34
+
35
+ # Statements that manage a transaction themselves; one is never opened in front of them.
36
+ _TRANSACTION_CONTROL_RX = re.compile(
37
+ r"(?:BEGIN|START\s+TRANSACTION|COMMIT|END|ROLLBACK|ABORT|SAVEPOINT|RELEASE)\b",
38
+ re.I,
39
+ )
40
+
41
+
42
+ def statement_start(sql: str) -> str:
43
+ """*sql* from its first keyword on: leading whitespace and comments removed."""
44
+ match = _LEADING_NOISE_RX.match(sql)
45
+ return sql[match.end() :] if match else sql
46
+
32
47
 
33
48
  def _default_dt_cast() -> dict[type, Callable]:
34
49
  """Build the default type-cast mapping used by all database backends."""
@@ -46,6 +61,13 @@ def _default_dt_cast() -> dict[type, Callable]:
46
61
  }
47
62
 
48
63
 
64
+ def _fetched(curs: Any) -> tuple[list[str], list] | None:
65
+ """The rows a just-executed cursor produced, as ``(column_names, rows)``; ``None`` if none."""
66
+ if curs.description is None:
67
+ return None
68
+ return [d[0] for d in curs.description], curs.fetchall()
69
+
70
+
49
71
  class Database(ABC):
50
72
  """Abstract base class for every DBMS adapter.
51
73
 
@@ -215,13 +237,17 @@ class Database(ABC):
215
237
  """Return a comma-separated string of *paramcount* parameter placeholders."""
216
238
  return ",".join((self.paramstr,) * paramcount)
217
239
 
218
- def execute(self, sql: Any, paramlist: list | None = None) -> None:
240
+ def execute(self, sql: Any, paramlist: list | None = None, *, fetch: bool = False) -> tuple[list[str], list] | None:
219
241
  """Execute *sql* (optionally with *paramlist*), updating ``$LAST_ROWCOUNT``.
220
242
 
243
+ With *fetch*, return ``(column_names, rows)`` when the statement
244
+ produces rows (a prompt shows them), else ``None``.
245
+
221
246
  Rolls back the current transaction and re-raises on any driver error.
222
247
  """
223
248
  if type(sql) in (tuple, list):
224
249
  sql = " ".join(sql)
250
+ self.before_statement(sql)
225
251
  try:
226
252
  with self._cursor() as curs:
227
253
  if paramlist is None:
@@ -232,6 +258,7 @@ class Database(ABC):
232
258
  _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
233
259
  except Exception:
234
260
  pass # Non-critical: some drivers lack rowcount support.
261
+ return _fetched(curs) if fetch else None
235
262
  except Exception:
236
263
  try:
237
264
  self.rollback()
@@ -244,6 +271,81 @@ class Database(ABC):
244
271
  """Execute a stored procedure or function by name."""
245
272
  ...
246
273
 
274
+ # ------------------------------------------------------------------
275
+ # Holding statements until commit (AUTOCOMMIT OFF, BEGIN BATCH)
276
+ # ------------------------------------------------------------------
277
+
278
+ #: Statements the DBMS commits on its own, with everything run before
279
+ #: them, even inside a transaction (MySQL and Oracle DDL). Matched against
280
+ #: the start of the statement; a warning names each one run while holding.
281
+ implicit_commit_rx: ClassVar[re.Pattern[str] | None] = None
282
+
283
+ #: Statements that cannot run inside a transaction on this DBMS; one is
284
+ #: not opened in front of them.
285
+ no_transaction_rx: ClassVar[re.Pattern[str] | None] = None
286
+
287
+ #: Set on a connection whose driver could only connect in autocommit mode
288
+ #: (an ODBC DSN without transactions): nothing can be held back.
289
+ driver_autocommits: bool = False
290
+
291
+ def holding(self) -> bool:
292
+ """Whether statements on this connection wait for a commit.
293
+
294
+ True while AUTOCOMMIT is OFF for this database, or inside BEGIN BATCH
295
+ (which holds every database).
296
+ """
297
+ if not self.autocommit:
298
+ return True
299
+ status = _state.status
300
+ return status is not None and status.batch.in_batch()
301
+
302
+ def before_statement(self, sql: str) -> None:
303
+ """Make sure *sql* is held until commit when execsql is holding statements.
304
+
305
+ Most drivers already hold every statement: they open a transaction
306
+ before the first one and keep it open until commit or rollback. An
307
+ adapter whose driver does not (SQLite for DDL, DuckDB for everything)
308
+ overrides :meth:`begin_transaction`. Where the DBMS itself commits a
309
+ statement regardless, a warning says so, since the work cannot be
310
+ rolled back.
311
+ """
312
+ if not self.holding():
313
+ return
314
+ if self.driver_autocommits:
315
+ self.warn_once(
316
+ "driver",
317
+ f"{self.name()} cannot hold statements until COMMIT: its driver commits each one as it runs. "
318
+ "AUTOCOMMIT OFF and BEGIN BATCH have no effect on it.",
319
+ )
320
+ return
321
+ start = statement_start(sql)
322
+ if self.implicit_commit_rx is not None and (m := self.implicit_commit_rx.match(start)):
323
+ from execsql.utils.errors import write_warning
324
+
325
+ write_warning(
326
+ f"{self.type.dbms_id} commits {' '.join(m.group(0).upper().split())} immediately, together with "
327
+ "everything run before it; it cannot be rolled back.",
328
+ always=True,
329
+ )
330
+ if _TRANSACTION_CONTROL_RX.match(start):
331
+ return
332
+ if self.no_transaction_rx is not None and self.no_transaction_rx.match(start):
333
+ return
334
+ self.begin_transaction()
335
+
336
+ def begin_transaction(self) -> None:
337
+ """Open a transaction if none is open. Most drivers do this themselves; a no-op here."""
338
+
339
+ def warn_once(self, key: str, message: str) -> None:
340
+ """Write *message* as a warning the first time *key* comes up on this connection."""
341
+ warned: set[str] = self.__dict__.setdefault("_warned", set())
342
+ if key in warned:
343
+ return
344
+ warned.add(key)
345
+ from execsql.utils.errors import write_warning
346
+
347
+ write_warning(message, always=True)
348
+
247
349
  def autocommit_on(self) -> None:
248
350
  """Enable autocommit mode so each statement is committed immediately."""
249
351
  self.autocommit = True
@@ -562,6 +664,7 @@ class Database(ABC):
562
664
  rows = iter(rowsource)
563
665
  eof = False
564
666
  total_rows = 0
667
+ self.before_statement(sql)
565
668
 
566
669
  # Optional rich progress bar for long-running imports.
567
670
  use_progress = getattr(_state.conf, "show_progress", False)
@@ -745,6 +848,7 @@ class Database(ABC):
745
848
  sq_name = self.schema_qualified_table_name(schema_name, table_name)
746
849
  quoted_col = self.quote_identifier(column_name)
747
850
  sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
851
+ self.before_statement(sql)
748
852
  with self._cursor() as curs:
749
853
  curs.execute(sql, (filedata,))
750
854
 
execsql/db/dsn.py CHANGED
@@ -109,6 +109,8 @@ class DsnDatabase(Database):
109
109
  if "Optional feature not implemented" in excdesc:
110
110
  try:
111
111
  _dsn_connect(autocommit=True)
112
+ # This ODBC driver has no transactions: nothing can be held.
113
+ self.driver_autocommits = True
112
114
  except Exception as e:
113
115
  raise ErrInfo(
114
116
  type="exception",
execsql/db/duckdb.py CHANGED
@@ -8,18 +8,55 @@ analytics databases via the ``duckdb`` package. Corresponds to ``-t k``
8
8
  on the CLI.
9
9
  """
10
10
 
11
+ import re
11
12
  from pathlib import Path
13
+ from typing import Any
12
14
 
13
- from execsql.db.base import Database
15
+ from execsql.db.base import Database, statement_start
14
16
  from execsql.exceptions import ErrInfo
15
17
  from execsql.utils.errors import exception_desc, fatal_error
16
18
  import execsql.state as _state
17
19
 
18
20
  __all__ = ["DuckDBDatabase"]
19
21
 
22
+ # Statements that return rows; any other statement DuckDB answers with a
23
+ # one-column "Count" result (rows inserted, updated or deleted).
24
+ _QUERY_RX = re.compile(r"(?:SELECT|WITH|VALUES|FROM|TABLE|SHOW|DESCRIBE|SUMMARIZE|EXPLAIN|PRAGMA|CALL)\b|\(", re.I)
25
+
26
+ # A statement that opens a transaction, or one that ends it.
27
+ _TRANSACTION_RX = re.compile(r"(?P<begin>BEGIN|START\s+TRANSACTION)\b|(?P<end>COMMIT|END|ROLLBACK|ABORT)\b", re.I)
28
+
29
+
30
+ class _SharedCursor:
31
+ """The connection, used as a cursor so a statement joins its open transaction.
32
+
33
+ A DuckDB cursor is a separate connection with a transaction of its own,
34
+ and closing it rolls that transaction back. ``close()`` therefore leaves
35
+ the connection open.
36
+ """
37
+
38
+ def __init__(self, conn: Any) -> None:
39
+ self._conn = conn
40
+
41
+ def __getattr__(self, name: str) -> Any:
42
+ return getattr(self._conn, name)
43
+
44
+ def close(self) -> None:
45
+ pass
46
+
20
47
 
21
48
  class DuckDBDatabase(Database):
22
- """DuckDB in-process analytics adapter using the duckdb package."""
49
+ """DuckDB in-process analytics adapter using the duckdb package.
50
+
51
+ The duckdb driver commits each statement unless a transaction was begun,
52
+ and has no way to ask whether one is open, so this adapter begins one
53
+ itself while statements are held (AUTOCOMMIT OFF, BEGIN BATCH) and keeps
54
+ track of it. While it is open, every statement runs on the connection
55
+ itself rather than on a cursor (see :class:`_SharedCursor`).
56
+ """
57
+
58
+ #: A transaction this adapter or a script's own BEGIN opened, not yet ended.
59
+ in_transaction: bool = False
23
60
 
24
61
  def __init__(self, DuckDB_fn: str) -> None:
25
62
  try:
@@ -43,6 +80,62 @@ class DuckDBDatabase(Database):
43
80
  def __repr__(self) -> str:
44
81
  return f"DuckDBDatabase({self.db_name!r})"
45
82
 
83
+ def cursor(self) -> Any:
84
+ """A cursor, or while a transaction is open, the connection itself."""
85
+ if self.conn is None:
86
+ self.open_db()
87
+ if self.in_transaction:
88
+ return _SharedCursor(self.conn)
89
+ return self.conn.cursor()
90
+
91
+ def begin_transaction(self) -> None:
92
+ """Open a transaction if none is open."""
93
+ if self.conn is None:
94
+ self.open_db()
95
+ if not self.in_transaction:
96
+ # A second BEGIN would abort the open transaction, hence the tracking.
97
+ self.conn.execute("BEGIN TRANSACTION")
98
+ self.in_transaction = True
99
+
100
+ def execute(self, sql: Any, paramlist: list | None = None, *, fetch: bool = False) -> tuple[list[str], list] | None:
101
+ """Execute *sql*, noting a transaction the statement itself begins or ends.
102
+
103
+ The driver's ``rowcount`` is always -1: DuckDB answers an INSERT,
104
+ UPDATE or DELETE with a one-column ``Count`` result instead, which is
105
+ read here to set ``$LAST_ROWCOUNT`` as other drivers do.
106
+ """
107
+ text = " ".join(sql) if type(sql) in (tuple, list) else sql
108
+ start = statement_start(text)
109
+ m = _TRANSACTION_RX.match(start)
110
+ if m and m.group("begin"):
111
+ self.in_transaction = True # so the BEGIN itself runs on the connection
112
+ query = bool(_QUERY_RX.match(start))
113
+ result = super().execute(sql, paramlist, fetch=fetch or not query)
114
+ if m and m.group("end"):
115
+ self.in_transaction = False
116
+ if not query and result is not None and result[0] == ["Count"]:
117
+ rows = result[1]
118
+ if _state.subvars is not None:
119
+ _state.subvars.add_substitution("$LAST_ROWCOUNT", rows[0][0] if rows else -1)
120
+ return None
121
+ return result if fetch else None
122
+
123
+ def commit(self) -> None:
124
+ """Commit the open transaction if autocommit is enabled."""
125
+ super().commit()
126
+ if self.autocommit:
127
+ self.in_transaction = False
128
+
129
+ def rollback(self) -> None:
130
+ """Roll back the open transaction."""
131
+ super().rollback()
132
+ self.in_transaction = False
133
+
134
+ def close(self) -> None:
135
+ """Close the connection; an open transaction is rolled back."""
136
+ super().close()
137
+ self.in_transaction = False
138
+
46
139
  def open_db(self) -> None:
47
140
  """Open a connection to the DuckDB database file."""
48
141
  import duckdb
execsql/db/mysql.py CHANGED
@@ -49,6 +49,17 @@ def _crlf_terminated(filename: str) -> bool:
49
49
  class MySQLDatabase(Database):
50
50
  """MySQL and MariaDB adapter using the pymysql package."""
51
51
 
52
+ #: Statements MySQL commits at once, together with everything before them
53
+ #: ("Statements That Cause an Implicit Commit" in the MySQL manual).
54
+ #: CREATE and DROP TEMPORARY TABLE are the exception.
55
+ implicit_commit_rx = re.compile(
56
+ r"(?:CREATE|ALTER|DROP)\s+(?!TEMPORARY\b)(?:OR\s+REPLACE\s+)?(?:UNIQUE\s+|FULLTEXT\s+|SPATIAL\s+)?\w+"
57
+ r"|RENAME\s+\w+|TRUNCATE\b|GRANT\b|REVOKE\b|LOCK\s+TABLES?\b|UNLOCK\s+TABLES?\b"
58
+ r"|BEGIN\b|START\s+TRANSACTION\b|(?:ANALYZE|CHECK|OPTIMIZE|REPAIR)\s+TABLE\b|FLUSH\b"
59
+ r"|(?:UN)?INSTALL\s+PLUGIN\b|SET\s+PASSWORD\b|SET\s+(?:SESSION\s+|@@)?autocommit\b",
60
+ re.I,
61
+ )
62
+
52
63
  def __init__(
53
64
  self,
54
65
  server_name: str,
execsql/db/oracle.py CHANGED
@@ -8,6 +8,7 @@ via the ``oracledb`` driver (python-oracledb). Corresponds to ``-t o``
8
8
  on the CLI.
9
9
  """
10
10
 
11
+ import re
11
12
  from typing import Any
12
13
 
13
14
  from execsql.db.base import Database
@@ -23,6 +24,14 @@ __all__ = ["OracleDatabase"]
23
24
  class OracleDatabase(Database):
24
25
  """Oracle adapter using the cx_Oracle (python-oracledb) driver."""
25
26
 
27
+ #: Oracle commits every DDL statement at once, together with everything
28
+ #: before it, including CREATE GLOBAL TEMPORARY TABLE.
29
+ implicit_commit_rx = re.compile(
30
+ r"(?:CREATE|ALTER|DROP)\s+(?:OR\s+REPLACE\s+)?(?:GLOBAL\s+TEMPORARY\s+|UNIQUE\s+|BITMAP\s+)?\w+"
31
+ r"|TRUNCATE\s+\w+|RENAME\b|GRANT\b|REVOKE\b|COMMENT\s+ON\b|(?:NO)?AUDIT\b|ANALYZE\b|PURGE\b|FLASHBACK\b",
32
+ re.I,
33
+ )
34
+
26
35
  #: No Oracle server runs in CI; this adapter has no tests.
27
36
  support_tier = SupportTier.BEST_EFFORT
28
37
  support_tier_name = "Oracle"
@@ -110,13 +119,12 @@ class OracleDatabase(Database):
110
119
  msg = f"Failed to open Oracle database {self.db_name} on {self.server_name}"
111
120
  raise ErrInfo(type="exception", exception_msg=exception_desc(), other_msg=msg) from e
112
121
 
113
- def execute(self, sql: Any, paramlist: list | None = None) -> None:
122
+ def execute(self, sql: Any, paramlist: list | None = None, *, fetch: bool = False) -> tuple[list[str], list] | None:
114
123
  """Execute a SQL command, stripping any trailing semicolon for Oracle."""
115
124
  # Strip any semicolon off the end and pass to the parent method.
116
125
  if sql[-1:] == ";":
117
- super().execute(sql[:-1], paramlist)
118
- else:
119
- super().execute(sql, paramlist)
126
+ return super().execute(sql[:-1], paramlist, fetch=fetch)
127
+ return super().execute(sql, paramlist, fetch=fetch)
120
128
 
121
129
  def select_data(self, sql: str) -> tuple[list[str], list]:
122
130
  """Return column names and all rows from a SELECT statement."""
execsql/db/sqlite.py CHANGED
@@ -26,6 +26,10 @@ DEFAULT_CONNECT_TIMEOUT = 30 # seconds
26
26
  class SQLiteDatabase(Database):
27
27
  """SQLite adapter using the Python standard-library sqlite3 module."""
28
28
 
29
+ #: SQLite refuses VACUUM, ATTACH and DETACH inside a transaction, and
30
+ #: ignores some PRAGMAs there (``foreign_keys``).
31
+ no_transaction_rx = re.compile(r"(?:VACUUM|ATTACH|DETACH|PRAGMA)\b", re.I)
32
+
29
33
  def __init__(self, SQLite_fn: str, timeout: float = DEFAULT_CONNECT_TIMEOUT) -> None:
30
34
  try:
31
35
  import sqlite3 # noqa: F401
@@ -48,6 +52,18 @@ class SQLiteDatabase(Database):
48
52
  def __repr__(self) -> str:
49
53
  return f"SQLiteDatabase({self.db_name!r})"
50
54
 
55
+ def begin_transaction(self) -> None:
56
+ """Open a transaction if none is open.
57
+
58
+ Python's sqlite3 module opens one on its own only before INSERT,
59
+ UPDATE, DELETE and REPLACE. Without this, a CREATE TABLE or DROP run
60
+ under AUTOCOMMIT OFF or in a batch would be committed at once.
61
+ """
62
+ if self.conn is None:
63
+ self.open_db()
64
+ if not self.conn.in_transaction:
65
+ self.conn.execute("BEGIN")
66
+
51
67
  def open_db(self) -> None:
52
68
  """Open a connection to the SQLite database file."""
53
69
  import sqlite3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execsql2
3
- Version: 2.23.0
3
+ Version: 2.23.1
4
4
  Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
5
5
  Project-URL: Homepage, https://execsql2.readthedocs.io
6
6
  Project-URL: Repository, https://github.com/geocoug/execsql
@@ -393,7 +393,7 @@ execsql-format --no-sql --in-place scripts/
393
393
  ```yaml
394
394
  repos:
395
395
  - repo: https://github.com/geocoug/execsql
396
- rev: v2.23.0
396
+ rev: v2.23.1
397
397
  hooks:
398
398
  - id: execsql-format
399
399
  ```
@@ -19,16 +19,16 @@ execsql/cli/run.py,sha256=Yb-Ri3cds0st1hUn4_w-PHMphTsArHhC4fx919SW9gU,39390
19
19
  execsql/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  execsql/data/execsql.conf.template,sha256=qqH_G3SiKBepljY3Go4rDDlhe3pIM6gOgsjAnLn0arg,11244
21
21
  execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
22
- execsql/db/access.py,sha256=tcnWNqcCzpBtYKHIdfYj7xaJiaiDIzq0JUSIGf4KrrI,19823
23
- execsql/db/base.py,sha256=oqRQXvf7kwCdgQMJQuJ_rKOI8UAnakaNCr_ivdC88BA,36102
24
- execsql/db/dsn.py,sha256=nP7cESX7pJdZSPzyo2nYj6972GpPVVc8ESwyYT0Z_yQ,6523
25
- execsql/db/duckdb.py,sha256=QQkGH8FVWEF9u5cM-Mtit-lBtmnWm4zo4BMMSiiEXdM,3176
22
+ execsql/db/access.py,sha256=2VlfEDXvV8xoi1t_hKzEaIidCJ1XGerYFPFvW5FqVBI,20489
23
+ execsql/db/base.py,sha256=2bK1TBU9IAHnQQcdgJ-wEwm5TzrhnwO6j6BGjR9VAHg,40656
24
+ execsql/db/dsn.py,sha256=hC_oWrBz2NSGyAOjZsROxGHs9eI_CSpnnken12wH1hU,6663
25
+ execsql/db/duckdb.py,sha256=h8fh_5Ps1UcXvfyG9UlSyP32Yoa0W8sn246RlQ4xOMU,6970
26
26
  execsql/db/factory.py,sha256=C_cyi3GjNx0opuwc6maFFeCSkjfOfoUjuf68YT4VAHE,5377
27
27
  execsql/db/firebird.py,sha256=fbzlGB6DeR-S9RFLUD4tJYPN-_76tyRLBkQl5LRJTDo,9640
28
- execsql/db/mysql.py,sha256=Mb35hJOAviuf_QGJgIL17ftF-Xr2eb78z0cFVMRn_5E,19835
29
- execsql/db/oracle.py,sha256=gBcSJaS92VV_LrxAPY8wFCqa4ouD8TiEhUB4dRK3la0,12174
28
+ execsql/db/mysql.py,sha256=vG_5NPoe82FLvdfY4e7ZnjX0-mgts4D3S40LwZKb7U8,20489
29
+ execsql/db/oracle.py,sha256=jjXwhk_og1qHqdAB16i_dZYXY0Ze7Bg5EvKdCKwjQtE,12667
30
30
  execsql/db/postgres.py,sha256=HfrWuJ5-K5le1sY9iGv_E51JHciPIdA0UvyLsTKlnO4,22568
31
- execsql/db/sqlite.py,sha256=4NRCO75DOYmkGrFlC6k_i8Hxd4jadLDbuRBYOI6CD8E,11498
31
+ execsql/db/sqlite.py,sha256=zfe-KNAuTIHDh_qk3yzCo6fMerlmtt7UFmg-lbdZFm4,12167
32
32
  execsql/db/sqlserver.py,sha256=O4E9OD3V3Xowsjl3KMdYHNCBeETlkJS9HUzNKEKuA9A,8943
33
33
  execsql/db/tiers.py,sha256=PH2sUYwlYqcANGVKXprWQKVIlM6fpWU9q74djn8oFwc,2924
34
34
  execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
@@ -102,23 +102,23 @@ execsql/utils/numeric.py,sha256=_7bi5R6pHjMCkOPne_Sh5nUj-B8e9g6WFmjptFHaunA,1573
102
102
  execsql/utils/regex.py,sha256=B3mSBhRRx_DpUnX_t9IoYd05KvWAebiLMCZqQAmEwcw,3885
103
103
  execsql/utils/strings.py,sha256=UQNjpRCEFa1UO6feU-M-9e24wWAvizs_iu_4fFusLxo,8516
104
104
  execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
105
- execsql2-2.23.0.data/data/execsql2_extras/README.md,sha256=vX4NTL095dUoA_hesyRMGYBorEZ_Y_tJ9qrd-MVV09I,5032
106
- execsql2-2.23.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
107
- execsql2-2.23.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
108
- execsql2-2.23.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
109
- execsql2-2.23.0.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
110
- execsql2-2.23.0.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
111
- execsql2-2.23.0.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
112
- execsql2-2.23.0.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
113
- execsql2-2.23.0.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
114
- execsql2-2.23.0.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
115
- execsql2-2.23.0.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
116
- execsql2-2.23.0.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
117
- execsql2-2.23.0.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
118
- execsql2-2.23.0.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
119
- execsql2-2.23.0.dist-info/METADATA,sha256=i-ekOSO01zUc7UtJwXvGJLTZtmvA78sZSbsRXUO0E20,21504
120
- execsql2-2.23.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
121
- execsql2-2.23.0.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
122
- execsql2-2.23.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
123
- execsql2-2.23.0.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
124
- execsql2-2.23.0.dist-info/RECORD,,
105
+ execsql2-2.23.1.data/data/execsql2_extras/README.md,sha256=vX4NTL095dUoA_hesyRMGYBorEZ_Y_tJ9qrd-MVV09I,5032
106
+ execsql2-2.23.1.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
107
+ execsql2-2.23.1.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
108
+ execsql2-2.23.1.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
109
+ execsql2-2.23.1.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
110
+ execsql2-2.23.1.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
111
+ execsql2-2.23.1.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
112
+ execsql2-2.23.1.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
113
+ execsql2-2.23.1.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
114
+ execsql2-2.23.1.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
115
+ execsql2-2.23.1.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
116
+ execsql2-2.23.1.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
117
+ execsql2-2.23.1.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
118
+ execsql2-2.23.1.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
119
+ execsql2-2.23.1.dist-info/METADATA,sha256=Zp_UKzwuqCVFmA3-yQrsaenr3QI_sFkgWAtNAQpl-OE,21504
120
+ execsql2-2.23.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
121
+ execsql2-2.23.1.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
122
+ execsql2-2.23.1.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
123
+ execsql2-2.23.1.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
124
+ execsql2-2.23.1.dist-info/RECORD,,