From 07654b6aecea7d8892a2502a79086ce172232d80 Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Sat, 25 Apr 2026 12:16:31 +0000 Subject: [PATCH] fix: preserve original FK value when identity lookup misses GetTableRowsUseCase was overwriting null and orphaned foreign key values with `{}` because the merge step's else branch assigned an empty object whenever the identity lookup map missed. Null FKs (filtered out before the identity query) and orphaned FKs (referenced row deleted) both trigger this path, silently destroying the original cell value. Fall back to the original cell value instead, so null stays null and orphaned ids are preserved as primitives. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../use-cases/get-table-rows.use.case.ts | 5 +- .../complex-postgres-table-e2e.test.ts | 80 +++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/backend/src/entities/table/use-cases/get-table-rows.use.case.ts b/backend/src/entities/table/use-cases/get-table-rows.use.case.ts index 179292878..1d950d8e2 100644 --- a/backend/src/entities/table/use-cases/get-table-rows.use.case.ts +++ b/backend/src/entities/table/use-cases/get-table-rows.use.case.ts @@ -320,11 +320,12 @@ export class GetTableRowsUseCase extends AbstractUseCase { + const parentTableName = 'FKBugFix_Parent'; + const childTableName = 'FKBugFix_Child'; + const knex = getTestKnex(connectionToTestDB); + + await knex.schema.dropTableIfExists(childTableName); + await knex.schema.dropTableIfExists(parentTableName); + + await knex.schema.createTable(parentTableName, (table) => { + table.increments('id').primary(); + table.string('label', 100); + }); + await knex.schema.createTable(childTableName, (table) => { + table.increments('id').primary(); + table.integer('parent_id'); + table.string('description', 100); + }); + + const insertedParentIds = await knex(parentTableName) + .insert([{ label: 'real-parent-1' }, { label: 'real-parent-to-orphan' }]) + .returning('id'); + const realParentId = (insertedParentIds[0] as any).id ?? insertedParentIds[0]; + const orphanedParentId = (insertedParentIds[1] as any).id ?? insertedParentIds[1]; + + await knex(childTableName).insert([ + { parent_id: realParentId, description: 'valid-fk' }, + { parent_id: null, description: 'null-fk' }, + { parent_id: orphanedParentId, description: 'orphan-fk' }, + ]); + + // Now make the orphan-fk row truly orphaned by deleting its parent, + // then register the FK with NOT VALID so Postgres reports it in metadata + // without rejecting the existing orphan row. + await knex(parentTableName).where({ id: orphanedParentId }).del(); + await knex.raw( + `ALTER TABLE "${childTableName}" ADD CONSTRAINT fk_bugfix_child_parent FOREIGN KEY (parent_id) REFERENCES "${parentTableName}" (id) NOT VALID`, + ); + + const userToken = (await registerUserAndReturnUserInfo(app)).token; + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', userToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createConnectionResponse.status, 201); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + + const getRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${childTableName}`) + .set('Cookie', userToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getRowsResponse.status, 200); + const rowsRO = JSON.parse(getRowsResponse.text); + + const validRow = rowsRO.rows.find((r: any) => r.description === 'valid-fk'); + const nullRow = rowsRO.rows.find((r: any) => r.description === 'null-fk'); + const orphanRow = rowsRO.rows.find((r: any) => r.description === 'orphan-fk'); + + t.truthy(validRow, 'valid-fk row should be present'); + t.truthy(nullRow, 'null-fk row should be present'); + t.truthy(orphanRow, 'orphan-fk row should be present'); + + // Sanity: FK was discovered (otherwise the response would not transform parent_id at all + // and all branches below would trivially pass). + t.is(typeof validRow.parent_id, 'object'); + t.truthy(validRow.parent_id); + t.is(validRow.parent_id.id, realParentId); + + // Bug: previously these were assigned `{}`. Both should preserve the original value. + t.is(nullRow.parent_id, null, 'null FK must remain null, not be converted to {}'); + t.is(orphanRow.parent_id, orphanedParentId, 'orphaned FK must keep its raw value, not become {}'); + }, +); + // GET /table/structure/:connectionId test.serial(