Repository navigation
Dust off make_full_schema and add CI using it to show schema diffs. - #20027
Conversation
Schema DiffPlease check that this looks as expected! common (sqlite)Unchanged common (postgres)--- /tmp/schema_diff_fb4wkuc9/before/common/full_schemas/9999/full.sql.postgres 2026-08-11 10:31:33.463625039 +0000
+++ /tmp/schema_diff_fb4wkuc9/after/common/full_schemas/9999/full.sql.postgres 2026-08-11 10:31:23.422978143 +0000
@@ -1,12 +1,8 @@
-\restrict hycDXNaQTcWwg2zqH4FcLG9rBhV4Ygg65vUDfQLBogFe1Xa1SEpewRYdLy4YUul
CREATE TABLE background_updates (
update_name text NOT NULL,
progress_json text NOT NULL,
depends_on text,
ordering integer DEFAULT 0 NOT NULL
);
ALTER TABLE ONLY background_updates
ADD CONSTRAINT background_updates_uniqueness UNIQUE (update_name);
-\unrestrict hycDXNaQTcWwg2zqH4FcLG9rBhV4Ygg65vUDfQLBogFe1Xa1SEpewRYdLy4YUul
-\restrict ONJJNiONvZYwI63NakJ0mdnmi5JcTLLN9A7UaapE9aBdVb7xyjMMxfUXUg5YvSb
-\unrestrict ONJJNiONvZYwI63NakJ0mdnmi5JcTLLN9A7UaapE9aBdVb7xyjMMxfUXUg5YvSb
main (sqlite)--- /tmp/schema_diff_fb4wkuc9/before/main/full_schemas/9999/full.sql.sqlite 2026-08-11 10:31:33.291631088 +0000
+++ /tmp/schema_diff_fb4wkuc9/after/main/full_schemas/9999/full.sql.sqlite 2026-08-11 10:31:23.282983061 +0000
@@ -972,21 +972,20 @@
);
CREATE UNIQUE INDEX sliding_sync_membership_snapshots_event_stream_ordering ON sliding_sync_membership_snapshots(event_stream_ordering);
CREATE TABLE sliding_sync_connections(
connection_key INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
-- Generally the device ID, but may be something else for e.g. puppeted accounts.
effective_device_id TEXT NOT NULL,
conn_id TEXT NOT NULL,
created_ts BIGINT NOT NULL
, last_used_ts BIGINT);
-CREATE TABLE sqlite_sequence(name,seq);
CREATE INDEX sliding_sync_connections_idx ON sliding_sync_connections(user_id, effective_device_id, conn_id);
CREATE INDEX sliding_sync_connections_ts_idx ON sliding_sync_connections(created_ts);
CREATE TABLE sliding_sync_connection_positions(
connection_position INTEGER PRIMARY KEY AUTOINCREMENT,
connection_key BIGINT NOT NULL REFERENCES sliding_sync_connections(connection_key) ON DELETE CASCADE,
created_ts BIGINT NOT NULL
);
CREATE INDEX sliding_sync_connection_positions_key ON sliding_sync_connection_positions(connection_key);
CREATE INDEX sliding_sync_connection_positions_ts_idx ON sliding_sync_connection_positions(created_ts);
CREATE TABLE sliding_sync_connection_required_state(
@@ -1238,13 +1237,13 @@
/* WARNING: Script requires that SQLITE_DBCONFIG_DEFENSIVE be disabled */
INSERT INTO appservice_stream_position VALUES('X',0);
INSERT INTO federation_stream_position VALUES('federation',-1,'master');
INSERT INTO federation_stream_position VALUES('events',-1,'master');
INSERT INTO event_push_summary_stream_ordering VALUES('X',0);
INSERT INTO user_directory_stream_pos VALUES('X',-1);
INSERT INTO stats_incremental_position VALUES('X',1);
INSERT INTO event_push_summary_last_receipt_stream_id VALUES('X',0);
INSERT INTO device_lists_changes_converted_stream_position VALUES('X',1,'',NULL);
INSERT INTO room_forgetter_stream_pos VALUES('X',0);
-INSERT INTO scheduled_tasks VALUES('delete_old_otks_task','delete_old_otks','scheduled',1786444289000,NULL,NULL,NULL,NULL);
+INSERT INTO scheduled_tasks VALUES('delete_old_otks_task','delete_old_otks','scheduled',1786444279000,NULL,NULL,NULL,NULL);
INSERT INTO delayed_events_stream_pos VALUES('X',0);
INSERT INTO device_lists_changes_in_room_max_pruned_stream_id VALUES('X',0);
main (postgres)--- /tmp/schema_diff_fb4wkuc9/before/main/full_schemas/9999/full.sql.postgres 2026-08-11 10:31:33.832612060 +0000
+++ /tmp/schema_diff_fb4wkuc9/after/main/full_schemas/9999/full.sql.postgres 2026-08-11 10:31:23.867962514 +0000
@@ -1,11 +1,10 @@
-\restrict KlVdCFGPDN7Tl5bUxj9VjzkaYOE3oMUUj4nFerNDZxkiALAPgfwWLZbi34N1pu0
CREATE FUNCTION check_event_stream_ordering() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF EXISTS (
SELECT 1 FROM events
WHERE events.event_id = NEW.event_id
AND events.stream_ordering != NEW.event_stream_ordering
) THEN
RAISE EXCEPTION 'Incorrect event_stream_ordering';
@@ -1938,22 +1937,20 @@
ALTER TABLE ONLY un_partial_stated_event_stream
ADD CONSTRAINT un_partial_stated_event_stream_event_id_fkey FOREIGN KEY (event_id) REFERENCES events(event_id) ON DELETE CASCADE;
ALTER TABLE ONLY un_partial_stated_room_stream
ADD CONSTRAINT un_partial_stated_room_stream_room_id_fkey FOREIGN KEY (room_id) REFERENCES rooms(room_id) ON DELETE CASCADE;
ALTER TABLE ONLY users_to_send_full_presence_to
ADD CONSTRAINT users_to_send_full_presence_to_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(name);
ALTER TABLE ONLY worker_read_write_locks
ADD CONSTRAINT worker_read_write_locks_lock_name_lock_key_write_lock_fkey FOREIGN KEY (lock_name, lock_key, write_lock) REFERENCES worker_read_write_locks_mode(lock_name, lock_key, write_lock);
ALTER TABLE ONLY worker_read_write_locks_mode
ADD CONSTRAINT worker_read_write_locks_mode_foreign FOREIGN KEY (lock_name, lock_key, token) REFERENCES worker_read_write_locks(lock_name, lock_key, token) DEFERRABLE INITIALLY DEFERRED;
-\unrestrict KlVdCFGPDN7Tl5bUxj9VjzkaYOE3oMUUj4nFerNDZxkiALAPgfwWLZbi34N1pu0
-\restrict 8e7NoW4Yo8Oxpo1j8DiQOGeYFfo6w6PRdjyESr3OeEz4O1naGLaWXJchjJ2IEnP
INSERT INTO appservice_stream_position VALUES ('X', 0);
INSERT INTO delayed_events_stream_pos VALUES ('X', 0);
INSERT INTO device_lists_changes_converted_stream_position VALUES ('X', 1, '', NULL);
INSERT INTO device_lists_changes_in_room_max_pruned_stream_id VALUES ('X', 0);
INSERT INTO event_push_summary_last_receipt_stream_id VALUES ('X', 0);
INSERT INTO event_push_summary_stream_ordering VALUES ('X', 0);
INSERT INTO federation_stream_position VALUES ('federation', -1, 'master');
INSERT INTO federation_stream_position VALUES ('events', -1, 'master');
INSERT INTO room_forgetter_stream_pos VALUES ('X', 0);
INSERT INTO scheduled_tasks VALUES ('delete_old_otks_task', 'delete_old_otks', 'scheduled', 990794562000, NULL, NULL, NULL, NULL);
@@ -1976,11 +1973,10 @@
SELECT pg_catalog.setval('quarantined_media_id_seq', 1, true);
SELECT pg_catalog.setval('receipts_sequence', 1, true);
SELECT pg_catalog.setval('sliding_sync_connection_positions_connection_position_seq', 1, false);
SELECT pg_catalog.setval('sliding_sync_connection_required_state_required_state_id_seq', 1, false);
SELECT pg_catalog.setval('sliding_sync_connections_connection_key_seq', 1, false);
SELECT pg_catalog.setval('sticky_events_sequence', 1, true);
SELECT pg_catalog.setval('thread_subscriptions_sequence', 2, true);
SELECT pg_catalog.setval('un_partial_stated_event_stream_sequence', 1, true);
SELECT pg_catalog.setval('un_partial_stated_room_stream_sequence', 1, true);
SELECT pg_catalog.setval('user_id_seq', 1, false);
-\unrestrict 8e7NoW4Yo8Oxpo1j8DiQOGeYFfo6w6PRdjyESr3OeEz4O1naGLaWXJchjJ2IEnP
state (sqlite)--- /tmp/schema_diff_fb4wkuc9/before/state/full_schemas/9999/full.sql.sqlite 2026-08-11 10:31:33.293358852 +0000
+++ /tmp/schema_diff_fb4wkuc9/after/state/full_schemas/9999/full.sql.sqlite 2026-08-11 10:31:23.286052638 +0000
@@ -16,19 +16,18 @@
);
CREATE INDEX state_group_edges_prev_idx ON state_group_edges (prev_state_group);
CREATE INDEX state_groups_state_type_idx ON state_groups_state (state_group, type, state_key);
CREATE INDEX state_groups_room_id_idx ON state_groups (room_id) ;
CREATE UNIQUE INDEX state_group_edges_unique_idx ON state_group_edges (state_group, prev_state_group) ;
CREATE TABLE state_groups_pending_deletion (
sequence_number INTEGER PRIMARY KEY AUTOINCREMENT,
state_group BIGINT NOT NULL,
insertion_ts BIGINT NOT NULL
);
-CREATE TABLE sqlite_sequence(name,seq);
CREATE UNIQUE INDEX state_groups_pending_deletion_state_group ON state_groups_pending_deletion(state_group);
CREATE INDEX state_groups_pending_deletion_insertion_ts ON state_groups_pending_deletion(insertion_ts);
CREATE TABLE state_groups_persisting (
state_group BIGINT NOT NULL,
instance_name TEXT NOT NULL,
PRIMARY KEY (state_group, instance_name)
);
CREATE INDEX state_groups_persisting_instance_name ON state_groups_persisting(instance_name);
state (postgres)--- /tmp/schema_diff_fb4wkuc9/before/state/full_schemas/9999/full.sql.postgres 2026-08-11 10:31:34.009605834 +0000
+++ /tmp/schema_diff_fb4wkuc9/after/state/full_schemas/9999/full.sql.postgres 2026-08-11 10:31:24.045956263 +0000
@@ -1,11 +1,10 @@
-\restrict NY9nMB6Pz6E8GlHW1faNoOjNx5aoA0LG0npd8dlsalJSjD0PLPeZgM90nIfuemd
CREATE TABLE state_group_edges (
state_group bigint NOT NULL,
prev_state_group bigint NOT NULL
);
CREATE SEQUENCE state_group_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
@@ -45,15 +44,12 @@
ADD CONSTRAINT state_groups_persisting_pkey PRIMARY KEY (state_group, instance_name);
ALTER TABLE ONLY state_groups
ADD CONSTRAINT state_groups_pkey PRIMARY KEY (id);
CREATE INDEX state_group_edges_prev_idx ON state_group_edges USING btree (prev_state_group);
CREATE UNIQUE INDEX state_group_edges_unique_idx ON state_group_edges USING btree (state_group, prev_state_group);
CREATE INDEX state_groups_pending_deletion_insertion_ts ON state_groups_pending_deletion USING btree (insertion_ts);
CREATE UNIQUE INDEX state_groups_pending_deletion_state_group ON state_groups_pending_deletion USING btree (state_group);
CREATE INDEX state_groups_persisting_instance_name ON state_groups_persisting USING btree (instance_name);
CREATE INDEX state_groups_room_id_idx ON state_groups USING btree (room_id);
CREATE INDEX state_groups_state_type_idx ON state_groups_state USING btree (state_group, type, state_key);
-\unrestrict NY9nMB6Pz6E8GlHW1faNoOjNx5aoA0LG0npd8dlsalJSjD0PLPeZgM90nIfuemd
-\restrict z3RYkophnxWGphTiuMvOOXYe47YYBsq25yfpwAUixRz3flP7UYo3bmD9YUjm0ye
SELECT pg_catalog.setval('state_group_id_seq', 1, false);
SELECT pg_catalog.setval('state_groups_pending_deletion_sequence_number_seq', 1, false);
-\unrestrict z3RYkophnxWGphTiuMvOOXYe47YYBsq25yfpwAUixRz3flP7UYo3bmD9YUjm0ye
|
2440b34 to
838a876
Compare
…pt. (#20028) Broken out of #20027 because I'd like to have it land first. > It seems a lot of time in our trial tests goes towards setting up the database. (The same is probably true of Complement too) > > We haven't done a full schema for about 20 schema versions, so no surprise! > > As a result, I want to produce a full schema soon. When running `make_full_schema.sh`, these drop statements now cause the error: ``` Parse error near line 2: table event_search_content may not be dropped Parse error near line 3: table event_search_segments may not be dropped Parse error near line 4: table event_search_segdir may not be dropped Parse error near line 5: table event_search_docsize may not be dropped Parse error near line 6: table event_search_stat may not be dropped Parse error near line 7: table user_directory_search_content may not be dropped Parse error near line 8: table user_directory_search_segments may not be dropped Parse error near line 9: table user_directory_search_segdir may not be dropped Parse error near line 10: table user_directory_search_docsize may not be dropped Parse error near line 11: table user_directory_search_stat may not be dropped ``` It seems SQLite has cracked down on code that edits the internal tables. Because SQLite dumps the schema with `CREATE TABLE IF NOT EXISTS` for these virtual tables, it's harmless to leave them in the schema dump. --------- Signed-off-by: Olivier 'reivilibre' <oliverw@matrix.org>
73e3899 to
115db87
Compare
…a version" This reverts commit b3fbb8144ace9c933e013d881f7bd92a02d21745.
…ion" This reverts commit 70cceba43d0131e7276182725454f01a5b1684b1.
df240e9 to
b153f65
Compare
b153f65 to
7a9bd79
Compare
7a9bd79 to
8ab5389
Compare
| @@ -0,0 +1,225 @@ | |||
| #!/usr/bin/env python3 | |||
There was a problem hiding this comment.
This was mostly LLM'd with a few minor tweaks. Since it's not production code, I don't think it's too problematic.
It looks 'fine' to me, I'm sure it could be better-structured, but other than the few tweaks I have made I'm not sure there's anything I'm rushing to tweak.
| pg_dump --format=plain --data-only --inserts --no-tablespaces --no-acl --no-owner "$POSTGRES_MAIN_DB_NAME" | cleanup_pg_schema >> "$OUTPUT_DIR/main/full_schemas/$SCHEMA_NUMBER/full.sql.postgres" | ||
| pg_dump --format=plain --schema-only --no-tablespaces --no-acl --no-owner "$POSTGRES_STATE_DB_NAME" | cleanup_pg_schema > "$OUTPUT_DIR/state/full_schemas/$SCHEMA_NUMBER/full.sql.postgres" | ||
| pg_dump --format=plain --data-only --inserts --no-tablespaces --no-acl --no-owner "$POSTGRES_STATE_DB_NAME" | cleanup_pg_schema >> "$OUTPUT_DIR/state/full_schemas/$SCHEMA_NUMBER/full.sql.postgres" | ||
| pg_dump --restrict-key=REMOVEME --format=plain --schema-only --no-tablespaces --no-acl --no-owner "$POSTGRES_COMMON_DB_NAME" | cleanup_pg_schema > "$OUTPUT_DIR/common/full_schemas/$SCHEMA_NUMBER/full.sql.postgres" |
There was a problem hiding this comment.
Why new --restrict-key=REMOVEME flag necessary?
There was a problem hiding this comment.
We set this as normally it's a random string but we want a consistent predictable string to match the find/replace in cleanup_pg_schema above. We should comment about that that detail.
There was a problem hiding this comment.
Previous versions of Postgres didn't have this \restrict/\unrestrict mechanism, it was added for https://www.postgresql.org/support/security/CVE-2025-8714/
| # - Remove the `\restrict` and `\unrestrict` psql meta-commands. | ||
| # We don't run the pg_dump output through psql so no meta-commands are | ||
| # supported and so the security feature (which doesn't apply anyway | ||
| # as we trust the source database) is not relevant. |
There was a problem hiding this comment.
Why do these even come into play?
I don't see \restrict/\unrestrict anywhere
There was a problem hiding this comment.
Explanation for myself: pg_dump will output \restrict mysecret/\unrestrict mysecret commands. It's security feature to avoid poisoned data running arbitrary commands. Any psql meta commands after \restrict mysecret does not work until you \unrestrict mysecret
Why do we care about disabling this? Seems like it couldn't hurt.
There was a problem hiding this comment.
It's a psql meta-command only interpreted by psql and not (AFAIK) supported by Postgres itself or psycopg2. (This is sort of what the comment at this line is trying to say, but if you missed it then it might be worth a reword)
There was a problem hiding this comment.
I think the comment is sufficient but I wasn't following on the first pass 👍
| contents: read | ||
|
|
||
| jobs: | ||
| # Posts a GitHub PR comment that shows what the effective change to the schema is. |
There was a problem hiding this comment.
Why do we care about having this?
There was a problem hiding this comment.
Two things really:
- an excuse to run
make_full_schemaregularly so we hopefully spot it breaking - as a review aid for schema changes — particularly when we make schema changes in a background update, or more weird ones where we have to do a complex rewrite for SQLite (example) — it's nice to see the diff of the end result and have another opportunity to spot a problem
There was a problem hiding this comment.
Lol, I didn't intend for this, but this schema diff review aid would also have spotted that the following schema file does nothing:
| with: | ||
| header: schema-diff | ||
| path: ${{ runner.temp }}/schema_diff.md | ||
| ignore_empty: true |
There was a problem hiding this comment.
ignore_empty: true prevents it from posting a comment for PRs that don't change the database at all?
There was a problem hiding this comment.
Naw, it's redundant here as it shouldn't be empty. Removed
There was a problem hiding this comment.
prevents it from posting a comment for PRs that don't change the database at all?
Do we always post comments? (even for PRs which don't change any database stuff)
(seems annoying if so)
There was a problem hiding this comment.
Only when you touch a schema delta, full schema, or the schema diff workflow: https://github.com/element-hq/synapse/pull/20027/files/8ab5389c0bc1c6eae241ebb61667b93da149a03c#diff-589b31c74e9c8e8b7c6808f05a82bcd2a1ca18a00320e396afb36bd507cb1411R6
I didn't want to waste CI time otherwise.
I realise that it will miss cases where you touch the code concerning a background update that could have an overall impact on the schema, but at the moment I think I'm fine with that (to avoid triggering it unnecessarily), I think that case is sufficiently rare (if you're fixing a background update, you're also likely to put in a schema delta to re-trigger it or something, to fix up servers that already ran the broken one).
| @@ -0,0 +1,225 @@ | |||
| #!/usr/bin/env python3 | |||
|
|
||
| NB at the time of writing, this script predates the split into separate `state`/`main` | ||
| databases so will require updates to handle that correctly. | ||
| Run `poetry run make_full_schema.sh -h` for usage instructions. |
There was a problem hiding this comment.
I'd prefer a snippet I can run:
| Run `poetry run make_full_schema.sh -h` for usage instructions. | |
| Run `poetry run scripts-dev/make_full_schema.sh -h` for usage instructions. |
There was a problem hiding this comment.
Good principle to live by
| NB at the time of writing, this script predates the split into separate `state`/`main` | ||
| databases so will require updates to handle that correctly. |
There was a problem hiding this comment.
Were these updates made so this is no longer relevant?
There was a problem hiding this comment.
The comment was stale: this was addressed in matrix-org/synapse#13792
| # - Remove the `\restrict` and `\unrestrict` psql meta-commands. | ||
| # We don't run the pg_dump output through psql so no meta-commands are | ||
| # supported and so the security feature (which doesn't apply anyway | ||
| # as we trust the source database) is not relevant. |
There was a problem hiding this comment.
I think the comment is sufficient but I wasn't following on the first pass 👍
|
Cheers for the review @MadLittleMods 🐑 |
…0117) Follows: #20027 Noticed in #20003 The problem was that `--no-root` prevents reinstalling the root project (Synapse). However, since we just did a `git checkout`, we need to reinstall the root project in case the Rust code changed, as the `poetry install` command is what causes the Rust to be recompiled. Doing otherwise causes the 'Rust module outdated' error when importing `synapse`. --------- Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
It seems a lot of time in our trial tests goes towards setting up the database. (The same is probably true of Complement too)
We haven't done a full schema for about 20 schema versions, so no surprise!
As a result, I want to produce a full schema soon.
In this PR I dust off
make_full_schema.sh(which seems to have broken after some SQLite changes)and add a CI workflow that runs it (producing a diff) when someone changes the schema.
The CI workflow also adds a sticky comment showing the diff on the schema, so you can better appreciate the final effect of a change.
Original commit schedule, with full messages:
make_full_schema: Use
--nosysto avoid dumpingsqlite_sequencemake_full_schema: don't rely on pip
Not present on my machine
make_full_schema: tweak example to use new minimum postgres version
make_full_schema: no longer drop sqlite internal tablesMoved to Remove broken
DROPstatements for SQLite inmake_full_schemascript. #20028CI: add schema diff 'sticky comment'
make_full_schema: improve documentation
Dead changes:
I wanted to make it possible to generate a versioned full schema without the manual work,
but you can't run the background updates without essentially starting up a homeserver,
at which point it might fail because you haven't run all the deltas yet.
There's no actual good way to do this, short of deleting the latest deltas (+ tweaking code to not crash without them)
or rolling back in the git history.
Backed out these changes, but they're preserved here.
update_synapse_database: allow updating to an arbitrary schema version
make_full_schema: don't update past the requested schema version
Revert "update_synapse_database: allow updating to an arbitrary schema version"
This reverts commit b3fbb8144ace9c933e013d881f7bd92a02d21745.
Revert "make_full_schema: don't update past the requested schema version"
This reverts commit 70cceba43d0131e7276182725454f01a5b1684b1.