from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import BigInteger, Boolean, DateTime, ForeignKey, Text, UniqueConstraint, func from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column from . import Base class Label(Base): __tablename__ = "labels" __table_args__ = (UniqueConstraint("owner_id", "name", name="uq_labels_owner_name"),) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) owner_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) name: Mapped[str] = mapped_column(Text(), nullable=False) color: Mapped[str] = mapped_column(Text(), nullable=False, default="default", server_default="default") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) # Last-edit time — the LWW comparison field for sync (a rename/recolor bumps it). # Sync-push overrides it with the winning client edit-time; web edits set server-now. updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) # Sync (M8): monotonic per-row revision (from sync_revision_seq via DB trigger) so a # label rename/recolor/merge/delete propagates to native clients independently of notes. sync_revision: Mapped[int | None] = mapped_column(BigInteger(), nullable=True) # Hard-delete tombstone (content-less) so a deleted label is removed on clients. purged_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) class NoteLabel(Base): """Join between a note and a label (both owned by the same user).""" __tablename__ = "note_labels" note_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("notes.id", ondelete="CASCADE"), primary_key=True ) label_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("labels.id", ondelete="CASCADE"), primary_key=True ) # True when this attachment came from a #tag in the note body (kept in sync with # the body); False = added manually via the label picker (independent of #tags). via_tag: Mapped[bool] = mapped_column(Boolean(), nullable=False, server_default=func.false())