- Another change to the database schema

- Enabled foreign key constraints (!!!)
- Fixed a facade bug exposed by foreign key constraints
This commit is contained in:
s3lph 2018-08-31 21:52:00 +02:00
parent 56ce2a73cb
commit 5d710f0c18
5 changed files with 48 additions and 7 deletions

View file

@ -111,8 +111,12 @@ class MatematDatabase(object):
if row is None: if row is None:
raise ValueError(f'No user with user ID {uid} exists.') raise ValueError(f'No user with user ID {uid} exists.')
# Unpack the row and construct the user # Unpack the row and construct the user
user_id, username, email, is_admin, is_member, balance, receipt_pref = row user_id, username, email, is_admin, is_member, balance, receipt_p = row
return User(user_id, username, balance, email, is_admin, is_member, ReceiptPreference(receipt_pref)) try:
receipt_pref: ReceiptPreference = ReceiptPreference(receipt_p)
except ValueError as e:
raise DatabaseConsistencyError(f'{receipt_p} is not a valid ReceiptPreference')
return User(user_id, username, balance, email, is_admin, is_member, receipt_pref)
def create_user(self, def create_user(self,
username: str, username: str,
@ -531,6 +535,10 @@ class MatematDatabase(object):
if amount < 0: if amount < 0:
raise ValueError('Cannot deposit a negative value') raise ValueError('Cannot deposit a negative value')
with self.db.transaction() as c: with self.db.transaction() as c:
c.execute('''SELECT username FROM users WHERE user_id = ?''',
[user.id])
if c.fetchone() is None:
raise DatabaseConsistencyError(f'No such user: {user.id}')
c.execute(''' c.execute('''
INSERT INTO transactions (user_id, value, old_balance) INSERT INTO transactions (user_id, value, old_balance)
VALUES (:user_id, :value, :old_balance) VALUES (:user_id, :value, :old_balance)

View file

@ -119,6 +119,22 @@ def migrate_schema_2_to_3(c: sqlite3.Cursor):
# Add missing column to users table # Add missing column to users table
c.execute('ALTER TABLE users ADD COLUMN receipt_pref INTEGER(1) NOT NULL DEFAULT 0') c.execute('ALTER TABLE users ADD COLUMN receipt_pref INTEGER(1) NOT NULL DEFAULT 0')
# Fix ON DELETE in transactions table
c.execute('''
CREATE TABLE transactions_new (
ta_id INTEGER PRIMARY KEY,
user_id INTEGER DEFAULT NULL,
value INTEGER(8) NOT NULL,
old_balance INTEGER(8) NOT NULL,
date INTEGER(8) DEFAULT (STRFTIME('%s', 'now')),
FOREIGN KEY (user_id) REFERENCES users(user_id)
ON DELETE SET NULL ON UPDATE CASCADE
)
''')
c.execute('INSERT INTO transactions_new SELECT * FROM transactions')
c.execute('DROP TABLE transactions')
c.execute('ALTER TABLE transactions_new RENAME TO transactions')
# Change modifications table # Change modifications table
c.execute(''' c.execute('''
CREATE TABLE modifications_new ( CREATE TABLE modifications_new (

View file

@ -129,12 +129,12 @@ SCHEMAS[3] = [
''' '''
CREATE TABLE transactions ( -- "superclass" of the following 3 tables CREATE TABLE transactions ( -- "superclass" of the following 3 tables
ta_id INTEGER PRIMARY KEY, ta_id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL, user_id INTEGER DEFAULT NULL,
value INTEGER(8) NOT NULL, value INTEGER(8) NOT NULL,
old_balance INTEGER(8) NOT NULL, old_balance INTEGER(8) NOT NULL,
date INTEGER(8) DEFAULT (STRFTIME('%s', 'now')), date INTEGER(8) DEFAULT (STRFTIME('%s', 'now')),
FOREIGN KEY (user_id) REFERENCES users(user_id) FOREIGN KEY (user_id) REFERENCES users(user_id)
ON DELETE CASCADE ON UPDATE CASCADE ON DELETE SET NULL ON UPDATE CASCADE
); );
''', ''',
''' '''

View file

@ -4,7 +4,7 @@ import unittest
import crypt import crypt
from matemat.db import MatematDatabase from matemat.db import MatematDatabase
from matemat.db.primitives import User from matemat.db.primitives import User, ReceiptPreference
from matemat.exceptions import AuthenticationError, DatabaseConsistencyError from matemat.exceptions import AuthenticationError, DatabaseConsistencyError
@ -24,12 +24,13 @@ class DatabaseTest(unittest.TestCase):
self.assertEqual('testuser@example.com', row[2]) self.assertEqual('testuser@example.com', row[2])
self.assertEqual(0, row[5]) self.assertEqual(0, row[5])
self.assertEqual(1, row[6]) self.assertEqual(1, row[6])
self.assertEqual(ReceiptPreference.NONE.value, row[7])
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
db.create_user('testuser', 'supersecurepassword2', 'testuser2@example.com') db.create_user('testuser', 'supersecurepassword2', 'testuser2@example.com')
def test_get_user(self) -> None: def test_get_user(self) -> None:
with self.db as db: with self.db as db:
with db.transaction(exclusive=False): with db.transaction() as c:
created = db.create_user('testuser', 'supersecurepassword', 'testuser@example.com', created = db.create_user('testuser', 'supersecurepassword', 'testuser@example.com',
admin=True, member=False) admin=True, member=False)
user = db.get_user(created.id) user = db.get_user(created.id)
@ -37,8 +38,14 @@ class DatabaseTest(unittest.TestCase):
self.assertEqual('testuser@example.com', user.email) self.assertEqual('testuser@example.com', user.email)
self.assertEqual(False, user.is_member) self.assertEqual(False, user.is_member)
self.assertEqual(True, user.is_admin) self.assertEqual(True, user.is_admin)
self.assertEqual(ReceiptPreference.NONE, user.receipt_pref)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
db.get_user(-1) db.get_user(-1)
# Write an invalid receipt preference to the database
c.execute('UPDATE users SET receipt_pref = 42 WHERE user_id = ?',
[user.id])
with self.assertRaises(DatabaseConsistencyError):
db.get_user(user.id)
def test_list_users(self) -> None: def test_list_users(self) -> None:
with self.db as db: with self.db as db:
@ -166,18 +173,21 @@ class DatabaseTest(unittest.TestCase):
with self.db as db: with self.db as db:
agent = db.create_user('admin', 'supersecurepassword', 'admin@example.com', True, True) agent = db.create_user('admin', 'supersecurepassword', 'admin@example.com', True, True)
user = db.create_user('testuser', 'supersecurepassword', 'testuser@example.com', True, True) user = db.create_user('testuser', 'supersecurepassword', 'testuser@example.com', True, True)
db.change_user(user, agent, email='newaddress@example.com', is_admin=False, is_member=False, balance=4200) db.change_user(user, agent, email='newaddress@example.com', is_admin=False, is_member=False, balance=4200,
receipt_pref=ReceiptPreference.MONTHLY)
# Changes must be reflected in the passed user object # Changes must be reflected in the passed user object
self.assertEqual('newaddress@example.com', user.email) self.assertEqual('newaddress@example.com', user.email)
self.assertFalse(user.is_admin) self.assertFalse(user.is_admin)
self.assertFalse(user.is_member) self.assertFalse(user.is_member)
self.assertEqual(4200, user.balance) self.assertEqual(4200, user.balance)
self.assertEqual(ReceiptPreference.MONTHLY, user.receipt_pref)
# Changes must be reflected in the database # Changes must be reflected in the database
checkuser = db.get_user(user.id) checkuser = db.get_user(user.id)
self.assertEqual('newaddress@example.com', user.email) self.assertEqual('newaddress@example.com', user.email)
self.assertFalse(checkuser.is_admin) self.assertFalse(checkuser.is_admin)
self.assertFalse(checkuser.is_member) self.assertFalse(checkuser.is_member)
self.assertEqual(4200, checkuser.balance) self.assertEqual(4200, checkuser.balance)
self.assertEqual(ReceiptPreference.MONTHLY, checkuser.receipt_pref)
# Balance change without an agent must fail # Balance change without an agent must fail
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
db.change_user(user, None, balance=0) db.change_user(user, None, balance=0)

View file

@ -62,7 +62,14 @@ class DatabaseWrapper(object):
return Transaction(self._sqlite_db, exclusive) return Transaction(self._sqlite_db, exclusive)
def _setup(self) -> None: def _setup(self) -> None:
# Enable foreign key enforcement
cursor = self._sqlite_db.cursor()
cursor.execute('PRAGMA foreign_keys = 1')
# Create or update schemas if necessary
with self.transaction() as c: with self.transaction() as c:
# Defer foreign key enforcement in the setup transaction
c.execute('PRAGMA defer_foreign_keys = 1')
version: int = self._user_version version: int = self._user_version
if version < 1: if version < 1:
# Don't use executescript, as it issues a COMMIT first # Don't use executescript, as it issues a COMMIT first