Skip to content
Clone

feat(schema): enhance schema synchronization with fingerprinting and state management

ZhenYi committed
parent 630f06b commit 7a80e88 Browse files

3 files changed

+66 −21

File tree 3
Modified lib/share/models/src/migrate/mod.rs View lib/share/models/src/migrate/mod.rs
@@ -1,14 +1,51 @@
1use sea_orm::{ConnectionTrait, DatabaseBackend, DatabaseConnection};
1use std::hash::{DefaultHasher, Hash, Hasher};
2
3use sea_orm::{ConnectionTrait, DatabaseBackend, DatabaseConnection, Statement};
4
5const SCHEMA_PRE_SYNC: &str = include_str!("../../schema_pre_sync.sql");
6const SCHEMA_EXTRAS: &str = include_str!("../../schema_extras.sql");
7const SYNC_FORMAT_VERSION: u32 = 1;
8const SYNC_STATE_DDL: &str = "CREATE TABLE IF NOT EXISTS schema_sync_state (
9 id smallint PRIMARY KEY CHECK (id = 1),
10 fingerprint text NOT NULL,
11 synced_at timestamptz NOT NULL DEFAULT now()
12)";
213
314pub async fn sync_schema(db: &DatabaseConnection) -> Result<(), sea_orm::DbErr> {
4 if db.get_database_backend() == DatabaseBackend::Postgres {
5 db.execute_unprepared(include_str!("../../schema_pre_sync.sql"))
6 .await?;
15 let builder = db.get_schema_registry("models::");
16 if db.get_database_backend() != DatabaseBackend::Postgres {
17 return builder.sync(db).await;
718 }
8 db.get_schema_registry("models::").sync(db).await?;
9 if db.get_database_backend() == DatabaseBackend::Postgres {
10 db.execute_unprepared(include_str!("../../schema_extras.sql"))
11 .await?;
19
20 db.execute_unprepared(SYNC_STATE_DDL).await?;
21
22 let mut hasher = DefaultHasher::new();
23 SYNC_FORMAT_VERSION.hash(&mut hasher);
24 SCHEMA_PRE_SYNC.hash(&mut hasher);
25 SCHEMA_EXTRAS.hash(&mut hasher);
26 format!("{builder:?}").hash(&mut hasher);
27 let fingerprint = format!("{:016x}", hasher.finish());
28
29 let stored = db
30 .query_one_raw(Statement::from_string(
31 DatabaseBackend::Postgres,
32 "SELECT fingerprint FROM schema_sync_state WHERE id = 1",
33 ))
34 .await?;
35 if let Some(row) = stored {
36 if row.try_get::<String>("", "fingerprint")? == fingerprint {
37 return Ok(());
38 }
1239 }
40
41 db.execute_unprepared(SCHEMA_PRE_SYNC).await?;
42 builder.sync(db).await?;
43 db.execute_unprepared(SCHEMA_EXTRAS).await?;
44
45 let update = format!(
46 "INSERT INTO schema_sync_state (id, fingerprint) VALUES (1, '{fingerprint}') \
47 ON CONFLICT (id) DO UPDATE SET fingerprint = EXCLUDED.fingerprint, synced_at = now()"
48 );
49 db.execute_unprepared(&update).await?;
1350 Ok(())
1451}
Modified lib/share/models/schema_extras.sql View lib/share/models/schema_extras.sql
@@ -156,9 +156,6 @@
156156CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_draft_unique
157157 ON chat_user_draft (user_id, room_id, thread_id) NULLS NOT DISTINCT;
158158
159CREATE UNIQUE INDEX IF NOT EXISTS idx_space_join_request_pending
160 ON space_join_request (space_id, user_id) WHERE status = 'pending';
161
162159CREATE INDEX IF NOT EXISTS idx_chat_webhook_token
163160 ON chat_room_webhook (token_hash);
164161
Modified lib/share/models/schema_pre_sync.sql View lib/share/models/schema_pre_sync.sql
@@ -1,14 +1,26 @@
11-- schema_pre_sync.sql
2-- Drops partial unique indexes created by schema_extras.sql before every
3-- sea-orm schema sync. The sync layer cannot express partial predicates via
4-- entity attributes, and treats these manually-created unique indexes as
5-- stale constraints (DROP CONSTRAINT), which fails because they are plain
6-- indexes. schema_extras.sql recreates all of them after the sync succeeds.
2-- Drops hand-managed unique indexes that sea-orm schema sync would reject.
3-- The sync only compares the indexed column list against the entity's
4-- #[sea_orm(indexed)] / unique_key declarations; a predicate, a NULLS NOT
5-- DISTINCT clause or a column list the entity cannot express all look like a
6-- stale unique key, and Postgres then issues DROP CONSTRAINT on a plain index,
7-- which fails. These indexes are recreated by schema_extras.sql after sync.
8--
9-- Only indexes whose column list differs from an entity unique key belong
10-- here. Indexes that do match (idx_issue_watch_issue_user, ..._project_user,
11-- idx_issue_reaction_issue, idx_issue_reaction_comment) must NOT be dropped:
12-- sync keeps them as-is and the drop would only rebuild them needlessly.
13--
14-- The migrate command only runs this file when the entity schema fingerprint
15-- changed, so the rebuild cost is paid on schema changes, not on every start.
716
8DROP INDEX IF EXISTS idx_issue_watch_issue_user;
9DROP INDEX IF EXISTS idx_issue_watch_project_user;
10DROP INDEX IF EXISTS idx_issue_reaction_issue;
11DROP INDEX IF EXISTS idx_issue_reaction_comment;
17-- Legacy plain indexes left behind by older unique_key definitions. They no
18-- longer match the entity column list, so sync would try to drop them and
19-- abort because they are plain indexes.
20DROP INDEX IF EXISTS "idx-issue_watch-issue_user";
21DROP INDEX IF EXISTS "idx-issue-reaction-issue_user_emoji";
22
23-- Hand-managed per-target unique keys (predicates not expressible in entities).
1224DROP INDEX IF EXISTS idx_issue_reaction_project;
1325DROP INDEX IF EXISTS idx_branch_protection_whitelist_user;
1426DROP INDEX IF EXISTS idx_branch_protection_whitelist_group;
@@ -16,7 +28,6 @@
1628DROP INDEX IF EXISTS idx_chat_dm_pair;
1729DROP INDEX IF EXISTS idx_chat_gdm_key;
1830DROP INDEX IF EXISTS idx_chat_draft_unique;
19DROP INDEX IF EXISTS idx_space_join_request_pending;
2031-- scheduled messages feature removed; drop the legacy table before sync
2132DROP TABLE IF EXISTS user_scheduled_message;
2233