🐸 Frogue(Fed)

~ololduck/Frogue

Raw

use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        // SQLite cannot drop NOT NULL constraints, so we rebuild the table.
        //
        // Key changes:
        // - author_id becomes nullable (for remote-authored comments)
        // - Add remote_actor_id (FK to ap_remote_actors, nullable)
        // - Add remote_object_id (unique, for idempotency on inbound Notes)
        // - Add content_format (default 'markdown', can be 'html' for remote
        //   content)
        // - CHECK constraint: exactly one of author_id or remote_actor_id
        manager
            .get_connection()
            .execute_unprepared(
                r#"
-- Create new table with federation columns
CREATE TABLE comments_new (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    uuid TEXT NOT NULL UNIQUE,
    author_id INTEGER,
    remote_actor_id INTEGER,
    remote_object_id TEXT UNIQUE,
    ticket_id INTEGER,
    patch_proposal_id INTEGER,
    content TEXT NOT NULL,
    content_format TEXT NOT NULL DEFAULT 'markdown',
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE,
    FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (remote_actor_id) REFERENCES ap_remote_actors(id) ON DELETE CASCADE,
    FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
    FOREIGN KEY (patch_proposal_id) REFERENCES patch_proposals(id) ON DELETE CASCADE,
    CHECK ((author_id IS NOT NULL) != (remote_actor_id IS NOT NULL)),
    CHECK ((ticket_id IS NOT NULL) != (patch_proposal_id IS NOT NULL))
);

-- Copy data from old table
INSERT INTO comments_new (
    id, uuid, author_id, ticket_id, patch_proposal_id, content,
    created_at, updated_at
)
SELECT id, uuid, author_id, ticket_id, patch_proposal_id, content,
       created_at, updated_at
FROM comments;

-- Drop old table
DROP TABLE comments;

-- Rename new table
ALTER TABLE comments_new RENAME TO comments;

-- Recreate indexes
CREATE INDEX index_comments_on_ticket_id_and_created_at
    ON comments (ticket_id, created_at);
CREATE INDEX index_comments_on_patch_proposal_id_and_created_at
    ON comments (patch_proposal_id, created_at);
"#,
            )
            .await?;

        Ok(())
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        // Revert to original schema
        manager
            .get_connection()
            .execute_unprepared(
                r#"
-- Recreate original table
CREATE TABLE comments_old (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    uuid TEXT NOT NULL UNIQUE,
    author_id INTEGER NOT NULL,
    ticket_id INTEGER,
    patch_proposal_id INTEGER,
    content TEXT NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE,
    FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
    FOREIGN KEY (patch_proposal_id) REFERENCES patch_proposals(id) ON DELETE CASCADE,
    CHECK ((ticket_id IS NOT NULL) != (patch_proposal_id IS NOT NULL))
);

-- Copy only locally-authored comments (author_id IS NOT NULL)
INSERT INTO comments_old (
    id, uuid, author_id, ticket_id, patch_proposal_id, content,
    created_at, updated_at
)
SELECT id, uuid, author_id, ticket_id, patch_proposal_id, content,
       created_at, updated_at
FROM comments
WHERE author_id IS NOT NULL;

-- Drop new table
DROP TABLE comments;

-- Rename back
ALTER TABLE comments_old RENAME TO comments;

-- Recreate indexes
CREATE INDEX index_comments_on_ticket_id_and_created_at
    ON comments (ticket_id, created_at);
CREATE INDEX index_comments_on_patch_proposal_id_and_created_at
    ON comments (patch_proposal_id, created_at);
"#,
            )
            .await?;

        Ok(())
    }
}