- 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:
raise ValueError(f'No user with user ID {uid} exists.')
# Unpack the row and construct the user
user_id, username, email, is_admin, is_member, balance, receipt_pref = row
return User(user_id, username, balance, email, is_admin, is_member, ReceiptPreference(receipt_pref))
user_id, username, email, is_admin, is_member, balance, receipt_p = row
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,
username: str,
@ -531,6 +535,10 @@ class MatematDatabase(object):
if amount < 0:
raise ValueError('Cannot deposit a negative value')
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('''
INSERT INTO transactions (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
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
c.execute('''
CREATE TABLE modifications_new (

View file

@ -129,12 +129,12 @@ SCHEMAS[3] = [
'''
CREATE TABLE transactions ( -- "superclass" of the following 3 tables
ta_id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
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 CASCADE ON UPDATE CASCADE
ON DELETE SET NULL ON UPDATE CASCADE
);
''',
'''

View file

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

View file

@ -62,7 +62,14 @@ class DatabaseWrapper(object):
return Transaction(self._sqlite_db, exclusive)
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:
# Defer foreign key enforcement in the setup transaction
c.execute('PRAGMA defer_foreign_keys = 1')
version: int = self._user_version
if version < 1:
# Don't use executescript, as it issues a COMMIT first