1
0
Fork 0
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:
s3lph 2018-07-14 21:47:16 +00:00
commit 2dcb8dc619
5 changed files with 50 additions and 29 deletions

View file

@ -18,7 +18,6 @@ This project intends to provide a well-tested and maintainable alternative to
- Python 3 (>=3.6)
- Python dependencies:
- apsw
- jinja2
## Usage

View file

@ -7,6 +7,7 @@ from hmac import compare_digest
from matemat.primitives import User, Product
from matemat.exceptions import AuthenticationError, DatabaseConsistencyError
from matemat.db import DatabaseWrapper
from matemat.db.wrapper import Transaction
# 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
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
APSW cursor). It is provided in case there is a real need for it (e.g. for unit testing).
Begin a new SQLite3 transaction (exclusive by default). You should never need to use the returned object (a
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():
db.foo()
db.bar()
: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)

View file

@ -75,3 +75,29 @@ class DatabaseTest(unittest.TestCase):
c = db._sqlite_db.cursor()
c.execute("SELECT * FROM users")
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()

View file

@ -1,43 +1,40 @@
from typing import Any
import apsw
import sqlite3
from matemat.exceptions import DatabaseConsistencyError
class Transaction(object):
def __init__(self, db: apsw.Connection, wrapper: 'DatabaseWrapper', exclusive: bool = True) -> None:
self._db: apsw.Connection = db
def __init__(self, db: sqlite3.Connection, exclusive: bool = True) -> None:
self._db: sqlite3.Connection = db
self._cursor = None
self._excl = exclusive
self._wrapper: DatabaseWrapper = wrapper
self._is_dummy: bool = False
def __enter__(self) -> Any:
if self._wrapper._in_transaction:
def __enter__(self) -> sqlite3.Cursor:
if self._db.in_transaction:
self._is_dummy = True
return self._db.cursor()
else:
self._is_dummy = False
self._cursor = self._db.cursor()
self._wrapper._in_transaction = True
if self._excl:
self._cursor.execute('BEGIN EXCLUSIVE')
self._db.execute('BEGIN EXCLUSIVE')
else:
self._cursor.execute('BEGIN')
self._db.execute('BEGIN')
self._cursor = self._db.cursor()
return self._cursor
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
if self._is_dummy:
return
if exc_type is None:
self._cursor.execute('COMMIT')
self._db.commit()
else:
self._cursor.execute('ROLLBACK')
self._wrapper._in_transaction = False
if exc_type == apsw.ConstraintError:
self._db.rollback()
if exc_type == sqlite3.IntegrityError:
raise DatabaseConsistencyError(str(exc_val))
@ -47,7 +44,7 @@ class DatabaseWrapper(object):
SCHEMA = '''
CREATE TABLE users (
user_id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
username TEXT UNIQUE NOT NULL,
email TEXT DEFAULT NULL,
password TEXT NOT NULL,
touchkey TEXT DEFAULT NULL,
@ -72,13 +69,12 @@ class DatabaseWrapper(object):
ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(product_id)
ON DELETE CASCADE ON UPDATE CASCADE
)
);
'''
def __init__(self, filename: str) -> None:
self._filename: str = filename
self._sqlite_db: apsw.Connection = None
self._in_transaction: bool = False
self._sqlite_db: sqlite3.Connection = None
def __enter__(self) -> 'DatabaseWrapper':
self.connect()
@ -88,13 +84,13 @@ class DatabaseWrapper(object):
self.close()
def transaction(self, exclusive: bool = True) -> Transaction:
return Transaction(self._sqlite_db, self, exclusive)
return Transaction(self._sqlite_db, exclusive)
def _setup(self) -> None:
with self.transaction() as c:
version: int = self._user_version
if version < 1:
c.execute(self.SCHEMA)
c.executescript(self.SCHEMA)
elif version < self.SCHEMA_VERSION:
self._upgrade(old=version, new=self.SCHEMA_VERSION)
self._user_version = self.SCHEMA_VERSION
@ -105,7 +101,7 @@ class DatabaseWrapper(object):
def connect(self) -> None:
if self.is_connected():
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()
def close(self) -> None:
@ -117,7 +113,7 @@ class DatabaseWrapper(object):
self._sqlite_db = None
def in_transaction(self) -> bool:
return self._in_transaction
return self._sqlite_db.in_transaction
def is_connected(self) -> bool:
return self._sqlite_db is not None

View file

@ -1,2 +1 @@
apsw
jinja2