Skip to content

Commit 265cee6

Browse files
authored
Merge pull request #2189 from teableio/fix/conditional-lookup-self-table
fix: correct self-table conditional lookup references
2 parents 52d5a3e + 8b88fbd commit 265cee6

2 files changed

Lines changed: 183 additions & 4 deletions

File tree

apps/nestjs-backend/src/features/record/query-builder/field-cte-visitor.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,9 +1355,14 @@ export class FieldCteVisitor implements IFieldVisitor<ICteResult> {
13551355
if (isDateLikeField(foreignField) || isDateLikeField(hostField)) {
13561356
return { ok: false, residual: null };
13571357
}
1358+
// When the foreign scope is the same table, compare the host record's fieldId
1359+
// against the foreign row's referenced field so "Field A is {Field B}" reads as
1360+
// host.FieldA = foreign.FieldB instead of the reverse.
1361+
const hostJoinField = foreignTable.id === table.id ? foreignField : hostField;
1362+
const foreignJoinField = foreignTable.id === table.id ? hostField : foreignField;
13581363
const joinKey = this.buildConditionalEqualityJoinKey(
1359-
hostField,
1360-
foreignField,
1364+
hostJoinField,
1365+
foreignJoinField,
13611366
mainAlias,
13621367
foreignAlias
13631368
);
@@ -1732,8 +1737,14 @@ export class FieldCteVisitor implements IFieldVisitor<ICteResult> {
17321737

17331738
const fieldReferenceSelectionMap = new Map<string, string>();
17341739
const fieldReferenceFieldMap = new Map<string, FieldCore>();
1740+
// For self-table conditional lookups, resolve field references against the foreign alias
1741+
// so comparisons like "Status is {Title}" are evaluated on the foreign row.
1742+
const referenceAlias = foreignTable.id === table.id ? foreignAliasUsed : mainAlias;
17351743
for (const mainField of table.fields.ordered) {
1736-
fieldReferenceSelectionMap.set(mainField.id, `"${mainAlias}"."${mainField.dbFieldName}"`);
1744+
fieldReferenceSelectionMap.set(
1745+
mainField.id,
1746+
`"${referenceAlias}"."${mainField.dbFieldName}"`
1747+
);
17371748
fieldReferenceFieldMap.set(mainField.id, mainField as FieldCore);
17381749
}
17391750

@@ -1922,8 +1933,14 @@ export class FieldCteVisitor implements IFieldVisitor<ICteResult> {
19221933

19231934
const fieldReferenceSelectionMap = new Map<string, string>();
19241935
const fieldReferenceFieldMap = new Map<string, FieldCore>();
1936+
// For self-table conditional lookups, resolve field references against the foreign alias
1937+
// so comparisons like "Status is {Title}" are evaluated on the foreign row.
1938+
const referenceAlias = foreignTable.id === table.id ? foreignAliasUsed : mainAlias;
19251939
for (const mainField of table.fields.ordered) {
1926-
fieldReferenceSelectionMap.set(mainField.id, `"${mainAlias}"."${mainField.dbFieldName}"`);
1940+
fieldReferenceSelectionMap.set(
1941+
mainField.id,
1942+
`"${referenceAlias}"."${mainField.dbFieldName}"`
1943+
);
19271944
fieldReferenceFieldMap.set(mainField.id, mainField as FieldCore);
19281945
}
19291946

apps/nestjs-backend/test/conditional-lookup.e2e-spec.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,168 @@ describe('OpenAPI Conditional Lookup field (e2e)', () => {
10031003
});
10041004
});
10051005

1006+
describe('self-table field-reference lookups projecting alternate fields', () => {
1007+
let table: ITableFullVo;
1008+
let titleId: string;
1009+
let nameId: string;
1010+
let nameMirrorId: string;
1011+
let title2Id: string;
1012+
let matchingLookupField: IFieldVo;
1013+
let rowAliceId: string;
1014+
let rowBobId: string;
1015+
let rowCharlieId: string;
1016+
let rowDaveId: string;
1017+
1018+
beforeAll(async () => {
1019+
table = await createTable(baseId, {
1020+
name: 'ConditionalLookup_Self_AltProjection',
1021+
fields: [
1022+
{ name: 'Title', type: FieldType.SingleLineText } as IFieldRo,
1023+
{ name: 'Name', type: FieldType.SingleLineText } as IFieldRo,
1024+
{ name: 'NameMirror', type: FieldType.SingleLineText } as IFieldRo,
1025+
{ name: 'Title2', type: FieldType.SingleLineText } as IFieldRo,
1026+
],
1027+
records: [
1028+
{ fields: { Title: 'T1', Name: 'Alice', NameMirror: 'Alice', Title2: 'T1-alt' } },
1029+
{ fields: { Title: 'T2', Name: 'Bob', NameMirror: 'Alice', Title2: 'T2-alt' } },
1030+
{ fields: { Title: 'T3', Name: 'Charlie', NameMirror: 'Charlie', Title2: 'T3-alt' } },
1031+
{ fields: { Title: 'T4', Name: 'Dave', Title2: 'T4-alt' } },
1032+
],
1033+
});
1034+
1035+
titleId = table.fields.find((f) => f.name === 'Title')!.id;
1036+
nameId = table.fields.find((f) => f.name === 'Name')!.id;
1037+
nameMirrorId = table.fields.find((f) => f.name === 'NameMirror')!.id;
1038+
title2Id = table.fields.find((f) => f.name === 'Title2')!.id;
1039+
1040+
rowAliceId = table.records[0].id;
1041+
rowBobId = table.records[1].id;
1042+
rowCharlieId = table.records[2].id;
1043+
rowDaveId = table.records[3].id;
1044+
1045+
const filter: IFilter = {
1046+
conjunction: 'and',
1047+
filterSet: [
1048+
{
1049+
fieldId: nameMirrorId,
1050+
operator: 'is',
1051+
value: { type: 'field', fieldId: nameId },
1052+
},
1053+
],
1054+
};
1055+
1056+
matchingLookupField = await createField(table.id, {
1057+
name: 'Matching Title2 Values',
1058+
type: FieldType.SingleLineText,
1059+
isLookup: true,
1060+
isConditionalLookup: true,
1061+
lookupOptions: {
1062+
foreignTableId: table.id,
1063+
lookupFieldId: title2Id,
1064+
filter,
1065+
} as ILookupOptionsRo,
1066+
} as IFieldRo);
1067+
});
1068+
1069+
afterAll(async () => {
1070+
await permanentDeleteTable(baseId, table.id);
1071+
});
1072+
1073+
it('should project the requested field from matching self-table rows', async () => {
1074+
const records = await getRecords(table.id, { fieldKeyType: FieldKeyType.Id });
1075+
const rowAlice = records.records.find((r) => r.id === rowAliceId)!;
1076+
const rowBob = records.records.find((r) => r.id === rowBobId)!;
1077+
const rowCharlie = records.records.find((r) => r.id === rowCharlieId)!;
1078+
const rowDave = records.records.find((r) => r.id === rowDaveId)!;
1079+
1080+
expect(rowAlice.fields[matchingLookupField.id]).toEqual(['T1-alt']);
1081+
expect(rowBob.fields[matchingLookupField.id]).toEqual(['T1-alt']);
1082+
expect(rowCharlie.fields[matchingLookupField.id]).toEqual(['T3-alt']);
1083+
expect(rowDave.fields[matchingLookupField.id] ?? []).toEqual([]);
1084+
});
1085+
});
1086+
1087+
describe('self-table field-reference lookups selecting alternate titles', () => {
1088+
let table: ITableFullVo;
1089+
let titleId: string;
1090+
let nameId: string;
1091+
let name2Id: string;
1092+
let title2Id: string;
1093+
let lookupAltTitleField: IFieldVo;
1094+
let row1Id: string;
1095+
let row2Id: string;
1096+
let row3Id: string;
1097+
let row4Id: string;
1098+
1099+
beforeAll(async () => {
1100+
table = await createTable(baseId, {
1101+
name: 'ConditionalLookup_Self_Title2',
1102+
fields: [
1103+
{ name: 'Title', type: FieldType.SingleLineText } as IFieldRo,
1104+
{ name: 'Name', type: FieldType.SingleLineText } as IFieldRo,
1105+
{ name: 'Name2', type: FieldType.SingleLineText } as IFieldRo,
1106+
{ name: 'Title2', type: FieldType.SingleLineText } as IFieldRo,
1107+
],
1108+
records: [
1109+
{ fields: { Title: '00001', Name: '张三', Name2: '张三', Title2: '00001' } },
1110+
{ fields: { Title: '00002', Name: '李四', Name2: null, Title2: null } },
1111+
{ fields: { Title: '00003', Name: '王五', Name2: '李四', Title2: '00002' } },
1112+
{ fields: { Title: '00004', Name: '赵六', Name2: '你好', Title2: null } },
1113+
],
1114+
});
1115+
1116+
titleId = table.fields.find((f) => f.name === 'Title')!.id;
1117+
nameId = table.fields.find((f) => f.name === 'Name')!.id;
1118+
name2Id = table.fields.find((f) => f.name === 'Name2')!.id;
1119+
title2Id = table.fields.find((f) => f.name === 'Title2')!.id;
1120+
1121+
row1Id = table.records[0].id;
1122+
row2Id = table.records[1].id;
1123+
row3Id = table.records[2].id;
1124+
row4Id = table.records[3].id;
1125+
1126+
const filter: IFilter = {
1127+
conjunction: 'and',
1128+
filterSet: [
1129+
{
1130+
fieldId: name2Id,
1131+
operator: 'is',
1132+
value: { type: 'field', fieldId: nameId },
1133+
},
1134+
],
1135+
};
1136+
1137+
lookupAltTitleField = await createField(table.id, {
1138+
name: 'Title2 via matching Name2',
1139+
type: FieldType.SingleLineText,
1140+
isLookup: true,
1141+
isConditionalLookup: true,
1142+
lookupOptions: {
1143+
foreignTableId: table.id,
1144+
lookupFieldId: title2Id,
1145+
filter,
1146+
} as ILookupOptionsRo,
1147+
} as IFieldRo);
1148+
});
1149+
1150+
afterAll(async () => {
1151+
await permanentDeleteTable(baseId, table.id);
1152+
});
1153+
1154+
it('should return Title2 from foreign rows where host Name2 matches foreign Name', async () => {
1155+
const records = await getRecords(table.id, { fieldKeyType: FieldKeyType.Id });
1156+
const row1 = records.records.find((r) => r.id === row1Id)!;
1157+
const row2 = records.records.find((r) => r.id === row2Id)!;
1158+
const row3 = records.records.find((r) => r.id === row3Id)!;
1159+
const row4 = records.records.find((r) => r.id === row4Id)!;
1160+
1161+
expect(row1.fields[lookupAltTitleField.id]).toEqual(['00001']);
1162+
expect(row2.fields[lookupAltTitleField.id] ?? []).toEqual([]);
1163+
expect(row3.fields[lookupAltTitleField.id] ?? []).toEqual([]);
1164+
expect(row4.fields[lookupAltTitleField.id] ?? []).toEqual([]);
1165+
});
1166+
});
1167+
10061168
describe('boolean field reference filters', () => {
10071169
let foreign: ITableFullVo;
10081170
let host: ITableFullVo;

0 commit comments

Comments
 (0)