From d6c468a9fc7c984bb50d2181ffe521a337619c11 Mon Sep 17 00:00:00 2001 From: zhenyi <434836402@qq.com> Date: Wed, 10 Jun 2026 18:48:43 +0800 Subject: [PATCH] feat(db): add sqlx migrate feature and renumber migration files - Add 'migrate' feature to sqlx dependency - Renumber migrations to fix duplicate version numbers (two 014 files) - Re-sequence migrations 009-012 for continuous ordering - Add ALTER TABLE ADD COLUMN IF NOT EXISTS baseline for notification table to handle existing databases missing newer columns - Remove deleted IM migration files (009-012) that were superseded --- Cargo.lock | 342 ++++-------------- Cargo.toml | 10 +- migrate/001_init.sql | 11 + migrate/009_fix_audit_trigger.sql | 41 +++ migrate/009_im_features.sql | 320 ---------------- migrate/010_add_deleted_at_user_tables.sql | 24 ++ migrate/010_channel_kinds.sql | 187 ---------- migrate/011_announcement.sql | 145 -------- migrate/011_repo_enhancements.sql | 13 + migrate/012_im_message_seq.sql | 13 - .../012_release_assets_pr_enhancements.sql | 44 +++ 11 files changed, 203 insertions(+), 947 deletions(-) create mode 100644 migrate/009_fix_audit_trigger.sql delete mode 100644 migrate/009_im_features.sql create mode 100644 migrate/010_add_deleted_at_user_tables.sql delete mode 100644 migrate/010_channel_kinds.sql delete mode 100644 migrate/011_announcement.sql create mode 100644 migrate/011_repo_enhancements.sql delete mode 100644 migrate/012_im_message_seq.sql create mode 100644 migrate/012_release_assets_pr_enhancements.sql diff --git a/Cargo.lock b/Cargo.lock index 91ab9a8..2859d01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -359,12 +359,12 @@ dependencies = [ "dotenvy", "etcd-client", "futures-util", + "hex", "hkdf 0.12.4", "hmac 0.12.1", "object_store", - "prost 0.14.3", - "prost-types 0.14.3", - "r2d2", + "prost", + "prost-types", "rand 0.8.6", "redis", "reqwest 0.13.4", @@ -377,7 +377,7 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", - "tonic 0.14.6", + "tonic", "tonic-prost", "tonic-prost-build", "tracing", @@ -402,6 +402,15 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + [[package]] name = "arcstr" version = "1.2.0" @@ -493,28 +502,6 @@ dependencies = [ "url", ] -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "async-trait" version = "0.1.89" @@ -612,74 +599,27 @@ dependencies = [ "fs_extra", ] -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http 1.4.1", - "http-body", - "http-body-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper", - "tower 0.5.3", - "tower-layer", - "tower-service", -] - [[package]] name = "axum" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ - "axum-core 0.5.6", + "axum-core", "bytes", "futures-util", "http 1.4.1", "http-body", "http-body-util", "itoa", - "matchit 0.8.4", + "matchit", "memchr", "mime", "percent-encoding", "pin-project-lite", "serde_core", "sync_wrapper", - "tower 0.5.3", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.4.1", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", + "tower", "tower-layer", "tower-service", ] @@ -702,6 +642,15 @@ dependencies = [ "tower-service", ] +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", +] + [[package]] name = "base64" version = "0.20.0" @@ -1441,17 +1390,19 @@ dependencies = [ [[package]] name = "etcd-client" -version = "0.14.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0452bcc559431b16f472b7ab86e2f9ccd5f3c2da3795afbd6b773665e047fe" +checksum = "5ed900ba953ca6bf1fadb75e0c6b73d8463b9e2bb6bdb7b4573e8e7295852fbe" dependencies = [ "http 1.4.1", - "prost 0.13.5", + "prost", "tokio", "tokio-stream", - "tonic 0.12.3", - "tonic-build 0.12.3", - "tower 0.4.13", + "tonic", + "tonic-build", + "tonic-prost", + "tonic-prost-build", + "tower", "tower-service", ] @@ -1868,7 +1819,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.14.0", + "indexmap", "slab", "tokio", "tokio-util", @@ -1887,7 +1838,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.4.1", - "indexmap 2.14.0", + "indexmap", "slab", "tokio", "tokio-util", @@ -1905,12 +1856,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.14.5" @@ -2351,16 +2296,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.14.0" @@ -2603,12 +2538,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "matchit" version = "0.8.4" @@ -3045,16 +2974,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset", - "indexmap 2.14.0", -] - [[package]] name = "petgraph" version = "0.8.3" @@ -3063,7 +2982,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap 2.14.0", + "indexmap", ] [[package]] @@ -3232,16 +3151,6 @@ dependencies = [ "syn", ] -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive 0.13.5", -] - [[package]] name = "prost" version = "0.14.3" @@ -3249,27 +3158,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", - "prost-derive 0.14.3", -] - -[[package]] -name = "prost-build" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" -dependencies = [ - "heck", - "itertools", - "log", - "multimap", - "once_cell", - "petgraph 0.7.1", - "prettyplease", - "prost 0.13.5", - "prost-types 0.13.5", - "regex", - "syn", - "tempfile", + "prost-derive", ] [[package]] @@ -3282,10 +3171,10 @@ dependencies = [ "itertools", "log", "multimap", - "petgraph 0.8.3", + "petgraph", "prettyplease", - "prost 0.14.3", - "prost-types 0.14.3", + "prost", + "prost-types", "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", @@ -3293,19 +3182,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "prost-derive" version = "0.14.3" @@ -3319,22 +3195,13 @@ dependencies = [ "syn", ] -[[package]] -name = "prost-types" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" -dependencies = [ - "prost 0.13.5", -] - [[package]] name = "prost-types" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" dependencies = [ - "prost 0.14.3", + "prost", ] [[package]] @@ -3465,17 +3332,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "r2d2" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" -dependencies = [ - "log", - "parking_lot", - "scheduled-thread-pool", -] - [[package]] name = "rand" version = "0.8.6" @@ -3644,19 +3500,21 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a12e6b5f4d8ef33944e833e2b1859ad478deab6e431d7337b30ee2efe21f7543" dependencies = [ + "arc-swap", "arcstr", "async-lock", + "backon", "bytes", "cfg-if", "combine", "crc16", + "futures-channel", "futures-util", "itoa", "log", "num-bigint", "percent-encoding", "pin-project-lite", - "r2d2", "rand 0.9.4", "ryu", "sha1_smol", @@ -3743,7 +3601,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", - "tower 0.5.3", + "tower", "tower-http", "tower-service", "url", @@ -3784,7 +3642,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tower 0.5.3", + "tower", "tower-http", "tower-service", "url", @@ -3891,6 +3749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -3999,15 +3858,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "scheduled-thread-pool" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" -dependencies = [ - "parking_lot", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -4355,7 +4205,7 @@ dependencies = [ "futures-util", "hashbrown 0.16.1", "hashlink", - "indexmap 2.14.0", + "indexmap", "log", "memchr", "percent-encoding", @@ -4783,36 +4633,6 @@ dependencies = [ "webpki-roots 0.26.11", ] -[[package]] -name = "tonic" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.7.9", - "base64 0.22.1", - "bytes", - "h2 0.4.14", - "http 1.4.1", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "prost 0.13.5", - "socket2 0.5.10", - "tokio", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tonic" version = "0.14.6" @@ -4820,7 +4640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", - "axum 0.8.9", + "axum", "base64 0.22.1", "bytes", "h2 0.4.14", @@ -4835,27 +4655,14 @@ dependencies = [ "socket2 0.6.4", "sync_wrapper", "tokio", + "tokio-rustls", "tokio-stream", - "tower 0.5.3", + "tower", "tower-layer", "tower-service", "tracing", ] -[[package]] -name = "tonic-build" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" -dependencies = [ - "prettyplease", - "proc-macro2", - "prost-build 0.13.5", - "prost-types 0.13.5", - "quote", - "syn", -] - [[package]] name = "tonic-build" version = "0.14.6" @@ -4875,8 +4682,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", - "prost 0.14.3", - "tonic 0.14.6", + "prost", + "tonic", ] [[package]] @@ -4887,32 +4694,12 @@ checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" dependencies = [ "prettyplease", "proc-macro2", - "prost-build 0.14.3", - "prost-types 0.14.3", + "prost-build", + "prost-types", "quote", "syn", "tempfile", - "tonic-build 0.14.6", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand 0.8.6", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", + "tonic-build", ] [[package]] @@ -4923,7 +4710,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.14.0", + "indexmap", "pin-project-lite", "slab", "sync_wrapper", @@ -4946,7 +4733,7 @@ dependencies = [ "http 1.4.1", "http-body", "pin-project-lite", - "tower 0.5.3", + "tower", "tower-layer", "tower-service", "url", @@ -5145,7 +4932,7 @@ version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" dependencies = [ - "indexmap 2.14.0", + "indexmap", "serde", "serde_json", "utoipa-gen", @@ -5173,6 +4960,7 @@ dependencies = [ "getrandom 0.4.2", "js-sys", "serde_core", + "sha1_smol", "wasm-bindgen", ] @@ -5320,7 +5108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.14.0", + "indexmap", "wasm-encoder", "wasmparser", ] @@ -5346,7 +5134,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap 2.14.0", + "indexmap", "semver", ] @@ -5688,7 +5476,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.14.0", + "indexmap", "prettyplease", "syn", "wasm-metadata", @@ -5719,7 +5507,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap 2.14.0", + "indexmap", "log", "serde", "serde_derive", @@ -5738,7 +5526,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.14.0", + "indexmap", "log", "semver", "serde", diff --git a/Cargo.toml b/Cargo.toml index fb872b1..30e424e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,19 +16,18 @@ path = "main.rs" name = "gen_openapi" path = "gen_openapi.rs" [dependencies] -sqlx = { version = "0.9.0", features = ["postgres","runtime-tokio","chrono","uuid","json"] } +sqlx = { version = "0.9.0", features = ["postgres","runtime-tokio","chrono","uuid","json","migrate"] } tokio = { version = "1.52.3", features = ["full"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = { version = "1.0.150", features = [] } chrono = { version = "0.4.19", features = ["serde"] } -uuid = { version = "1.23.1", features = ["serde","v4","v7"] } +uuid = { version = "1.23.1", features = ["serde","v4","v7","v5"] } reqwest = { version = "0.13.4", features = ["json"] } tracing = { version = "0.1.44", features = [] } tracing-subscriber = { version = "0.3.23", features = ["fmt"] } dotenvy = "0.15.7" thiserror = "2" -redis = { version = "1.2.1", features = ["cluster","cluster-async","aio","tokio-comp","r2d2"] } -r2d2 = { version = "0.8.10", features = [] } +redis = { version = "1.2.1", features = ["cluster","cluster-async","aio","tokio-comp","connection-manager"] } dashmap = "6.1" object_store = { version = "0.13.2", features = ["tokio","aws","cloud"] } argon2 = "0.5" @@ -46,13 +45,14 @@ prost = "0.14.3" prost-types = "0.14.3" tonic-prost = "0.14.6" url = "2.5" -etcd-client = "0.14" +etcd-client = { version = "0.18.0", features = ["tls"] } tokio-stream = "0.1" async-nats = "0.49" futures-util = "0.3" utoipa = { version = "5.5.0", features = ["uuid","chrono","actix_extras","decimal","macros"]} actix-web = { version = "4", features = ["secure-cookies"] } actix-multipart = "0.7" +hex = "0.4.3" [build-dependencies] tonic-prost-build = "0.14.6" diff --git a/migrate/001_init.sql b/migrate/001_init.sql index b8bacd4..8ba295a 100644 --- a/migrate/001_init.sql +++ b/migrate/001_init.sql @@ -1905,6 +1905,17 @@ CREATE TABLE IF NOT EXISTS notification ( deleted_at TIMESTAMPTZ NULL ); +ALTER TABLE notification ADD COLUMN IF NOT EXISTS repo_id UUID NULL REFERENCES repo(id) ON DELETE CASCADE; +ALTER TABLE notification ADD COLUMN IF NOT EXISTS issue_id UUID NULL REFERENCES issue(id) ON DELETE CASCADE; +ALTER TABLE notification ADD COLUMN IF NOT EXISTS pull_request_id UUID NULL REFERENCES pull_request(id) ON DELETE CASCADE; +ALTER TABLE notification ADD COLUMN IF NOT EXISTS channel_id UUID NULL REFERENCES channel(id) ON DELETE CASCADE; +ALTER TABLE notification ADD COLUMN IF NOT EXISTS message_id UUID NULL REFERENCES message(id) ON DELETE CASCADE; +ALTER TABLE notification ADD COLUMN IF NOT EXISTS target_type TEXT NULL; +ALTER TABLE notification ADD COLUMN IF NOT EXISTS target_id UUID NULL; +ALTER TABLE notification ADD COLUMN IF NOT EXISTS action_url TEXT NULL; +ALTER TABLE notification ADD COLUMN IF NOT EXISTS priority TEXT NOT NULL DEFAULT 'normal'; +ALTER TABLE notification ADD COLUMN IF NOT EXISTS metadata JSONB NULL; +ALTER TABLE notification ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ NULL; CREATE INDEX IF NOT EXISTS idx_notification_user_id ON notification (user_id); CREATE INDEX IF NOT EXISTS idx_notification_actor_id ON notification (actor_id); CREATE INDEX IF NOT EXISTS idx_notification_workspace_id ON notification (workspace_id); diff --git a/migrate/009_fix_audit_trigger.sql b/migrate/009_fix_audit_trigger.sql new file mode 100644 index 0000000..e938ecd --- /dev/null +++ b/migrate/009_fix_audit_trigger.sql @@ -0,0 +1,41 @@ +-- 013: Fix record_workspace_audit() for tables without 'id' column. +-- workspace_billing, workspace_custom_branding, workspace_settings +-- use workspace_id as their PRIMARY KEY and have no separate id column. +-- The original function unconditionally referenced NEW.id, which crashed +-- with "record 'new' has no field 'id'" (42703). + +CREATE OR REPLACE FUNCTION record_workspace_audit() +RETURNS TRIGGER AS $$ +DECLARE + ws_id UUID; + actor UUID := app_current_user_id(); + action_text TEXT; + target_id_val UUID; +BEGIN + ws_id := COALESCE(NEW.workspace_id, OLD.workspace_id); + + IF ws_id IS NULL THEN + RETURN COALESCE(NEW, OLD); + END IF; + + action_text := CASE TG_OP + WHEN 'INSERT' THEN 'created' + WHEN 'UPDATE' THEN 'updated' + WHEN 'DELETE' THEN 'deleted' + END; + + -- Attempt to read NEW.id / OLD.id. For tables whose PK is workspace_id + -- there is no id column; catch the error and use ws_id instead. + BEGIN + target_id_val := COALESCE(NEW.id, OLD.id); + EXCEPTION + WHEN undefined_column THEN + target_id_val := ws_id; + END; + + INSERT INTO workspace_audit_log (workspace_id, actor_id, action, target_type, target_id, created_at) + VALUES (ws_id, actor, action_text, TG_TABLE_NAME, target_id_val, NOW()); + + RETURN COALESCE(NEW, OLD); +END; +$$ LANGUAGE plpgsql; diff --git a/migrate/009_im_features.sql b/migrate/009_im_features.sql deleted file mode 100644 index 7a3972a..0000000 --- a/migrate/009_im_features.sql +++ /dev/null @@ -1,320 +0,0 @@ --- 009: IM Features — Discord/Slack-class messaging support --- --- New tables: --- user_presence, user_activity, --- channel_category, channel_permission_overwrite, im_integration, --- message_attachment, message_embed, message_draft, message_pin, --- message_edit_history, saved_message, thread_read_state, --- custom_emoji - --- ============================================================ --- 1. User Presence --- ============================================================ - --- models/users/user_presence.rs → user_presence -CREATE TABLE IF NOT EXISTS user_presence ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - status TEXT NOT NULL, - custom_status_text TEXT NULL, - custom_status_emoji TEXT NULL, - device_type TEXT NULL, - ip_address TEXT NULL, - last_active_at TIMESTAMPTZ NOT NULL, - last_seen_at TIMESTAMPTZ NULL, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - CONSTRAINT uq_user_presence_user_id UNIQUE (user_id) -); -CREATE INDEX IF NOT EXISTS idx_user_presence_status ON user_presence (status); - --- ============================================================ --- 2. User Activity (Rich Presence) --- ============================================================ - --- models/users/user_activity.rs → user_activity -CREATE TABLE IF NOT EXISTS user_activity ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - activity_type TEXT NOT NULL, - name TEXT NOT NULL, - details TEXT NULL, - state TEXT NULL, - application_id TEXT NULL, - assets JSONB NULL, - party_id TEXT NULL, - party_current_size INTEGER NULL, - party_max_size INTEGER NULL, - large_image_url TEXT NULL, - small_image_url TEXT NULL, - start_at TIMESTAMPTZ NULL, - end_at TIMESTAMPTZ NULL, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL -); -CREATE INDEX IF NOT EXISTS idx_user_activity_user_id ON user_activity (user_id); - --- ============================================================ --- 3. Channel Categories --- ============================================================ - --- models/channels/channel_categories.rs → channel_category -CREATE TABLE IF NOT EXISTS channel_category ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE, - name TEXT NOT NULL, - position INTEGER NOT NULL, - collapsed BOOLEAN NOT NULL, - created_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL -); -CREATE INDEX IF NOT EXISTS idx_channel_category_workspace_id ON channel_category (workspace_id); - --- ============================================================ --- 4. ALTER channel — add category_id (after channel_category exists) --- ============================================================ - -ALTER TABLE channel ADD COLUMN IF NOT EXISTS category_id UUID NULL REFERENCES channel_category(id) ON DELETE SET NULL; -CREATE INDEX IF NOT EXISTS idx_channel_category_id ON channel (category_id); - --- ============================================================ --- 5. Channel Permission Overwrites --- ============================================================ - --- models/channels/channel_permission_overwrites.rs → channel_permission_overwrite -CREATE TABLE IF NOT EXISTS channel_permission_overwrite ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - target_type TEXT NOT NULL, - target_id UUID NOT NULL, - allow TEXT[] NOT NULL, - deny TEXT[] NOT NULL, - created_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - CONSTRAINT uq_channel_perm_overwrite UNIQUE (channel_id, target_type, target_id) -); -CREATE INDEX IF NOT EXISTS idx_channel_perm_overwrite_channel_id ON channel_permission_overwrite (channel_id); -CREATE INDEX IF NOT EXISTS idx_channel_perm_overwrite_target ON channel_permission_overwrite (target_type, target_id); - --- ============================================================ --- 6. IM Integrations (External Bridge) --- ============================================================ - --- models/channels/im_integrations.rs → im_integration -CREATE TABLE IF NOT EXISTS im_integration ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE, - provider TEXT NOT NULL, - name TEXT NOT NULL, - external_workspace_id TEXT NULL, - internal_channel_id UUID NULL REFERENCES channel(id) ON DELETE SET NULL, - external_channel_id TEXT NULL, - bot_token_ciphertext TEXT NULL, - webhook_url TEXT NULL, - sync_direction TEXT NOT NULL, - user_mapping JSONB NULL, - enabled BOOLEAN NOT NULL, - installed_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - last_sync_at TIMESTAMPTZ NULL, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL -); -CREATE INDEX IF NOT EXISTS idx_im_integration_workspace_id ON im_integration (workspace_id); -CREATE INDEX IF NOT EXISTS idx_im_integration_internal_channel_id ON im_integration (internal_channel_id); -CREATE INDEX IF NOT EXISTS idx_im_integration_provider ON im_integration (provider); - --- ============================================================ --- 7. Message Attachments --- ============================================================ - --- models/channels/message_attachments.rs → message_attachment -CREATE TABLE IF NOT EXISTS message_attachment ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE, - channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - filename TEXT NOT NULL, - url TEXT NOT NULL, - proxy_url TEXT NULL, - size_bytes BIGINT NOT NULL, - mime_type TEXT NOT NULL, - width INTEGER NULL, - height INTEGER NULL, - duration_ms BIGINT NULL, - thumbnail_url TEXT NULL, - blurhash TEXT NULL, - created_at TIMESTAMPTZ NOT NULL -); -CREATE INDEX IF NOT EXISTS idx_message_attachment_message_id ON message_attachment (message_id); -CREATE INDEX IF NOT EXISTS idx_message_attachment_channel_id ON message_attachment (channel_id); - --- ============================================================ --- 8. Message Embeds (Rich Text / Link Previews) --- ============================================================ - --- models/channels/message_embeds.rs → message_embed -CREATE TABLE IF NOT EXISTS message_embed ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE, - embed_type TEXT NOT NULL, - title TEXT NULL, - description TEXT NULL, - url TEXT NULL, - author_name TEXT NULL, - author_url TEXT NULL, - author_icon_url TEXT NULL, - thumbnail_url TEXT NULL, - thumbnail_width INTEGER NULL, - thumbnail_height INTEGER NULL, - image_url TEXT NULL, - image_width INTEGER NULL, - image_height INTEGER NULL, - video_url TEXT NULL, - video_width INTEGER NULL, - video_height INTEGER NULL, - color INTEGER NULL, - fields JSONB NULL, - footer_text TEXT NULL, - footer_icon_url TEXT NULL, - provider_name TEXT NULL, - provider_url TEXT NULL, - "timestamp" TIMESTAMPTZ NULL, - created_at TIMESTAMPTZ NOT NULL -); -CREATE INDEX IF NOT EXISTS idx_message_embed_message_id ON message_embed (message_id); - --- ============================================================ --- 9. Message Drafts --- ============================================================ - --- models/channels/message_drafts.rs → message_draft -CREATE TABLE IF NOT EXISTS message_draft ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - thread_id UUID NULL, - reply_to_message_id UUID NULL, - content TEXT NOT NULL, - attachments JSONB NULL, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - CONSTRAINT uq_message_draft_user_channel UNIQUE (user_id, channel_id) -); -CREATE INDEX IF NOT EXISTS idx_message_draft_user_id ON message_draft (user_id); -CREATE INDEX IF NOT EXISTS idx_message_draft_channel_id ON message_draft (channel_id); - --- ============================================================ --- 10. Custom Emojis --- ============================================================ - --- models/channels/custom_emojis.rs → custom_emoji -CREATE TABLE IF NOT EXISTS custom_emoji ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE, - name TEXT NOT NULL, - url TEXT NOT NULL, - animated BOOLEAN NOT NULL, - managed BOOLEAN NOT NULL, - created_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - CONSTRAINT uq_custom_emoji_workspace_name UNIQUE (workspace_id, name) -); -CREATE INDEX IF NOT EXISTS idx_custom_emoji_workspace_id ON custom_emoji (workspace_id); - --- ============================================================ --- 11. Message Pins --- ============================================================ - --- models/channels/message_pins.rs → message_pin -CREATE TABLE IF NOT EXISTS message_pin ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE, - channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - pinned_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - pinned_at TIMESTAMPTZ NOT NULL, - CONSTRAINT uq_message_pin_message_id UNIQUE (message_id) -); -CREATE INDEX IF NOT EXISTS idx_message_pin_channel_id ON message_pin (channel_id); -CREATE INDEX IF NOT EXISTS idx_message_pin_message_id ON message_pin (message_id); - --- ============================================================ --- 12. Message Edit History --- ============================================================ - --- models/channels/message_edit_history.rs → message_edit_history -CREATE TABLE IF NOT EXISTS message_edit_history ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE, - channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - previous_body TEXT NOT NULL, - edited_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - edited_at TIMESTAMPTZ NOT NULL -); -CREATE INDEX IF NOT EXISTS idx_message_edit_history_message_id ON message_edit_history (message_id); -CREATE INDEX IF NOT EXISTS idx_message_edit_history_channel_id ON message_edit_history (channel_id); - --- ============================================================ --- 13. Saved Messages --- ============================================================ - --- models/channels/saved_messages.rs → saved_message -CREATE TABLE IF NOT EXISTS saved_message ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE, - channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - note TEXT NULL, - created_at TIMESTAMPTZ NOT NULL, - CONSTRAINT uq_saved_message_user_message UNIQUE (user_id, message_id) -); -CREATE INDEX IF NOT EXISTS idx_saved_message_user_id ON saved_message (user_id); -CREATE INDEX IF NOT EXISTS idx_saved_message_message_id ON saved_message (message_id); - --- ============================================================ --- 14. Thread Read States --- ============================================================ - --- models/channels/thread_read_states.rs → thread_read_state -CREATE TABLE IF NOT EXISTS thread_read_state ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - thread_id UUID NOT NULL, - channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - last_read_message_id UUID NULL, - last_read_at TIMESTAMPTZ NULL, - updated_at TIMESTAMPTZ NOT NULL, - CONSTRAINT uq_thread_read_state_user_thread UNIQUE (user_id, thread_id) -); -CREATE INDEX IF NOT EXISTS idx_thread_read_state_user_id ON thread_read_state (user_id); -CREATE INDEX IF NOT EXISTS idx_thread_read_state_thread_id ON thread_read_state (thread_id); -CREATE INDEX IF NOT EXISTS idx_thread_read_state_channel_id ON thread_read_state (channel_id); - --- ============================================================ --- 15. Triggers — auto-refresh updated_at --- ============================================================ - -DROP TRIGGER IF EXISTS trg_user_presence_updated_at ON user_presence; -CREATE TRIGGER trg_user_presence_updated_at BEFORE UPDATE ON user_presence FOR EACH ROW EXECUTE FUNCTION set_updated_at(); - -DROP TRIGGER IF EXISTS trg_user_activity_updated_at ON user_activity; -CREATE TRIGGER trg_user_activity_updated_at BEFORE UPDATE ON user_activity FOR EACH ROW EXECUTE FUNCTION set_updated_at(); - -DROP TRIGGER IF EXISTS trg_channel_category_updated_at ON channel_category; -CREATE TRIGGER trg_channel_category_updated_at BEFORE UPDATE ON channel_category FOR EACH ROW EXECUTE FUNCTION set_updated_at(); - -DROP TRIGGER IF EXISTS trg_channel_permission_overwrite_updated_at ON channel_permission_overwrite; -CREATE TRIGGER trg_channel_permission_overwrite_updated_at BEFORE UPDATE ON channel_permission_overwrite FOR EACH ROW EXECUTE FUNCTION set_updated_at(); - -DROP TRIGGER IF EXISTS trg_im_integration_updated_at ON im_integration; -CREATE TRIGGER trg_im_integration_updated_at BEFORE UPDATE ON im_integration FOR EACH ROW EXECUTE FUNCTION set_updated_at(); - -DROP TRIGGER IF EXISTS trg_message_draft_updated_at ON message_draft; -CREATE TRIGGER trg_message_draft_updated_at BEFORE UPDATE ON message_draft FOR EACH ROW EXECUTE FUNCTION set_updated_at(); - -DROP TRIGGER IF EXISTS trg_custom_emoji_updated_at ON custom_emoji; -CREATE TRIGGER trg_custom_emoji_updated_at BEFORE UPDATE ON custom_emoji FOR EACH ROW EXECUTE FUNCTION set_updated_at(); - -DROP TRIGGER IF EXISTS trg_thread_read_state_updated_at ON thread_read_state; -CREATE TRIGGER trg_thread_read_state_updated_at BEFORE UPDATE ON thread_read_state FOR EACH ROW EXECUTE FUNCTION set_updated_at(); diff --git a/migrate/010_add_deleted_at_user_tables.sql b/migrate/010_add_deleted_at_user_tables.sql new file mode 100644 index 0000000..e1b35b1 --- /dev/null +++ b/migrate/010_add_deleted_at_user_tables.sql @@ -0,0 +1,24 @@ +-- Add deleted_at columns to user-related tables for soft-delete support. +-- Previously, user_delete_account hard-deleted rows from these tables. +-- After this migration, all deletions are soft (mark as deleted). + +ALTER TABLE "user" ADD COLUMN IF NOT EXISTS restore_token_hash VARCHAR(64); +ALTER TABLE "user" ADD COLUMN IF NOT EXISTS restore_token_expires_at TIMESTAMPTZ; + +ALTER TABLE user_2fa ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; +ALTER TABLE user_activity ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; +ALTER TABLE user_appearance ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; +ALTER TABLE user_block ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; +ALTER TABLE user_device ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; +ALTER TABLE user_follow ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; +ALTER TABLE user_mail ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; +ALTER TABLE user_notify_setting ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; +ALTER TABLE user_oauth ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; +ALTER TABLE user_password ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; +ALTER TABLE user_password_reset ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; +ALTER TABLE user_presence ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; +ALTER TABLE user_profile ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; +ALTER TABLE user_security_log ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; + +CREATE INDEX IF NOT EXISTS idx_user_security_log_user_deleted ON user_security_log(user_id, deleted_at); +CREATE INDEX IF NOT EXISTS idx_user_activity_user_deleted ON user_activity(user_id, deleted_at); diff --git a/migrate/010_channel_kinds.sql b/migrate/010_channel_kinds.sql deleted file mode 100644 index 93c65ac..0000000 --- a/migrate/010_channel_kinds.sql +++ /dev/null @@ -1,187 +0,0 @@ --- 010: Channel Kinds — text, voice, stage, forum, announcement --- --- ALTER: --- channel — add channel_kind + voice/forum/stage fields --- message_thread — add forum post fields (title, tags, pinned, locked) --- --- New tables: --- forum_tag, voice_participant, stage, --- message_poll, message_poll_option, message_poll_vote - --- ============================================================ --- 1. ALTER channel — add channel_kind + voice/forum/stage fields --- ============================================================ - -ALTER TABLE channel ADD COLUMN IF NOT EXISTS channel_kind TEXT NOT NULL DEFAULT 'text'; -ALTER TABLE channel ADD COLUMN IF NOT EXISTS position INTEGER NULL; -ALTER TABLE channel ADD COLUMN IF NOT EXISTS nsfw BOOLEAN NOT NULL DEFAULT FALSE; - --- Voice / Stage specific -ALTER TABLE channel ADD COLUMN IF NOT EXISTS bitrate INTEGER NULL; -ALTER TABLE channel ADD COLUMN IF NOT EXISTS user_limit INTEGER NULL; -ALTER TABLE channel ADD COLUMN IF NOT EXISTS rtc_region TEXT NULL; - --- Forum specific -ALTER TABLE channel ADD COLUMN IF NOT EXISTS default_auto_archive_duration INTEGER NULL; -ALTER TABLE channel ADD COLUMN IF NOT EXISTS default_reaction_emoji TEXT NULL; -ALTER TABLE channel ADD COLUMN IF NOT EXISTS default_sort_order TEXT NULL; -ALTER TABLE channel ADD COLUMN IF NOT EXISTS default_forum_layout TEXT NULL; -ALTER TABLE channel ADD COLUMN IF NOT EXISTS require_tag BOOLEAN NULL; -ALTER TABLE channel ADD COLUMN IF NOT EXISTS available_tags JSONB NULL; -ALTER TABLE channel ADD COLUMN IF NOT EXISTS default_thread_rate_limit INTEGER NULL; - --- General -ALTER TABLE channel ADD COLUMN IF NOT EXISTS rate_limit_per_user INTEGER NULL; -ALTER TABLE channel ADD COLUMN IF NOT EXISTS parent_channel_id UUID NULL REFERENCES channel(id) ON DELETE SET NULL; - -CREATE INDEX IF NOT EXISTS idx_channel_channel_kind ON channel (channel_kind); -CREATE INDEX IF NOT EXISTS idx_channel_parent_channel_id ON channel (parent_channel_id); - --- ============================================================ --- 2. ALTER message_thread — add forum post fields --- ============================================================ - -ALTER TABLE message_thread ADD COLUMN IF NOT EXISTS title TEXT NULL; -ALTER TABLE message_thread ADD COLUMN IF NOT EXISTS tags TEXT[] NOT NULL DEFAULT '{}'; -ALTER TABLE message_thread ADD COLUMN IF NOT EXISTS pinned BOOLEAN NOT NULL DEFAULT FALSE; -ALTER TABLE message_thread ADD COLUMN IF NOT EXISTS locked BOOLEAN NOT NULL DEFAULT FALSE; -ALTER TABLE message_thread ADD COLUMN IF NOT EXISTS rate_limit_per_user INTEGER NULL; -ALTER TABLE message_thread ADD COLUMN IF NOT EXISTS auto_archive_at TIMESTAMPTZ NULL; - -CREATE INDEX IF NOT EXISTS idx_message_thread_pinned ON message_thread (pinned) WHERE pinned; - --- ============================================================ --- 3. Forum Tags --- ============================================================ - --- models/channels/forum_tags.rs → forum_tag -CREATE TABLE IF NOT EXISTS forum_tag ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - name TEXT NOT NULL, - emoji_id TEXT NULL, - emoji_name TEXT NULL, - moderated BOOLEAN NOT NULL, - position INTEGER NOT NULL, - created_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - CONSTRAINT uq_forum_tag_channel_name UNIQUE (channel_id, name) -); -CREATE INDEX IF NOT EXISTS idx_forum_tag_channel_id ON forum_tag (channel_id); - --- ============================================================ --- 4. Voice Participants --- ============================================================ - --- models/channels/voice_participants.rs → voice_participant -CREATE TABLE IF NOT EXISTS voice_participant ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - session_id TEXT NULL, - deafened BOOLEAN NOT NULL, - muted BOOLEAN NOT NULL, - self_deafened BOOLEAN NOT NULL, - self_muted BOOLEAN NOT NULL, - self_video BOOLEAN NOT NULL, - streaming BOOLEAN NOT NULL, - speaking BOOLEAN NOT NULL, - joined_at TIMESTAMPTZ NOT NULL, - left_at TIMESTAMPTZ NULL -); -CREATE INDEX IF NOT EXISTS idx_voice_participant_channel_id ON voice_participant (channel_id); -CREATE INDEX IF NOT EXISTS idx_voice_participant_user_id ON voice_participant (user_id); -CREATE INDEX IF NOT EXISTS idx_voice_participant_channel_user ON voice_participant (channel_id, user_id); - --- ============================================================ --- 5. Stages --- ============================================================ - --- models/channels/stages.rs → stage -CREATE TABLE IF NOT EXISTS stage ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - topic TEXT NOT NULL, - privacy_level TEXT NOT NULL, - discoverable BOOLEAN NOT NULL, - started_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - started_at TIMESTAMPTZ NOT NULL, - ended_at TIMESTAMPTZ NULL, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - CONSTRAINT uq_stage_active_channel UNIQUE (channel_id, ended_at) -); -CREATE INDEX IF NOT EXISTS idx_stage_channel_id ON stage (channel_id); -CREATE INDEX IF NOT EXISTS idx_stage_channel_active ON stage (channel_id) WHERE ended_at IS NULL; - --- ============================================================ --- 6. Message Polls --- ============================================================ - --- models/channels/message_polls.rs → message_poll -CREATE TABLE IF NOT EXISTS message_poll ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - message_id UUID NOT NULL REFERENCES message(id) ON DELETE CASCADE, - channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - question TEXT NOT NULL, - description TEXT NULL, - layout TEXT NOT NULL, - allow_multiselect BOOLEAN NOT NULL, - duration_hours INTEGER NULL, - ends_at TIMESTAMPTZ NULL, - total_votes BIGINT NOT NULL, - metadata JSONB NULL, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - CONSTRAINT uq_message_poll_message_id UNIQUE (message_id) -); -CREATE INDEX IF NOT EXISTS idx_message_poll_channel_id ON message_poll (channel_id); -CREATE INDEX IF NOT EXISTS idx_message_poll_ends_at ON message_poll (ends_at) WHERE ends_at IS NOT NULL; - --- ============================================================ --- 7. Message Poll Options --- ============================================================ - --- models/channels/message_poll_options.rs → message_poll_option -CREATE TABLE IF NOT EXISTS message_poll_option ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - poll_id UUID NOT NULL REFERENCES message_poll(id) ON DELETE CASCADE, - position INTEGER NOT NULL, - text TEXT NOT NULL, - emoji_id TEXT NULL, - emoji_name TEXT NULL, - vote_count BIGINT NOT NULL, - created_at TIMESTAMPTZ NOT NULL -); -CREATE INDEX IF NOT EXISTS idx_message_poll_option_poll_id ON message_poll_option (poll_id); - --- ============================================================ --- 8. Message Poll Votes --- ============================================================ - --- models/channels/message_poll_votes.rs → message_poll_vote -CREATE TABLE IF NOT EXISTS message_poll_vote ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - poll_id UUID NOT NULL REFERENCES message_poll(id) ON DELETE CASCADE, - option_id UUID NOT NULL REFERENCES message_poll_option(id) ON DELETE CASCADE, - user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - voted_at TIMESTAMPTZ NOT NULL, - CONSTRAINT uq_message_poll_vote_user_option UNIQUE (poll_id, option_id, user_id) -); -CREATE INDEX IF NOT EXISTS idx_message_poll_vote_poll_id ON message_poll_vote (poll_id); -CREATE INDEX IF NOT EXISTS idx_message_poll_vote_user_id ON message_poll_vote (user_id); -CREATE INDEX IF NOT EXISTS idx_message_poll_vote_option_id ON message_poll_vote (option_id); - --- ============================================================ --- 9. Triggers — auto-refresh updated_at --- ============================================================ - -DROP TRIGGER IF EXISTS trg_forum_tag_updated_at ON forum_tag; -CREATE TRIGGER trg_forum_tag_updated_at BEFORE UPDATE ON forum_tag FOR EACH ROW EXECUTE FUNCTION set_updated_at(); - -DROP TRIGGER IF EXISTS trg_stage_updated_at ON stage; -CREATE TRIGGER trg_stage_updated_at BEFORE UPDATE ON stage FOR EACH ROW EXECUTE FUNCTION set_updated_at(); - -DROP TRIGGER IF EXISTS trg_message_poll_updated_at ON message_poll; -CREATE TRIGGER trg_message_poll_updated_at BEFORE UPDATE ON message_poll FOR EACH ROW EXECUTE FUNCTION set_updated_at(); diff --git a/migrate/011_announcement.sql b/migrate/011_announcement.sql deleted file mode 100644 index 70448c6..0000000 --- a/migrate/011_announcement.sql +++ /dev/null @@ -1,145 +0,0 @@ --- 011: Announcement Channel — articles, comments, reactions, follows, cross-posts --- --- New tables: --- article, article_comment, article_reaction, --- channel_follow, article_cross_post - --- ============================================================ --- 1. Articles --- ============================================================ - --- models/channels/articles.rs → article -CREATE TABLE IF NOT EXISTS article ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - author_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - title TEXT NOT NULL, - slug TEXT NOT NULL, - summary TEXT NULL, - body TEXT NOT NULL, - cover_image_url TEXT NULL, - status TEXT NOT NULL, - visibility TEXT NOT NULL, - tags TEXT[] NOT NULL, - published_at TIMESTAMPTZ NULL, - published_by UUID NULL REFERENCES "user"(id) ON DELETE SET NULL, - scheduled_at TIMESTAMPTZ NULL, - unpublished_at TIMESTAMPTZ NULL, - views_count BIGINT NOT NULL DEFAULT 0, - comments_count BIGINT NOT NULL DEFAULT 0, - reactions_count BIGINT NOT NULL DEFAULT 0, - cross_posted BOOLEAN NOT NULL DEFAULT FALSE, - cross_posted_from UUID NULL REFERENCES article(id) ON DELETE SET NULL, - metadata JSONB NULL, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - deleted_at TIMESTAMPTZ NULL, - CONSTRAINT uq_article_channel_slug UNIQUE (channel_id, slug) -); -CREATE INDEX IF NOT EXISTS idx_article_channel_id ON article (channel_id); -CREATE INDEX IF NOT EXISTS idx_article_author_id ON article (author_id); -CREATE INDEX IF NOT EXISTS idx_article_status ON article (status); -CREATE INDEX IF NOT EXISTS idx_article_published_at ON article (published_at DESC) WHERE published_at IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_article_cross_posted_from ON article (cross_posted_from) WHERE cross_posted_from IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_article_deleted ON article (deleted_at) WHERE deleted_at IS NOT NULL; - --- ============================================================ --- 2. Article Comments --- ============================================================ - --- models/channels/article_comments.rs → article_comment -CREATE TABLE IF NOT EXISTS article_comment ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - article_id UUID NOT NULL REFERENCES article(id) ON DELETE CASCADE, - channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - author_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - parent_comment_id UUID NULL REFERENCES article_comment(id) ON DELETE CASCADE, - body TEXT NOT NULL, - edited_at TIMESTAMPTZ NULL, - deleted_at TIMESTAMPTZ NULL, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL -); -CREATE INDEX IF NOT EXISTS idx_article_comment_article_id ON article_comment (article_id); -CREATE INDEX IF NOT EXISTS idx_article_comment_author_id ON article_comment (author_id); -CREATE INDEX IF NOT EXISTS idx_article_comment_parent ON article_comment (parent_comment_id) WHERE parent_comment_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_article_comment_deleted ON article_comment (deleted_at) WHERE deleted_at IS NOT NULL; - --- ============================================================ --- 3. Article Reactions --- ============================================================ - --- models/channels/article_reactions.rs → article_reaction -CREATE TABLE IF NOT EXISTS article_reaction ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - article_id UUID NOT NULL REFERENCES article(id) ON DELETE CASCADE, - channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - content TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL, - CONSTRAINT uq_article_reaction UNIQUE (article_id, user_id, content) -); -CREATE INDEX IF NOT EXISTS idx_article_reaction_article_id ON article_reaction (article_id); -CREATE INDEX IF NOT EXISTS idx_article_reaction_user_id ON article_reaction (user_id); - --- ============================================================ --- 4. Channel Follows --- ============================================================ - --- models/channels/channel_follows.rs → channel_follow -CREATE TABLE IF NOT EXISTS channel_follow ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - source_channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, - target_workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE, - target_channel_id UUID NULL REFERENCES channel(id) ON DELETE SET NULL, - webhook_url TEXT NULL, - webhook_secret_ciphertext TEXT NULL, - enabled BOOLEAN NOT NULL, - followed_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, - unfollowed_at TIMESTAMPTZ NULL, - last_delivery_at TIMESTAMPTZ NULL, - last_delivery_status TEXT NULL, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - CONSTRAINT uq_channel_follow_source_target UNIQUE (source_channel_id, target_workspace_id, target_channel_id) -); -CREATE INDEX IF NOT EXISTS idx_channel_follow_source ON channel_follow (source_channel_id); -CREATE INDEX IF NOT EXISTS idx_channel_follow_target_ws ON channel_follow (target_workspace_id); -CREATE INDEX IF NOT EXISTS idx_channel_follow_target_channel ON channel_follow (target_channel_id) WHERE target_channel_id IS NOT NULL; - --- ============================================================ --- 5. Article Cross-Posts --- ============================================================ - --- models/channels/article_cross_posts.rs → article_cross_post -CREATE TABLE IF NOT EXISTS article_cross_post ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - article_id UUID NOT NULL REFERENCES article(id) ON DELETE CASCADE, - follow_id UUID NOT NULL REFERENCES channel_follow(id) ON DELETE CASCADE, - target_workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE, - target_channel_id UUID NULL REFERENCES channel(id) ON DELETE SET NULL, - status TEXT NOT NULL, - attempts INTEGER NOT NULL DEFAULT 0, - last_error TEXT NULL, - sent_at TIMESTAMPTZ NULL, - delivered_at TIMESTAMPTZ NULL, - failed_at TIMESTAMPTZ NULL, - created_at TIMESTAMPTZ NOT NULL -); -CREATE INDEX IF NOT EXISTS idx_article_cross_post_article_id ON article_cross_post (article_id); -CREATE INDEX IF NOT EXISTS idx_article_cross_post_follow_id ON article_cross_post (follow_id); -CREATE INDEX IF NOT EXISTS idx_article_cross_post_status ON article_cross_post (status); -CREATE INDEX IF NOT EXISTS idx_article_cross_post_target_ws ON article_cross_post (target_workspace_id); - --- ============================================================ --- 6. Triggers — auto-refresh updated_at --- ============================================================ - -DROP TRIGGER IF EXISTS trg_article_updated_at ON article; -CREATE TRIGGER trg_article_updated_at BEFORE UPDATE ON article FOR EACH ROW EXECUTE FUNCTION set_updated_at(); - -DROP TRIGGER IF EXISTS trg_article_comment_updated_at ON article_comment; -CREATE TRIGGER trg_article_comment_updated_at BEFORE UPDATE ON article_comment FOR EACH ROW EXECUTE FUNCTION set_updated_at(); - -DROP TRIGGER IF EXISTS trg_channel_follow_updated_at ON channel_follow; -CREATE TRIGGER trg_channel_follow_updated_at BEFORE UPDATE ON channel_follow FOR EACH ROW EXECUTE FUNCTION set_updated_at(); diff --git a/migrate/011_repo_enhancements.sql b/migrate/011_repo_enhancements.sql new file mode 100644 index 0000000..164219c --- /dev/null +++ b/migrate/011_repo_enhancements.sql @@ -0,0 +1,13 @@ +-- Add repository-level feature toggles, merge settings, topics, and homepage. +-- models/repos/repo.rs → repo + +ALTER TABLE repo ADD COLUMN IF NOT EXISTS topics TEXT[] NOT NULL DEFAULT '{}'; +ALTER TABLE repo ADD COLUMN IF NOT EXISTS homepage TEXT; +ALTER TABLE repo ADD COLUMN IF NOT EXISTS has_issues BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE repo ADD COLUMN IF NOT EXISTS has_wiki BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE repo ADD COLUMN IF NOT EXISTS has_pull_requests BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE repo ADD COLUMN IF NOT EXISTS allow_forking BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE repo ADD COLUMN IF NOT EXISTS allow_merge_commit BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE repo ADD COLUMN IF NOT EXISTS allow_squash_merge BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE repo ADD COLUMN IF NOT EXISTS allow_rebase_merge BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE repo ADD COLUMN IF NOT EXISTS delete_branch_on_merge BOOLEAN NOT NULL DEFAULT false; diff --git a/migrate/012_im_message_seq.sql b/migrate/012_im_message_seq.sql deleted file mode 100644 index 4173ef2..0000000 --- a/migrate/012_im_message_seq.sql +++ /dev/null @@ -1,13 +0,0 @@ -ALTER TABLE message ADD COLUMN IF NOT EXISTS seq BIGINT NOT NULL DEFAULT 0; - -WITH ranked AS ( - SELECT id, ROW_NUMBER() OVER (PARTITION BY channel_id ORDER BY created_at ASC, id ASC) AS rn - FROM message - WHERE seq = 0 -) -UPDATE message m -SET seq = ranked.rn -FROM ranked -WHERE m.id = ranked.id; - -CREATE UNIQUE INDEX IF NOT EXISTS idx_message_channel_seq ON message (channel_id, seq); diff --git a/migrate/012_release_assets_pr_enhancements.sql b/migrate/012_release_assets_pr_enhancements.sql new file mode 100644 index 0000000..e20492a --- /dev/null +++ b/migrate/012_release_assets_pr_enhancements.sql @@ -0,0 +1,44 @@ +-- Release Assets - binary attachments for releases +CREATE TABLE IF NOT EXISTS repo_release_asset ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + release_id UUID NOT NULL REFERENCES repo_release(id) ON DELETE CASCADE, + filename TEXT NOT NULL, + size_bytes BIGINT NOT NULL, + mime_type TEXT NOT NULL DEFAULT 'application/octet-stream', + storage_path TEXT NOT NULL, + url TEXT, + download_count BIGINT NOT NULL DEFAULT 0, + uploaded_by UUID REFERENCES "user"(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + deleted_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS idx_repo_release_asset_release ON repo_release_asset(release_id, deleted_at); +CREATE INDEX IF NOT EXISTS idx_repo_release_asset_deleted ON repo_release_asset(deleted_at) WHERE deleted_at IS NOT NULL; + +-- PR Templates (mirrors issue_template structure) +CREATE TABLE IF NOT EXISTS pr_template ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + repo_id UUID NOT NULL REFERENCES repo(id) ON DELETE CASCADE, + name TEXT NOT NULL, + description TEXT, + title_template TEXT, + body_template TEXT NOT NULL DEFAULT '', + labels TEXT[] NOT NULL DEFAULT '{}', + active BOOLEAN NOT NULL DEFAULT true, + created_by UUID REFERENCES "user"(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_pr_template_repo ON pr_template(repo_id); + +-- PR Review Requests +CREATE TABLE IF NOT EXISTS pr_review_request ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + pull_request_id UUID NOT NULL REFERENCES pull_request(id) ON DELETE CASCADE, + reviewer_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, + requested_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_pr_review_request_pr ON pr_review_request(pull_request_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_pr_review_request_pr_reviewer ON pr_review_request(pull_request_id, reviewer_id);