diff --git a/.changeset/famous-tips-raise.md b/.changeset/famous-tips-raise.md new file mode 100644 index 0000000000..1a44e19608 --- /dev/null +++ b/.changeset/famous-tips-raise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fix for duplicate results in `queryEntities` when providing an `orderField` parameter diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index 070b93ab2c..aad09d7de1 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -1970,6 +1970,87 @@ describe('DefaultEntitiesCatalog', () => { ).resolves.toEqual(['BB']); }, ); + + it.each(databases.eachSupportedId())( + 'should not return duplicate entities when using orderField, %p', + async databaseId => { + await createDatabase(databaseId); + + // Create a few test entities with different names to sort by + const entities = [ + { + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'a-entity', + title: 'A Test Entity', + uid: 'uid-a', + }, + spec: {}, + }, + { + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'b-entity', + title: 'B Test Entity', + uid: 'uid-b', + }, + spec: {}, + }, + { + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'c-entity', + title: 'C Test Entity', + uid: 'uid-c', + }, + spec: {}, + }, + ]; + + await Promise.all(entities.map(e => addEntityToSearch(e))); + + // Manually insert duplicate search entries for the same entities + // I'm not sure exactly how this happens but I have seen it in the real world + await knex('search').insert([ + { + entity_id: 'uid-a', + key: 'metadata.title', + value: 'a test entity', + original_value: 'A Test Entity', + }, + { + entity_id: 'uid-b', + key: 'metadata.title', + value: 'b test entity', + original_value: 'B Test Entity', + }, + ]); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: mockServices.logger.mock(), + stitcher, + }); + + // Query with orderField + const response = await catalog.queryEntities({ + orderFields: [{ field: 'metadata.title', order: 'asc' }], + credentials: mockCredentials.none(), + }); + + const resultEntities = entitiesResponseToObjects(response.items); + + // Ensure we get exactly 3 entities back, sorted, with no duplicates + expect(resultEntities.map(e => e!.metadata.name)).toEqual([ + 'a-entity', + 'b-entity', + 'c-entity', + ]); + }, + ); }); describe('removeEntityByUid', () => { diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index f0e0667bef..a7b8fdc6c9 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -285,6 +285,7 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { if (sortField) { inner + .distinct() .leftOuterJoin('search', qb => qb .on('search.entity_id', 'final_entities.entity_id')