diff --git a/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.ts b/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.ts index bbee57c84f..341420655d 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch.ts @@ -17,6 +17,7 @@ import { DEFAULT_NAMESPACE, Entity } from '@backstage/catalog-model'; import { InputError } from '@backstage/errors'; import { DbSearchRow } from '../../tables'; +import { NULL_SENTINEL } from './util'; // These are excluded in the generic loop, either because they do not make sense // to index, or because they are special-case always inserted whether they are @@ -235,7 +236,7 @@ export function buildEntitySearch( // violate the unique constraint on (entity_id, key, value). const seen = new Set(); return rows.filter(row => { - const k = `${row.key}\0${row.value === null ? '\x01' : row.value}`; + const k = `${row.key}\0${row.value === null ? NULL_SENTINEL : row.value}`; if (seen.has(k)) return false; seen.add(k); return true; diff --git a/plugins/catalog-backend/src/database/operations/stitcher/syncSearchRows.ts b/plugins/catalog-backend/src/database/operations/stitcher/syncSearchRows.ts index dce65b4109..1dde84c017 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/syncSearchRows.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/syncSearchRows.ts @@ -16,15 +16,7 @@ import { Knex } from 'knex'; import { DbSearchRow } from '../../tables'; -import { BATCH_SIZE } from './util'; - -// The Postgres sync uses COALESCE(x, NULL_SENTINEL) to allow Postgres to -// include nullable columns in the Hash Cond of anti-joins (IS NOT DISTINCT -// FROM prevents this). As a consequence, values that are exactly this -// sentinel character are not searchable — they would be treated as NULL. -// This is the SOH (Start of Heading) control character which does not -// appear in real entity metadata. -const NULL_SENTINEL = '\x01'; +import { BATCH_SIZE, NULL_SENTINEL } from './util'; function filterSentinelValues(entries: DbSearchRow[]): DbSearchRow[] { return entries.filter( @@ -55,7 +47,9 @@ export async function syncSearchRows( // consistently pick the same original_value for a given input order. const dedupMap = new Map(); for (const entry of searchEntries) { - const k = `${entry.key}\0${entry.value === null ? '\x01' : entry.value}`; + const k = `${entry.key}\0${ + entry.value === null ? NULL_SENTINEL : entry.value + }`; if (!dedupMap.has(k)) { dedupMap.set(k, entry); } diff --git a/plugins/catalog-backend/src/database/operations/stitcher/util.ts b/plugins/catalog-backend/src/database/operations/stitcher/util.ts index 4a2dabcfa6..bac099514a 100644 --- a/plugins/catalog-backend/src/database/operations/stitcher/util.ts +++ b/plugins/catalog-backend/src/database/operations/stitcher/util.ts @@ -24,6 +24,12 @@ import stableStringify from 'fast-json-stable-stringify'; // enough to get the speed benefits. export const BATCH_SIZE = 50; +// The SOH (Start of Heading) control character, used as a stand-in for NULL +// in contexts where NULL cannot participate in equality comparisons (SQL +// COALESCE, JS dedup keys). It cannot appear in real entity metadata values +// since they are human-readable strings. +export const NULL_SENTINEL = '\x01'; + export function generateStableHash(entity: Entity) { return createHash('sha1') .update(stableStringify({ ...entity }))