fix(#2279): idempotent column-guard migrations, drop invalid ADD COLUMN IF NOT EXISTS
This commit is contained in:
parent
c47faf0af9
commit
dfbf198ef3
5 changed files with 188 additions and 121 deletions
|
|
@ -10,6 +10,8 @@ use anyhow::{Context, Result};
|
|||
use chrono::{DateTime, Utc};
|
||||
use hive_sh4re::wire_time::now_unix;
|
||||
use hive_sh4re::{InboxRow, Message};
|
||||
|
||||
use crate::db::Migration;
|
||||
use rusqlite::{Connection, OptionalExtension, params};
|
||||
use serde::Serialize;
|
||||
use tokio::sync::broadcast;
|
||||
|
|
@ -157,52 +159,60 @@ pub struct Broker {
|
|||
}
|
||||
|
||||
/// Ordered schema migrations for the broker. Tracked in `schema_versions`
|
||||
/// under key `"broker"`. Legacy databases (fully migrated via the old
|
||||
/// per-column check approach) are detected via the `priority` column on
|
||||
/// `messages` — the last column added before versioning.
|
||||
const BROKER_MIGRATIONS: &[&str] = &[
|
||||
/// under key `"broker"`. Each migration declares the column it adds, so a
|
||||
/// legacy DB (fully or partially migrated via the old per-column approach)
|
||||
/// is converged by skipping the migrations whose column already exists.
|
||||
const BROKER_MIGRATIONS: &[Migration] = &[
|
||||
// v1: acked_at on messages, with backfill so existing delivered rows
|
||||
// are not phantom-requeued on the next open.
|
||||
//
|
||||
// IF NOT EXISTS: safe to run on a partially-migrated legacy DB where
|
||||
// acked_at already exists but priority (the legacy marker) does not.
|
||||
// The BEGIN/COMMIT block makes ALTER + UPDATE atomic — if the UPDATE
|
||||
// fails the version counter is not bumped and the whole step retries.
|
||||
"BEGIN;\
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS acked_at INTEGER;\
|
||||
// are not phantom-requeued on the next open. The BEGIN/COMMIT block
|
||||
// makes ALTER + UPDATE atomic — either both land or neither, so the
|
||||
// guard column (acked_at) faithfully marks the whole step as done.
|
||||
Migration {
|
||||
sql: "BEGIN;\
|
||||
ALTER TABLE messages ADD COLUMN acked_at INTEGER;\
|
||||
UPDATE messages SET acked_at = delivered_at \
|
||||
WHERE delivered_at IS NOT NULL AND acked_at IS NULL;\
|
||||
COMMIT;",
|
||||
adds_column: Some(("messages", "acked_at")),
|
||||
},
|
||||
// v2: in_reply_to for thread-parent tracking. NULL = root of a thread.
|
||||
"ALTER TABLE messages ADD COLUMN IF NOT EXISTS in_reply_to INTEGER",
|
||||
Migration {
|
||||
sql: "ALTER TABLE messages ADD COLUMN in_reply_to INTEGER",
|
||||
adds_column: Some(("messages", "in_reply_to")),
|
||||
},
|
||||
// v3: priority for operator-message fast-path. Rebuild the delivery
|
||||
// index to include priority as a secondary sort key.
|
||||
"BEGIN;\
|
||||
ALTER TABLE messages ADD COLUMN IF NOT EXISTS \
|
||||
// index to include priority as a secondary sort key. Atomic BEGIN/COMMIT
|
||||
// so priority existing implies the index rebuild also committed.
|
||||
Migration {
|
||||
sql: "BEGIN;\
|
||||
ALTER TABLE messages ADD COLUMN \
|
||||
priority INTEGER NOT NULL DEFAULT 0;\
|
||||
DROP INDEX IF EXISTS idx_messages_undelivered;\
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_undelivered \
|
||||
ON messages (recipient, priority DESC, id) WHERE delivered_at IS NULL;\
|
||||
COMMIT;",
|
||||
adds_column: Some(("messages", "priority")),
|
||||
},
|
||||
// v4: attempt_count on reminders for the MAX_REMINDER_ATTEMPTS cap.
|
||||
"ALTER TABLE reminders ADD COLUMN IF NOT EXISTS \
|
||||
Migration {
|
||||
sql: "ALTER TABLE reminders ADD COLUMN \
|
||||
attempt_count INTEGER NOT NULL DEFAULT 0",
|
||||
adds_column: Some(("reminders", "attempt_count")),
|
||||
},
|
||||
// v5: last_error on reminders — last delivery failure surfaced on the
|
||||
// dashboard so a stuck reminder is visible without digging in logs.
|
||||
"ALTER TABLE reminders ADD COLUMN IF NOT EXISTS last_error TEXT",
|
||||
Migration {
|
||||
sql: "ALTER TABLE reminders ADD COLUMN last_error TEXT",
|
||||
adds_column: Some(("reminders", "last_error")),
|
||||
},
|
||||
];
|
||||
|
||||
impl Broker {
|
||||
pub fn open(path: &Path) -> Result<Self> {
|
||||
let conn = crate::db::open(path, "broker")?;
|
||||
conn.execute_batch(SCHEMA).context("apply broker schema")?;
|
||||
crate::db::apply_versioned_migrations(
|
||||
&conn,
|
||||
"broker",
|
||||
("messages", "priority"),
|
||||
BROKER_MIGRATIONS,
|
||||
)
|
||||
.context("broker migrations")?;
|
||||
crate::db::apply_versioned_migrations(&conn, "broker", BROKER_MIGRATIONS)
|
||||
.context("broker migrations")?;
|
||||
let (events, _) = broadcast::channel(EVENT_CHANNEL);
|
||||
Ok(Self {
|
||||
conn: Mutex::new(conn),
|
||||
|
|
|
|||
Loading…
Reference in a new issue