@backstage/plugin-catalog-backend 3.8.0-next.0 → 3.8.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @backstage/plugin-catalog-backend
2
2
 
3
+ ## 3.8.0-next.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 9698738: Dropped the legacy `search_entity_id_idx` index which is now redundant with the covering unique index on `(entity_id, key, value)`. The old index caused the query planner to choose an inefficient scan pattern for catalog list queries with multiple sort fields, leading to severely degraded performance on large catalogs.
8
+ - ccfa4f1: Optimized `entitiesBatch` on PostgreSQL to use `= ANY(array)` instead of `WHERE IN ($1, $2, ...)`. This produces a single stable query plan regardless of batch size, instead of up to 200 different plans that pollute the query plan cache. On PostgreSQL, batching is no longer needed so all entity refs are fetched in a single query.
9
+ - 24775dc: Added a migration that tunes PostgreSQL automatic vacuum thresholds on the `search`, `final_entities`, `relations`, and `refresh_state_references` tables, and fixes column statistics for `entity_id` in the `search` table. This prevents the query planner from falling back to sequential scans when table maintenance falls behind, keeping catalog queries fast on large installations.
10
+
3
11
  ## 3.8.0-next.0
4
12
 
5
13
  ### Minor Changes
@@ -211,12 +211,24 @@ class DefaultEntitiesCatalog {
211
211
  return combined.slice(skip, skip + limit + 1);
212
212
  }
213
213
  async entitiesBatch(request) {
214
+ if (request.entityRefs.length === 0) {
215
+ return { items: process.processRawEntitiesResult([], request.fields) };
216
+ }
214
217
  const lookup = /* @__PURE__ */ new Map();
215
- for (const chunk of lodash.chunk(request.entityRefs, 200)) {
218
+ const isPg = this.database.client.config.client === "pg";
219
+ const chunks = isPg ? [request.entityRefs] : lodash.chunk(request.entityRefs, 200);
220
+ for (const chunk of chunks) {
216
221
  let query = this.database("final_entities").select({
217
222
  entityRef: "final_entities.entity_ref",
218
223
  entity: "final_entities.final_entity"
219
- }).whereIn("final_entities.entity_ref", chunk);
224
+ });
225
+ if (isPg) {
226
+ query = query.whereRaw("final_entities.entity_ref = ANY(?::text[])", [
227
+ chunk
228
+ ]);
229
+ } else {
230
+ query = query.whereIn("final_entities.entity_ref", chunk);
231
+ }
220
232
  if (request?.filter || request?.query) {
221
233
  query = applyEntityFilterToQuery.applyEntityFilterToQuery({
222
234
  filter: request.filter,
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultEntitiesCatalog.cjs.js","sources":["../../src/service/DefaultEntitiesCatalog.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Entity, stringifyEntityRef } from '@backstage/catalog-model';\nimport { InputError, NotFoundError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport { chunk as lodashChunk, isEqual } from 'lodash';\nimport {\n Cursor,\n EntitiesBatchRequest,\n EntitiesBatchResponse,\n EntitiesCatalog,\n EntitiesRequest,\n EntitiesResponse,\n EntityAncestryResponse,\n EntityFacetsRequest,\n EntityFacetsResponse,\n EntityOrder,\n EntityPagination,\n QueryEntitiesRequest,\n QueryEntitiesResponse,\n TotalItemsMode,\n} from '../catalog/types';\nimport {\n DbFinalEntitiesRow,\n DbPageInfo,\n DbRefreshStateReferencesRow,\n DbRefreshStateRow,\n DbRelationsRow,\n DbSearchRow,\n} from '../database/tables';\nimport { markForStitching } from '../database/operations/stitcher/markForStitching';\n\nimport {\n expandLegacyCompoundRelationsInEntity,\n isQueryEntitiesCursorRequest,\n isQueryEntitiesInitialRequest,\n} from './util';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { applyEntityFilterToQuery } from './request/applyEntityFilterToQuery';\nimport { processRawEntitiesResult } from './response';\n\nconst DEFAULT_LIMIT = 200;\n\nfunction parsePagination(input?: EntityPagination): EntityPagination {\n if (!input) {\n return {};\n }\n\n let { limit, offset } = input;\n\n if (input.after === undefined) {\n return { limit, offset };\n }\n\n let cursor;\n try {\n const json = Buffer.from(input.after, 'base64').toString('utf8');\n cursor = JSON.parse(json);\n } catch {\n throw new InputError('Malformed after cursor, could not be parsed');\n }\n\n if (cursor.limit !== undefined) {\n if (!Number.isInteger(cursor.limit)) {\n throw new InputError('Malformed after cursor, limit was not an number');\n }\n limit = cursor.limit;\n }\n\n if (cursor.offset !== undefined) {\n if (!Number.isInteger(cursor.offset)) {\n throw new InputError('Malformed after cursor, offset was not a number');\n }\n offset = cursor.offset;\n }\n\n return { limit, offset };\n}\n\nfunction stringifyPagination(\n input: Required<Omit<EntityPagination, 'after'>>,\n): string {\n const { limit, offset } = input;\n const json = JSON.stringify({ limit, offset });\n const base64 = Buffer.from(json, 'utf8').toString('base64');\n return base64;\n}\n\nexport class DefaultEntitiesCatalog implements EntitiesCatalog {\n private readonly database: Knex;\n private readonly logger: LoggerService;\n private readonly enableRelationsCompatibility: boolean;\n\n constructor(options: {\n database: Knex;\n logger: LoggerService;\n enableRelationsCompatibility?: boolean;\n }) {\n this.database = options.database;\n this.logger = options.logger;\n this.enableRelationsCompatibility = Boolean(\n options.enableRelationsCompatibility,\n );\n }\n\n async entities(request?: EntitiesRequest): Promise<EntitiesResponse> {\n const db = this.database;\n const { limit, offset } = parsePagination(request?.pagination);\n const primaryOrder = request?.order?.[0];\n\n // When exactly one order field is specified we run a two-phase fetch\n // that drives from the search-by-key index for that field. The index\n // walks rows in already-sorted order, so the planner can short-circuit\n // on LIMIT instead of having to materialise and sort the full filtered\n // set. Phase 2 appends entities that lack the order field (NULLS LAST)\n // and is skipped when phase 1 already fills the request.\n //\n // Multi-field ordering falls back to the original LEFT JOIN shape\n // because tie-breaking on a second field requires materialisation of\n // the full set anyway.\n const useFastPath = primaryOrder && (request?.order?.length ?? 0) <= 1;\n let rows: DbFinalEntitiesRow[];\n if (useFastPath) {\n rows = await this.runOrderedEntitiesQuery(\n request!,\n primaryOrder,\n limit,\n offset,\n );\n } else {\n let entitiesQuery =\n db<DbFinalEntitiesRow>('final_entities').select('final_entities.*');\n\n request?.order?.forEach(({ field }, index) => {\n const alias = `order_${index}`;\n entitiesQuery = entitiesQuery.leftOuterJoin(\n { [alias]: 'search' },\n function search(inner) {\n inner\n .on(`${alias}.entity_id`, 'final_entities.entity_id')\n .andOn(`${alias}.key`, db.raw('?', [field]));\n },\n );\n });\n\n entitiesQuery = entitiesQuery.whereNotNull('final_entities.final_entity');\n\n if (request?.filter) {\n entitiesQuery = applyEntityFilterToQuery({\n filter: request.filter,\n targetQuery: entitiesQuery,\n onEntityIdField: 'final_entities.entity_id',\n knex: db,\n });\n }\n\n if (request?.order) {\n request.order.forEach(({ order }, index) => {\n if (db.client.config.client === 'pg') {\n entitiesQuery = entitiesQuery.orderBy([\n { column: `order_${index}.value`, order, nulls: 'last' },\n ]);\n } else {\n entitiesQuery = entitiesQuery.orderBy([\n {\n column: `order_${index}.value`,\n order: undefined,\n nulls: 'last',\n },\n { column: `order_${index}.value`, order },\n ]);\n }\n });\n entitiesQuery.orderBy('final_entities.entity_id', 'asc');\n } else {\n entitiesQuery = entitiesQuery.orderBy(\n 'final_entities.entity_ref',\n 'asc',\n );\n }\n\n if (limit !== undefined) {\n entitiesQuery = entitiesQuery.limit(limit + 1);\n }\n if (offset !== undefined) {\n entitiesQuery = entitiesQuery.offset(offset);\n }\n\n rows = await entitiesQuery;\n }\n\n let pageInfo: DbPageInfo;\n if (limit === undefined || rows.length <= limit) {\n pageInfo = { hasNextPage: false };\n } else {\n rows = rows.slice(0, -1);\n pageInfo = {\n hasNextPage: true,\n endCursor: stringifyPagination({\n limit,\n offset: (offset ?? 0) + limit,\n }),\n };\n }\n\n return {\n entities: processRawEntitiesResult(\n rows.map(r => r.final_entity!),\n this.enableRelationsCompatibility\n ? e => {\n expandLegacyCompoundRelationsInEntity(e);\n if (request?.fields) {\n return request.fields(e);\n }\n return e;\n }\n : request?.fields,\n ),\n pageInfo,\n };\n }\n\n /**\n * Two-phase fetch used when the caller has specified an order field.\n * See entities() for a longer description of the rationale.\n */\n private async runOrderedEntitiesQuery(\n request: EntitiesRequest,\n primaryOrder: EntityOrder,\n limit: number | undefined,\n offset: number | undefined,\n ): Promise<DbFinalEntitiesRow[]> {\n const db = this.database;\n const isPg = db.client.config.client === 'pg';\n const wantedRows =\n limit === undefined ? Number.MAX_SAFE_INTEGER : (offset ?? 0) + limit + 1;\n\n const applyFilter = <T extends object>(\n query: Knex.QueryBuilder<T>,\n ): Knex.QueryBuilder<T> => {\n if (!request.filter) {\n return query;\n }\n return applyEntityFilterToQuery({\n filter: request.filter,\n targetQuery: query,\n onEntityIdField: 'final_entities.entity_id',\n knex: db,\n });\n };\n\n // Phase 1 -- entities that have a non-NULL value for the order field.\n // Rows where the key exists but value IS NULL (e.g. the entity field is\n // explicitly null, or exceeded MAX_VALUE_LENGTH in buildEntitySearch) are\n // excluded here so they fall through to Phase 2 and sort in the same\n // NULLS-LAST bucket as entities that have no row for the key at all —\n // preserving the semantics of the previous LEFT JOIN approach.\n let withField = db('search as order_0')\n .innerJoin(\n 'final_entities',\n 'final_entities.entity_id',\n 'order_0.entity_id',\n )\n .where('order_0.key', primaryOrder.field)\n .whereNotNull('order_0.value')\n .whereNotNull('final_entities.final_entity')\n .select<DbFinalEntitiesRow[]>('final_entities.*');\n withField = applyFilter(withField);\n withField = isPg\n ? withField.orderBy([\n { column: 'order_0.value', order: primaryOrder.order, nulls: 'last' },\n { column: 'final_entities.entity_id', order: 'asc' },\n ])\n : withField.orderBy([\n { column: 'order_0.value', order: undefined, nulls: 'last' },\n { column: 'order_0.value', order: primaryOrder.order },\n { column: 'final_entities.entity_id', order: 'asc' },\n ]);\n if (wantedRows < Number.MAX_SAFE_INTEGER) {\n withField = withField.limit(wantedRows);\n }\n const withFieldRows = await withField;\n\n // If phase 1 already covered everything we asked for, skip the second\n // phase entirely. This is the common UI case where every entity in the\n // filtered set has the order field.\n if (withFieldRows.length >= wantedRows) {\n const skip = offset ?? 0;\n return withFieldRows.slice(skip, skip + (limit ?? wantedRows) + 1);\n }\n\n // Phase 2 -- entities that lack the order field, appended after.\n let withoutField = db<DbFinalEntitiesRow>('final_entities')\n .select<DbFinalEntitiesRow[]>('final_entities.*')\n .whereNotNull('final_entities.final_entity')\n .whereNotExists(qb =>\n qb\n .from('search')\n .where('search.entity_id', db.ref('final_entities.entity_id'))\n .andWhere('search.key', primaryOrder.field)\n .whereNotNull('search.value'),\n );\n withoutField = applyFilter(withoutField);\n withoutField = withoutField.orderBy(\n 'final_entities.entity_id',\n 'asc', // NULL group always stable-sorted ASC regardless of primary direction\n );\n if (limit !== undefined) {\n // Phase 2 only contributes the rows that phase 1 didn't cover.\n const remaining =\n wantedRows - Math.min(withFieldRows.length, (offset ?? 0) + limit + 1);\n withoutField = withoutField.limit(Math.max(0, remaining));\n }\n const withoutFieldRows = await withoutField;\n\n const combined = [...withFieldRows, ...withoutFieldRows];\n if (limit === undefined) {\n return combined.slice(offset ?? 0);\n }\n const skip = offset ?? 0;\n return combined.slice(skip, skip + limit + 1);\n }\n\n async entitiesBatch(\n request: EntitiesBatchRequest,\n ): Promise<EntitiesBatchResponse> {\n const lookup = new Map<string, string>();\n\n for (const chunk of lodashChunk(request.entityRefs, 200)) {\n let query = this.database<DbFinalEntitiesRow>('final_entities')\n .select({\n entityRef: 'final_entities.entity_ref',\n entity: 'final_entities.final_entity',\n })\n .whereIn('final_entities.entity_ref', chunk);\n\n if (request?.filter || request?.query) {\n query = applyEntityFilterToQuery({\n filter: request.filter,\n query: request.query,\n targetQuery: query,\n onEntityIdField: 'final_entities.entity_id',\n knex: this.database,\n });\n }\n\n for (const row of await query) {\n lookup.set(row.entityRef, row.entity ? row.entity : null);\n }\n }\n\n const items = request.entityRefs.map(ref => lookup.get(ref) ?? null);\n\n return { items: processRawEntitiesResult(items, request.fields) };\n }\n\n async queryEntities(\n request: QueryEntitiesRequest,\n ): Promise<QueryEntitiesResponse> {\n const limit = request.limit ?? DEFAULT_LIMIT;\n\n const { totalItemsMode, ...cursor } = {\n orderFields: [] as EntityOrder[],\n isPrevious: false,\n ...parseCursorFromRequest(request),\n } satisfies Omit<Cursor, 'orderFieldValues'> & {\n orderFieldValues?: (string | null)[];\n totalItemsMode: TotalItemsMode;\n };\n\n const shouldComputeTotalItems =\n cursor.totalItems === undefined && totalItemsMode !== 'exclude';\n const isFetchingBackwards = cursor.isPrevious;\n\n if (cursor.orderFields.length > 1) {\n this.logger.warn(`Only one sort field is supported, ignoring the rest`);\n }\n\n const sortField = cursor.orderFields.at(0);\n const sortKey = sortField?.field.toLocaleLowerCase('en-US');\n\n const normalizedFullTextFilterTerm = cursor.fullTextFilter?.term?.trim();\n const textFilterFields = cursor.fullTextFilter?.fields ?? [\n sortKey || 'metadata.uid',\n ];\n\n // Shared predicate logic applied to both the list CTE and the\n // standalone count query so they stay in sync. The `searchInScope`\n // flag indicates whether a `search` table is already joined in the\n // target query (true for the list CTE when a sort field is set),\n // enabling a fast-path LIKE on the already-joined row.\n const applyPredicates = (\n q: Knex.QueryBuilder,\n options?: { searchInScope?: boolean },\n ) => {\n if (cursor.filter || cursor.query) {\n applyEntityFilterToQuery({\n filter: cursor.filter,\n query: cursor.query,\n targetQuery: q,\n onEntityIdField: 'final_entities.entity_id',\n knex: this.database,\n });\n }\n\n if (normalizedFullTextFilterTerm) {\n if (\n options?.searchInScope &&\n sortField &&\n textFilterFields.length === 1 &&\n textFilterFields[0] === sortKey\n ) {\n q.andWhereRaw(\n 'search.value like ?',\n `%${normalizedFullTextFilterTerm.toLocaleLowerCase('en-US')}%`,\n );\n } else {\n const matchQuery = this.database<DbSearchRow>('search')\n .select('search.entity_id')\n .whereIn(\n 'search.key',\n textFilterFields.map(field => field.toLocaleLowerCase('en-US')),\n )\n .andWhere(function keyFilter() {\n this.andWhereRaw(\n 'search.value like ?',\n `%${normalizedFullTextFilterTerm.toLocaleLowerCase('en-US')}%`,\n );\n });\n q.andWhere('final_entities.entity_id', 'in', matchQuery);\n }\n }\n };\n\n // The list CTE. When a sort field is specified, the search table for\n // that key drives the query via INNER JOIN so that the covering index\n // walks rows in sort order, letting LIMIT short-circuit. Entities\n // that lack the sort field are excluded — this aligns totalItems with\n // the set reachable through cursor pagination.\n const dbQuery = this.database.with(\n 'filtered',\n ['entity_id', 'final_entity', ...(sortField ? ['value'] : [])],\n inner => {\n if (sortField) {\n inner\n .from('search')\n .innerJoin(\n 'final_entities',\n 'final_entities.entity_id',\n 'search.entity_id',\n )\n .where('search.key', sortKey!)\n .whereNotNull('search.value')\n .whereNotNull('final_entities.final_entity')\n .select({\n entity_id: 'final_entities.entity_id',\n final_entity: 'final_entities.final_entity',\n value: 'search.value',\n });\n } else {\n inner\n .from<DbFinalEntitiesRow>('final_entities')\n .whereNotNull('final_entity')\n .select({\n entity_id: 'final_entities.entity_id',\n final_entity: 'final_entities.final_entity',\n });\n }\n\n applyPredicates(inner, { searchInScope: !!sortField });\n },\n );\n\n // The list query references the CTE exactly once, allowing Postgres\n // 12+ to inline it and short-circuit on LIMIT.\n dbQuery.from('filtered').select('*');\n\n // Standalone count query — runs concurrently with the list so the\n // CTE stays single-referenced and inlineable.\n let countQuery: Knex.QueryBuilder | undefined;\n if (shouldComputeTotalItems) {\n countQuery = this.database('final_entities')\n .whereNotNull('final_entities.final_entity')\n .count('*', { as: 'count' });\n\n if (sortField) {\n countQuery.whereExists(\n this.database('search')\n .select(this.database.raw(1))\n .whereRaw('search.entity_id = final_entities.entity_id')\n .where('search.key', sortKey!)\n .whereNotNull('search.value'),\n );\n }\n\n applyPredicates(countQuery);\n }\n\n const isOrderingDescending = sortField?.order === 'desc';\n\n // Move forward (or backward) in the set to the correct cursor position\n if (cursor.orderFieldValues) {\n if (cursor.orderFieldValues.length === 2) {\n // The first will be the sortField value, the second the entity_id\n const [first, second] = cursor.orderFieldValues;\n dbQuery.andWhere(function nested() {\n this.where(\n 'filtered.value',\n isFetchingBackwards !== isOrderingDescending ? '<' : '>',\n first,\n )\n .orWhere('filtered.value', '=', first)\n .andWhere(\n 'filtered.entity_id',\n isFetchingBackwards !== isOrderingDescending ? '<' : '>',\n second,\n );\n });\n } else if (cursor.orderFieldValues.length === 1) {\n // This will be the entity_id\n const [first] = cursor.orderFieldValues;\n dbQuery.andWhere('entity_id', isFetchingBackwards ? '<' : '>', first);\n }\n }\n\n let order = sortField?.order ?? 'asc';\n if (isFetchingBackwards) {\n order = invertOrder(order);\n }\n dbQuery.orderBy([\n ...(sortField ? [{ column: 'filtered.value', order }] : []),\n { column: 'filtered.entity_id', order },\n ]);\n\n // Apply a manually set initial offset\n if (\n isQueryEntitiesInitialRequest(request) &&\n request.offset !== undefined\n ) {\n dbQuery.offset(request.offset);\n }\n // fetch an extra item to check if there are more items.\n dbQuery.limit(isFetchingBackwards ? limit : limit + 1);\n\n // Run list and count queries concurrently\n const [rows, countResult] = await Promise.all([\n limit > 0 ? dbQuery : Promise.resolve([]),\n countQuery ?? Promise.resolve(undefined),\n ]);\n\n let totalItems: number;\n if (cursor.totalItems !== undefined) {\n totalItems = cursor.totalItems;\n } else if (totalItemsMode === 'exclude') {\n totalItems = 0;\n } else if (countResult?.[0]) {\n totalItems = Number(countResult[0].count);\n } else {\n totalItems = 0;\n }\n\n if (isFetchingBackwards) {\n rows.reverse();\n }\n const hasMoreResults =\n limit > 0 && (isFetchingBackwards || rows.length > limit);\n\n // discard the extra item only when fetching forward.\n if (rows.length > limit) {\n rows.length -= 1;\n }\n\n const isInitialRequest = cursor.firstSortFieldValues === undefined;\n\n const firstRow = rows[0];\n const lastRow = rows[rows.length - 1];\n\n const firstSortFieldValues =\n cursor.firstSortFieldValues || sortFieldsFromRow(firstRow, sortField);\n\n const nextCursor: Cursor | undefined = hasMoreResults\n ? {\n ...cursor,\n orderFieldValues: sortFieldsFromRow(lastRow, sortField),\n firstSortFieldValues,\n isPrevious: false,\n totalItems,\n }\n : undefined;\n\n const prevCursor: Cursor | undefined =\n !isInitialRequest &&\n rows.length > 0 &&\n !isEqual(\n sortFieldsFromRow(firstRow, sortField),\n cursor.firstSortFieldValues,\n )\n ? {\n ...cursor,\n orderFieldValues: sortFieldsFromRow(firstRow, sortField),\n firstSortFieldValues: cursor.firstSortFieldValues,\n isPrevious: true,\n totalItems,\n }\n : undefined;\n\n return {\n items: processRawEntitiesResult(\n rows.map(r => r.final_entity!),\n request.fields,\n ),\n pageInfo: {\n ...(!!prevCursor && { prevCursor }),\n ...(!!nextCursor && { nextCursor }),\n },\n totalItems,\n };\n }\n\n async removeEntityByUid(uid: string): Promise<void> {\n const relationPeerRefs = await this.database.transaction(async tx => {\n const dbConfig = tx.client.config;\n\n // Clear the hashed state of the immediate parents of the deleted entity.\n // This makes sure that when they get reprocessed, their output is written\n // down again. The reason for wanting to do this, is that if the user\n // deletes entities that ARE still emitted by the parent, the parent\n // processing will still generate the same output hash as always, which\n // means it'll never try to write down the children again (it assumes that\n // they already exist). This means that without the code below, the database\n // never \"heals\" from accidental deletes.\n if (dbConfig.client.includes('mysql')) {\n // MySQL doesn't support the syntax we need to do this in a single query,\n // http://dev.mysql.com/doc/refman/5.6/en/update.html\n const results = await tx<DbRefreshStateRow>('refresh_state')\n .select('entity_id')\n .whereIn('entity_ref', function parents(builder) {\n return builder\n .from<DbRefreshStateRow>('refresh_state')\n .innerJoin<DbRefreshStateReferencesRow>(\n 'refresh_state_references',\n {\n 'refresh_state_references.target_entity_ref':\n 'refresh_state.entity_ref',\n },\n )\n .where('refresh_state.entity_id', '=', uid)\n .select('refresh_state_references.source_entity_ref');\n });\n await tx<DbRefreshStateRow>('refresh_state')\n .update({\n result_hash: 'child-was-deleted',\n next_update_at: tx.fn.now(),\n })\n .whereIn(\n 'entity_id',\n results.map(key => key.entity_id),\n );\n } else {\n await tx<DbRefreshStateRow>('refresh_state')\n .update({\n result_hash: 'child-was-deleted',\n next_update_at: tx.fn.now(),\n })\n .whereIn('entity_ref', function parents(builder) {\n return builder\n .from<DbRefreshStateRow>('refresh_state')\n .innerJoin<DbRefreshStateReferencesRow>(\n 'refresh_state_references',\n {\n 'refresh_state_references.target_entity_ref':\n 'refresh_state.entity_ref',\n },\n )\n .where('refresh_state.entity_id', '=', uid)\n .select('refresh_state_references.source_entity_ref');\n });\n }\n\n const relationPeers = await tx\n .from<DbRelationsRow>('relations')\n .innerJoin<DbRefreshStateRow>('refresh_state', {\n 'refresh_state.entity_ref': 'relations.target_entity_ref',\n })\n .where('relations.originating_entity_id', '=', uid)\n .andWhere('refresh_state.entity_id', '!=', uid)\n .select({ ref: 'relations.target_entity_ref' })\n .union(other =>\n other\n .from<DbRelationsRow>('relations')\n .innerJoin<DbRefreshStateRow>('refresh_state', {\n 'refresh_state.entity_ref': 'relations.source_entity_ref',\n })\n .where('relations.originating_entity_id', '=', uid)\n .andWhere('refresh_state.entity_id', '!=', uid)\n .select({ ref: 'relations.source_entity_ref' }),\n );\n\n await tx<DbRefreshStateRow>('refresh_state')\n .where('entity_id', uid)\n .delete();\n\n return new Set(relationPeers.map(p => p.ref));\n });\n\n if (relationPeerRefs.size > 0) {\n await markForStitching({\n knex: this.database,\n entityRefs: relationPeerRefs,\n });\n }\n }\n\n async entityAncestry(rootRef: string): Promise<EntityAncestryResponse> {\n const [rootRow] = await this.database<DbFinalEntitiesRow>('final_entities')\n .where('final_entities.entity_ref', '=', rootRef)\n .select({\n entityJson: 'final_entities.final_entity',\n });\n\n if (!rootRow) {\n throw new NotFoundError(`No such entity ${rootRef}`);\n }\n\n const rootEntity = JSON.parse(rootRow.entityJson) as Entity;\n const seenEntityRefs = new Set<string>();\n const todo = new Array<Entity>();\n const items = new Array<{ entity: Entity; parentEntityRefs: string[] }>();\n\n for (\n let current: Entity | undefined = rootEntity;\n current;\n current = todo.pop()\n ) {\n const currentRef = stringifyEntityRef(current);\n seenEntityRefs.add(currentRef);\n\n const parentRows = await this.database<DbRefreshStateReferencesRow>(\n 'refresh_state_references',\n )\n .innerJoin<DbFinalEntitiesRow>('final_entities', {\n 'refresh_state_references.source_entity_ref':\n 'final_entities.entity_ref',\n })\n .where('refresh_state_references.target_entity_ref', '=', currentRef)\n .select({\n parentEntityRef: 'final_entities.entity_ref',\n parentEntityJson: 'final_entities.final_entity',\n });\n\n const parentRefs: string[] = [];\n for (const { parentEntityRef, parentEntityJson } of parentRows) {\n parentRefs.push(parentEntityRef);\n if (!seenEntityRefs.has(parentEntityRef)) {\n seenEntityRefs.add(parentEntityRef);\n todo.push(JSON.parse(parentEntityJson));\n }\n }\n\n items.push({\n entity: current,\n parentEntityRefs: parentRefs,\n });\n }\n\n return {\n rootEntityRef: stringifyEntityRef(rootEntity),\n items,\n };\n }\n\n async facets(request: EntityFacetsRequest): Promise<EntityFacetsResponse> {\n const query = this.database<DbSearchRow>('search')\n .whereIn(\n 'search.key',\n request.facets.map(f => f.toLocaleLowerCase('en-US')),\n )\n .whereNotNull('search.original_value')\n .select({\n facet: 'search.key',\n value: 'search.original_value',\n count: this.database.raw('count(*)'),\n })\n .groupBy(['search.key', 'search.original_value'])\n .orderBy(['search.key', 'search.original_value']);\n\n if (request.filter || request.query) {\n // Build a subquery that finds matching entity IDs via\n // final_entities, so that the EXISTS-based filters correlate\n // against one-row-per-entity rather than the much larger search\n // table. The whereNotNull guard on final_entity excludes\n // not-yet-stitched (or future tombstoned) entities.\n const entityIdSubquery = this.database('final_entities')\n .select('final_entities.entity_id')\n .whereNotNull('final_entities.final_entity');\n\n applyEntityFilterToQuery({\n filter: request.filter,\n query: request.query,\n targetQuery: entityIdSubquery,\n onEntityIdField: 'final_entities.entity_id',\n knex: this.database,\n });\n\n // Use INNER JOIN rather than `WHERE search.entity_id IN (...)`. The\n // results are the same but the JOIN form gives the planner more\n // freedom in join shape and ordering. On PostgreSQL with large\n // search tables, the IN form tends to materialize the full filtered\n // entity set up front and spill to temp; the JOIN form lets the\n // planner pick a much cheaper plan based on actual selectivities.\n query.innerJoin(\n entityIdSubquery.as('filtered_entities'),\n 'search.entity_id',\n 'filtered_entities.entity_id',\n );\n }\n\n const rows = await query;\n\n const facets: EntityFacetsResponse['facets'] = {};\n for (const facet of request.facets) {\n const facetLowercase = facet.toLocaleLowerCase('en-US');\n facets[facet] = rows\n .filter(row => row.facet === facetLowercase)\n .map(row => ({\n value: String(row.value),\n count: Number(row.count),\n }));\n }\n\n return { facets };\n }\n}\n\nfunction parseCursorFromRequest(\n request?: QueryEntitiesRequest,\n): Partial<Cursor> & { totalItemsMode: TotalItemsMode } {\n if (isQueryEntitiesInitialRequest(request)) {\n const {\n filter,\n query,\n orderFields: sortFields = [],\n fullTextFilter,\n totalItems: totalItemsMode = 'include',\n } = request;\n return {\n filter,\n query,\n orderFields: sortFields,\n fullTextFilter,\n totalItemsMode,\n };\n }\n if (isQueryEntitiesCursorRequest(request)) {\n return {\n ...request.cursor,\n // Doesn't matter — cursor already carries the computed totalItems\n // number from the first page, so the count query is skipped regardless.\n totalItemsMode: 'exclude',\n };\n }\n return {\n totalItemsMode: 'include',\n };\n}\n\nfunction invertOrder(order: EntityOrder['order']) {\n return order === 'asc' ? 'desc' : 'asc';\n}\n\nfunction sortFieldsFromRow(\n row: DbSearchRow & DbFinalEntitiesRow,\n sortField?: EntityOrder | undefined,\n) {\n return sortField ? [row?.value, row?.entity_id] : [row?.entity_id];\n}\n"],"names":["InputError","applyEntityFilterToQuery","processRawEntitiesResult","expandLegacyCompoundRelationsInEntity","skip","lodashChunk","isQueryEntitiesInitialRequest","isEqual","markForStitching","NotFoundError","stringifyEntityRef","isQueryEntitiesCursorRequest"],"mappings":"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"}
1
+ {"version":3,"file":"DefaultEntitiesCatalog.cjs.js","sources":["../../src/service/DefaultEntitiesCatalog.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Entity, stringifyEntityRef } from '@backstage/catalog-model';\nimport { InputError, NotFoundError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport { chunk as lodashChunk, isEqual } from 'lodash';\nimport {\n Cursor,\n EntitiesBatchRequest,\n EntitiesBatchResponse,\n EntitiesCatalog,\n EntitiesRequest,\n EntitiesResponse,\n EntityAncestryResponse,\n EntityFacetsRequest,\n EntityFacetsResponse,\n EntityOrder,\n EntityPagination,\n QueryEntitiesRequest,\n QueryEntitiesResponse,\n TotalItemsMode,\n} from '../catalog/types';\nimport {\n DbFinalEntitiesRow,\n DbPageInfo,\n DbRefreshStateReferencesRow,\n DbRefreshStateRow,\n DbRelationsRow,\n DbSearchRow,\n} from '../database/tables';\nimport { markForStitching } from '../database/operations/stitcher/markForStitching';\n\nimport {\n expandLegacyCompoundRelationsInEntity,\n isQueryEntitiesCursorRequest,\n isQueryEntitiesInitialRequest,\n} from './util';\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { applyEntityFilterToQuery } from './request/applyEntityFilterToQuery';\nimport { processRawEntitiesResult } from './response';\n\nconst DEFAULT_LIMIT = 200;\n\nfunction parsePagination(input?: EntityPagination): EntityPagination {\n if (!input) {\n return {};\n }\n\n let { limit, offset } = input;\n\n if (input.after === undefined) {\n return { limit, offset };\n }\n\n let cursor;\n try {\n const json = Buffer.from(input.after, 'base64').toString('utf8');\n cursor = JSON.parse(json);\n } catch {\n throw new InputError('Malformed after cursor, could not be parsed');\n }\n\n if (cursor.limit !== undefined) {\n if (!Number.isInteger(cursor.limit)) {\n throw new InputError('Malformed after cursor, limit was not an number');\n }\n limit = cursor.limit;\n }\n\n if (cursor.offset !== undefined) {\n if (!Number.isInteger(cursor.offset)) {\n throw new InputError('Malformed after cursor, offset was not a number');\n }\n offset = cursor.offset;\n }\n\n return { limit, offset };\n}\n\nfunction stringifyPagination(\n input: Required<Omit<EntityPagination, 'after'>>,\n): string {\n const { limit, offset } = input;\n const json = JSON.stringify({ limit, offset });\n const base64 = Buffer.from(json, 'utf8').toString('base64');\n return base64;\n}\n\nexport class DefaultEntitiesCatalog implements EntitiesCatalog {\n private readonly database: Knex;\n private readonly logger: LoggerService;\n private readonly enableRelationsCompatibility: boolean;\n\n constructor(options: {\n database: Knex;\n logger: LoggerService;\n enableRelationsCompatibility?: boolean;\n }) {\n this.database = options.database;\n this.logger = options.logger;\n this.enableRelationsCompatibility = Boolean(\n options.enableRelationsCompatibility,\n );\n }\n\n async entities(request?: EntitiesRequest): Promise<EntitiesResponse> {\n const db = this.database;\n const { limit, offset } = parsePagination(request?.pagination);\n const primaryOrder = request?.order?.[0];\n\n // When exactly one order field is specified we run a two-phase fetch\n // that drives from the search-by-key index for that field. The index\n // walks rows in already-sorted order, so the planner can short-circuit\n // on LIMIT instead of having to materialise and sort the full filtered\n // set. Phase 2 appends entities that lack the order field (NULLS LAST)\n // and is skipped when phase 1 already fills the request.\n //\n // Multi-field ordering falls back to the original LEFT JOIN shape\n // because tie-breaking on a second field requires materialisation of\n // the full set anyway.\n const useFastPath = primaryOrder && (request?.order?.length ?? 0) <= 1;\n let rows: DbFinalEntitiesRow[];\n if (useFastPath) {\n rows = await this.runOrderedEntitiesQuery(\n request!,\n primaryOrder,\n limit,\n offset,\n );\n } else {\n let entitiesQuery =\n db<DbFinalEntitiesRow>('final_entities').select('final_entities.*');\n\n request?.order?.forEach(({ field }, index) => {\n const alias = `order_${index}`;\n entitiesQuery = entitiesQuery.leftOuterJoin(\n { [alias]: 'search' },\n function search(inner) {\n inner\n .on(`${alias}.entity_id`, 'final_entities.entity_id')\n .andOn(`${alias}.key`, db.raw('?', [field]));\n },\n );\n });\n\n entitiesQuery = entitiesQuery.whereNotNull('final_entities.final_entity');\n\n if (request?.filter) {\n entitiesQuery = applyEntityFilterToQuery({\n filter: request.filter,\n targetQuery: entitiesQuery,\n onEntityIdField: 'final_entities.entity_id',\n knex: db,\n });\n }\n\n if (request?.order) {\n request.order.forEach(({ order }, index) => {\n if (db.client.config.client === 'pg') {\n entitiesQuery = entitiesQuery.orderBy([\n { column: `order_${index}.value`, order, nulls: 'last' },\n ]);\n } else {\n entitiesQuery = entitiesQuery.orderBy([\n {\n column: `order_${index}.value`,\n order: undefined,\n nulls: 'last',\n },\n { column: `order_${index}.value`, order },\n ]);\n }\n });\n entitiesQuery.orderBy('final_entities.entity_id', 'asc');\n } else {\n entitiesQuery = entitiesQuery.orderBy(\n 'final_entities.entity_ref',\n 'asc',\n );\n }\n\n if (limit !== undefined) {\n entitiesQuery = entitiesQuery.limit(limit + 1);\n }\n if (offset !== undefined) {\n entitiesQuery = entitiesQuery.offset(offset);\n }\n\n rows = await entitiesQuery;\n }\n\n let pageInfo: DbPageInfo;\n if (limit === undefined || rows.length <= limit) {\n pageInfo = { hasNextPage: false };\n } else {\n rows = rows.slice(0, -1);\n pageInfo = {\n hasNextPage: true,\n endCursor: stringifyPagination({\n limit,\n offset: (offset ?? 0) + limit,\n }),\n };\n }\n\n return {\n entities: processRawEntitiesResult(\n rows.map(r => r.final_entity!),\n this.enableRelationsCompatibility\n ? e => {\n expandLegacyCompoundRelationsInEntity(e);\n if (request?.fields) {\n return request.fields(e);\n }\n return e;\n }\n : request?.fields,\n ),\n pageInfo,\n };\n }\n\n /**\n * Two-phase fetch used when the caller has specified an order field.\n * See entities() for a longer description of the rationale.\n */\n private async runOrderedEntitiesQuery(\n request: EntitiesRequest,\n primaryOrder: EntityOrder,\n limit: number | undefined,\n offset: number | undefined,\n ): Promise<DbFinalEntitiesRow[]> {\n const db = this.database;\n const isPg = db.client.config.client === 'pg';\n const wantedRows =\n limit === undefined ? Number.MAX_SAFE_INTEGER : (offset ?? 0) + limit + 1;\n\n const applyFilter = <T extends object>(\n query: Knex.QueryBuilder<T>,\n ): Knex.QueryBuilder<T> => {\n if (!request.filter) {\n return query;\n }\n return applyEntityFilterToQuery({\n filter: request.filter,\n targetQuery: query,\n onEntityIdField: 'final_entities.entity_id',\n knex: db,\n });\n };\n\n // Phase 1 -- entities that have a non-NULL value for the order field.\n // Rows where the key exists but value IS NULL (e.g. the entity field is\n // explicitly null, or exceeded MAX_VALUE_LENGTH in buildEntitySearch) are\n // excluded here so they fall through to Phase 2 and sort in the same\n // NULLS-LAST bucket as entities that have no row for the key at all —\n // preserving the semantics of the previous LEFT JOIN approach.\n let withField = db('search as order_0')\n .innerJoin(\n 'final_entities',\n 'final_entities.entity_id',\n 'order_0.entity_id',\n )\n .where('order_0.key', primaryOrder.field)\n .whereNotNull('order_0.value')\n .whereNotNull('final_entities.final_entity')\n .select<DbFinalEntitiesRow[]>('final_entities.*');\n withField = applyFilter(withField);\n withField = isPg\n ? withField.orderBy([\n { column: 'order_0.value', order: primaryOrder.order, nulls: 'last' },\n { column: 'final_entities.entity_id', order: 'asc' },\n ])\n : withField.orderBy([\n { column: 'order_0.value', order: undefined, nulls: 'last' },\n { column: 'order_0.value', order: primaryOrder.order },\n { column: 'final_entities.entity_id', order: 'asc' },\n ]);\n if (wantedRows < Number.MAX_SAFE_INTEGER) {\n withField = withField.limit(wantedRows);\n }\n const withFieldRows = await withField;\n\n // If phase 1 already covered everything we asked for, skip the second\n // phase entirely. This is the common UI case where every entity in the\n // filtered set has the order field.\n if (withFieldRows.length >= wantedRows) {\n const skip = offset ?? 0;\n return withFieldRows.slice(skip, skip + (limit ?? wantedRows) + 1);\n }\n\n // Phase 2 -- entities that lack the order field, appended after.\n let withoutField = db<DbFinalEntitiesRow>('final_entities')\n .select<DbFinalEntitiesRow[]>('final_entities.*')\n .whereNotNull('final_entities.final_entity')\n .whereNotExists(qb =>\n qb\n .from('search')\n .where('search.entity_id', db.ref('final_entities.entity_id'))\n .andWhere('search.key', primaryOrder.field)\n .whereNotNull('search.value'),\n );\n withoutField = applyFilter(withoutField);\n withoutField = withoutField.orderBy(\n 'final_entities.entity_id',\n 'asc', // NULL group always stable-sorted ASC regardless of primary direction\n );\n if (limit !== undefined) {\n // Phase 2 only contributes the rows that phase 1 didn't cover.\n const remaining =\n wantedRows - Math.min(withFieldRows.length, (offset ?? 0) + limit + 1);\n withoutField = withoutField.limit(Math.max(0, remaining));\n }\n const withoutFieldRows = await withoutField;\n\n const combined = [...withFieldRows, ...withoutFieldRows];\n if (limit === undefined) {\n return combined.slice(offset ?? 0);\n }\n const skip = offset ?? 0;\n return combined.slice(skip, skip + limit + 1);\n }\n\n async entitiesBatch(\n request: EntitiesBatchRequest,\n ): Promise<EntitiesBatchResponse> {\n if (request.entityRefs.length === 0) {\n return { items: processRawEntitiesResult([], request.fields) };\n }\n\n const lookup = new Map<string, string>();\n const isPg = this.database.client.config.client === 'pg';\n\n const chunks = isPg\n ? [request.entityRefs]\n : lodashChunk(request.entityRefs, 200);\n\n for (const chunk of chunks) {\n let query = this.database<DbFinalEntitiesRow>('final_entities').select({\n entityRef: 'final_entities.entity_ref',\n entity: 'final_entities.final_entity',\n });\n\n if (isPg) {\n query = query.whereRaw('final_entities.entity_ref = ANY(?::text[])', [\n chunk,\n ]);\n } else {\n query = query.whereIn('final_entities.entity_ref', chunk);\n }\n\n if (request?.filter || request?.query) {\n query = applyEntityFilterToQuery({\n filter: request.filter,\n query: request.query,\n targetQuery: query,\n onEntityIdField: 'final_entities.entity_id',\n knex: this.database,\n });\n }\n\n for (const row of await query) {\n lookup.set(row.entityRef, row.entity ? row.entity : null);\n }\n }\n\n const items = request.entityRefs.map(ref => lookup.get(ref) ?? null);\n\n return { items: processRawEntitiesResult(items, request.fields) };\n }\n\n async queryEntities(\n request: QueryEntitiesRequest,\n ): Promise<QueryEntitiesResponse> {\n const limit = request.limit ?? DEFAULT_LIMIT;\n\n const { totalItemsMode, ...cursor } = {\n orderFields: [] as EntityOrder[],\n isPrevious: false,\n ...parseCursorFromRequest(request),\n } satisfies Omit<Cursor, 'orderFieldValues'> & {\n orderFieldValues?: (string | null)[];\n totalItemsMode: TotalItemsMode;\n };\n\n const shouldComputeTotalItems =\n cursor.totalItems === undefined && totalItemsMode !== 'exclude';\n const isFetchingBackwards = cursor.isPrevious;\n\n if (cursor.orderFields.length > 1) {\n this.logger.warn(`Only one sort field is supported, ignoring the rest`);\n }\n\n const sortField = cursor.orderFields.at(0);\n const sortKey = sortField?.field.toLocaleLowerCase('en-US');\n\n const normalizedFullTextFilterTerm = cursor.fullTextFilter?.term?.trim();\n const textFilterFields = cursor.fullTextFilter?.fields ?? [\n sortKey || 'metadata.uid',\n ];\n\n // Shared predicate logic applied to both the list CTE and the\n // standalone count query so they stay in sync. The `searchInScope`\n // flag indicates whether a `search` table is already joined in the\n // target query (true for the list CTE when a sort field is set),\n // enabling a fast-path LIKE on the already-joined row.\n const applyPredicates = (\n q: Knex.QueryBuilder,\n options?: { searchInScope?: boolean },\n ) => {\n if (cursor.filter || cursor.query) {\n applyEntityFilterToQuery({\n filter: cursor.filter,\n query: cursor.query,\n targetQuery: q,\n onEntityIdField: 'final_entities.entity_id',\n knex: this.database,\n });\n }\n\n if (normalizedFullTextFilterTerm) {\n if (\n options?.searchInScope &&\n sortField &&\n textFilterFields.length === 1 &&\n textFilterFields[0] === sortKey\n ) {\n q.andWhereRaw(\n 'search.value like ?',\n `%${normalizedFullTextFilterTerm.toLocaleLowerCase('en-US')}%`,\n );\n } else {\n const matchQuery = this.database<DbSearchRow>('search')\n .select('search.entity_id')\n .whereIn(\n 'search.key',\n textFilterFields.map(field => field.toLocaleLowerCase('en-US')),\n )\n .andWhere(function keyFilter() {\n this.andWhereRaw(\n 'search.value like ?',\n `%${normalizedFullTextFilterTerm.toLocaleLowerCase('en-US')}%`,\n );\n });\n q.andWhere('final_entities.entity_id', 'in', matchQuery);\n }\n }\n };\n\n // The list CTE. When a sort field is specified, the search table for\n // that key drives the query via INNER JOIN so that the covering index\n // walks rows in sort order, letting LIMIT short-circuit. Entities\n // that lack the sort field are excluded — this aligns totalItems with\n // the set reachable through cursor pagination.\n const dbQuery = this.database.with(\n 'filtered',\n ['entity_id', 'final_entity', ...(sortField ? ['value'] : [])],\n inner => {\n if (sortField) {\n inner\n .from('search')\n .innerJoin(\n 'final_entities',\n 'final_entities.entity_id',\n 'search.entity_id',\n )\n .where('search.key', sortKey!)\n .whereNotNull('search.value')\n .whereNotNull('final_entities.final_entity')\n .select({\n entity_id: 'final_entities.entity_id',\n final_entity: 'final_entities.final_entity',\n value: 'search.value',\n });\n } else {\n inner\n .from<DbFinalEntitiesRow>('final_entities')\n .whereNotNull('final_entity')\n .select({\n entity_id: 'final_entities.entity_id',\n final_entity: 'final_entities.final_entity',\n });\n }\n\n applyPredicates(inner, { searchInScope: !!sortField });\n },\n );\n\n // The list query references the CTE exactly once, allowing Postgres\n // 12+ to inline it and short-circuit on LIMIT.\n dbQuery.from('filtered').select('*');\n\n // Standalone count query — runs concurrently with the list so the\n // CTE stays single-referenced and inlineable.\n let countQuery: Knex.QueryBuilder | undefined;\n if (shouldComputeTotalItems) {\n countQuery = this.database('final_entities')\n .whereNotNull('final_entities.final_entity')\n .count('*', { as: 'count' });\n\n if (sortField) {\n countQuery.whereExists(\n this.database('search')\n .select(this.database.raw(1))\n .whereRaw('search.entity_id = final_entities.entity_id')\n .where('search.key', sortKey!)\n .whereNotNull('search.value'),\n );\n }\n\n applyPredicates(countQuery);\n }\n\n const isOrderingDescending = sortField?.order === 'desc';\n\n // Move forward (or backward) in the set to the correct cursor position\n if (cursor.orderFieldValues) {\n if (cursor.orderFieldValues.length === 2) {\n // The first will be the sortField value, the second the entity_id\n const [first, second] = cursor.orderFieldValues;\n dbQuery.andWhere(function nested() {\n this.where(\n 'filtered.value',\n isFetchingBackwards !== isOrderingDescending ? '<' : '>',\n first,\n )\n .orWhere('filtered.value', '=', first)\n .andWhere(\n 'filtered.entity_id',\n isFetchingBackwards !== isOrderingDescending ? '<' : '>',\n second,\n );\n });\n } else if (cursor.orderFieldValues.length === 1) {\n // This will be the entity_id\n const [first] = cursor.orderFieldValues;\n dbQuery.andWhere('entity_id', isFetchingBackwards ? '<' : '>', first);\n }\n }\n\n let order = sortField?.order ?? 'asc';\n if (isFetchingBackwards) {\n order = invertOrder(order);\n }\n dbQuery.orderBy([\n ...(sortField ? [{ column: 'filtered.value', order }] : []),\n { column: 'filtered.entity_id', order },\n ]);\n\n // Apply a manually set initial offset\n if (\n isQueryEntitiesInitialRequest(request) &&\n request.offset !== undefined\n ) {\n dbQuery.offset(request.offset);\n }\n // fetch an extra item to check if there are more items.\n dbQuery.limit(isFetchingBackwards ? limit : limit + 1);\n\n // Run list and count queries concurrently\n const [rows, countResult] = await Promise.all([\n limit > 0 ? dbQuery : Promise.resolve([]),\n countQuery ?? Promise.resolve(undefined),\n ]);\n\n let totalItems: number;\n if (cursor.totalItems !== undefined) {\n totalItems = cursor.totalItems;\n } else if (totalItemsMode === 'exclude') {\n totalItems = 0;\n } else if (countResult?.[0]) {\n totalItems = Number(countResult[0].count);\n } else {\n totalItems = 0;\n }\n\n if (isFetchingBackwards) {\n rows.reverse();\n }\n const hasMoreResults =\n limit > 0 && (isFetchingBackwards || rows.length > limit);\n\n // discard the extra item only when fetching forward.\n if (rows.length > limit) {\n rows.length -= 1;\n }\n\n const isInitialRequest = cursor.firstSortFieldValues === undefined;\n\n const firstRow = rows[0];\n const lastRow = rows[rows.length - 1];\n\n const firstSortFieldValues =\n cursor.firstSortFieldValues || sortFieldsFromRow(firstRow, sortField);\n\n const nextCursor: Cursor | undefined = hasMoreResults\n ? {\n ...cursor,\n orderFieldValues: sortFieldsFromRow(lastRow, sortField),\n firstSortFieldValues,\n isPrevious: false,\n totalItems,\n }\n : undefined;\n\n const prevCursor: Cursor | undefined =\n !isInitialRequest &&\n rows.length > 0 &&\n !isEqual(\n sortFieldsFromRow(firstRow, sortField),\n cursor.firstSortFieldValues,\n )\n ? {\n ...cursor,\n orderFieldValues: sortFieldsFromRow(firstRow, sortField),\n firstSortFieldValues: cursor.firstSortFieldValues,\n isPrevious: true,\n totalItems,\n }\n : undefined;\n\n return {\n items: processRawEntitiesResult(\n rows.map(r => r.final_entity!),\n request.fields,\n ),\n pageInfo: {\n ...(!!prevCursor && { prevCursor }),\n ...(!!nextCursor && { nextCursor }),\n },\n totalItems,\n };\n }\n\n async removeEntityByUid(uid: string): Promise<void> {\n const relationPeerRefs = await this.database.transaction(async tx => {\n const dbConfig = tx.client.config;\n\n // Clear the hashed state of the immediate parents of the deleted entity.\n // This makes sure that when they get reprocessed, their output is written\n // down again. The reason for wanting to do this, is that if the user\n // deletes entities that ARE still emitted by the parent, the parent\n // processing will still generate the same output hash as always, which\n // means it'll never try to write down the children again (it assumes that\n // they already exist). This means that without the code below, the database\n // never \"heals\" from accidental deletes.\n if (dbConfig.client.includes('mysql')) {\n // MySQL doesn't support the syntax we need to do this in a single query,\n // http://dev.mysql.com/doc/refman/5.6/en/update.html\n const results = await tx<DbRefreshStateRow>('refresh_state')\n .select('entity_id')\n .whereIn('entity_ref', function parents(builder) {\n return builder\n .from<DbRefreshStateRow>('refresh_state')\n .innerJoin<DbRefreshStateReferencesRow>(\n 'refresh_state_references',\n {\n 'refresh_state_references.target_entity_ref':\n 'refresh_state.entity_ref',\n },\n )\n .where('refresh_state.entity_id', '=', uid)\n .select('refresh_state_references.source_entity_ref');\n });\n await tx<DbRefreshStateRow>('refresh_state')\n .update({\n result_hash: 'child-was-deleted',\n next_update_at: tx.fn.now(),\n })\n .whereIn(\n 'entity_id',\n results.map(key => key.entity_id),\n );\n } else {\n await tx<DbRefreshStateRow>('refresh_state')\n .update({\n result_hash: 'child-was-deleted',\n next_update_at: tx.fn.now(),\n })\n .whereIn('entity_ref', function parents(builder) {\n return builder\n .from<DbRefreshStateRow>('refresh_state')\n .innerJoin<DbRefreshStateReferencesRow>(\n 'refresh_state_references',\n {\n 'refresh_state_references.target_entity_ref':\n 'refresh_state.entity_ref',\n },\n )\n .where('refresh_state.entity_id', '=', uid)\n .select('refresh_state_references.source_entity_ref');\n });\n }\n\n const relationPeers = await tx\n .from<DbRelationsRow>('relations')\n .innerJoin<DbRefreshStateRow>('refresh_state', {\n 'refresh_state.entity_ref': 'relations.target_entity_ref',\n })\n .where('relations.originating_entity_id', '=', uid)\n .andWhere('refresh_state.entity_id', '!=', uid)\n .select({ ref: 'relations.target_entity_ref' })\n .union(other =>\n other\n .from<DbRelationsRow>('relations')\n .innerJoin<DbRefreshStateRow>('refresh_state', {\n 'refresh_state.entity_ref': 'relations.source_entity_ref',\n })\n .where('relations.originating_entity_id', '=', uid)\n .andWhere('refresh_state.entity_id', '!=', uid)\n .select({ ref: 'relations.source_entity_ref' }),\n );\n\n await tx<DbRefreshStateRow>('refresh_state')\n .where('entity_id', uid)\n .delete();\n\n return new Set(relationPeers.map(p => p.ref));\n });\n\n if (relationPeerRefs.size > 0) {\n await markForStitching({\n knex: this.database,\n entityRefs: relationPeerRefs,\n });\n }\n }\n\n async entityAncestry(rootRef: string): Promise<EntityAncestryResponse> {\n const [rootRow] = await this.database<DbFinalEntitiesRow>('final_entities')\n .where('final_entities.entity_ref', '=', rootRef)\n .select({\n entityJson: 'final_entities.final_entity',\n });\n\n if (!rootRow) {\n throw new NotFoundError(`No such entity ${rootRef}`);\n }\n\n const rootEntity = JSON.parse(rootRow.entityJson) as Entity;\n const seenEntityRefs = new Set<string>();\n const todo = new Array<Entity>();\n const items = new Array<{ entity: Entity; parentEntityRefs: string[] }>();\n\n for (\n let current: Entity | undefined = rootEntity;\n current;\n current = todo.pop()\n ) {\n const currentRef = stringifyEntityRef(current);\n seenEntityRefs.add(currentRef);\n\n const parentRows = await this.database<DbRefreshStateReferencesRow>(\n 'refresh_state_references',\n )\n .innerJoin<DbFinalEntitiesRow>('final_entities', {\n 'refresh_state_references.source_entity_ref':\n 'final_entities.entity_ref',\n })\n .where('refresh_state_references.target_entity_ref', '=', currentRef)\n .select({\n parentEntityRef: 'final_entities.entity_ref',\n parentEntityJson: 'final_entities.final_entity',\n });\n\n const parentRefs: string[] = [];\n for (const { parentEntityRef, parentEntityJson } of parentRows) {\n parentRefs.push(parentEntityRef);\n if (!seenEntityRefs.has(parentEntityRef)) {\n seenEntityRefs.add(parentEntityRef);\n todo.push(JSON.parse(parentEntityJson));\n }\n }\n\n items.push({\n entity: current,\n parentEntityRefs: parentRefs,\n });\n }\n\n return {\n rootEntityRef: stringifyEntityRef(rootEntity),\n items,\n };\n }\n\n async facets(request: EntityFacetsRequest): Promise<EntityFacetsResponse> {\n const query = this.database<DbSearchRow>('search')\n .whereIn(\n 'search.key',\n request.facets.map(f => f.toLocaleLowerCase('en-US')),\n )\n .whereNotNull('search.original_value')\n .select({\n facet: 'search.key',\n value: 'search.original_value',\n count: this.database.raw('count(*)'),\n })\n .groupBy(['search.key', 'search.original_value'])\n .orderBy(['search.key', 'search.original_value']);\n\n if (request.filter || request.query) {\n // Build a subquery that finds matching entity IDs via\n // final_entities, so that the EXISTS-based filters correlate\n // against one-row-per-entity rather than the much larger search\n // table. The whereNotNull guard on final_entity excludes\n // not-yet-stitched (or future tombstoned) entities.\n const entityIdSubquery = this.database('final_entities')\n .select('final_entities.entity_id')\n .whereNotNull('final_entities.final_entity');\n\n applyEntityFilterToQuery({\n filter: request.filter,\n query: request.query,\n targetQuery: entityIdSubquery,\n onEntityIdField: 'final_entities.entity_id',\n knex: this.database,\n });\n\n // Use INNER JOIN rather than `WHERE search.entity_id IN (...)`. The\n // results are the same but the JOIN form gives the planner more\n // freedom in join shape and ordering. On PostgreSQL with large\n // search tables, the IN form tends to materialize the full filtered\n // entity set up front and spill to temp; the JOIN form lets the\n // planner pick a much cheaper plan based on actual selectivities.\n query.innerJoin(\n entityIdSubquery.as('filtered_entities'),\n 'search.entity_id',\n 'filtered_entities.entity_id',\n );\n }\n\n const rows = await query;\n\n const facets: EntityFacetsResponse['facets'] = {};\n for (const facet of request.facets) {\n const facetLowercase = facet.toLocaleLowerCase('en-US');\n facets[facet] = rows\n .filter(row => row.facet === facetLowercase)\n .map(row => ({\n value: String(row.value),\n count: Number(row.count),\n }));\n }\n\n return { facets };\n }\n}\n\nfunction parseCursorFromRequest(\n request?: QueryEntitiesRequest,\n): Partial<Cursor> & { totalItemsMode: TotalItemsMode } {\n if (isQueryEntitiesInitialRequest(request)) {\n const {\n filter,\n query,\n orderFields: sortFields = [],\n fullTextFilter,\n totalItems: totalItemsMode = 'include',\n } = request;\n return {\n filter,\n query,\n orderFields: sortFields,\n fullTextFilter,\n totalItemsMode,\n };\n }\n if (isQueryEntitiesCursorRequest(request)) {\n return {\n ...request.cursor,\n // Doesn't matter — cursor already carries the computed totalItems\n // number from the first page, so the count query is skipped regardless.\n totalItemsMode: 'exclude',\n };\n }\n return {\n totalItemsMode: 'include',\n };\n}\n\nfunction invertOrder(order: EntityOrder['order']) {\n return order === 'asc' ? 'desc' : 'asc';\n}\n\nfunction sortFieldsFromRow(\n row: DbSearchRow & DbFinalEntitiesRow,\n sortField?: EntityOrder | undefined,\n) {\n return sortField ? [row?.value, row?.entity_id] : [row?.entity_id];\n}\n"],"names":["InputError","applyEntityFilterToQuery","processRawEntitiesResult","expandLegacyCompoundRelationsInEntity","skip","lodashChunk","isQueryEntitiesInitialRequest","isEqual","markForStitching","NotFoundError","stringifyEntityRef","isQueryEntitiesCursorRequest"],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ /*
2
+ * Copyright 2026 The Backstage Authors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ // @ts-check
18
+
19
+ /**
20
+ * Tunes autovacuum thresholds on high-churn catalog tables and fixes
21
+ * the `n_distinct` estimate on the `search` table.
22
+ *
23
+ * ## Autovacuum scale factors
24
+ *
25
+ * Several catalog tables experience high churn from entity stitching
26
+ * and ingestion cycles. At the default
27
+ * `autovacuum_vacuum_scale_factor` of 0.2, autovacuum only triggers
28
+ * after 20% of the table changes. On large tables this allows the
29
+ * visibility map to degrade enough that PostgreSQL avoids index-only
30
+ * scans on covering indexes, falling back to sequential scans.
31
+ *
32
+ * This migration sets both scale factors to 0.01 on:
33
+ *
34
+ * - `search` (13.6M rows, ~28 rows per entity, full churn on stitch)
35
+ * - `final_entities` (490K rows, updated on every stitch)
36
+ * - `relations` (3.7M rows, deleted and re-inserted on stitch)
37
+ * - `refresh_state_references` (490K rows, deleted and re-inserted
38
+ * on ingestion)
39
+ *
40
+ * This triggers autovacuum after ~1% of each table changes, keeping
41
+ * the visibility map healthy and enabling index-only scans.
42
+ *
43
+ * ## n_distinct override (search table only)
44
+ *
45
+ * The default ANALYZE samples ~30K rows. With ~490K distinct
46
+ * `entity_id` values in a 13.6M-row table, the sample consistently
47
+ * underestimates `n_distinct` by ~12x (e.g. 38K estimated vs 490K
48
+ * actual). This causes the planner to severely underestimate the
49
+ * output of HashAggregate nodes in catalog list queries.
50
+ *
51
+ * Setting `n_distinct = -1` tells the planner to assume the column
52
+ * has as many distinct values as there are rows. This is a slight
53
+ * overestimate (the actual ratio is ~1:28), but it is far more
54
+ * accurate than the sampled estimate and safe for planning purposes.
55
+ *
56
+ * ## Cost
57
+ *
58
+ * All operations are metadata-only (instant) on any table size.
59
+ * They modify `pg_class.reloptions` and `pg_attribute.attoptions`
60
+ * respectively — no table scan, no lock contention.
61
+ *
62
+ * MySQL and SQLite do not support these settings; this migration is a
63
+ * no-op on those engines.
64
+ */
65
+
66
+ /**
67
+ * @param {import('knex').Knex} knex
68
+ */
69
+ exports.up = async function up(knex) {
70
+ if (!knex.client.config.client.includes('pg')) {
71
+ return;
72
+ }
73
+
74
+ const tables = [
75
+ 'search',
76
+ 'final_entities',
77
+ 'relations',
78
+ 'refresh_state_references',
79
+ ];
80
+
81
+ for (const table of tables) {
82
+ await knex.raw(
83
+ `ALTER TABLE ?? SET (
84
+ autovacuum_vacuum_scale_factor = 0.01,
85
+ autovacuum_analyze_scale_factor = 0.01
86
+ )`,
87
+ [table],
88
+ );
89
+ }
90
+
91
+ await knex.raw(
92
+ `ALTER TABLE search ALTER COLUMN entity_id SET (n_distinct = -1)`,
93
+ );
94
+ };
95
+
96
+ /**
97
+ * @param {import('knex').Knex} knex
98
+ */
99
+ exports.down = async function down(knex) {
100
+ if (!knex.client.config.client.includes('pg')) {
101
+ return;
102
+ }
103
+
104
+ const tables = [
105
+ 'search',
106
+ 'final_entities',
107
+ 'relations',
108
+ 'refresh_state_references',
109
+ ];
110
+
111
+ for (const table of tables) {
112
+ await knex.raw(
113
+ `ALTER TABLE ?? RESET (
114
+ autovacuum_vacuum_scale_factor,
115
+ autovacuum_analyze_scale_factor
116
+ )`,
117
+ [table],
118
+ );
119
+ }
120
+
121
+ await knex.raw(
122
+ `ALTER TABLE search ALTER COLUMN entity_id RESET (n_distinct)`,
123
+ );
124
+ };
@@ -0,0 +1,103 @@
1
+ /*
2
+ * Copyright 2026 The Backstage Authors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ // @ts-check
18
+
19
+ /**
20
+ * Drops the legacy `search_entity_id_idx` index on the `search` table.
21
+ *
22
+ * This single-column index on `entity_id` was created by the original
23
+ * migration (20210302150147) and served as the supporting index for the
24
+ * foreign key on `search.entity_id`. It is now fully redundant: the
25
+ * UNIQUE index `search_entity_key_value_idx` (entity_id, key, value),
26
+ * introduced in 20260510000000, has `entity_id` as its leading column
27
+ * and covers both FK cascade checks and all entity_id lookups.
28
+ *
29
+ * Worse, the old index actively harms performance. Because it is 4.4x
30
+ * smaller than the covering index, the PostgreSQL planner prefers it
31
+ * for entity_id lookups in correlated EXISTS subqueries. This forces a
32
+ * read-all-then-filter-by-key pattern (~28 rows per entity) instead of
33
+ * a direct (entity_id, key) seek on the covering index (1 row). On
34
+ * catalog list queries with multiple sort-field EXISTS checks, this
35
+ * causes minutes-long execution times.
36
+ *
37
+ * ## Cost
38
+ *
39
+ * - **PostgreSQL**: `DROP INDEX CONCURRENTLY` — non-blocking for
40
+ * reads and writes, but may wait for concurrent transactions to
41
+ * finish. Reclaims ~384 MB on a 490K-entity catalog.
42
+ * - **MySQL / SQLite**: standard `DROP INDEX`. Reclaims proportionally
43
+ * less space on smaller catalogs.
44
+ *
45
+ * This migration is safe to run manually before deploying — the
46
+ * `IF EXISTS` / idempotent checks ensure it is a no-op if the index
47
+ * has already been removed.
48
+ */
49
+
50
+ /**
51
+ * @param {import('knex').Knex} knex
52
+ */
53
+ exports.up = async function up(knex) {
54
+ const client = knex.client.config.client;
55
+
56
+ if (client.includes('pg')) {
57
+ const result = await knex.raw(
58
+ `SELECT 1 FROM pg_class WHERE relname = 'search_entity_id_idx' AND relkind = 'i'`,
59
+ );
60
+ if (result.rows.length > 0) {
61
+ await knex.raw('DROP INDEX CONCURRENTLY IF EXISTS search_entity_id_idx');
62
+ }
63
+ } else if (client.includes('mysql')) {
64
+ const [rows] = await knex.raw(
65
+ `SHOW INDEX FROM \`search\` WHERE Key_name = 'search_entity_id_idx'`,
66
+ );
67
+ if (rows.length > 0) {
68
+ await knex.schema.alterTable('search', table => {
69
+ table.dropIndex([], 'search_entity_id_idx');
70
+ });
71
+ }
72
+ } else {
73
+ await knex.raw('DROP INDEX IF EXISTS search_entity_id_idx');
74
+ }
75
+ };
76
+
77
+ /**
78
+ * @param {import('knex').Knex} knex
79
+ */
80
+ exports.down = async function down(knex) {
81
+ const client = knex.client.config.client;
82
+
83
+ if (client.includes('pg')) {
84
+ await knex.raw(
85
+ 'CREATE INDEX CONCURRENTLY IF NOT EXISTS search_entity_id_idx ON search (entity_id)',
86
+ );
87
+ } else if (client.includes('mysql')) {
88
+ const [rows] = await knex.raw(
89
+ `SHOW INDEX FROM \`search\` WHERE Key_name = 'search_entity_id_idx'`,
90
+ );
91
+ if (rows.length === 0) {
92
+ await knex.schema.alterTable('search', table => {
93
+ table.index(['entity_id'], 'search_entity_id_idx');
94
+ });
95
+ }
96
+ } else {
97
+ await knex.raw(
98
+ 'CREATE INDEX IF NOT EXISTS search_entity_id_idx ON search (entity_id)',
99
+ );
100
+ }
101
+ };
102
+
103
+ exports.config = { transaction: false };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-catalog-backend",
3
- "version": "3.8.0-next.0",
3
+ "version": "3.8.0-next.1",
4
4
  "description": "The Backstage backend plugin that provides the Backstage catalog",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",
@@ -77,13 +77,13 @@
77
77
  },
78
78
  "dependencies": {
79
79
  "@backstage/backend-openapi-utils": "0.6.10-next.0",
80
- "@backstage/backend-plugin-api": "1.9.2-next.0",
81
- "@backstage/catalog-client": "1.16.0-next.0",
80
+ "@backstage/backend-plugin-api": "1.9.2-next.1",
81
+ "@backstage/catalog-client": "1.16.0-next.1",
82
82
  "@backstage/catalog-model": "1.9.0",
83
83
  "@backstage/config": "1.3.8",
84
84
  "@backstage/errors": "1.3.1",
85
85
  "@backstage/filter-predicates": "0.1.3",
86
- "@backstage/integration": "2.0.3-next.0",
86
+ "@backstage/integration": "2.0.3-next.1",
87
87
  "@backstage/plugin-catalog-common": "1.1.10",
88
88
  "@backstage/plugin-catalog-node": "2.2.2-next.0",
89
89
  "@backstage/plugin-events-node": "0.4.23-next.0",
@@ -112,9 +112,9 @@
112
112
  "zod-validation-error": "^4.0.2"
113
113
  },
114
114
  "devDependencies": {
115
- "@backstage/backend-defaults": "0.17.2-next.0",
116
- "@backstage/backend-test-utils": "1.11.4-next.0",
117
- "@backstage/cli": "0.36.3-next.0",
115
+ "@backstage/backend-defaults": "0.17.3-next.2",
116
+ "@backstage/backend-test-utils": "1.11.4-next.1",
117
+ "@backstage/cli": "0.36.3-next.1",
118
118
  "@backstage/plugin-catalog-backend-module-logs": "0.1.23-next.0",
119
119
  "@backstage/plugin-scaffolder-common": "2.2.1-next.0",
120
120
  "@backstage/repo-tools": "0.17.3-next.0",