Skip to content

Commit 00e220b

Browse files
committed
Move DB migration code out of sogs.db
`sogs.db` was getting messy with all the migration code; this moves it all into multiple files in `sogs.migrations.*`, where each migration submodule has a `migrate(conn)` function to perform a migration. Aside from moving all the migrations, this also renames/updates the existing `sogs.migrate01x` to `sogs.migrations.v_0_1_x` for consistency.
1 parent 18617cb commit 00e220b

File tree

10 files changed

+563
-486
lines changed

10 files changed

+563
-486
lines changed

sogs/db.py

Lines changed: 3 additions & 464 deletions
Large diffs are not rendered by default.

sogs/migrations/__init__.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import logging
2+
import coloredlogs
3+
4+
from .. import config
5+
6+
from . import (
7+
import_hacks,
8+
message_details_deleter,
9+
message_views,
10+
new_columns,
11+
new_tables,
12+
seqno_etc,
13+
user_perm_futures,
14+
v_0_1_x,
15+
)
16+
17+
logger = logging.getLogger(__name__)
18+
coloredlogs.install(milliseconds=True, isatty=True, logger=logger, level=config.LOG_LEVEL)
19+
20+
21+
def migrate(conn):
22+
"""Perform database migrations/updates/etc."""
23+
24+
from .. import db
25+
26+
# NB: migration order here matters; some later migrations require earlier migrations
27+
for migration in (
28+
v_0_1_x,
29+
new_tables,
30+
new_columns,
31+
message_details_deleter,
32+
import_hacks,
33+
seqno_etc,
34+
message_views,
35+
user_perm_futures,
36+
):
37+
changes = False
38+
with db.transaction(conn):
39+
changes = migration.migrate(conn)
40+
if changes:
41+
db.metadata.clear()
42+
db.metadata.reflect(bind=db.engine, views=True)
43+
44+
45+
# migrate_v01x,
46+
# add_new_tables,
47+
# add_new_columns,
48+
# create_message_details_deleter,
49+
# check_for_hacks,
50+
# seqno_etc_updates,
51+
# update_message_views,
52+
# user_perm_future_updates,
53+
54+
# add_accessible_perm_bit,
55+
# ):

sogs/migrations/import_hacks.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import logging
2+
3+
4+
def migrate(conn):
5+
"""
6+
The 0.1.x migration sets up a file_id_hacks table to map old ids to new ids; if it's present and
7+
non-empty then we enable "hack" mode. (This should empty out over 15 days as attachments
8+
expire).
9+
10+
We also have a room_import_hacks table that lets us map old message ids to new ids (because in
11+
the old database message ids overlapped, but in the new database they are unique). The consists
12+
of a max id and an offset that lets us figure out the new (current database) id. For instance,
13+
some range of messages in room xyz with old ids [1,5000] could get inserted as ids [4321, 9320],
14+
so max would be 5000 and offset would be 4320: old message id 3333 will have new message id
15+
3333+4320 = 7653. We read all the offsets once at startup and stash them in ROOM_IMPORT_HACKS.
16+
"""
17+
18+
from .. import db
19+
20+
if 'file_id_hacks' in db.metadata.tables:
21+
# If the table exists but is empty (i.e. because all the attachments expired) then we should
22+
# drop it.
23+
n_fid_hacks = conn.execute("SELECT COUNT(*) FROM file_id_hacks").first()[0]
24+
if n_fid_hacks == 0:
25+
logging.warning("Dropping file_id_hacks old sogs import table (no longer required)")
26+
db.metadata.tables['file_id_hacks'].drop(db.engine)
27+
else:
28+
logging.warning("Keeping file_id_hacks old sogs import table (still required)")
29+
global HAVE_FILE_ID_HACKS
30+
db.HAVE_FILE_ID_HACKS = True
31+
32+
try:
33+
rows = conn.execute(
34+
"SELECT room, old_message_id_max, message_id_offset FROM room_import_hacks"
35+
)
36+
for (room, id_max, offset) in rows:
37+
db.ROOM_IMPORT_HACKS[room] = (id_max, offset)
38+
except Exception:
39+
pass
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
def migrate(conn):
2+
3+
from .. import db
4+
5+
if db.engine.name == "sqlite":
6+
conn.execute(
7+
"""
8+
CREATE TRIGGER IF NOT EXISTS message_details_deleter INSTEAD OF DELETE ON message_details
9+
FOR EACH ROW WHEN OLD.data IS NOT NULL
10+
BEGIN
11+
UPDATE messages SET data = NULL, data_size = NULL, signature = NULL
12+
WHERE id = OLD.id;
13+
END
14+
"""
15+
)
16+
17+
return False # No need to refresh metadata even if we added the trigger above.

sogs/migrations/message_views.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import logging
2+
3+
4+
def migrate(conn):
5+
from .. import db
6+
7+
if db.engine.name == "sqlite":
8+
if any(
9+
x not in db.metadata.tables['message_metadata'].c for x in ('whisper_to', 'filtered')
10+
):
11+
logging.warning("DB migration: replacing message_metadata/message_details views")
12+
conn.execute("DROP VIEW IF EXISTS message_metadata")
13+
conn.execute("DROP VIEW IF EXISTS message_details")
14+
conn.execute(
15+
"""
16+
CREATE VIEW message_details AS
17+
SELECT messages.*, uposter.session_id, uwhisper.session_id AS whisper_to
18+
FROM messages
19+
JOIN users uposter ON messages."user" = uposter.id
20+
LEFT JOIN users uwhisper ON messages.whisper = uwhisper.id
21+
"""
22+
)
23+
conn.execute(
24+
"""
25+
CREATE TRIGGER message_details_deleter INSTEAD OF DELETE ON message_details
26+
FOR EACH ROW WHEN OLD.data IS NOT NULL
27+
BEGIN
28+
UPDATE messages SET data = NULL, data_size = NULL, signature = NULL
29+
WHERE id = OLD.id;
30+
END
31+
"""
32+
)
33+
conn.execute(
34+
"""
35+
CREATE VIEW message_metadata AS
36+
SELECT id, room, "user", session_id, posted, edited, seqno, filtered, whisper_to,
37+
length(data) AS data_unpadded, data_size, length(signature) as signature_length
38+
FROM message_details
39+
"""
40+
)
41+
42+
return True
43+
44+
# else: don't worry about this for postgresql because initial pg support had the fix
45+
46+
return False

sogs/migrations/new_columns.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import logging
2+
3+
4+
def migrate(conn):
5+
"""
6+
New columns that might need to be added that don't require more complex migrations beyond simply
7+
adding the column.
8+
"""
9+
10+
from .. import db
11+
12+
new_table_cols = {
13+
'messages': {
14+
'whisper': 'INTEGER REFERENCES users(id)',
15+
'whisper_mods': 'BOOLEAN NOT NULL DEFAULT FALSE',
16+
'filtered': 'BOOLEAN NOT NULL DEFAULT FALSE',
17+
}
18+
}
19+
20+
added = False
21+
22+
for table, cols in new_table_cols.items():
23+
for name, definition in cols.items():
24+
if name not in db.metadata.tables[table].c:
25+
logging.warning(f"DB migration: Adding new column {table}.{name}")
26+
conn.execute(f"ALTER TABLE {table} ADD COLUMN {name} {definition}")
27+
added = True
28+
29+
return added

sogs/migrations/new_tables.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import logging
2+
3+
4+
# { table_name => { 'sqlite': ['query1', 'query2'], 'pgsql': "query1; query2" } }
5+
table_creations = {
6+
'user_request_nonces': {
7+
'sqlite': [
8+
"""
9+
CREATE TABLE user_request_nonces (
10+
user INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
11+
nonce BLOB NOT NULL UNIQUE,
12+
expiry FLOAT NOT NULL DEFAULT ((julianday('now') - 2440587.5 + 1.0)*86400.0) /* now + 24h */
13+
)
14+
""",
15+
"""
16+
CREATE INDEX user_request_nonces_expiry ON user_request_nonces(expiry)
17+
""",
18+
],
19+
'pgsql': """
20+
CREATE TABLE user_request_nonces (
21+
"user" BIGINT NOT NULL REFERENCES users ON DELETE CASCADE,
22+
nonce BYTEA NOT NULL UNIQUE,
23+
expiry FLOAT NOT NULL DEFAULT (extract(epoch from now() + '24 hours'))
24+
);
25+
CREATE INDEX user_request_nonces_expiry ON user_request_nonces(expiry)
26+
""",
27+
},
28+
'inbox': {
29+
'sqlite': [
30+
"""
31+
CREATE TABLE inbox (
32+
id INTEGER PRIMARY KEY,
33+
recipient INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
34+
sender INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
35+
body BLOB NOT NULL,
36+
posted_at FLOAT DEFAULT ((julianday('now') - 2440587.5)*86400.0),
37+
expiry FLOAT DEFAULT ((julianday('now') - 2440587.5 + 1.0)*86400.0) /* now + 24h */
38+
)
39+
""",
40+
"""
41+
CREATE INDEX inbox_recipient ON inbox(recipient)
42+
""",
43+
],
44+
'pgsql': """
45+
CREATE TABLE inbox (
46+
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
47+
recipient BIGINT NOT NULL REFERENCES users ON DELETE CASCADE,
48+
sender BIGINT NOT NULL REFERENCES users ON DELETE CASCADE,
49+
body BYTEA NOT NULL,
50+
posted_at FLOAT DEFAULT (extract(epoch from now())),
51+
expiry FLOAT DEFAULT (extract(epoch from now() + '15 days'))
52+
);
53+
CREATE INDEX inbox_recipient ON inbox(recipient);
54+
""",
55+
},
56+
}
57+
58+
59+
def migrate(conn):
60+
"""Adds new tables that don't have any special migration requirement beyond creation"""
61+
62+
from .. import db
63+
64+
added = False
65+
66+
for table, v in table_creations.items():
67+
if table in db.metadata.tables:
68+
continue
69+
70+
logging.warning(f"DB migration: Adding new table {table}")
71+
72+
if db.engine.name == 'sqlite':
73+
for query in v['sqlite']:
74+
conn.execute(query)
75+
else:
76+
conn.execute(v['pgsql'])
77+
78+
added = True
79+
80+
return added

0 commit comments

Comments
 (0)