Skip to content

Commit e65a892

Browse files
committed
feat(dbapi): commit and rollback
1 parent e80d32b commit e65a892

5 files changed

Lines changed: 144 additions & 8 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ main.dSYM/
1515
SqliteCloud.egg-info
1616

1717
playground.ipynb
18+
19+
src/tests/assets/*-shm
20+
src/tests/assets/*-wal

src/sqlitecloud/dbapi2.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# PEP 249 – Python Database API Specification v2.0
44
# https://peps.python.org/pep-0249/
55
#
6+
import logging
67
from typing import (
78
Any,
89
Callable,
@@ -123,10 +124,10 @@ class Connection:
123124

124125
row_factory: Optional[Callable[["Cursor", Tuple], object]] = None
125126

126-
def __init__(self, SQLiteCloud_connection: SQLiteCloudConnect) -> None:
127+
def __init__(self, sqlitecloud_connection: SQLiteCloudConnect) -> None:
127128
self._driver = Driver()
128129
self.row_factory = None
129-
self.SQLiteCloud_connection = SQLiteCloud_connection
130+
self.sqlitecloud_connection = sqlitecloud_connection
130131

131132
@property
132133
def sqlcloud_connection(self) -> SQLiteCloudConnect:
@@ -136,7 +137,7 @@ def sqlcloud_connection(self) -> SQLiteCloudConnect:
136137
Returns:
137138
SQLiteCloudConnect: The SQLite Cloud connection object.
138139
"""
139-
return self.SQLiteCloud_connection
140+
return self.sqlitecloud_connection
140141

141142
def execute(
142143
self,
@@ -194,17 +195,42 @@ def close(self):
194195
DB-API 2.0 interface does not manage the Sqlite Cloud PubSub feature.
195196
Therefore, only the main socket is closed.
196197
"""
197-
self._driver.disconnect(self.SQLiteCloud_connection, True)
198+
self._driver.disconnect(self.sqlitecloud_connection, True)
198199

199200
def commit(self):
200201
"""
201-
Not implementied yet.
202+
Commit any pending transactions on database.
202203
"""
204+
try:
205+
self._driver.execute("COMMIT;", self.sqlitecloud_connection)
206+
except SQLiteCloudException as e:
207+
if (
208+
e.errcode == 1
209+
and e.xerrcode == 1
210+
and "no transaction is active" in e.errmsg
211+
):
212+
# compliance to sqlite3
213+
logging.warning(e)
203214

204215
def rollback(self):
205216
"""
206-
Not implemented yet.
207-
"""
217+
Causes the database to roll back to the start of any pending transaction.
218+
A transaction will also rool back if the database is closed or if an error occurs
219+
and the roll back conflict resolution algorithm is specified.
220+
221+
See the documentation on the `ON CONFLICT <https://docs.sqlitecloud.io/docs/sqlite/lang_conflict>`
222+
clause for additional information about the ROLLBACK conflict resolution algorithm.
223+
"""
224+
try:
225+
self._driver.execute("ROLLBACK;", self.sqlitecloud_connection)
226+
except SQLiteCloudException as e:
227+
if (
228+
e.errcode == 1
229+
and e.xerrcode == 1
230+
and "no transaction is active" in e.errmsg
231+
):
232+
# compliance to sqlite3
233+
logging.warning(e)
208234

209235
def cursor(self):
210236
"""

src/tests/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ def sqlitecloud_connection():
3535

3636
@pytest.fixture()
3737
def sqlitecloud_dbapi2_connection():
38+
# fixture and declaration are split to be able
39+
# to create multiple instances of the connection
40+
# when calling the getter function directly from
41+
# the test.
42+
# Fixtures are both cached and cannot be called
43+
# directly whithin the test.
44+
yield next(get_sqlitecloud_dbapi2_connection())
45+
46+
47+
def get_sqlitecloud_dbapi2_connection():
3848
account = SQLiteCloudAccount()
3949
account.username = os.getenv("SQLITE_USER")
4050
account.password = os.getenv("SQLITE_PASSWORD")

src/tests/integration/test_dbapi2.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,21 @@ def test_row_factory(self, sqlitecloud_dbapi2_connection):
246246
assert row["AlbumId"] == 1
247247
assert row["Title"] == "For Those About To Rock We Salute You"
248248
assert row["ArtistId"] == 1
249+
250+
def test_commit_without_any_transaction_does_not_raise_exception(
251+
self, sqlitecloud_dbapi2_connection
252+
):
253+
connection = sqlitecloud_dbapi2_connection
254+
255+
connection.commit()
256+
257+
assert True
258+
259+
def test_rollback_without_any_transaction_does_not_raise_exception(
260+
self, sqlitecloud_dbapi2_connection
261+
):
262+
connection = sqlitecloud_dbapi2_connection
263+
264+
connection.rollback()
265+
266+
assert True

src/tests/integration/test_sqlite3_parity.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import os
22
import sqlite3
3+
import time
34

45
import pytest
56

7+
import sqlitecloud
68
from sqlitecloud.datatypes import SQLiteCloudException
9+
from tests.conftest import get_sqlitecloud_dbapi2_connection
710

811

912
class TestSQLite3FeatureParity:
1013
@pytest.fixture()
1114
def sqlite3_connection(self):
15+
yield next(self.get_sqlite3_connection())
16+
17+
def get_sqlite3_connection(self):
18+
# set isolation_level=None to enable autocommit
19+
# and to be aligned with the behavior of SQLite Cloud
1220
connection = sqlite3.connect(
13-
os.path.join(os.path.dirname(__file__), "../assets/chinook.sqlite")
21+
os.path.join(os.path.dirname(__file__), "../assets/chinook.sqlite"),
22+
isolation_level=None,
1423
)
1524
yield connection
1625
connection.close()
@@ -243,3 +252,73 @@ def test_fetchall(self, sqlitecloud_dbapi2_connection, sqlite3_connection):
243252
assert sqlitecloud_results == sqlite3_results
244253
assert len(sqlitecloud_results) == 0
245254
assert len(sqlite3_results) == 0
255+
256+
def test_autocommit_mode_enabled_by_default(
257+
self, sqlitecloud_dbapi2_connection, sqlite3_connection
258+
):
259+
seed = str(int(time.time()))
260+
261+
connections = [
262+
(sqlitecloud_dbapi2_connection, next(get_sqlitecloud_dbapi2_connection())),
263+
(sqlite3_connection, next(self.get_sqlite3_connection())),
264+
]
265+
266+
for (connection, control_connection) in connections:
267+
connection.execute(
268+
"INSERT INTO albums (Title, ArtistId) VALUES (? , 1);",
269+
(f"Test {seed}",),
270+
)
271+
272+
cursor2 = control_connection.execute(
273+
"SELECT * FROM albums WHERE Title = ?", (f"Test {seed}",)
274+
)
275+
assert cursor2.fetchone() is not None
276+
277+
def test_explicit_transaction_to_commit(
278+
self,
279+
sqlitecloud_dbapi2_connection: sqlitecloud.Connection,
280+
sqlite3_connection: sqlite3.Connection,
281+
):
282+
seed = str(int(time.time()))
283+
284+
connections = [
285+
(sqlitecloud_dbapi2_connection, next(get_sqlitecloud_dbapi2_connection())),
286+
(sqlite3_connection, next(self.get_sqlite3_connection())),
287+
]
288+
289+
for (connection, control_connection) in connections:
290+
cursor1 = connection.execute("BEGIN;")
291+
cursor1.execute(
292+
"INSERT INTO albums (Title, ArtistId) VALUES (?, 1);", (f"Test {seed}",)
293+
)
294+
295+
cursor2 = control_connection.execute(
296+
"SELECT * FROM albums WHERE Title = ?", (f"Test {seed}",)
297+
)
298+
assert cursor2.fetchone() is None
299+
300+
connection.commit()
301+
302+
cursor2.execute("SELECT * FROM albums WHERE Title = ?", (f"Test {seed}",))
303+
assert cursor2.fetchone() is not None
304+
305+
def test_explicit_transaction_to_rollback(
306+
self,
307+
sqlitecloud_dbapi2_connection: sqlitecloud.Connection,
308+
sqlite3_connection: sqlite3.Connection,
309+
):
310+
seed = str(int(time.time()))
311+
312+
for connection in [sqlitecloud_dbapi2_connection, sqlite3_connection]:
313+
cursor1 = connection.execute("BEGIN;")
314+
cursor1.execute(
315+
"INSERT INTO albums (Title, ArtistId) VALUES (?, 1);", (f"Test {seed}",)
316+
)
317+
318+
cursor1.execute("SELECT * FROM albums WHERE Title = ?", (f"Test {seed}",))
319+
assert cursor1.fetchone() is not None
320+
321+
connection.rollback()
322+
323+
cursor1.execute("SELECT * FROM albums WHERE Title = ?", (f"Test {seed}",))
324+
assert cursor1.fetchone() is None

0 commit comments

Comments
 (0)