Files
inkwell/alembic/versions/0015_sync_revision.py
T
bvandeusenandClaude Opus 5.5 a51a563fe7
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 13s
Android / Build the server image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / integration (push) Successful in 1m30s
CI & Build / Build & push image (push) Successful in 32s
Server comments describe the code to a stranger (#6221)
Rule 32 "Comments describe the product to a stranger" sweep over
src/, alembic/ and tests/. Comments and docstrings now say what the code
does and the reason as a present-tense constraint, briefly.

Removed:
- issue, task, note, rule, milestone and family-idea references
- "used to" / before-and-after narration
- quotes of decisions and incident stories from the deploying instance
- paragraph-length arguments, where a line or two carries the fact

Kept every hazard as a present-tense fact, e.g. timing oracles, the
proxy hop direction, and migrations that must stay frozen.

Also:
- Three test names lose "still"/"used to"/"pre_m304" phrasing.
- pyproject's description drops "FabledSword family".

The history these comments carried stays in Scribe and in git log.
A comment-stripping diff (Scribe snippet #6226 "codeonly.py — prove a
diff changed only comments") shows no code changed apart from the
renamed tests and the description string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:07:47 -04:00

90 lines
3.5 KiB
Python

"""sync revision + tombstone infrastructure
Revision ID: 0015
Revises: 0014
Create Date: 2026-07-23
The delta-sync substrate for the local-first native clients. A shared sequence
(sync_revision_seq) feeds a per-row monotonic `sync_revision` on notes + labels,
assigned by a DB TRIGGER on every insert/update so no mutation site can ever
forget to bump it. Child-row changes (items / attachments / label membership) bump
their parent note's revision, since a note syncs as a whole. `purged_at` is the
hard-delete tombstone (distinct from `deleted_at` = recoverable trash) so an
offline client learns a row is gone instead of resurrecting it.
"""
from alembic import op
import sqlalchemy as sa
revision = "0015"
down_revision = "0014"
branch_labels = None
depends_on = None
_REVISIONED = ("notes", "labels")
_CHILD_TABLES = ("note_items", "note_attachments", "note_labels")
def upgrade() -> None:
op.execute("CREATE SEQUENCE IF NOT EXISTS sync_revision_seq")
for table in _REVISIONED:
op.add_column(table, sa.Column("sync_revision", sa.BigInteger(), nullable=True))
op.add_column(table, sa.Column("purged_at", sa.DateTime(timezone=True), nullable=True))
# Backfill: give every existing row a distinct, increasing revision so it's
# pullable from a since=0 initial sync.
op.execute(f"UPDATE {table} SET sync_revision = nextval('sync_revision_seq')")
op.create_index("ix_notes_owner_sync_revision", "notes", ["owner_id", "sync_revision"])
op.create_index("ix_labels_owner_sync_revision", "labels", ["owner_id", "sync_revision"])
# A row stamps itself with a fresh revision on every insert/update.
op.execute(
"""
CREATE OR REPLACE FUNCTION ts_set_sync_revision() RETURNS trigger AS $$
BEGIN
NEW.sync_revision := nextval('sync_revision_seq');
RETURN NEW;
END;
$$ LANGUAGE plpgsql
"""
)
# A child-row change re-bumps its parent note (the parent's own BEFORE trigger then
# assigns the final, higher revision — a harmless double-bump; gaps are fine).
op.execute(
"""
CREATE OR REPLACE FUNCTION ts_bump_parent_note_revision() RETURNS trigger AS $$
BEGIN
UPDATE notes SET sync_revision = nextval('sync_revision_seq')
WHERE id = COALESCE(NEW.note_id, OLD.note_id);
RETURN NULL;
END;
$$ LANGUAGE plpgsql
"""
)
for table in _REVISIONED:
op.execute(
f"CREATE TRIGGER trg_{table}_sync_revision BEFORE INSERT OR UPDATE ON {table} "
"FOR EACH ROW EXECUTE PROCEDURE ts_set_sync_revision()"
)
for child in _CHILD_TABLES:
op.execute(
f"CREATE TRIGGER trg_{child}_bump_note AFTER INSERT OR UPDATE OR DELETE ON {child} "
"FOR EACH ROW EXECUTE PROCEDURE ts_bump_parent_note_revision()"
)
def downgrade() -> None:
for child in _CHILD_TABLES:
op.execute(f"DROP TRIGGER IF EXISTS trg_{child}_bump_note ON {child}")
for table in _REVISIONED:
op.execute(f"DROP TRIGGER IF EXISTS trg_{table}_sync_revision ON {table}")
op.execute("DROP FUNCTION IF EXISTS ts_bump_parent_note_revision()")
op.execute("DROP FUNCTION IF EXISTS ts_set_sync_revision()")
op.drop_index("ix_labels_owner_sync_revision", table_name="labels")
op.drop_index("ix_notes_owner_sync_revision", table_name="notes")
for table in _REVISIONED:
op.drop_column(table, "purged_at")
op.drop_column(table, "sync_revision")
op.execute("DROP SEQUENCE IF EXISTS sync_revision_seq")