Self-hosted knowledge management for SMEs: a split-screen Markdown editor whose sections an LLM refines while you write, and RAG question answering over the documents that result. FastAPI + Postgres/pgvector on the back, SvelteKit on the front, everything OpenAI-compatible and self-hostable. Squashed into a single commit; the development history stays local. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
406 lines
13 KiB
Python
406 lines
13 KiB
Python
"""initial schema
|
|
|
|
Revision ID: 563ae5ac1d0d
|
|
Revises:
|
|
Create Date: 2026-07-18 14:41:47.620604
|
|
|
|
"""
|
|
|
|
from typing import Sequence, Union
|
|
|
|
import pgvector.sqlalchemy
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
from alembic import op
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = "563ae5ac1d0d"
|
|
down_revision: Union[str, Sequence[str], None] = None
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
"""Upgrade schema."""
|
|
op.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.create_table(
|
|
"departments",
|
|
sa.Column("name", sa.String(length=200), nullable=False),
|
|
sa.Column("id", sa.Uuid(), nullable=False),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
sa.UniqueConstraint("name"),
|
|
)
|
|
op.create_table(
|
|
"jobs",
|
|
sa.Column("type", sa.String(length=100), nullable=False),
|
|
sa.Column("payload", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
|
sa.Column(
|
|
"status",
|
|
sa.Enum(
|
|
"pending",
|
|
"running",
|
|
"done",
|
|
"failed",
|
|
name="jobstatus",
|
|
native_enum=False,
|
|
length=32,
|
|
),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"run_after",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column("attempts", sa.Integer(), nullable=False),
|
|
sa.Column("last_error", sa.Text(), nullable=True),
|
|
sa.Column("id", sa.Uuid(), nullable=False),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
)
|
|
op.create_index(
|
|
"ix_jobs_status_run_after", "jobs", ["status", "run_after"], unique=False
|
|
)
|
|
op.create_table(
|
|
"templates",
|
|
sa.Column("name", sa.String(length=200), nullable=False),
|
|
sa.Column("version", sa.String(length=20), nullable=False),
|
|
sa.Column("config", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
|
sa.Column("is_builtin", sa.Boolean(), nullable=False),
|
|
sa.Column("id", sa.Uuid(), nullable=False),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
)
|
|
op.create_table(
|
|
"users",
|
|
sa.Column("email", sa.String(length=320), nullable=False),
|
|
sa.Column("name", sa.String(length=200), nullable=False),
|
|
sa.Column(
|
|
"role",
|
|
sa.Enum("member", "admin", name="userrole", native_enum=False, length=32),
|
|
nullable=False,
|
|
),
|
|
sa.Column("password_hash", sa.String(length=255), nullable=False),
|
|
sa.Column("department_id", sa.Uuid(), nullable=True),
|
|
sa.Column("id", sa.Uuid(), nullable=False),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
["department_id"], ["departments.id"], ondelete="SET NULL"
|
|
),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
)
|
|
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
|
|
op.create_table(
|
|
"auth_sessions",
|
|
sa.Column("user_id", sa.Uuid(), nullable=False),
|
|
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("id", sa.Uuid(), nullable=False),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
)
|
|
op.create_index(
|
|
op.f("ix_auth_sessions_user_id"), "auth_sessions", ["user_id"], unique=False
|
|
)
|
|
op.create_table(
|
|
"documents",
|
|
sa.Column("title", sa.String(length=500), nullable=False),
|
|
sa.Column(
|
|
"status",
|
|
sa.Enum(
|
|
"draft",
|
|
"published",
|
|
"archived",
|
|
name="documentstatus",
|
|
native_enum=False,
|
|
length=32,
|
|
),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"visibility",
|
|
sa.Enum(
|
|
"public",
|
|
"department",
|
|
"restricted",
|
|
name="documentvisibility",
|
|
native_enum=False,
|
|
length=32,
|
|
),
|
|
nullable=False,
|
|
),
|
|
sa.Column("content_md", sa.Text(), nullable=False),
|
|
sa.Column("meta", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
|
sa.Column("author_id", sa.Uuid(), nullable=True),
|
|
sa.Column("department_id", sa.Uuid(), nullable=True),
|
|
sa.Column("id", sa.Uuid(), nullable=False),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.ForeignKeyConstraint(["author_id"], ["users.id"], ondelete="SET NULL"),
|
|
sa.ForeignKeyConstraint(
|
|
["department_id"], ["departments.id"], ondelete="SET NULL"
|
|
),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
)
|
|
op.create_table(
|
|
"chunks",
|
|
sa.Column("document_id", sa.Uuid(), nullable=False),
|
|
sa.Column("chunk_index", sa.Integer(), nullable=False),
|
|
sa.Column("content", sa.Text(), nullable=False),
|
|
sa.Column(
|
|
"embedding", pgvector.sqlalchemy.vector.VECTOR(dim=1024), nullable=False
|
|
),
|
|
sa.Column(
|
|
"tsv",
|
|
postgresql.TSVECTOR(),
|
|
sa.Computed(
|
|
"to_tsvector('german'::regconfig, "
|
|
"content || ' ' || coalesce(meta->>'heading_path', ''))",
|
|
persisted=True,
|
|
),
|
|
nullable=True,
|
|
),
|
|
sa.Column("meta", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
|
sa.Column("id", sa.Uuid(), nullable=False),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.ForeignKeyConstraint(["document_id"], ["documents.id"], ondelete="CASCADE"),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
sa.UniqueConstraint("document_id", "chunk_index"),
|
|
)
|
|
op.create_index(
|
|
op.f("ix_chunks_document_id"), "chunks", ["document_id"], unique=False
|
|
)
|
|
op.create_index(
|
|
"ix_chunks_embedding_hnsw",
|
|
"chunks",
|
|
["embedding"],
|
|
unique=False,
|
|
postgresql_using="hnsw",
|
|
postgresql_ops={"embedding": "vector_cosine_ops"},
|
|
)
|
|
op.create_index(
|
|
"ix_chunks_tsv", "chunks", ["tsv"], unique=False, postgresql_using="gin"
|
|
)
|
|
op.create_table(
|
|
"conversations",
|
|
sa.Column(
|
|
"mode",
|
|
sa.Enum(
|
|
"query",
|
|
"insight",
|
|
name="conversationmode",
|
|
native_enum=False,
|
|
length=32,
|
|
),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"status",
|
|
sa.Enum(
|
|
"active",
|
|
"completed",
|
|
"abandoned",
|
|
name="conversationstatus",
|
|
native_enum=False,
|
|
length=32,
|
|
),
|
|
nullable=False,
|
|
),
|
|
sa.Column("user_id", sa.Uuid(), nullable=False),
|
|
sa.Column("id", sa.Uuid(), nullable=False),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
)
|
|
op.create_index(
|
|
op.f("ix_conversations_user_id"), "conversations", ["user_id"], unique=False
|
|
)
|
|
op.create_table(
|
|
"doc_permissions",
|
|
sa.Column("document_id", sa.Uuid(), nullable=False),
|
|
sa.Column("department_id", sa.Uuid(), nullable=False),
|
|
sa.Column(
|
|
"level",
|
|
sa.Enum("read", name="permissionlevel", native_enum=False, length=32),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
["department_id"], ["departments.id"], ondelete="CASCADE"
|
|
),
|
|
sa.ForeignKeyConstraint(["document_id"], ["documents.id"], ondelete="CASCADE"),
|
|
sa.PrimaryKeyConstraint("document_id", "department_id"),
|
|
)
|
|
op.create_table(
|
|
"messages",
|
|
sa.Column("conversation_id", sa.Uuid(), nullable=False),
|
|
sa.Column(
|
|
"role",
|
|
sa.Enum(
|
|
"user",
|
|
"assistant",
|
|
"system",
|
|
name="messagerole",
|
|
native_enum=False,
|
|
length=32,
|
|
),
|
|
nullable=False,
|
|
),
|
|
sa.Column("content", sa.Text(), nullable=False),
|
|
sa.Column("id", sa.Uuid(), nullable=False),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
["conversation_id"], ["conversations.id"], ondelete="CASCADE"
|
|
),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
)
|
|
op.create_index(
|
|
op.f("ix_messages_conversation_id"),
|
|
"messages",
|
|
["conversation_id"],
|
|
unique=False,
|
|
)
|
|
# ### end Alembic commands ###
|
|
|
|
|
|
def downgrade() -> None:
|
|
"""Downgrade schema."""
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.drop_index(op.f("ix_messages_conversation_id"), table_name="messages")
|
|
op.drop_table("messages")
|
|
op.drop_table("doc_permissions")
|
|
op.drop_index(op.f("ix_conversations_user_id"), table_name="conversations")
|
|
op.drop_table("conversations")
|
|
op.drop_index("ix_chunks_tsv", table_name="chunks", postgresql_using="gin")
|
|
op.drop_index(
|
|
"ix_chunks_embedding_hnsw",
|
|
table_name="chunks",
|
|
postgresql_using="hnsw",
|
|
postgresql_ops={"embedding": "vector_cosine_ops"},
|
|
)
|
|
op.drop_index(op.f("ix_chunks_document_id"), table_name="chunks")
|
|
op.drop_table("chunks")
|
|
op.drop_table("documents")
|
|
op.drop_index(op.f("ix_auth_sessions_user_id"), table_name="auth_sessions")
|
|
op.drop_table("auth_sessions")
|
|
op.drop_index(op.f("ix_users_email"), table_name="users")
|
|
op.drop_table("users")
|
|
op.drop_table("templates")
|
|
op.drop_index("ix_jobs_status_run_after", table_name="jobs")
|
|
op.drop_table("jobs")
|
|
op.drop_table("departments")
|
|
# ### end Alembic commands ###
|