Skip to content

ENG-2175 Backfill format on existing node type schema rows - #1346

Open
sid597 wants to merge 2 commits into
eng-2158-publish-node-type-format-with-schemasfrom
eng-2175-backfill-format-on-existing-node-type-schema-rows
Open

ENG-2175 Backfill format on existing node type schema rows#1346
sid597 wants to merge 2 commits into
eng-2158-publish-node-type-format-with-schemasfrom
eng-2175-backfill-format-on-existing-node-type-schema-rows

Conversation

@sid597

@sid597 sid597 commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Schema rows synced before #1316 never get literal_content.format: publish skips schemas that already exist, and the periodic sync only re-selects types edited since the last watermark. This backfills them through the sync producer. Stacked on #1316; only the last commit is this PR.

How it works:

  • On the first sync cycle per session, probe own-space schema rows (is_schema = true, is_relation = false) for a missing format.
  • Force the matching node types into the concept batch, so discourseNodeSchemaToLocalConcept recomputes the complete row.
  • Report backfilled / already set / orphaned counts. Silent when there is nothing to do. Re-runs are no-ops.

Decisions, annotated inline on the diff:

  • Sync producer, not publish path: upsert_concepts replaces literal_content wholesale, and the publish converter writes a different shape.
  • No "waiting for sync" bucket, unlike the core_title backfill: the schema batch is not shared-gated today, and the probe result is intersected with rows that already exist, so nothing new can be published.
  • Rider: getNodeExtraData now returns author_local_id instead of author_uid. Without it this backfill would blank author_id on publish-origin rows. Same change as a493612 on eng-2155.

Known gaps, shared with the core_title backfill on #1332: the orphan toast repeats on each load, and the posthog event has no space attribution. Worth one joint fix later rather than forking the two reports now.

@linear-code

linear-code Bot commented Aug 24, 2026

Copy link
Copy Markdown

ENG-2175

@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
discourse-graph Skipped Skipped Aug 24, 2026 10:34am

Request Review

@sid597 sid597 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notes on the non-obvious decisions, one per thread.

import { type DiscourseNode } from "./getDiscourseNodes";

export const SCHEMA_FORMAT_PROBE_SELECT =
"source_local_id, format:literal_content->>format";

@sid597 sid597 Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostgREST reads an absent jsonb key and a JSON null the same way: both come back as null through ->>. The Roam producers never write a null format, so both mean "backfill", and we partition client-side instead of filtering with .is(...). supabase-js types the projection string; the declared row type widens it to string | null, which is what actually arrives.

This mirrors eng-2155's core_title helper but stays in apps/roam: Obsidian schema rows carry source_data.format from the start, so only Roam needs it. Obsidian's template_content probe pushes the null test into the query instead; here we need the "already set" count, so we fetch all own-space schema rows (tens) and split.

return {
nodeTypeIdsToBackfill: intersection(missingFormatIds, localTypeIds),
withFormatCount,
orphanedCount: difference(missingFormatIds, localTypeIds).size,

@sid597 sid597 Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Orphans are probed rows that no longer match a configured node type. We report the count and never guess a format. They can persist: cleanupOrphanedNodes deletes rows whose Roam block is gone, but a type removed from the DG config keeps its page and its row. The core_title backfill accepted the same permanence for renamed pages.

An empty format counts as already set. The producer wrote the key (format defaults to ""), so there is nothing to recompute and the backfill converges.

if (sharedNodeTypeIds.has(nodeType.type)) {
if (
sharedNodeTypeIds.has(nodeType.type) ||
backfillNodeTypeIds.has(nodeType.type)

@sid597 sid597 Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A second force-include beside sharedNodeTypeIds instead of smuggling the backfill through it; they mean different things. The set is an intersection with ids already in my_concepts, so the backfill can only rewrite existing rows. It cannot publish a schema the user never synced, including in shared-content-only mode.

});
};

const probeSchemaFormatBackfill = async ({

@sid597 sid597 Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Runs only on isInitialSync, so the second cycle probes nothing and the report goes silent. Paged through getAllPages like the other my_concepts reads in this file; the expected scale is node types per space, so one page in practice. A probe failure fails the cycle like any other phase; initialSync stays true, so the next attempt probes again.

});
};

const reportSchemaFormatBackfill = ({

@sid597 sid597 Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberately independent of showToast, same as the core_title report on #1332: it announces a one-time data migration, not cycle status. skipped keeps the property name from the core_title posthog event; the toast calls the same number "already had one".

node_uid: string,
): {
author_uid: string;
author_local_id: string;

@sid597 sid597 Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required by the backfill, not opportunistic. concept_local_input has no author_uid field, so jsonb_populate_record dropped it and every sync-produced concept carried a NULL author_id. upsert_concepts sets author_id unconditionally on conflict, so re-upserting publish-origin schema rows (which do have an author) would blank them, and dbNodeSchemaToCrossApp then throws "Missing author". Identical to a493612 on eng-2155; whichever merges second sees a no-op conflict.

page_uid below is the same dead-key class but inert: nothing maps to it and nothing breaks because of it. Removing it belongs with the literal_content shape unification.

@supabase

supabase Bot commented Aug 24, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0e90eb4607

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1092 to +1093
.from("my_concepts")
.select(SCHEMA_FORMAT_PROBE_SELECT)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Paginate the schema-format probe

When a space has more than 1,000 non-relation schema rows, this unpaginated select is truncated by the repository's Supabase max_rows = 1000 setting (packages/database/supabase/config.toml:18). Because the query includes schemas that already have a format and initialSync is then cleared, rows beyond that first page are never added to nodeTypeIdsToBackfill; subsequent sessions will generally fetch the same first page and leave the remaining legacy rows unchanged. Use the existing getAllPages helper with a stable ordering, as the other potentially large my_concepts probes in this file do.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to getAllPages with .order("id") in 29d605a, matching the other my_concepts reads in this file. Expected scale is node types per space, so one page in practice, but you're right that a truncation at max_rows = 1000 would have been silent and would never converge.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +1122 to +1129
if (backfilled === 0 && orphaned === 0) return;
const messages = [
`Backfilled format for ${backfilled} node type${backfilled === 1 ? "" : "s"}.`,
`${skipped} already had one.`,
];
if (orphaned > 0) {
messages.push(`${orphaned} no longer match a node type in this graph.`);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Toast reports "0 node types" backfilled

When no formatless rows match a local node type but some are orphaned, the guard still shows the toast, and its message is hardcoded to lead with Backfilled format for 0 node type(s). before the orphan warning. Users see a confusing zero-count line.

Prompt for agents
In reportSchemaFormatBackfill in apps/roam/src/utils/syncDgNodesToSupabase.ts, the early-return guard only suppresses the toast when both backfilled and orphaned are 0. When backfilled is 0 but orphaned > 0, the message array still unconditionally includes the line 'Backfilled format for 0 node types.' which is misleading. Consider only including the 'Backfilled format for N node types.' line when backfilled > 0, and only including the 'N already had one.' line when relevant, so the toast reads sensibly in the orphaned-only case.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Known shape, kept deliberately: it mirrors reportCoreTitleBackfill on #1332, which reads the same way in the orphaned-only case. The PR body tracks a joint fix for the two reports (recurring orphan toast, message composition) so they don't fork.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant