@@ -117,17 +117,25 @@ def database_init():
117
117
metadata .clear ()
118
118
metadata .reflect (bind = engine , views = True )
119
119
120
+ if 'messages' not in metadata .tables :
121
+ msg = (
122
+ "Critical error: SQL schema creation failed; "
123
+ f"tables: { ', ' .join (metadata .tables .keys ())} "
124
+ )
125
+ logging .critical (msg )
126
+ raise RuntimeError (msg )
127
+
120
128
changes = False
121
129
122
130
# Database migrations/updates/etc.
123
131
for migrate in (
124
132
migrate_v01x ,
125
133
add_new_tables ,
126
134
add_new_columns ,
127
- update_message_views ,
128
135
create_message_details_deleter ,
129
136
check_for_hacks ,
130
137
seqno_etc_updates ,
138
+ update_message_views ,
131
139
user_perm_future_updates ,
132
140
):
133
141
with transaction (conn ):
@@ -220,7 +228,7 @@ def add_new_tables(conn):
220
228
221
229
222
230
def update_message_views (conn ):
223
- if engine .name ! = "sqlite" :
231
+ if engine .name = = "sqlite" :
224
232
if any (x not in metadata .tables ['message_metadata' ].c for x in ('whisper_to' , 'filtered' )):
225
233
logging .warning ("DB migration: replacing message_metadata/message_details views" )
226
234
conn .execute ("DROP VIEW IF EXISTS message_metadata" )
@@ -232,19 +240,30 @@ def update_message_views(conn):
232
240
FROM messages
233
241
JOIN users uposter ON messages."user" = uposter.id
234
242
LEFT JOIN users uwhisper ON messages.whisper = uwhisper.id
243
+ """
244
+ )
245
+ conn .execute (
246
+ """
247
+ CREATE TRIGGER message_details_deleter INSTEAD OF DELETE ON message_details
248
+ FOR EACH ROW WHEN OLD.data IS NOT NULL
249
+ BEGIN
250
+ UPDATE messages SET data = NULL, data_size = NULL, signature = NULL
251
+ WHERE id = OLD.id;
252
+ END
235
253
"""
236
254
)
237
255
conn .execute (
238
256
"""
239
257
CREATE VIEW message_metadata AS
240
- SELECT id, room, "user", session_id, posted, edited, updated , filtered, whisper_to,
258
+ SELECT id, room, "user", session_id, posted, edited, seqno , filtered, whisper_to,
241
259
length(data) AS data_unpadded, data_size, length(signature) as signature_length
242
260
FROM message_details
243
261
"""
244
262
)
245
263
246
264
return True
247
- # else: don't worry about this for postgresql because initial pg support had the fix
265
+
266
+ # else: don't worry about this for postgresql because initial pg support had the fix
248
267
249
268
return False
250
269
@@ -334,7 +353,7 @@ def seqno_etc_updates(conn):
334
353
"""
335
354
CREATE TABLE pinned_messages (
336
355
room INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
337
- message INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
356
+ message INTEGER NOT NULL REFERENCES messages (id) ON DELETE CASCADE,
338
357
pinned_by INTEGER NOT NULL REFERENCES users(id),
339
358
pinned_at FLOAT NOT NULL DEFAULT ((julianday('now') - 2440587.5)*86400.0), /* unix epoch when pinned */
340
359
PRIMARY KEY(room, message)
@@ -417,7 +436,7 @@ def seqno_etc_updates(conn):
417
436
"""
418
437
CREATE TABLE pinned_messages (
419
438
room BIGINT NOT NULL REFERENCES rooms ON DELETE CASCADE,
420
- message BIGINT NOT NULL REFERENCES rooms ON DELETE CASCADE,
439
+ message BIGINT NOT NULL REFERENCES messages ON DELETE CASCADE,
421
440
pinned_by BIGINT NOT NULL REFERENCES users,
422
441
pinned_at FLOAT NOT NULL DEFAULT (extract(epoch from now())),
423
442
PRIMARY KEY(room, message)
@@ -531,7 +550,7 @@ def user_perm_future_updates(conn):
531
550
conn .execute (
532
551
"""
533
552
CREATE TABLE user_ban_futures (
534
- room INTEGER NOT NULL REFERENCES rooms ON DELETE CASCADE,
553
+ room INTEGER REFERENCES rooms ON DELETE CASCADE,
535
554
"user" INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
536
555
at FLOAT NOT NULL, /* when the change should take effect (unix epoch) */
537
556
banned BOOLEAN NOT NULL /* if true then ban at `at`, if false then unban */
@@ -568,24 +587,17 @@ def create_admin_user(dbconn):
568
587
)
569
588
570
589
571
- if config .DB_URL .startswith ('postgresql' ):
572
- # rooms.token is a 'citext' (case-insensitive text), which sqlalchemy doesn't recognize out of
573
- # the box. Map it to a plain TEXT which is good enough for what we need (if we actually needed
574
- # to generate this wouldn't suffice: we'd have to use something like the sqlalchemy-citext
575
- # module).
576
- from sqlalchemy .dialects .postgresql .base import ischema_names
577
-
578
- if 'citext' not in ischema_names :
579
- ischema_names ['citext' ] = ischema_names ['text' ]
580
-
581
-
582
590
engine , engine_initial_pid , metadata = None , None , None
583
591
584
592
585
593
def _init_engine (* args , ** kwargs ):
586
594
"""
587
595
Initializes or reinitializes db.engine. (Only the test suite should be calling this externally
588
596
to reinitialize).
597
+
598
+ Arguments:
599
+ sogs_preinit - a callable to invoke after setting up `engine` but before calling
600
+ `database_init()`.
589
601
"""
590
602
global engine , engine_initial_pid , metadata , have_returning
591
603
@@ -597,9 +609,17 @@ def _init_engine(*args, **kwargs):
597
609
return
598
610
args = (config .DB_URL ,)
599
611
600
- # Disable *sqlalchemy*-level autocommit, which works so badly that it got completely removed in
601
- # 2.0. (We put the actual sqlite into driver-level autocommit mode below).
602
- engine = sqlalchemy .create_engine (* args , ** kwargs ).execution_options (autocommit = False )
612
+ preinit = kwargs .pop ('sogs_preinit' , None )
613
+
614
+ exec_opts_args = {}
615
+ if args [0 ].startswith ('postgresql' ):
616
+ exec_opts_args ['isolation_level' ] = 'READ COMMITTED'
617
+ else :
618
+ # SQLite's Python code is seriously broken, so we have to force off autocommit mode and turn
619
+ # on driver-level autocommit (which we do below).
620
+ exec_opts_args ['autocommit' ] = False
621
+
622
+ engine = sqlalchemy .create_engine (* args , ** kwargs ).execution_options (** exec_opts_args )
603
623
engine_initial_pid = os .getpid ()
604
624
metadata = sqlalchemy .MetaData ()
605
625
@@ -619,6 +639,10 @@ def sqlite_fix_connect(dbapi_connection, connection_record):
619
639
# disable pysqlite's emitting of the BEGIN statement entirely.
620
640
# also stops it from emitting COMMIT before any DDL.
621
641
dbapi_connection .isolation_level = None
642
+ # Enforce foreign keys. It is very sad that this is not default.
643
+ cursor = dbapi_connection .cursor ()
644
+ cursor .execute ("PRAGMA foreign_keys=ON" )
645
+ cursor .close ()
622
646
623
647
@sqlalchemy .event .listens_for (engine , "begin" )
624
648
def do_begin (conn ):
@@ -628,6 +652,18 @@ def do_begin(conn):
628
652
else :
629
653
have_returning = True
630
654
655
+ # rooms.token is a 'citext' (case-insensitive text), which sqlalchemy doesn't recognize out
656
+ # of the box. Map it to a plain TEXT which is good enough for what we need (if we actually
657
+ # needed to generate this wouldn't suffice: we'd have to use something like the
658
+ # sqlalchemy-citext module).
659
+ from sqlalchemy .dialects .postgresql .base import ischema_names
660
+
661
+ if 'citext' not in ischema_names :
662
+ ischema_names ['citext' ] = ischema_names ['text' ]
663
+
664
+ if preinit :
665
+ preinit ()
666
+
631
667
database_init ()
632
668
633
669
0 commit comments