Files
upstream-tldraw/apps/dotcom/zero-cache/migrations/040_add_comments.sql
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

116 lines
5.3 KiB
PL/PgSQL

-- Comments for app files, written by the file's Durable Object from the room's comment records
-- (see TLComment / TLCommentThread in tlschema). Postgres is the sole durable store for comment
-- records: the columns collectively carry every record field, so the Durable Object can rebuild
-- its room's comment records from rows alone on cold start. `lastChangedClock` preserves each
-- record's sync clock across reloads and guards upserts against no-op replays (an update that
-- doesn't advance the clock is skipped, producing no WAL entry for Zero). `anchor`, `meta`,
-- `resolvedBy`, `editedAt`, and `lastChangedClock` are persistence/rehydration columns not
-- declared in the Zero schema, so they never reach clients.
CREATE TABLE comment_thread (
"id" VARCHAR PRIMARY KEY,
"fileId" VARCHAR NOT NULL,
"pageId" VARCHAR NOT NULL,
-- the thread's TLCommentAnchor (discriminated union) as JSON — canonical, used to rebuild the record
"anchor" JSONB NOT NULL,
-- denormalized from anchor for queries; only set for shape-anchored threads
"shapeId" VARCHAR,
"resolvedAt" BIGINT,
"resolvedBy" VARCHAR,
-- soft-deletion flag (see TLCommentThread.isDeleted): threads are never hard-deleted by
-- clients, so the row (and its comments) stays for recovery while room loads and Zero
-- queries filter it out
"isDeleted" BOOLEAN DEFAULT FALSE NOT NULL,
-- no FK to "user": deleting a user must not cascade-delete threads (and thereby other
-- authors' comments); the comment table's authorId FK handles per-author cleanup
"createdBy" VARCHAR NOT NULL,
"createdAt" BIGINT NOT NULL,
"meta" JSONB NOT NULL,
"lastChangedClock" BIGINT NOT NULL,
CONSTRAINT comment_thread_file_id_fkey FOREIGN KEY ("fileId") REFERENCES public."file"("id") ON DELETE CASCADE,
-- resolvedAt/resolvedBy encode one nullable `resolved` record field, so they must be set or
-- null together; rehydration (rowToThreadRecord) relies on resolvedBy being non-null whenever
-- resolvedAt is
CONSTRAINT comment_thread_resolved_check CHECK (("resolvedAt" IS NULL) = ("resolvedBy" IS NULL))
);
CREATE INDEX comment_thread_file_id_idx ON comment_thread("fileId");
CREATE TABLE comment (
"id" VARCHAR PRIMARY KEY,
"fileId" VARCHAR NOT NULL,
"threadId" VARCHAR NOT NULL,
"pageId" VARCHAR NOT NULL,
"authorId" VARCHAR NOT NULL,
-- author display fields, denormalized from the user table by the triggers below (same
-- pattern as file."ownerName") — joining the user row would sync private fields to readers
"authorName" VARCHAR DEFAULT '' NOT NULL,
"authorColor" VARCHAR DEFAULT '' NOT NULL,
"authorAvatar" VARCHAR DEFAULT '' NOT NULL,
"body" JSONB NOT NULL,
"createdAt" BIGINT NOT NULL,
-- null until first edited (exact record field; updatedAt below is the derived sort key)
"editedAt" BIGINT,
-- soft-deletion flag (see TLComment.isDeleted): same model as the thread's — the row stays
-- for recovery while room loads and Zero queries filter it out
"isDeleted" BOOLEAN DEFAULT FALSE NOT NULL,
"updatedAt" BIGINT NOT NULL,
"meta" JSONB NOT NULL,
"lastChangedClock" BIGINT NOT NULL,
CONSTRAINT comment_file_id_fkey FOREIGN KEY ("fileId") REFERENCES public."file"("id") ON DELETE CASCADE,
CONSTRAINT comment_author_id_fkey FOREIGN KEY ("authorId") REFERENCES public."user"("id") ON DELETE CASCADE,
CONSTRAINT comment_thread_id_fkey FOREIGN KEY ("threadId") REFERENCES public."comment_thread"("id") ON DELETE CASCADE
);
CREATE INDEX comment_file_id_idx ON comment("fileId");
CREATE INDEX comment_thread_id_idx ON comment("threadId");
CREATE INDEX comment_author_id_idx ON comment("authorId");
-- Stamp author details on insert — the Durable Object only knows authorId. BEFORE INSERT
-- keeps it a single write with no extra WAL entry for Zero.
CREATE OR REPLACE FUNCTION set_comment_author_details() RETURNS TRIGGER AS $$
DECLARE
author_name VARCHAR;
author_color VARCHAR;
author_avatar VARCHAR;
BEGIN
SELECT u."name", u."color", u."avatar" INTO author_name, author_color, author_avatar
FROM public."user" u
WHERE u."id" = NEW."authorId";
-- a missing user row keeps the defaults, so the insert still fails its author FK check
-- (see isCommentAuthorFkViolation) rather than erroring here
IF FOUND THEN
NEW."authorName" := author_name;
NEW."authorColor" := author_color;
NEW."authorAvatar" := author_avatar;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_comment_author_details_trigger"
BEFORE INSERT OR UPDATE OF "authorId" ON comment
FOR EACH ROW
EXECUTE FUNCTION set_comment_author_details();
-- Propagate user renames, recolors, and avatar changes to their existing comments.
CREATE OR REPLACE FUNCTION update_comment_author_details() RETURNS TRIGGER AS $$
BEGIN
UPDATE comment
SET "authorName" = NEW."name",
"authorColor" = NEW."color",
"authorAvatar" = NEW."avatar"
WHERE "authorId" = NEW."id";
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "update_comment_author_details_trigger"
AFTER UPDATE OF "name", "color", "avatar" ON public."user"
FOR EACH ROW
WHEN (OLD."name" IS DISTINCT FROM NEW."name" OR OLD."color" IS DISTINCT FROM NEW."color" OR OLD."avatar" IS DISTINCT FROM NEW."avatar")
EXECUTE FUNCTION update_comment_author_details();
ALTER PUBLICATION zero_data ADD TABLE public."comment_thread";
ALTER PUBLICATION zero_data ADD TABLE public."comment";