Skip to content

Commit fb0bb6d

Browse files
authored
Merge pull request #51 from jagerman/pgsql-testing
Add PostgreSQL to test suite
2 parents 8ed87d1 + 7566071 commit fb0bb6d

File tree

7 files changed

+187
-39
lines changed

7 files changed

+187
-39
lines changed

.drone.jsonnet

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ local debian_pipeline(name,
2727
image,
2828
arch='amd64',
2929
deps=default_deps,
30+
before_pytest=[],
31+
pytest_opts='',
3032
extra_cmds=[],
33+
services=[],
3134
allow_fail=false) = {
3235
kind: 'pipeline',
3336
type: 'docker',
@@ -51,13 +54,29 @@ local debian_pipeline(name,
5154
'eatmydata ' + apt_get_quiet + ' update',
5255
'eatmydata ' + apt_get_quiet + ' dist-upgrade -y',
5356
'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y ' + std.join(' ', deps),
54-
'PYTHONPATH=. python3 -mpytest -vv --color=yes --sql-tracing',
57+
] + before_pytest + [
58+
'PYTHONPATH=. python3 -mpytest -vv --color=yes --sql-tracing ' + pytest_opts,
5559
]
5660
+ extra_cmds,
5761
},
5862
],
63+
services: services,
5964
};
6065

66+
local debian_pg_pipeline(name, image, pg_tag='bullseye') = debian_pipeline(
67+
name,
68+
image,
69+
deps=default_deps + ['python3-psycopg2', 'postgresql-client'],
70+
services=[
71+
{ name: 'pg', image: 'postgres:bullseye', environment: { POSTGRES_USER: 'ci', POSTGRES_PASSWORD: 'ci' } },
72+
],
73+
before_pytest=[
74+
'for i in $(seq 0 30); do if pg_isready -d ci -h pg -U ci -t 1; then break; fi; if [ "$i" = 30 ]; then echo "Timeout waiting for postgresql" >&2; exit 1; fi; sleep 1; done',
75+
],
76+
pytest_opts='--pgsql "postgresql://ci:ci@pg/ci"'
77+
);
78+
79+
6180
[
6281
{
6382
name: 'Lint checks',
@@ -92,6 +111,9 @@ local debian_pipeline(name,
92111
debian_pipeline('Ubuntu latest (amd64)', docker_base + 'ubuntu-rolling'),
93112
debian_pipeline('Ubuntu LTS (amd64)', docker_base + 'ubuntu-lts'),
94113

114+
debian_pg_pipeline('PostgreSQL 14/sid', docker_base + 'debian-sid', pg_tag='14-bullseye'),
115+
debian_pg_pipeline('PostgreSQL 12/focal', docker_base + 'ubuntu-focal', pg_tag='12-bullseye'),
116+
95117
// ARM builds (ARM64 and armhf)
96118
debian_pipeline('Debian sid (ARM64)', docker_base + 'debian-sid', arch='arm64'),
97119
debian_pipeline('Debian stable (armhf)', docker_base + 'debian-stable/arm32v7', arch='arm64'),

sogs/config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
# Default config settings; most of these are configurable via config.ini (see it for details).
1313
DB_URL = 'sqlite:///sogs.db'
14-
DB_SCHEMA_FILE = 'schema.sql' # Not configurable, just a constant
1514
KEY_FILE = 'key_x25519'
1615
URL_BASE = 'http://example.net'
1716
HTTP_SHOW_INDEX = True

sogs/db.py

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,25 @@ def database_init():
117117
metadata.clear()
118118
metadata.reflect(bind=engine, views=True)
119119

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+
120128
changes = False
121129

122130
# Database migrations/updates/etc.
123131
for migrate in (
124132
migrate_v01x,
125133
add_new_tables,
126134
add_new_columns,
127-
update_message_views,
128135
create_message_details_deleter,
129136
check_for_hacks,
130137
seqno_etc_updates,
138+
update_message_views,
131139
user_perm_future_updates,
132140
):
133141
with transaction(conn):
@@ -220,7 +228,7 @@ def add_new_tables(conn):
220228

221229

222230
def update_message_views(conn):
223-
if engine.name != "sqlite":
231+
if engine.name == "sqlite":
224232
if any(x not in metadata.tables['message_metadata'].c for x in ('whisper_to', 'filtered')):
225233
logging.warning("DB migration: replacing message_metadata/message_details views")
226234
conn.execute("DROP VIEW IF EXISTS message_metadata")
@@ -232,19 +240,30 @@ def update_message_views(conn):
232240
FROM messages
233241
JOIN users uposter ON messages."user" = uposter.id
234242
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
235253
"""
236254
)
237255
conn.execute(
238256
"""
239257
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,
241259
length(data) AS data_unpadded, data_size, length(signature) as signature_length
242260
FROM message_details
243261
"""
244262
)
245263

246264
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
248267

249268
return False
250269

@@ -334,7 +353,7 @@ def seqno_etc_updates(conn):
334353
"""
335354
CREATE TABLE pinned_messages (
336355
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,
338357
pinned_by INTEGER NOT NULL REFERENCES users(id),
339358
pinned_at FLOAT NOT NULL DEFAULT ((julianday('now') - 2440587.5)*86400.0), /* unix epoch when pinned */
340359
PRIMARY KEY(room, message)
@@ -417,7 +436,7 @@ def seqno_etc_updates(conn):
417436
"""
418437
CREATE TABLE pinned_messages (
419438
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,
421440
pinned_by BIGINT NOT NULL REFERENCES users,
422441
pinned_at FLOAT NOT NULL DEFAULT (extract(epoch from now())),
423442
PRIMARY KEY(room, message)
@@ -531,7 +550,7 @@ def user_perm_future_updates(conn):
531550
conn.execute(
532551
"""
533552
CREATE TABLE user_ban_futures (
534-
room INTEGER NOT NULL REFERENCES rooms ON DELETE CASCADE,
553+
room INTEGER REFERENCES rooms ON DELETE CASCADE,
535554
"user" INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
536555
at FLOAT NOT NULL, /* when the change should take effect (unix epoch) */
537556
banned BOOLEAN NOT NULL /* if true then ban at `at`, if false then unban */
@@ -568,24 +587,17 @@ def create_admin_user(dbconn):
568587
)
569588

570589

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-
582590
engine, engine_initial_pid, metadata = None, None, None
583591

584592

585593
def _init_engine(*args, **kwargs):
586594
"""
587595
Initializes or reinitializes db.engine. (Only the test suite should be calling this externally
588596
to reinitialize).
597+
598+
Arguments:
599+
sogs_preinit - a callable to invoke after setting up `engine` but before calling
600+
`database_init()`.
589601
"""
590602
global engine, engine_initial_pid, metadata, have_returning
591603

@@ -597,9 +609,17 @@ def _init_engine(*args, **kwargs):
597609
return
598610
args = (config.DB_URL,)
599611

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)
603623
engine_initial_pid = os.getpid()
604624
metadata = sqlalchemy.MetaData()
605625

@@ -619,6 +639,10 @@ def sqlite_fix_connect(dbapi_connection, connection_record):
619639
# disable pysqlite's emitting of the BEGIN statement entirely.
620640
# also stops it from emitting COMMIT before any DDL.
621641
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()
622646

623647
@sqlalchemy.event.listens_for(engine, "begin")
624648
def do_begin(conn):
@@ -628,6 +652,18 @@ def do_begin(conn):
628652
else:
629653
have_returning = True
630654

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+
631667
database_init()
632668

633669

sogs/model/room.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ def get_messages_for(
525525
# - anything directed to us specifically
526526
# - anything we sent (i.e. outbound whispers)
527527
# - non-whispers
528-
"whisper_mods OR whisper = :user OR user = :user OR whisper IS NULL"
528+
'whisper_mods OR whisper = :user OR "user" = :user OR whisper IS NULL'
529529
if mod
530530
# For a regular user we want to see:
531531
# - anything with whisper_to sent to us

sogs/schema.pgsql

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ CREATE TABLE rooms (
1717
upload BOOLEAN NOT NULL DEFAULT TRUE, /* Whether file uploads are allowed by default */
1818
CHECK(token SIMILAR TO '[a-zA-Z0-9_-]+')
1919
);
20-
CREATE INDEX rooms_token ON rooms(token);
2120

2221
-- Trigger to expire an old room image attachment when the room image is changed
2322
CREATE OR REPLACE FUNCTION trigger_room_image_expiry()
@@ -90,8 +89,8 @@ EXECUTE PROCEDURE trigger_messages_insert_history();
9089

9190
CREATE TABLE pinned_messages (
9291
room BIGINT NOT NULL REFERENCES rooms ON DELETE CASCADE,
93-
message BIGINT NOT NULL REFERENCES rooms ON DELETE CASCADE,
94-
pinned_by BIGINT NOT NULL REFERENCES users,
92+
message BIGINT NOT NULL REFERENCES messages ON DELETE CASCADE,
93+
pinned_by BIGINT NOT NULL, /* foreign key to users(id) */
9594
pinned_at FLOAT NOT NULL DEFAULT (extract(epoch from now())),
9695
PRIMARY KEY(room, message)
9796
);
@@ -154,6 +153,7 @@ CREATE INDEX users_last_active ON users(last_active);
154153
ALTER TABLE messages ADD CONSTRAINT messages_user_fk FOREIGN KEY ("user") REFERENCES users;
155154
ALTER TABLE messages ADD CONSTRAINT messages_whisper_fk FOREIGN KEY (whisper) REFERENCES users;
156155
ALTER TABLE files ADD CONSTRAINT files_uploader_fk FOREIGN KEY (uploader) REFERENCES users;
156+
ALTER TABLE pinned_messages ADD CONSTRAINT pinned_messages_pinned_by FOREIGN KEY (pinned_by) REFERENCES users;
157157

158158
-- Create a trigger to maintain the implication "admin implies moderator"
159159
CREATE OR REPLACE FUNCTION trigger_user_admins_are_mods()
@@ -194,7 +194,7 @@ EXECUTE PROCEDURE trigger_message_details_deleter();
194194
-- View of `messages` that is useful for manually inspecting table contents by only returning the
195195
-- length (rather than raw bytes) for data/signature.
196196
CREATE VIEW message_metadata AS
197-
SELECT id, room, "user", session_id, posted, edited, seqno, whisper_to,
197+
SELECT id, room, "user", session_id, posted, edited, seqno, filtered, whisper_to,
198198
length(data) AS data_unpadded, data_size, length(signature) as signature_length
199199
FROM message_details;
200200

@@ -376,7 +376,7 @@ CREATE INDEX user_permissions_future_room_user ON user_permission_futures(room,
376376
-- their user_permissions.banned to TRUE then add a row here with banned = FALSE to schedule the
377377
-- unban. (You can also schedule a future *ban* here, but the utility of that is less clear).
378378
CREATE TABLE user_ban_futures (
379-
room INTEGER NOT NULL REFERENCES rooms ON DELETE CASCADE,
379+
room INTEGER REFERENCES rooms ON DELETE CASCADE,
380380
"user" INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
381381
at FLOAT NOT NULL, /* when the change should take effect (unix epoch) */
382382
banned BOOLEAN NOT NULL /* if true then ban at `at`, if false then unban */

sogs/schema.sqlite

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ END;
8181

8282
CREATE TABLE pinned_messages (
8383
room INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
84-
message INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
84+
message INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
8585
pinned_by INTEGER NOT NULL REFERENCES users(id),
8686
pinned_at FLOAT NOT NULL DEFAULT ((julianday('now') - 2440587.5)*86400.0), /* unix epoch when pinned */
8787
PRIMARY KEY(room, message)
@@ -165,7 +165,7 @@ END;
165165
-- View of `messages` that is useful for manually inspecting table contents by only returning the
166166
-- length (rather than raw bytes) for data/signature.
167167
CREATE VIEW message_metadata AS
168-
SELECT id, room, user, session_id, posted, edited, seqno, whisper_to,
168+
SELECT id, room, user, session_id, posted, edited, seqno, filtered, whisper_to,
169169
length(data) AS data_unpadded, data_size, length(signature) as signature_length
170170
FROM message_details;
171171

0 commit comments

Comments
 (0)