A note_user_state row per (note, recipient) holds what used to be the owner's columns as far as anyone else could tell. The board filters and orders through the viewer's own state; PATCH and reorder write it for a note shared at any level; the feed's revision for a shared note is the later of the note's and the caller's row, so a recipient's pin reaches their devices and no one else's. Push takes the three with their own `state_at` stamp (protocol 7, `shared_state`), so pinning a copy whose text is behind never makes that text win over the owner's edit. A body is only stamped as an edit when it changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
56 lines
2.3 KiB
Python
56 lines
2.3 KiB
Python
"""note_user_state: a recipient's own pin, archive and place for a shared note
|
|
|
|
Revision ID: 0036
|
|
Revises: 0035
|
|
Create Date: 2026-10-07
|
|
|
|
Pin, archive and board order are personal organization, and until now they were
|
|
columns on `notes`, so on a shared note they could only ever mean the owner's
|
|
(#5176). This table holds them for everyone else: one row per (note, person it is
|
|
shared with) who has pinned, archived or moved it. The owner keeps the note's own
|
|
columns; a recipient with no row sees the note unpinned, unarchived, in the owner's
|
|
order.
|
|
|
|
`sync_revision` is stamped by the same `ts_set_sync_revision()` trigger the notes
|
|
and labels use (0015), so a recipient's change reaches their own devices on the one
|
|
cursor they already page by, and never moves the note on anyone else's.
|
|
|
|
## Downgrade
|
|
|
|
Drops the table. Recipients lose their own pins and archives; the notes are
|
|
untouched.
|
|
"""
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
|
|
revision = "0036"
|
|
down_revision = "0035"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"note_user_state",
|
|
sa.Column("note_id", UUID(as_uuid=True), sa.ForeignKey("notes.id", ondelete="CASCADE"), nullable=False),
|
|
sa.Column("user_id", UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
|
|
sa.Column("pinned", sa.Boolean(), nullable=False, server_default=sa.false()),
|
|
sa.Column("archived", sa.Boolean(), nullable=False, server_default=sa.false()),
|
|
sa.Column("position", sa.Integer(), nullable=True),
|
|
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
|
|
sa.Column("sync_revision", sa.BigInteger(), nullable=True),
|
|
sa.PrimaryKeyConstraint("note_id", "user_id"),
|
|
)
|
|
op.create_index("ix_note_user_state_user_revision", "note_user_state", ["user_id", "sync_revision"])
|
|
op.execute(
|
|
"CREATE TRIGGER trg_note_user_state_sync_revision BEFORE INSERT OR UPDATE ON note_user_state "
|
|
"FOR EACH ROW EXECUTE PROCEDURE ts_set_sync_revision()"
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.execute("DROP TRIGGER IF EXISTS trg_note_user_state_sync_revision ON note_user_state")
|
|
op.drop_index("ix_note_user_state_user_revision", table_name="note_user_state")
|
|
op.drop_table("note_user_state")
|