- Another change to the database schema
- Enabled foreign key constraints (!!!) - Fixed a facade bug exposed by foreign key constraints
This commit is contained in:
parent
56ce2a73cb
commit
5d710f0c18
5 changed files with 48 additions and 7 deletions
|
@ -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)
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
''',
|
''',
|
||||||
'''
|
'''
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue