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
This commit is contained in:
zhenyi
2026-06-10 18:48:43 +08:00
parent d98e4d59e3
commit d6c468a9fc
11 changed files with 203 additions and 947 deletions
Generated
+65 -277
View File
@@ -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",
+5 -5
View File
@@ -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"
+11
View File
@@ -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);
+41
View File
@@ -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;
-320
View File
@@ -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();
@@ -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);
-187
View File
@@ -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();
-145
View File
@@ -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();
+13
View File
@@ -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;
-13
View File
@@ -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);
@@ -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);