forked from s3lph/matemat
Merge branch '10-builtin-sqlite3' into 'master'
Resolve "Get rid of APSW" Closes #10 See merge request s3lph/matemat!14
This commit is contained in:
commit
2dcb8dc619
5 changed files with 50 additions and 29 deletions
|
@ -18,7 +18,6 @@ This project intends to provide a well-tested and maintainable alternative to
|
||||||
|
|
||||||
- Python 3 (>=3.6)
|
- Python 3 (>=3.6)
|
||||||
- Python dependencies:
|
- Python dependencies:
|
||||||
- apsw
|
|
||||||
- jinja2
|
- jinja2
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
|
@ -7,6 +7,7 @@ from hmac import compare_digest
|
||||||
from matemat.primitives import User, Product
|
from matemat.primitives import User, Product
|
||||||
from matemat.exceptions import AuthenticationError, DatabaseConsistencyError
|
from matemat.exceptions import AuthenticationError, DatabaseConsistencyError
|
||||||
from matemat.db import DatabaseWrapper
|
from matemat.db import DatabaseWrapper
|
||||||
|
from matemat.db.wrapper import Transaction
|
||||||
|
|
||||||
# TODO: Change to METHOD_BLOWFISH when adopting Python 3.7
|
# TODO: Change to METHOD_BLOWFISH when adopting Python 3.7
|
||||||
"""
|
"""
|
||||||
|
@ -50,19 +51,19 @@ class MatematDatabase(object):
|
||||||
# Pass context manager stuff through to the database wrapper
|
# Pass context manager stuff through to the database wrapper
|
||||||
self.db.__exit__(exc_type, exc_val, exc_tb)
|
self.db.__exit__(exc_type, exc_val, exc_tb)
|
||||||
|
|
||||||
def transaction(self, exclusive: bool = True) -> Any:
|
def transaction(self, exclusive: bool = True) -> Transaction:
|
||||||
"""
|
"""
|
||||||
Begin a new SQLite3 transaction (exclusive by default). You should never need to use the returned object (an
|
Begin a new SQLite3 transaction (exclusive by default). You should never need to use the returned object (a
|
||||||
APSW cursor). It is provided in case there is a real need for it (e.g. for unit testing).
|
Transaction instance). It is provided in case there is a real need for it (e.g. for unit testing).
|
||||||
|
|
||||||
This function should be used with the Context Manager 'with' syntax (PEP 343), e.g.:
|
This function should be used with the Context Manager 'with' syntax (PEP 343), yielding a sqlite3.Cursor, e.g.:
|
||||||
|
|
||||||
with db.transaction():
|
with db.transaction():
|
||||||
db.foo()
|
db.foo()
|
||||||
db.bar()
|
db.bar()
|
||||||
|
|
||||||
:param exclusive: Whether to begin an exclusive transaction or not, defaults to True (exclusive).
|
:param exclusive: Whether to begin an exclusive transaction or not, defaults to True (exclusive).
|
||||||
:return: An APSW cursor.
|
:return: A a transaction object.
|
||||||
"""
|
"""
|
||||||
return self.db.transaction(exclusive=exclusive)
|
return self.db.transaction(exclusive=exclusive)
|
||||||
|
|
||||||
|
|
|
@ -75,3 +75,29 @@ class DatabaseTest(unittest.TestCase):
|
||||||
c = db._sqlite_db.cursor()
|
c = db._sqlite_db.cursor()
|
||||||
c.execute("SELECT * FROM users")
|
c.execute("SELECT * FROM users")
|
||||||
self.assertIsNone(c.fetchone())
|
self.assertIsNone(c.fetchone())
|
||||||
|
|
||||||
|
def test_connect_twice(self):
|
||||||
|
"""
|
||||||
|
If a connection is already established, a RuntimeError should be raised when attempting to connect a
|
||||||
|
second time.
|
||||||
|
"""
|
||||||
|
self.db.connect()
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
self.db.connect()
|
||||||
|
self.db.close()
|
||||||
|
|
||||||
|
def test_close_not_opened(self):
|
||||||
|
"""
|
||||||
|
Attempting to close an unopened connection should raise a RuntimeError.
|
||||||
|
"""
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
self.db.close()
|
||||||
|
|
||||||
|
def test_close_in_transaction(self):
|
||||||
|
"""
|
||||||
|
Attempting to close a connection inside a transaction should raise a RuntimeError.
|
||||||
|
"""
|
||||||
|
with self.db as db:
|
||||||
|
with db.transaction():
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
self.db.close()
|
||||||
|
|
|
@ -1,43 +1,40 @@
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import apsw
|
import sqlite3
|
||||||
|
|
||||||
from matemat.exceptions import DatabaseConsistencyError
|
from matemat.exceptions import DatabaseConsistencyError
|
||||||
|
|
||||||
|
|
||||||
class Transaction(object):
|
class Transaction(object):
|
||||||
|
|
||||||
def __init__(self, db: apsw.Connection, wrapper: 'DatabaseWrapper', exclusive: bool = True) -> None:
|
def __init__(self, db: sqlite3.Connection, exclusive: bool = True) -> None:
|
||||||
self._db: apsw.Connection = db
|
self._db: sqlite3.Connection = db
|
||||||
self._cursor = None
|
self._cursor = None
|
||||||
self._excl = exclusive
|
self._excl = exclusive
|
||||||
self._wrapper: DatabaseWrapper = wrapper
|
|
||||||
self._is_dummy: bool = False
|
self._is_dummy: bool = False
|
||||||
|
|
||||||
def __enter__(self) -> Any:
|
def __enter__(self) -> sqlite3.Cursor:
|
||||||
if self._wrapper._in_transaction:
|
if self._db.in_transaction:
|
||||||
self._is_dummy = True
|
self._is_dummy = True
|
||||||
return self._db.cursor()
|
return self._db.cursor()
|
||||||
else:
|
else:
|
||||||
self._is_dummy = False
|
self._is_dummy = False
|
||||||
self._cursor = self._db.cursor()
|
|
||||||
self._wrapper._in_transaction = True
|
|
||||||
if self._excl:
|
if self._excl:
|
||||||
self._cursor.execute('BEGIN EXCLUSIVE')
|
self._db.execute('BEGIN EXCLUSIVE')
|
||||||
else:
|
else:
|
||||||
self._cursor.execute('BEGIN')
|
self._db.execute('BEGIN')
|
||||||
|
self._cursor = self._db.cursor()
|
||||||
return self._cursor
|
return self._cursor
|
||||||
|
|
||||||
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||||
if self._is_dummy:
|
if self._is_dummy:
|
||||||
return
|
return
|
||||||
if exc_type is None:
|
if exc_type is None:
|
||||||
self._cursor.execute('COMMIT')
|
self._db.commit()
|
||||||
else:
|
else:
|
||||||
self._cursor.execute('ROLLBACK')
|
self._db.rollback()
|
||||||
self._wrapper._in_transaction = False
|
if exc_type == sqlite3.IntegrityError:
|
||||||
if exc_type == apsw.ConstraintError:
|
|
||||||
raise DatabaseConsistencyError(str(exc_val))
|
raise DatabaseConsistencyError(str(exc_val))
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,7 +44,7 @@ class DatabaseWrapper(object):
|
||||||
SCHEMA = '''
|
SCHEMA = '''
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
user_id INTEGER PRIMARY KEY,
|
user_id INTEGER PRIMARY KEY,
|
||||||
username TEXT NOT NULL,
|
username TEXT UNIQUE NOT NULL,
|
||||||
email TEXT DEFAULT NULL,
|
email TEXT DEFAULT NULL,
|
||||||
password TEXT NOT NULL,
|
password TEXT NOT NULL,
|
||||||
touchkey TEXT DEFAULT NULL,
|
touchkey TEXT DEFAULT NULL,
|
||||||
|
@ -72,13 +69,12 @@ class DatabaseWrapper(object):
|
||||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
FOREIGN KEY (product_id) REFERENCES products(product_id)
|
FOREIGN KEY (product_id) REFERENCES products(product_id)
|
||||||
ON DELETE CASCADE ON UPDATE CASCADE
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
)
|
);
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, filename: str) -> None:
|
def __init__(self, filename: str) -> None:
|
||||||
self._filename: str = filename
|
self._filename: str = filename
|
||||||
self._sqlite_db: apsw.Connection = None
|
self._sqlite_db: sqlite3.Connection = None
|
||||||
self._in_transaction: bool = False
|
|
||||||
|
|
||||||
def __enter__(self) -> 'DatabaseWrapper':
|
def __enter__(self) -> 'DatabaseWrapper':
|
||||||
self.connect()
|
self.connect()
|
||||||
|
@ -88,13 +84,13 @@ class DatabaseWrapper(object):
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def transaction(self, exclusive: bool = True) -> Transaction:
|
def transaction(self, exclusive: bool = True) -> Transaction:
|
||||||
return Transaction(self._sqlite_db, self, exclusive)
|
return Transaction(self._sqlite_db, exclusive)
|
||||||
|
|
||||||
def _setup(self) -> None:
|
def _setup(self) -> None:
|
||||||
with self.transaction() as c:
|
with self.transaction() as c:
|
||||||
version: int = self._user_version
|
version: int = self._user_version
|
||||||
if version < 1:
|
if version < 1:
|
||||||
c.execute(self.SCHEMA)
|
c.executescript(self.SCHEMA)
|
||||||
elif version < self.SCHEMA_VERSION:
|
elif version < self.SCHEMA_VERSION:
|
||||||
self._upgrade(old=version, new=self.SCHEMA_VERSION)
|
self._upgrade(old=version, new=self.SCHEMA_VERSION)
|
||||||
self._user_version = self.SCHEMA_VERSION
|
self._user_version = self.SCHEMA_VERSION
|
||||||
|
@ -105,7 +101,7 @@ class DatabaseWrapper(object):
|
||||||
def connect(self) -> None:
|
def connect(self) -> None:
|
||||||
if self.is_connected():
|
if self.is_connected():
|
||||||
raise RuntimeError(f'Database connection to {self._filename} is already established.')
|
raise RuntimeError(f'Database connection to {self._filename} is already established.')
|
||||||
self._sqlite_db = apsw.Connection(self._filename)
|
self._sqlite_db = sqlite3.connect(self._filename)
|
||||||
self._setup()
|
self._setup()
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
|
@ -117,7 +113,7 @@ class DatabaseWrapper(object):
|
||||||
self._sqlite_db = None
|
self._sqlite_db = None
|
||||||
|
|
||||||
def in_transaction(self) -> bool:
|
def in_transaction(self) -> bool:
|
||||||
return self._in_transaction
|
return self._sqlite_db.in_transaction
|
||||||
|
|
||||||
def is_connected(self) -> bool:
|
def is_connected(self) -> bool:
|
||||||
return self._sqlite_db is not None
|
return self._sqlite_db is not None
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
apsw
|
|
||||||
jinja2
|
jinja2
|
||||||
|
|
Loading…
Reference in a new issue