Fix migrations 0004 and 0005 to be fully idempotent

The Dockerfile runs `alembic stamp --purge base && alembic upgrade head`
on every restart, so all migrations re-run against an existing database.
Migration 0004 used `op.add_column()` which fails on re-run with
"column already exists". Migration 0005 used `op.create_table()` which
would fail similarly.

Both now use raw SQL with idempotency guards:
- 0004: `DO $$ BEGIN ALTER TABLE ADD COLUMN ... EXCEPTION WHEN
  duplicate_column` + `IF EXISTS` check before touching the tasks table
- 0005: `CREATE TABLE IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS`

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 18:57:12 -05:00
parent d2b8ab8fe8
commit b761db4c44
2 changed files with 72 additions and 76 deletions
+27 -46
View File
@@ -15,54 +15,35 @@ depends_on = None
def upgrade() -> None:
op.create_table(
"conversations",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("title", sa.Text(), nullable=False, server_default=""),
sa.Column("model", sa.Text(), nullable=False, server_default=""),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
)
conn = op.get_bind()
op.create_table(
"messages",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column(
"conversation_id",
sa.Integer(),
sa.ForeignKey("conversations.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("role", sa.Text(), nullable=False),
sa.Column("content", sa.Text(), nullable=False, server_default=""),
sa.Column(
"context_note_id",
sa.Integer(),
sa.ForeignKey("notes.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
)
conn.execute(sa.text("""
CREATE TABLE IF NOT EXISTS conversations (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL DEFAULT '',
model TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
"""))
op.create_index("ix_messages_conversation_id", "messages", ["conversation_id"])
conn.execute(sa.text("""
CREATE TABLE IF NOT EXISTS messages (
id SERIAL PRIMARY KEY,
conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
role TEXT NOT NULL,
content TEXT NOT NULL DEFAULT '',
context_note_id INTEGER REFERENCES notes(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
"""))
conn.execute(sa.text(
"CREATE INDEX IF NOT EXISTS ix_messages_conversation_id ON messages (conversation_id)"
))
def downgrade() -> None:
op.drop_index("ix_messages_conversation_id", table_name="messages")
op.drop_table("messages")
op.drop_table("conversations")
op.execute("DROP INDEX IF EXISTS ix_messages_conversation_id")
op.execute("DROP TABLE IF EXISTS messages")
op.execute("DROP TABLE IF EXISTS conversations")