• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

teableio / teable / 18796737877

25 Oct 2025 02:18AM UTC coverage: 75.183% (+0.03%) from 75.153%
18796737877

Pull #2042

github

web-flow
Merge fb797848f into 48774d441
Pull Request #2042: fix/conditional rollup bugs

10263 of 11059 branches covered (92.8%)

322 of 416 new or added lines in 9 files covered. (77.4%)

8 existing lines in 5 files now uncovered.

50961 of 67783 relevant lines covered (75.18%)

4504.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.22
/apps/nestjs-backend/src/features/record/query-builder/field-cte-visitor.ts
1
/* eslint-disable sonarjs/cognitive-complexity */
2✔
2
/* eslint-disable sonarjs/no-duplicated-branches */
2✔
3
/* eslint-disable sonarjs/no-duplicate-string */
2✔
4
/* eslint-disable @typescript-eslint/naming-convention */
2✔
5
/* eslint-disable @typescript-eslint/no-empty-function */
2✔
6
import { Logger } from '@nestjs/common';
7
import {
8
  DriverClient,
9
  FieldType,
10
  Relationship,
11
  type IFilter,
12
  type IFilterItem,
13
  type IFieldVisitor,
14
  type AttachmentFieldCore,
15
  type AutoNumberFieldCore,
16
  type CheckboxFieldCore,
17
  type CreatedByFieldCore,
18
  type CreatedTimeFieldCore,
19
  type DateFieldCore,
20
  type FormulaFieldCore,
21
  type LastModifiedByFieldCore,
22
  type LastModifiedTimeFieldCore,
23
  type LinkFieldCore,
24
  type LongTextFieldCore,
25
  type MultipleSelectFieldCore,
26
  type NumberFieldCore,
27
  type RatingFieldCore,
28
  type RollupFieldCore,
29
  type ConditionalRollupFieldCore,
30
  type IConditionalLookupOptions,
31
  type SingleLineTextFieldCore,
32
  type SingleSelectFieldCore,
33
  type UserFieldCore,
34
  type ButtonFieldCore,
35
  type Tables,
36
  type TableDomain,
37
  type ILinkFieldOptions,
38
  type FieldCore,
39
  type IRollupFieldOptions,
40
  DbFieldType,
41
  SortFunc,
42
  isFieldReferenceValue,
43
  isLinkLookupOptions,
44
  normalizeConditionalLimit,
45
  contains as FilterOperatorContains,
46
  doesNotContain as FilterOperatorDoesNotContain,
47
  hasAllOf as FilterOperatorHasAllOf,
48
  hasAnyOf as FilterOperatorHasAnyOf,
49
  hasNoneOf as FilterOperatorHasNoneOf,
50
  is as FilterOperatorIs,
51
  isAfter as FilterOperatorIsAfter,
52
  isAnyOf as FilterOperatorIsAnyOf,
53
  isBefore as FilterOperatorIsBefore,
54
  isExactly as FilterOperatorIsExactly,
55
  isGreater as FilterOperatorIsGreater,
56
  isGreaterEqual as FilterOperatorIsGreaterEqual,
57
  isLess as FilterOperatorIsLess,
58
  isLessEqual as FilterOperatorIsLessEqual,
59
  isNoneOf as FilterOperatorIsNoneOf,
60
  isNotEmpty as FilterOperatorIsNotEmpty,
61
  isNotExactly as FilterOperatorIsNotExactly,
62
  isEmpty as FilterOperatorIsEmpty,
63
  isOnOrAfter as FilterOperatorIsOnOrAfter,
64
  isOnOrBefore as FilterOperatorIsOnOrBefore,
65
} from '@teable/core';
66
import type { Knex } from 'knex';
67
import { match } from 'ts-pattern';
68
import type { IDbProvider } from '../../../db-provider/db.provider.interface';
69
import { ID_FIELD_NAME } from '../../field/constant';
70
import { FieldFormattingVisitor } from './field-formatting-visitor';
71
import { FieldSelectVisitor } from './field-select-visitor';
72
import type { IFieldSelectName } from './field-select.type';
73
import type {
74
  IMutableQueryBuilderState,
75
  IReadonlyQueryBuilderState,
76
} from './record-query-builder.interface';
77
import { RecordQueryBuilderManager, ScopedSelectionState } from './record-query-builder.manager';
78
import {
79
  getLinkUsesJunctionTable,
80
  getTableAliasFromTable,
81
  getOrderedFieldsByProjection,
82
  isDateLikeField,
83
} from './record-query-builder.util';
84
import type { IRecordQueryDialectProvider } from './record-query-dialect.interface';
85

86
type ICteResult = void;
87

88
const JUNCTION_ALIAS = 'j';
2✔
89

90
const SUPPORTED_EQUALITY_RESIDUAL_OPERATORS = new Set<string>([
2✔
91
  FilterOperatorIs.value,
2✔
92
  FilterOperatorContains.value,
2✔
93
  FilterOperatorDoesNotContain.value,
2✔
94
  FilterOperatorIsGreater.value,
2✔
95
  FilterOperatorIsGreaterEqual.value,
2✔
96
  FilterOperatorIsLess.value,
2✔
97
  FilterOperatorIsLessEqual.value,
2✔
98
  FilterOperatorIsEmpty.value,
2✔
99
  FilterOperatorIsNotEmpty.value,
2✔
100
  FilterOperatorIsAnyOf.value,
2✔
101
  FilterOperatorIsNoneOf.value,
2✔
102
  FilterOperatorHasAnyOf.value,
2✔
103
  FilterOperatorHasAllOf.value,
2✔
104
  FilterOperatorHasNoneOf.value,
2✔
105
  FilterOperatorIsExactly.value,
2✔
106
  FilterOperatorIsNotExactly.value,
2✔
107
  FilterOperatorIsBefore.value,
2✔
108
  FilterOperatorIsAfter.value,
2✔
109
  FilterOperatorIsOnOrBefore.value,
2✔
110
  FilterOperatorIsOnOrAfter.value,
2✔
111
]);
2✔
112

113
class FieldCteSelectionVisitor implements IFieldVisitor<IFieldSelectName> {
16,856✔
114
  constructor(
16,856✔
115
    private readonly qb: Knex.QueryBuilder,
16,856✔
116
    private readonly dbProvider: IDbProvider,
16,856✔
117
    private readonly dialect: IRecordQueryDialectProvider,
16,856✔
118
    private readonly table: TableDomain,
16,856✔
119
    private readonly foreignTable: TableDomain,
16,856✔
120
    private readonly state: IReadonlyQueryBuilderState,
16,856✔
121
    private readonly joinedCtes?: Set<string>, // Track which CTEs are already JOINed in current scope
16,856✔
122
    private readonly isSingleValueRelationshipContext: boolean = false, // In ManyOne/OneOne CTEs, avoid aggregates
16,856✔
123
    private readonly foreignAliasOverride?: string,
16,856✔
124
    private readonly currentLinkFieldId?: string
16,856✔
125
  ) {}
16,856✔
126
  private get fieldCteMap() {
16,856✔
127
    return this.state.getFieldCteMap();
35✔
128
  }
35✔
129
  private getForeignAlias(): string {
16,856✔
130
    return this.foreignAliasOverride || getTableAliasFromTable(this.foreignTable);
20,835!
131
  }
20,835✔
132
  private getJsonAggregationFunction(fieldReference: string): string {
16,856✔
133
    return this.dialect.jsonAggregateNonNull(fieldReference);
94✔
134
  }
94✔
135
  /**
16,856✔
136
   * Build a subquery (SELECT 1 WHERE ...) for foreign table filter using provider's filterQuery.
137
   * The subquery references the current foreign alias in-scope and carries proper bindings.
138
   */
16,856✔
139
  private buildForeignFilterSubquery(filter: IFilter): string {
16,856✔
140
    const foreignAlias = this.getForeignAlias();
42✔
141
    // Build selectionMap mapping foreign field ids to alias-qualified columns
42✔
142
    const selectionMap = new Map<string, string>();
42✔
143
    for (const f of this.foreignTable.fields.ordered) {
42✔
144
      selectionMap.set(f.id, `"${foreignAlias}"."${f.dbFieldName}"`);
217✔
145
    }
217✔
146
    // Build field map for filter compiler
42✔
147
    const fieldMap = this.foreignTable.fieldList.reduce(
42✔
148
      (map, f) => {
42✔
149
        map[f.id] = f as FieldCore;
217✔
150
        return map;
217✔
151
      },
217✔
152
      {} as Record<string, FieldCore>
42✔
153
    );
154
    // Build subquery with WHERE conditions
42✔
155
    const sub = this.qb.client.queryBuilder().select(this.qb.client.raw('1'));
42✔
156
    this.dbProvider
42✔
157
      .filterQuery(sub, fieldMap, filter, undefined, { selectionMap } as unknown as {
42✔
158
        selectionMap: Map<string, string>;
159
      })
160
      .appendQueryBuilder();
42✔
161
    return `(${sub.toQuery()})`;
42✔
162
  }
42✔
163
  /**
16,856✔
164
   * Generate rollup aggregation expression based on rollup function
165
   */
16,856✔
166
  // eslint-disable-next-line sonarjs/cognitive-complexity
16,856✔
167
  private generateRollupAggregation(
16,856✔
168
    expression: string,
1,136✔
169
    fieldExpression: string,
1,136✔
170
    targetField: FieldCore,
1,136✔
171
    orderByField?: string,
1,136✔
172
    rowPresenceExpr?: string
1,136✔
173
  ): string {
1,136✔
174
    // Parse the rollup function from expression like 'sum({values})'
1,136✔
175
    const functionMatch = expression.match(/^(\w+)\(\{values\}\)$/);
1,136✔
176
    if (!functionMatch) {
1,136!
177
      throw new Error(`Invalid rollup expression: ${expression}`);
×
178
    }
×
179
    const functionName = functionMatch[1].toLowerCase();
1,136✔
180
    return this.dialect.rollupAggregate(functionName, fieldExpression, {
1,136✔
181
      targetField,
1,136✔
182
      orderByField,
1,136✔
183
      rowPresenceExpr,
1,136✔
184
    });
1,136✔
185
  }
1,136✔
186

187
  /**
16,856✔
188
   * Generate rollup expression for single-value relationships (ManyOne/OneOne)
189
   * Avoids using aggregate functions so GROUP BY is not required.
190
   */
16,856✔
191
  private generateSingleValueRollupAggregation(
16,856✔
192
    rollupField: FieldCore,
1,015✔
193
    targetField: FieldCore,
1,015✔
194
    expression: string,
1,015✔
195
    fieldExpression: string
1,015✔
196
  ): string {
1,015✔
197
    const functionMatch = expression.match(/^(\w+)\(\{values\}\)$/);
1,015✔
198
    if (!functionMatch) {
1,015!
199
      throw new Error(`Invalid rollup expression: ${expression}`);
×
200
    }
×
201

202
    const functionName = functionMatch[1].toLowerCase();
1,015✔
203

204
    return this.dialect.singleValueRollupAggregate(functionName, fieldExpression, {
1,015✔
205
      rollupField,
1,015✔
206
      targetField,
1,015✔
207
    });
1,015✔
208
  }
1,015✔
209
  private buildSingleValueRollup(
16,856✔
210
    field: FieldCore,
1,015✔
211
    targetField: FieldCore,
1,015✔
212
    expression: string
1,015✔
213
  ): string {
1,015✔
214
    const rollupOptions = field.options as IRollupFieldOptions;
1,015✔
215
    const rollupFilter = (field as FieldCore).getFilter?.();
1,015✔
216
    if (rollupFilter) {
1,015!
217
      const sub = this.buildForeignFilterSubquery(rollupFilter);
×
218
      const filteredExpr =
×
219
        this.dbProvider.driver === DriverClient.Pg
×
220
          ? `CASE WHEN EXISTS ${sub} THEN ${expression} ELSE NULL END`
×
221
          : expression;
×
NEW
222
      return this.generateSingleValueRollupAggregation(
×
NEW
223
        field,
×
NEW
224
        targetField,
×
NEW
225
        rollupOptions.expression,
×
NEW
226
        filteredExpr
×
227
      );
UNCOV
228
    }
×
229
    return this.generateSingleValueRollupAggregation(
1,015✔
230
      field,
1,015✔
231
      targetField,
1,015✔
232
      rollupOptions.expression,
1,015✔
233
      expression
1,015✔
234
    );
235
  }
1,015✔
236
  private buildAggregateRollup(
16,856✔
237
    rollupField: FieldCore,
1,136✔
238
    targetField: FieldCore,
1,136✔
239
    expression: string
1,136✔
240
  ): string {
1,136✔
241
    const linkField = rollupField.getLinkField(this.table);
1,136✔
242
    const options = linkField?.options as ILinkFieldOptions | undefined;
1,136✔
243
    const rollupOptions = rollupField.options as IRollupFieldOptions;
1,136✔
244

245
    let orderByField: string | undefined;
1,136✔
246
    if (this.dbProvider.driver === DriverClient.Pg && linkField && options) {
1,136✔
247
      const usesJunctionTable = getLinkUsesJunctionTable(linkField);
1,136✔
248
      const hasOrderColumn = linkField.getHasOrderColumn();
1,136✔
249
      if (usesJunctionTable) {
1,136✔
250
        orderByField = hasOrderColumn
396✔
251
          ? `${JUNCTION_ALIAS}."${linkField.getOrderColumnName()}" IS NULL DESC, ${JUNCTION_ALIAS}."${linkField.getOrderColumnName()}" ASC, ${JUNCTION_ALIAS}."__id" ASC`
392✔
252
          : `${JUNCTION_ALIAS}."__id" ASC`;
396✔
253
      } else if (options.relationship === Relationship.OneMany) {
1,136✔
254
        const foreignAlias = this.getForeignAlias();
740✔
255
        orderByField = hasOrderColumn
740✔
256
          ? `"${foreignAlias}"."${linkField.getOrderColumnName()}" IS NULL DESC, "${foreignAlias}"."${linkField.getOrderColumnName()}" ASC, "${foreignAlias}"."__id" ASC`
740✔
257
          : `"${foreignAlias}"."__id" ASC`;
740!
258
      }
740✔
259
    }
1,136✔
260

261
    const rowPresenceField = `"${this.getForeignAlias()}"."__id"`;
1,136✔
262

263
    const rollupFilter = (rollupField as FieldCore).getFilter?.();
1,136✔
264
    if (rollupFilter && this.dbProvider.driver === DriverClient.Pg) {
1,136!
265
      const sub = this.buildForeignFilterSubquery(rollupFilter);
×
266
      const filteredExpr = `CASE WHEN EXISTS ${sub} THEN ${expression} ELSE NULL END`;
×
267
      return this.generateRollupAggregation(
×
268
        rollupOptions.expression,
×
269
        filteredExpr,
×
270
        targetField,
×
271
        orderByField,
×
272
        rowPresenceField
×
273
      );
274
    }
×
275

276
    return this.generateRollupAggregation(
1,136✔
277
      rollupOptions.expression,
1,136✔
278
      expression,
1,136✔
279
      targetField,
1,136✔
280
      orderByField,
1,136✔
281
      rowPresenceField
1,136✔
282
    );
283
  }
1,136✔
284
  private visitLookupField(field: FieldCore): IFieldSelectName {
16,856✔
285
    if (!field.isLookup) {
3,285!
286
      throw new Error('Not a lookup field');
×
287
    }
×
288

289
    // If this lookup field is marked as error, don't attempt to resolve.
3,285✔
290
    // Emit a typed NULL so the expression matches the physical column.
3,285✔
291
    if (field.hasError) {
3,285✔
292
      return this.dialect.typedNullFor(field.dbFieldType);
62✔
293
    }
62✔
294

295
    if (field.isConditionalLookup) {
3,285!
296
      const cteName = this.fieldCteMap.get(field.id);
×
297
      if (!cteName) {
×
298
        return this.dialect.typedNullFor(field.dbFieldType);
×
299
      }
×
300
      return `"${cteName}"."conditional_lookup_${field.id}"`;
×
301
    }
✔
302

303
    const qb = this.qb.client.queryBuilder();
3,223✔
304
    const selectVisitor = new FieldSelectVisitor(
3,223✔
305
      qb,
3,223✔
306
      this.dbProvider,
3,223✔
307
      this.foreignTable,
3,223✔
308
      new ScopedSelectionState(this.state),
3,223✔
309
      this.dialect,
3,223✔
310
      undefined,
3,223✔
311
      true,
3,223✔
312
      true
3,223✔
313
    );
314

315
    const foreignAlias = this.getForeignAlias();
3,223✔
316
    const targetLookupField = field.getForeignLookupField(this.foreignTable);
3,223✔
317

318
    if (!targetLookupField) {
3,285✔
319
      // Try to fetch via the CTE of the foreign link if present
81✔
320
      const nestedLinkFieldId = getLinkFieldId(field.lookupOptions);
81✔
321
      const fieldCteMap = this.state.getFieldCteMap();
81✔
322
      // Guard against self-referencing the CTE being defined (would require WITH RECURSIVE)
81✔
323
      if (
81✔
324
        nestedLinkFieldId &&
81✔
325
        fieldCteMap.has(nestedLinkFieldId) &&
81✔
326
        nestedLinkFieldId !== this.currentLinkFieldId
81✔
327
      ) {
81✔
328
        const nestedCteName = fieldCteMap.get(nestedLinkFieldId)!;
80✔
329
        // Check if this CTE is JOINed in current scope
80✔
330
        if (this.joinedCtes?.has(nestedLinkFieldId)) {
80!
331
          const linkExpr = `"${nestedCteName}"."link_value"`;
×
332
          return this.isSingleValueRelationshipContext
×
333
            ? linkExpr
×
334
            : field.isMultipleCellValue
×
335
              ? this.getJsonAggregationFunction(linkExpr)
×
336
              : linkExpr;
×
337
        } else {
80✔
338
          // Fallback to subquery if CTE not JOINed in current scope
80✔
339
          const linkExpr = `((SELECT link_value FROM "${nestedCteName}" WHERE "${nestedCteName}"."main_record_id" = "${foreignAlias}"."${ID_FIELD_NAME}"))`;
80✔
340
          return this.isSingleValueRelationshipContext
80✔
341
            ? linkExpr
30✔
342
            : field.isMultipleCellValue
50✔
343
              ? this.getJsonAggregationFunction(linkExpr)
50!
344
              : linkExpr;
×
345
        }
80✔
346
      }
80✔
347
      // If still not found or field has error, return NULL instead of throwing
1✔
348
      return this.dialect.typedNullFor(field.dbFieldType);
1✔
349
    }
1✔
350

351
    // If the target is a Link field, read its link_value from the JOINed CTE or subquery
3,142✔
352
    if (targetLookupField.type === FieldType.Link) {
3,271✔
353
      const nestedLinkFieldId = (targetLookupField as LinkFieldCore).id;
80✔
354
      const fieldCteMap = this.state.getFieldCteMap();
80✔
355
      if (fieldCteMap.has(nestedLinkFieldId) && nestedLinkFieldId !== this.currentLinkFieldId) {
80✔
356
        const nestedCteName = fieldCteMap.get(nestedLinkFieldId)!;
80✔
357
        // Check if this CTE is JOINed in current scope
80✔
358
        if (this.joinedCtes?.has(nestedLinkFieldId)) {
80!
359
          const linkExpr = `"${nestedCteName}"."link_value"`;
×
360
          return this.isSingleValueRelationshipContext
×
361
            ? linkExpr
×
362
            : field.isMultipleCellValue
×
363
              ? this.getJsonAggregationFunction(linkExpr)
×
364
              : linkExpr;
×
365
        } else {
80✔
366
          // Fallback to subquery if CTE not JOINed in current scope
80✔
367
          const linkExpr = `((SELECT link_value FROM "${nestedCteName}" WHERE "${nestedCteName}"."main_record_id" = "${foreignAlias}"."${ID_FIELD_NAME}"))`;
80✔
368
          return this.isSingleValueRelationshipContext
80✔
369
            ? linkExpr
36✔
370
            : field.isMultipleCellValue
44✔
371
              ? this.getJsonAggregationFunction(linkExpr)
44!
372
              : linkExpr;
×
373
        }
80✔
374
      }
80!
375
      // If self-referencing or missing, return NULL
×
376
      return this.dialect.typedNullFor(field.dbFieldType);
×
377
    }
✔
378

379
    // If the target is a Rollup field, read its precomputed rollup value from the link CTE
3,062✔
380
    if (targetLookupField.type === FieldType.Rollup) {
3,257✔
381
      const rollupField = targetLookupField as RollupFieldCore;
14✔
382
      const rollupLinkField = rollupField.getLinkField(this.foreignTable);
14✔
383
      if (rollupLinkField) {
14✔
384
        const nestedLinkFieldId = rollupLinkField.id;
14✔
385
        if (this.fieldCteMap.has(nestedLinkFieldId)) {
14✔
386
          const nestedCteName = this.fieldCteMap.get(nestedLinkFieldId)!;
14✔
387
          let expr: string;
14✔
388
          if (this.joinedCtes?.has(nestedLinkFieldId)) {
14✔
389
            expr = `"${nestedCteName}"."rollup_${rollupField.id}"`;
14✔
390
          } else {
14!
391
            expr = `((SELECT "rollup_${rollupField.id}" FROM "${nestedCteName}" WHERE "${nestedCteName}"."main_record_id" = "${foreignAlias}"."${ID_FIELD_NAME}"))`;
×
392
          }
×
393
          return this.isSingleValueRelationshipContext
14✔
394
            ? expr
14!
395
            : field.isMultipleCellValue
×
396
              ? this.getJsonAggregationFunction(expr)
×
397
              : expr;
×
398
        }
14✔
399
      }
14✔
400
    }
14✔
401

402
    // If the target is itself a lookup, reference its precomputed value from the JOINed CTE or subquery
3,048✔
403
    let expression: string;
3,048✔
404
    if (targetLookupField.isLookup) {
3,257✔
405
      const nestedLinkFieldId = getLinkFieldId(targetLookupField.lookupOptions);
48✔
406
      const fieldCteMap = this.state.getFieldCteMap();
48✔
407
      // Prefer nested CTE if available; otherwise, derive CTE name and use subquery
48✔
408
      if (nestedLinkFieldId) {
48✔
409
        // Derive CTE name deterministically to reference the pre-generated nested CTE
48✔
410
        const derivedCteName = `CTE_${getTableAliasFromTable(this.foreignTable)}_${nestedLinkFieldId}`;
48✔
411
        const nestedCteName = fieldCteMap.get(nestedLinkFieldId) ?? derivedCteName;
48!
412
        if (nestedCteName) {
48✔
413
          if (this.joinedCtes?.has(nestedLinkFieldId)) {
48✔
414
            expression = `"${nestedCteName}"."lookup_${targetLookupField.id}"`;
35✔
415
          } else {
41✔
416
            expression = `((SELECT "lookup_${targetLookupField.id}" FROM "${nestedCteName}" WHERE "${nestedCteName}"."main_record_id" = "${foreignAlias}"."${ID_FIELD_NAME}"))`;
13✔
417
          }
13✔
418
        } else {
48!
419
          // As a last resort, fallback to direct select using select visitor
×
420
          const targetFieldResult = targetLookupField.accept(selectVisitor);
×
421
          expression =
×
422
            typeof targetFieldResult === 'string'
×
423
              ? targetFieldResult
×
424
              : targetFieldResult.toSQL().sql;
×
425
        }
×
426
      } else {
48!
427
        const targetFieldResult = targetLookupField.accept(selectVisitor);
×
428
        expression =
×
429
          typeof targetFieldResult === 'string' ? targetFieldResult : targetFieldResult.toSQL().sql;
×
430
      }
×
431
    } else {
3,257✔
432
      const targetFieldResult = targetLookupField.accept(selectVisitor);
3,000✔
433
      expression =
3,000✔
434
        typeof targetFieldResult === 'string' ? targetFieldResult : targetFieldResult.toSQL().sql;
3,000!
435
      // Self-join: ensure expression uses the foreign alias override
3,000✔
436
      const defaultForeignAlias = getTableAliasFromTable(this.foreignTable);
3,000✔
437
      if (defaultForeignAlias !== foreignAlias) {
3,000✔
438
        expression = expression.replaceAll(`"${defaultForeignAlias}"`, `"${foreignAlias}"`);
7✔
439
      }
7✔
440

441
      // For Postgres multi-value lookups targeting datetime-like fields, normalize the
3,000✔
442
      // element expression to an ISO8601 UTC string so downstream JSON comparisons using
3,000✔
443
      // lexicographical ranges (jsonpath @ >= "..." && @ <= "...") behave correctly.
3,000✔
444
      // Do NOT alter single-value lookups to preserve native type comparisons in filters.
3,000✔
445
      if (
3,000✔
446
        this.dbProvider.driver === DriverClient.Pg &&
3,000✔
447
        field.isMultipleCellValue &&
3,000✔
448
        isDateLikeField(targetLookupField)
2,023✔
449
      ) {
3,000✔
450
        // Format: 2020-01-10T16:00:00.000Z
108✔
451
        expression = `to_char(${expression} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`;
108✔
452
      }
108✔
453
    }
3,000✔
454
    // Build deterministic order-by for multi-value lookups using the link field configuration
3,048✔
455
    const linkForOrderingId = getLinkFieldId(field.lookupOptions);
3,048✔
456
    let orderByClause: string | undefined;
3,048✔
457
    if (linkForOrderingId) {
3,048✔
458
      try {
3,048✔
459
        const linkForOrdering = this.table.getField(linkForOrderingId) as LinkFieldCore;
3,048✔
460
        const usesJunctionTable = getLinkUsesJunctionTable(linkForOrdering);
3,048✔
461
        const hasOrderColumn = linkForOrdering.getHasOrderColumn();
3,048✔
462
        if (this.dbProvider.driver === DriverClient.Pg) {
3,048✔
463
          if (usesJunctionTable) {
3,048✔
464
            orderByClause = hasOrderColumn
886✔
465
              ? `${JUNCTION_ALIAS}."${linkForOrdering.getOrderColumnName()}" IS NULL DESC, ${JUNCTION_ALIAS}."${linkForOrdering.getOrderColumnName()}" ASC, ${JUNCTION_ALIAS}."__id" ASC`
876✔
466
              : `${JUNCTION_ALIAS}."__id" ASC`;
886✔
467
          } else {
3,048✔
468
            orderByClause = hasOrderColumn
2,162✔
469
              ? `"${foreignAlias}"."${linkForOrdering.getOrderColumnName()}" IS NULL DESC, "${foreignAlias}"."${linkForOrdering.getOrderColumnName()}" ASC, "${foreignAlias}"."__id" ASC`
2,162✔
470
              : `"${foreignAlias}"."__id" ASC`;
2,162!
471
          }
2,162✔
472
        }
3,048✔
473
      } catch (_) {
3,048!
474
        // ignore ordering if link field not found in current table context
×
475
      }
×
476
    }
3,048✔
477

478
    // Field-specific filter applied here
3,048✔
479
    const filter = field.getFilter?.();
3,048✔
480
    if (!filter) {
3,285✔
481
      if (!field.isMultipleCellValue || this.isSingleValueRelationshipContext) {
3,012✔
482
        return expression;
1,052✔
483
      }
1,052✔
484
      if (this.dbProvider.driver === DriverClient.Pg && orderByClause) {
3,012✔
485
        return `json_agg(${expression} ORDER BY ${orderByClause}) FILTER (WHERE ${expression} IS NOT NULL)`;
1,960✔
486
      }
1,960!
487
      // For SQLite, ensure deterministic ordering by aggregating from an ordered correlated subquery
×
488
      if (this.dbProvider.driver === DriverClient.Sqlite) {
×
489
        try {
×
490
          const linkForOrderingId = getLinkFieldId(field.lookupOptions);
×
491
          const fieldCteMap = this.state.getFieldCteMap();
×
492
          const mainAlias = getTableAliasFromTable(this.table);
×
493
          const foreignDb = this.foreignTable.dbTableName;
×
494
          // Prefer order from link CTE's JSON array (preserves insertion order)
×
495
          if (
×
496
            linkForOrderingId &&
×
497
            fieldCteMap.has(linkForOrderingId) &&
×
498
            this.joinedCtes?.has(linkForOrderingId) &&
×
499
            linkForOrderingId !== this.currentLinkFieldId
×
500
          ) {
×
501
            const cteName = fieldCteMap.get(linkForOrderingId)!;
×
502
            const exprForInner = expression.replaceAll(`"${this.getForeignAlias()}"`, '"f"');
×
503
            return `(
×
504
              SELECT CASE WHEN COUNT(*) > 0
505
                THEN json_group_array(CASE WHEN ${exprForInner} IS NOT NULL THEN ${exprForInner} END)
×
506
                ELSE NULL END
507
              FROM json_each(
508
                CASE
509
                  WHEN json_valid((SELECT "link_value" FROM "${cteName}" WHERE "${cteName}"."main_record_id" = "${mainAlias}"."__id"))
×
510
                   AND json_type((SELECT "link_value" FROM "${cteName}" WHERE "${cteName}"."main_record_id" = "${mainAlias}"."__id")) = 'array'
×
511
                  THEN (SELECT "link_value" FROM "${cteName}" WHERE "${cteName}"."main_record_id" = "${mainAlias}"."__id")
×
512
                  ELSE json('[]')
513
                END
514
              ) AS je
515
              JOIN "${foreignDb}" AS f ON f."__id" = json_extract(je.value, '$.id')
×
516
              ORDER BY je.key ASC
517
            )`;
×
518
          }
×
519
          // Fallback to FK/junction ordering using the current link field
×
520
          const baseLink = field as LinkFieldCore;
×
521
          const opts = baseLink.options as ILinkFieldOptions;
×
522
          const usesJunctionTable = getLinkUsesJunctionTable(baseLink);
×
523
          const hasOrderColumn = baseLink.getHasOrderColumn();
×
524
          const fkHost = opts.fkHostTableName!;
×
525
          const selfKey = opts.selfKeyName;
×
526
          const foreignKey = opts.foreignKeyName;
×
527
          const exprForInner = expression.replaceAll(`"${this.getForeignAlias()}"`, '"f"');
×
528
          if (usesJunctionTable) {
×
529
            const ordCol = hasOrderColumn ? `j."${baseLink.getOrderColumnName()}"` : undefined;
×
530
            const order = ordCol
×
531
              ? `(CASE WHEN ${ordCol} IS NULL THEN 0 ELSE 1 END) ASC, ${ordCol} ASC, j."__id" ASC`
×
532
              : `j."__id" ASC`;
×
533
            return `(
×
534
              SELECT CASE WHEN COUNT(*) > 0
535
                THEN json_group_array(CASE WHEN ${exprForInner} IS NOT NULL THEN ${exprForInner} END)
×
536
                ELSE NULL END
537
              FROM "${fkHost}" AS j
×
538
              JOIN "${foreignDb}" AS f ON j."${foreignKey}" = f."__id"
×
539
              WHERE j."${selfKey}" = "${mainAlias}"."__id"
×
540
              ORDER BY ${order}
×
541
            )`;
×
542
          }
×
543
          const ordCol = hasOrderColumn ? `f."${opts.selfKeyName}_order"` : undefined;
×
544
          const order = ordCol
×
545
            ? `(CASE WHEN ${ordCol} IS NULL THEN 0 ELSE 1 END) ASC, ${ordCol} ASC, f."__id" ASC`
×
546
            : `f."__id" ASC`;
×
547
          return `(
×
548
            SELECT CASE WHEN COUNT(*) > 0
549
              THEN json_group_array(CASE WHEN ${exprForInner} IS NOT NULL THEN ${exprForInner} END)
×
550
              ELSE NULL END
551
            FROM "${foreignDb}" AS f
×
552
            WHERE f."${selfKey}" = "${mainAlias}"."__id"
×
553
            ORDER BY ${order}
×
554
          )`;
×
555
        } catch (_) {
×
556
          // fallback to non-deterministic aggregation
×
557
        }
×
558
      }
×
559
      return this.getJsonAggregationFunction(expression);
×
560
    }
✔
561
    const sub = this.buildForeignFilterSubquery(filter);
36✔
562

563
    if (!field.isMultipleCellValue || this.isSingleValueRelationshipContext) {
3,285✔
564
      // Single value: conditionally null out for both PG and SQLite
×
565
      if (this.dbProvider.driver === DriverClient.Pg) {
×
566
        return `CASE WHEN EXISTS ${sub} THEN ${expression} ELSE NULL END`;
×
567
      }
×
568
      return `CASE WHEN EXISTS ${sub} THEN ${expression} ELSE NULL END`;
×
569
    }
✔
570

571
    if (this.dbProvider.driver === DriverClient.Pg) {
36✔
572
      if (orderByClause) {
36✔
573
        return `json_agg(${expression} ORDER BY ${orderByClause}) FILTER (WHERE (EXISTS ${sub}) AND ${expression} IS NOT NULL)`;
36✔
574
      }
36!
575
      return `json_agg(${expression}) FILTER (WHERE (EXISTS ${sub}) AND ${expression} IS NOT NULL)`;
×
576
    }
×
577

578
    // SQLite: use a correlated, ordered subquery to produce deterministic ordering
×
579
    try {
×
580
      const linkForOrderingId = getLinkFieldId(field.lookupOptions);
×
581
      const fieldCteMap = this.state.getFieldCteMap();
×
582
      const mainAlias = getTableAliasFromTable(this.table);
×
583
      const foreignDb = this.foreignTable.dbTableName;
×
584
      // Prefer order from link CTE JSON array
×
585
      if (
×
586
        linkForOrderingId &&
×
587
        fieldCteMap.has(linkForOrderingId) &&
×
588
        this.joinedCtes?.has(linkForOrderingId) &&
×
589
        linkForOrderingId !== this.currentLinkFieldId
×
590
      ) {
3,285!
591
        const cteName = fieldCteMap.get(linkForOrderingId)!;
×
592
        const exprForInner = expression.replaceAll(`"${this.getForeignAlias()}"`, '"f"');
×
593
        const subForInner = sub.replaceAll(`"${this.getForeignAlias()}"`, '"f"');
×
594
        return `(
×
595
          SELECT CASE WHEN SUM(CASE WHEN (EXISTS ${subForInner}) THEN 1 ELSE 0 END) > 0
×
596
            THEN json_group_array(CASE WHEN (EXISTS ${subForInner}) AND ${exprForInner} IS NOT NULL THEN ${exprForInner} END)
×
597
            ELSE NULL END
598
          FROM json_each(
599
            CASE
600
              WHEN json_valid((SELECT "link_value" FROM "${cteName}" WHERE "${cteName}"."main_record_id" = "${mainAlias}"."__id"))
×
601
               AND json_type((SELECT "link_value" FROM "${cteName}" WHERE "${cteName}"."main_record_id" = "${mainAlias}"."__id")) = 'array'
×
602
              THEN (SELECT "link_value" FROM "${cteName}" WHERE "${cteName}"."main_record_id" = "${mainAlias}"."__id")
×
603
              ELSE json('[]')
604
            END
605
          ) AS je
606
          JOIN "${foreignDb}" AS f ON f."__id" = json_extract(je.value, '$.id')
×
607
          ORDER BY je.key ASC
608
        )`;
×
609
      }
×
610
      if (linkForOrderingId) {
×
611
        const linkForOrdering = this.table.getField(linkForOrderingId) as LinkFieldCore;
×
612
        const opts = linkForOrdering.options as ILinkFieldOptions;
×
613
        const usesJunctionTable = getLinkUsesJunctionTable(linkForOrdering);
×
614
        const hasOrderColumn = linkForOrdering.getHasOrderColumn();
×
615
        const fkHost = opts.fkHostTableName!;
×
616
        const selfKey = opts.selfKeyName;
×
617
        const foreignKey = opts.foreignKeyName;
×
618
        // Adapt expression and filter subquery to inner alias "f"
×
619
        const exprForInner = expression.replaceAll(`"${this.getForeignAlias()}"`, '"f"');
×
620
        const subForInner = sub.replaceAll(`"${this.getForeignAlias()}"`, '"f"');
×
621
        if (usesJunctionTable) {
×
622
          const ordCol = hasOrderColumn ? `j."${linkForOrdering.getOrderColumnName()}"` : undefined;
×
623
          const order = ordCol
×
624
            ? `(CASE WHEN ${ordCol} IS NULL THEN 0 ELSE 1 END) ASC, ${ordCol} ASC, j."__id" ASC`
×
625
            : `j."__id" ASC`;
×
626
          return `(
×
627
            SELECT CASE WHEN SUM(CASE WHEN (EXISTS ${subForInner}) THEN 1 ELSE 0 END) > 0
×
628
              THEN json_group_array(CASE WHEN (EXISTS ${subForInner}) AND ${exprForInner} IS NOT NULL THEN ${exprForInner} END)
×
629
              ELSE NULL END
630
            FROM "${fkHost}" AS j
×
631
            JOIN "${foreignDb}" AS f ON j."${foreignKey}" = f."__id"
×
632
            WHERE j."${selfKey}" = "${mainAlias}"."__id"
×
633
            ORDER BY ${order}
×
634
          )`;
×
635
        } else {
×
636
          const ordCol = hasOrderColumn ? `f."${selfKey}_order"` : undefined;
×
637
          const order = ordCol
×
638
            ? `(CASE WHEN ${ordCol} IS NULL THEN 0 ELSE 1 END) ASC, ${ordCol} ASC, f."__id" ASC`
×
639
            : `f."__id" ASC`;
×
640
          return `(
×
641
            SELECT CASE WHEN SUM(CASE WHEN (EXISTS ${subForInner}) THEN 1 ELSE 0 END) > 0
×
642
              THEN json_group_array(CASE WHEN (EXISTS ${subForInner}) AND ${exprForInner} IS NOT NULL THEN ${exprForInner} END)
×
643
              ELSE NULL END
644
            FROM "${foreignDb}" AS f
×
645
            WHERE f."${selfKey}" = "${mainAlias}"."__id"
×
646
            ORDER BY ${order}
×
647
          )`;
×
648
        }
×
649
      }
×
650
      // Default ordering using the current link field
×
651
      const baseLink = field as LinkFieldCore;
×
652
      const opts = baseLink.options as ILinkFieldOptions;
×
653
      const usesJunctionTable = getLinkUsesJunctionTable(baseLink);
×
654
      const hasOrderColumn = baseLink.getHasOrderColumn();
×
655
      const fkHost = opts.fkHostTableName!;
×
656
      const selfKey = opts.selfKeyName;
×
657
      const foreignKey = opts.foreignKeyName;
×
658
      const exprForInner = expression.replaceAll(`"${this.getForeignAlias()}"`, '"f"');
×
659
      const subForInner = sub.replaceAll(`"${this.getForeignAlias()}"`, '"f"');
×
660
      if (usesJunctionTable) {
×
661
        const ordCol = hasOrderColumn ? `j."${baseLink.getOrderColumnName()}"` : undefined;
×
662
        const order = ordCol
×
663
          ? `(CASE WHEN ${ordCol} IS NULL THEN 0 ELSE 1 END) ASC, ${ordCol} ASC, j."__id" ASC`
×
664
          : `j."__id" ASC`;
×
665
        return `(
×
666
          SELECT CASE WHEN SUM(CASE WHEN (EXISTS ${subForInner}) THEN 1 ELSE 0 END) > 0
×
667
            THEN json_group_array(CASE WHEN (EXISTS ${subForInner}) AND ${exprForInner} IS NOT NULL THEN ${exprForInner} END)
×
668
            ELSE NULL END
669
          FROM "${fkHost}" AS j
×
670
          JOIN "${foreignDb}" AS f ON j."${foreignKey}" = f."__id"
×
671
          WHERE j."${selfKey}" = "${mainAlias}"."__id"
×
672
          ORDER BY ${order}
×
673
        )`;
×
674
      }
×
675
      {
×
676
        const ordCol = hasOrderColumn ? `f."${selfKey}_order"` : undefined;
3,285!
677
        const order = ordCol
3,285!
678
          ? `(CASE WHEN ${ordCol} IS NULL THEN 0 ELSE 1 END) ASC, ${ordCol} ASC, f."__id" ASC`
×
679
          : `f."__id" ASC`;
3,285!
680
        return `(
3,285✔
681
          SELECT CASE WHEN SUM(CASE WHEN (EXISTS ${subForInner}) THEN 1 ELSE 0 END) > 0
3,285✔
682
            THEN json_group_array(CASE WHEN (EXISTS ${subForInner}) AND ${exprForInner} IS NOT NULL THEN ${exprForInner} END)
3,285✔
683
            ELSE NULL END
684
          FROM "${foreignDb}" AS f
3,285✔
685
          WHERE f."${selfKey}" = "${mainAlias}"."__id"
3,285✔
686
          ORDER BY ${order}
3,285✔
687
        )`;
3,285✔
688
      }
3,285✔
689
    } catch (_) {
3,285!
690
      // fall back
×
691
    }
×
692
    // Fallback: emulate FILTER and null removal using CASE inside the aggregate
×
693
    return `json_group_array(CASE WHEN (EXISTS ${sub}) AND ${expression} IS NOT NULL THEN ${expression} END)`;
×
694
  }
×
695
  visitNumberField(field: NumberFieldCore): IFieldSelectName {
16,856✔
696
    return this.visitLookupField(field);
1,573✔
697
  }
1,573✔
698
  visitSingleLineTextField(field: SingleLineTextFieldCore): IFieldSelectName {
16,856✔
699
    return this.visitLookupField(field);
770✔
700
  }
770✔
701
  visitLongTextField(field: LongTextFieldCore): IFieldSelectName {
16,856✔
702
    return this.visitLookupField(field);
×
703
  }
×
704
  visitAttachmentField(field: AttachmentFieldCore): IFieldSelectName {
16,856✔
705
    return this.visitLookupField(field);
×
706
  }
×
707
  visitCheckboxField(field: CheckboxFieldCore): IFieldSelectName {
16,856✔
708
    return this.visitLookupField(field);
79✔
709
  }
79✔
710
  visitDateField(field: DateFieldCore): IFieldSelectName {
16,856✔
711
    return this.visitLookupField(field);
200✔
712
  }
200✔
713
  visitRatingField(field: RatingFieldCore): IFieldSelectName {
16,856✔
714
    return this.visitLookupField(field);
×
715
  }
×
716
  visitAutoNumberField(field: AutoNumberFieldCore): IFieldSelectName {
16,856✔
717
    return this.visitLookupField(field);
×
718
  }
×
719
  visitLinkField(field: LinkFieldCore): IFieldSelectName {
16,856✔
720
    // If this Link field is itself a lookup (lookup-of-link), treat it as a generic lookup
11,554✔
721
    // so we resolve via nested CTEs instead of using physical link options.
11,554✔
722
    if (field.isLookup) {
11,554✔
723
      return this.visitLookupField(field);
162✔
724
    }
162✔
725
    const foreignTable = this.foreignTable;
11,392✔
726
    const driver = this.dbProvider.driver;
11,392✔
727
    const junctionAlias = JUNCTION_ALIAS;
11,392✔
728

729
    const targetLookupField = foreignTable.mustGetField(field.options.lookupFieldId);
11,392✔
730
    const usesJunctionTable = getLinkUsesJunctionTable(field);
11,392✔
731
    const foreignTableAlias = this.getForeignAlias();
11,392✔
732
    const isMultiValue = field.getIsMultiValue();
11,392✔
733
    const hasOrderColumn = field.getHasOrderColumn();
11,392✔
734

735
    // Use table alias for cleaner SQL
11,392✔
736
    const recordIdRef = `"${foreignTableAlias}"."${ID_FIELD_NAME}"`;
11,392✔
737

738
    const qb = this.qb.client.queryBuilder();
11,392✔
739
    const selectVisitor = new FieldSelectVisitor(
11,392✔
740
      qb,
11,392✔
741
      this.dbProvider,
11,392✔
742
      foreignTable,
11,392✔
743
      new ScopedSelectionState(this.state),
11,392✔
744
      this.dialect,
11,392✔
745
      foreignTableAlias,
11,392✔
746
      true,
11,392✔
747
      true
11,392✔
748
    );
749
    const targetFieldResult = targetLookupField.accept(selectVisitor);
11,392✔
750
    let rawSelectionExpression =
11,392✔
751
      typeof targetFieldResult === 'string' ? targetFieldResult : targetFieldResult.toSQL().sql;
11,554✔
752

753
    // Apply field formatting to build the display expression
11,554✔
754
    const formattingVisitor = new FieldFormattingVisitor(rawSelectionExpression, this.dialect);
11,554✔
755
    let formattedSelectionExpression = targetLookupField.accept(formattingVisitor);
11,554✔
756
    // Self-join: ensure expressions use the foreign alias override
11,554✔
757
    const defaultForeignAlias = getTableAliasFromTable(foreignTable);
11,554✔
758
    if (defaultForeignAlias !== foreignTableAlias) {
11,554✔
759
      formattedSelectionExpression = formattedSelectionExpression.replaceAll(
159✔
760
        `"${defaultForeignAlias}"`,
159✔
761
        `"${foreignTableAlias}"`
159✔
762
      );
763
      rawSelectionExpression = rawSelectionExpression.replaceAll(
159✔
764
        `"${defaultForeignAlias}"`,
159✔
765
        `"${foreignTableAlias}"`
159✔
766
      );
767
    }
159✔
768

769
    // Determine if this relationship should return multiple values (array) or single value (object)
11,392✔
770
    // Apply field-level filter for Link (only affects this column)
11,392✔
771
    const linkFieldFilter = (field as FieldCore).getFilter?.();
11,392✔
772
    const linkFilterSub = linkFieldFilter
11,554✔
773
      ? this.buildForeignFilterSubquery(linkFieldFilter)
6✔
774
      : undefined;
11,386✔
775
    return match(driver)
11,554✔
776
      .with(DriverClient.Pg, () => {
11,554✔
777
        // Build JSON object with id and title, then strip null values to remove title key when null
11,392✔
778
        const conditionalJsonObject = this.dialect.buildLinkJsonObject(
11,392✔
779
          recordIdRef,
11,392✔
780
          formattedSelectionExpression,
11,392✔
781
          rawSelectionExpression
11,392✔
782
        );
783

784
        if (isMultiValue) {
11,392✔
785
          // Filter out null records and return empty array if no valid records exist
5,900✔
786
          // Build an ORDER BY clause with NULLS FIRST semantics and stable tie-breaks using __id
5,900✔
787

788
          const orderByClause = match({ usesJunctionTable, hasOrderColumn })
5,900✔
789
            .with({ usesJunctionTable: true, hasOrderColumn: true }, () => {
5,900✔
790
              // ManyMany with order column: NULLS FIRST, then order column ASC, then junction __id ASC
2,509✔
791
              const linkField = field as LinkFieldCore;
2,509✔
792
              const ord = `${junctionAlias}."${linkField.getOrderColumnName()}"`;
2,509✔
793
              return `${ord} IS NULL DESC, ${ord} ASC, ${junctionAlias}."__id" ASC`;
2,509✔
794
            })
2,509✔
795
            .with({ usesJunctionTable: true, hasOrderColumn: false }, () => {
5,900✔
796
              // ManyMany without order column: order by junction __id
216✔
797
              return `${junctionAlias}."__id" ASC`;
216✔
798
            })
216✔
799
            .with({ usesJunctionTable: false, hasOrderColumn: true }, () => {
5,900✔
800
              // OneMany/ManyOne/OneOne with order column: NULLS FIRST, then order ASC, then foreign __id ASC
3,175✔
801
              const linkField = field as LinkFieldCore;
3,175✔
802
              const ord = `"${foreignTableAlias}"."${linkField.getOrderColumnName()}"`;
3,175✔
803
              return `${ord} IS NULL DESC, ${ord} ASC, "${foreignTableAlias}"."__id" ASC`;
3,175✔
804
            })
3,175✔
805
            .with({ usesJunctionTable: false, hasOrderColumn: false }, () => `${recordIdRef} ASC`) // Fallback to record ID if no order column is available
5,900✔
806
            .exhaustive();
5,900✔
807

808
          const baseFilter = `${recordIdRef} IS NOT NULL`;
5,900✔
809
          const appliedFilter = linkFilterSub
5,900✔
810
            ? `(EXISTS ${linkFilterSub}) AND ${baseFilter}`
6✔
811
            : baseFilter;
5,894✔
812
          return `json_agg(${conditionalJsonObject} ORDER BY ${orderByClause}) FILTER (WHERE ${appliedFilter})`;
5,900✔
813
        } else {
11,392✔
814
          // For single value relationships (ManyOne, OneOne)
5,492✔
815
          // If lookup field is a Formula, return array-of-one to keep API consistent with tests
5,492✔
816
          const isFormulaLookup = targetLookupField.type === FieldType.Formula;
5,492✔
817
          const cond = linkFilterSub
5,492!
818
            ? `${recordIdRef} IS NOT NULL AND EXISTS ${linkFilterSub}`
×
819
            : `${recordIdRef} IS NOT NULL`;
5,492✔
820
          if (isFormulaLookup) {
5,492✔
821
            return `CASE WHEN ${cond} THEN jsonb_build_array(${conditionalJsonObject})::jsonb ELSE '[]'::jsonb END`;
26✔
822
          }
26✔
823
          // Otherwise, return single object or null
5,466✔
824
          return `CASE WHEN ${cond} THEN ${conditionalJsonObject} ELSE NULL END`;
5,466✔
825
        }
5,466✔
826
      })
11,392✔
827
      .with(DriverClient.Sqlite, () => {
11,554✔
828
        // Create conditional JSON object that only includes title if it's not null
×
829
        const conditionalJsonObject = this.dialect.buildLinkJsonObject(
×
830
          recordIdRef,
×
831
          formattedSelectionExpression,
×
832
          rawSelectionExpression
×
833
        );
834

835
        if (isMultiValue) {
×
836
          // For SQLite, build a correlated, ordered subquery to ensure deterministic ordering
×
837
          const mainAlias = getTableAliasFromTable(this.table);
×
838
          const foreignDb = this.foreignTable.dbTableName;
×
839
          const usesJunctionTable = getLinkUsesJunctionTable(field);
×
840
          const hasOrderColumn = field.getHasOrderColumn();
×
841

842
          const innerIdRef = `"f"."${ID_FIELD_NAME}"`;
×
843
          const innerTitleExpr = formattedSelectionExpression.replaceAll(
×
844
            `"${foreignTableAlias}"`,
×
845
            '"f"'
×
846
          );
847
          const innerRawExpr = rawSelectionExpression.replaceAll(`"${foreignTableAlias}"`, '"f"');
×
848
          const innerJson = `CASE WHEN ${innerRawExpr} IS NOT NULL THEN json_object('id', ${innerIdRef}, 'title', ${innerTitleExpr}) ELSE json_object('id', ${innerIdRef}) END`;
×
849
          const innerFilter = linkFilterSub
×
850
            ? `(EXISTS ${linkFilterSub.replaceAll(`"${foreignTableAlias}"`, '"f"')})`
×
851
            : '1=1';
×
852

853
          const opts = field.options as ILinkFieldOptions;
×
854
          return (
×
855
            this.dialect.buildDeterministicLookupAggregate({
×
856
              tableDbName: this.table.dbTableName,
×
857
              mainAlias: getTableAliasFromTable(this.table),
×
858
              foreignDbName: this.foreignTable.dbTableName,
×
859
              foreignAlias: foreignTableAlias,
×
860
              linkFieldOrderColumn: hasOrderColumn
×
861
                ? `${JUNCTION_ALIAS}."${field.getOrderColumnName()}"`
×
862
                : undefined,
×
863
              linkFieldHasOrderColumn: hasOrderColumn,
×
864
              usesJunctionTable,
×
865
              selfKeyName: opts.selfKeyName,
×
866
              foreignKeyName: opts.foreignKeyName,
×
867
              recordIdRef,
×
868
              formattedSelectionExpression,
×
869
              rawSelectionExpression,
×
870
              linkFilterSubquerySql: linkFilterSub,
×
871
              // Pass the actual junction table name here; the dialect will alias it as "j".
×
872
              junctionAlias: opts.fkHostTableName!,
×
873
            }) || this.getJsonAggregationFunction(conditionalJsonObject)
×
874
          );
875
        } else {
×
876
          // For single value relationships
×
877
          // If lookup field is a Formula, keep array-of-one when present, but return NULL when empty
×
878
          const isFormulaLookup = targetLookupField.type === FieldType.Formula;
×
879
          if (isFormulaLookup) {
×
880
            return `CASE WHEN ${recordIdRef} IS NOT NULL THEN json_array(${conditionalJsonObject}) ELSE NULL END`;
×
881
          }
×
882
          return `CASE WHEN ${recordIdRef} IS NOT NULL THEN ${conditionalJsonObject} ELSE NULL END`;
×
883
        }
×
884
      })
×
885
      .otherwise(() => {
11,554✔
886
        throw new Error(`Unsupported database driver: ${driver}`);
×
887
      });
×
888
  }
11,554✔
889
  visitRollupField(field: RollupFieldCore): IFieldSelectName {
16,856✔
890
    if (field.isLookup) {
2,192✔
891
      return this.visitLookupField(field);
14✔
892
    }
14✔
893

894
    // If rollup field is marked as error, don't attempt to resolve; just return NULL
2,178✔
895
    if (field.hasError) {
2,192✔
896
      return this.dialect.typedNullFor(field.dbFieldType);
27✔
897
    }
27✔
898

899
    const qb = this.qb.client.queryBuilder();
2,151✔
900
    const scopedState = new ScopedSelectionState(this.state);
2,151✔
901
    const selectVisitor = new FieldSelectVisitor(
2,151✔
902
      qb,
2,151✔
903
      this.dbProvider,
2,151✔
904
      this.foreignTable,
2,151✔
905
      scopedState,
2,151✔
906
      this.dialect,
2,151✔
907
      this.getForeignAlias(),
2,151✔
908
      true,
2,151✔
909
      false
2,151✔
910
    );
911

912
    const foreignAlias = this.getForeignAlias();
2,151✔
913
    const targetLookupField = field.getForeignLookupField(this.foreignTable);
2,151✔
914
    if (!targetLookupField) {
2,192!
915
      return this.dialect.typedNullFor(field.dbFieldType);
×
916
    }
✔
917
    // If the target of rollup depends on a foreign link CTE, reference the JOINed CTE columns or use subquery
2,151✔
918
    if (targetLookupField.type === FieldType.Formula) {
2,192✔
919
      const formulaField = targetLookupField as FormulaFieldCore;
27✔
920
      const referenced = formulaField.getReferenceFields(this.foreignTable);
27✔
921
      for (const ref of referenced) {
27✔
922
        // Pre-generate nested CTEs for foreign-table link dependencies if any lookup/rollup targets are themselves lookup fields.
27✔
923
        ref.accept(selectVisitor);
27✔
924
      }
27✔
925
    }
27✔
926

927
    // If the target of rollup depends on a foreign link CTE, reference the JOINed CTE columns or use subquery
2,151✔
928
    let expression: string;
2,151✔
929
    const nestedLinkFieldId = getLinkFieldId(targetLookupField.lookupOptions);
2,151✔
930
    if (nestedLinkFieldId) {
2,192!
931
      if (this.fieldCteMap.has(nestedLinkFieldId)) {
×
932
        const nestedCteName = this.fieldCteMap.get(nestedLinkFieldId)!;
×
933
        const columnName = targetLookupField.isLookup
×
934
          ? `lookup_${targetLookupField.id}`
×
935
          : targetLookupField.type === FieldType.Rollup
×
936
            ? `rollup_${targetLookupField.id}`
×
937
            : undefined;
×
938
        if (columnName) {
×
939
          // Check if this CTE is JOINed in current scope
×
940
          if (this.joinedCtes?.has(nestedLinkFieldId)) {
×
941
            expression = `"${nestedCteName}"."${columnName}"`;
×
942
          } else {
×
943
            // Fallback to subquery if CTE not JOINed in current scope
×
944
            expression = `((SELECT "${columnName}" FROM "${nestedCteName}" WHERE "${nestedCteName}"."main_record_id" = "${foreignAlias}"."${ID_FIELD_NAME}"))`;
×
945
          }
×
946
        } else {
×
947
          const targetFieldResult = targetLookupField.accept(selectVisitor);
×
948
          expression =
×
949
            typeof targetFieldResult === 'string'
×
950
              ? targetFieldResult
×
951
              : targetFieldResult.toSQL().sql;
×
952
        }
×
953
      } else {
×
954
        const targetFieldResult = targetLookupField.accept(selectVisitor);
×
955
        expression =
×
956
          typeof targetFieldResult === 'string' ? targetFieldResult : targetFieldResult.toSQL().sql;
×
957
      }
×
958
    } else {
2,192✔
959
      const targetFieldResult = targetLookupField.accept(selectVisitor);
2,151✔
960
      expression =
2,151✔
961
        typeof targetFieldResult === 'string' ? targetFieldResult : targetFieldResult.toSQL().sql;
2,151✔
962
    }
2,151✔
963

964
    if (
2,151✔
965
      targetLookupField.isConditionalLookup ||
2,151✔
966
      (targetLookupField.type === FieldType.ConditionalRollup && !targetLookupField.isLookup)
2,192✔
967
    ) {
2,192✔
968
      const nestedCteName = this.fieldCteMap.get(targetLookupField.id);
6✔
969
      if (nestedCteName) {
6✔
970
        const columnName =
6✔
971
          targetLookupField.type === FieldType.ConditionalRollup && !targetLookupField.isLookup
6✔
972
            ? `conditional_rollup_${targetLookupField.id}`
2✔
973
            : `conditional_lookup_${targetLookupField.id}`;
6✔
974
        if (this.joinedCtes?.has(targetLookupField.id)) {
6!
975
          expression = `"${nestedCteName}"."${columnName}"`;
×
976
        } else {
6✔
977
          expression = `((SELECT "${columnName}" FROM "${nestedCteName}" WHERE "${nestedCteName}"."main_record_id" = "${foreignAlias}"."${ID_FIELD_NAME}"))`;
6✔
978
        }
6✔
979
      }
6✔
980
    }
6✔
981
    const linkField = field.getLinkField(this.table);
2,151✔
982
    const options = linkField?.options as ILinkFieldOptions;
2,151✔
983
    const isSingleValueRelationship =
2,192✔
984
      options.relationship === Relationship.ManyOne || options.relationship === Relationship.OneOne;
2,192✔
985

986
    if (isSingleValueRelationship) {
2,192✔
987
      return this.buildSingleValueRollup(field, targetLookupField, expression);
1,015✔
988
    }
1,015✔
989
    return this.buildAggregateRollup(field, targetLookupField, expression);
1,136✔
990
  }
1,136✔
991

992
  visitConditionalRollupField(field: ConditionalRollupFieldCore): IFieldSelectName {
16,856✔
993
    const cteName = this.fieldCteMap.get(field.id);
1✔
994
    if (!cteName) {
1!
995
      return this.dialect.typedNullFor(field.dbFieldType);
×
996
    }
×
997

998
    return `"${cteName}"."conditional_rollup_${field.id}"`;
1✔
999
  }
1✔
1000
  visitSingleSelectField(field: SingleSelectFieldCore): IFieldSelectName {
16,856✔
1001
    return this.visitLookupField(field);
213✔
1002
  }
213✔
1003
  visitMultipleSelectField(field: MultipleSelectFieldCore): IFieldSelectName {
16,856✔
1004
    return this.visitLookupField(field);
154✔
1005
  }
154✔
1006
  visitFormulaField(field: FormulaFieldCore): IFieldSelectName {
16,856✔
1007
    return this.visitLookupField(field);
78✔
1008
  }
78✔
1009
  visitCreatedTimeField(field: CreatedTimeFieldCore): IFieldSelectName {
16,856✔
1010
    return this.visitLookupField(field);
×
1011
  }
×
1012
  visitLastModifiedTimeField(field: LastModifiedTimeFieldCore): IFieldSelectName {
16,856✔
1013
    return this.visitLookupField(field);
×
1014
  }
×
1015
  visitUserField(field: UserFieldCore): IFieldSelectName {
16,856✔
1016
    return this.visitLookupField(field);
42✔
1017
  }
42✔
1018
  visitCreatedByField(field: CreatedByFieldCore): IFieldSelectName {
16,856✔
1019
    return this.visitLookupField(field);
×
1020
  }
×
1021
  visitLastModifiedByField(field: LastModifiedByFieldCore): IFieldSelectName {
16,856✔
1022
    return this.visitLookupField(field);
×
1023
  }
×
1024
  visitButtonField(field: ButtonFieldCore): IFieldSelectName {
16,856✔
1025
    return this.visitLookupField(field);
×
1026
  }
×
1027
}
16,856✔
1028

1029
export class FieldCteVisitor implements IFieldVisitor<ICteResult> {
2✔
1030
  private logger = new Logger(FieldCteVisitor.name);
17,691✔
1031

1032
  static generateCTENameForField(table: TableDomain, field: LinkFieldCore) {
17,691✔
1033
    return `CTE_${getTableAliasFromTable(table)}_${field.id}`;
11,465✔
1034
  }
11,465✔
1035

1036
  private readonly _table: TableDomain;
17,691✔
1037
  private readonly state: IMutableQueryBuilderState;
17,691✔
1038
  private readonly conditionalRollupGenerationStack = new Set<string>();
17,691✔
1039
  private readonly conditionalLookupGenerationStack = new Set<string>();
17,691✔
1040
  private filteredIdSet?: Set<string>;
17,691✔
1041
  private readonly projection?: string[];
17,691✔
1042

1043
  constructor(
17,691✔
1044
    public readonly qb: Knex.QueryBuilder,
17,691✔
1045
    private readonly dbProvider: IDbProvider,
17,691✔
1046
    private readonly tables: Tables,
17,691✔
1047
    state: IMutableQueryBuilderState | undefined,
17,691✔
1048
    private readonly dialect: IRecordQueryDialectProvider,
17,691✔
1049
    projection?: string[]
17,691✔
1050
  ) {
17,691✔
1051
    this.state = state ?? new RecordQueryBuilderManager('table');
17,691!
1052
    this._table = tables.mustGetEntryTable();
17,691✔
1053
    this.projection = projection;
17,691✔
1054
  }
17,691✔
1055

1056
  get table() {
17,691✔
1057
    return this._table;
206,094✔
1058
  }
206,094✔
1059

1060
  get fieldCteMap(): ReadonlyMap<string, string> {
17,691✔
1061
    return this.state.getFieldCteMap();
121✔
1062
  }
121✔
1063

1064
  private getBaseIdSubquery(): Knex.QueryBuilder | undefined {
17,691✔
1065
    const baseCteName = this.state.getBaseCteName();
11,879✔
1066
    if (!baseCteName) {
11,879✔
1067
      return undefined;
8,400✔
1068
    }
8,400✔
1069
    return this.qb.client.queryBuilder().select(ID_FIELD_NAME).from(baseCteName);
3,479✔
1070
  }
3,479✔
1071

1072
  private applyMainTableRestriction(builder: Knex.QueryBuilder, alias: string): void {
17,691✔
1073
    const subquery = this.getBaseIdSubquery();
11,879✔
1074
    if (!subquery) {
11,879✔
1075
      return;
8,400✔
1076
    }
8,400✔
1077
    builder.whereIn(`${alias}.${ID_FIELD_NAME}`, subquery);
3,479✔
1078
  }
3,479✔
1079

1080
  private fromTableWithRestriction(
17,691✔
1081
    builder: Knex.QueryBuilder,
12,099✔
1082
    table: TableDomain,
12,099✔
1083
    alias: string
12,099✔
1084
  ): void {
12,099✔
1085
    const source =
12,099✔
1086
      table.id === this.table.id
12,099✔
1087
        ? this.state.getOriginalMainTableSource() ?? table.dbTableName
11,879✔
1088
        : table.dbTableName;
12,099✔
1089
    builder.from(`${source} as ${alias}`);
12,099✔
1090
    if (table.id === this.table.id) {
12,099✔
1091
      this.applyMainTableRestriction(builder, alias);
11,879✔
1092
    }
11,879✔
1093
  }
12,099✔
1094

1095
  /**
17,691✔
1096
   * Apply an explicit cast to align the SQL expression type with the target field's DB column type.
1097
   * This prevents Postgres from rejecting UPDATE ... FROM assignments due to type mismatches
1098
   * (e.g., assigning a text expression to a double precision column).
1099
   */
17,691✔
1100
  private castExpressionForDbType(expression: string, field: FieldCore): string {
17,691✔
1101
    if (this.dbProvider.driver !== DriverClient.Pg) return expression;
661!
1102
    const castSuffix = (() => {
661✔
1103
      switch (field.dbFieldType) {
661✔
1104
        case DbFieldType.Json:
661✔
1105
          return '::jsonb';
299✔
1106
        case DbFieldType.Integer:
661!
1107
          return '::integer';
×
1108
        case DbFieldType.Real:
661✔
1109
          return '::double precision';
333✔
1110
        case DbFieldType.DateTime:
661✔
1111
          return '::timestamptz';
1✔
1112
        case DbFieldType.Boolean:
661✔
1113
          return '::boolean';
9✔
1114
        case DbFieldType.Blob:
661!
1115
          return '::bytea';
×
1116
        case DbFieldType.Text:
661✔
1117
        default:
661✔
1118
          return '::text';
19✔
1119
      }
661✔
1120
    })();
661✔
1121
    return `(${expression})${castSuffix}`;
661✔
1122
  }
661✔
1123

1124
  private parseRollupFunction(expression: string): string {
17,691✔
1125
    const match = expression.match(/^(\w+)\(\{values\}\)$/);
1,388✔
1126
    if (!match) {
1,388!
1127
      throw new Error(`Invalid rollup expression: ${expression}`);
×
1128
    }
×
1129
    return match[1].toLowerCase();
1,388✔
1130
  }
1,388✔
1131

1132
  private shouldUseFormattedExpressionForAggregation(fn: string): boolean {
17,691✔
1133
    switch (fn) {
380✔
1134
      case 'array_join':
380✔
1135
      case 'concatenate':
380✔
1136
        return true;
19✔
1137
      default:
380✔
1138
        return false;
361✔
1139
    }
380✔
1140
  }
380✔
1141

1142
  private rollupFunctionSupportsOrdering(expression: string): boolean {
17,691✔
1143
    const fn = this.parseRollupFunction(expression);
380✔
1144
    switch (fn) {
380✔
1145
      case 'array_join':
380✔
1146
      case 'array_compact':
380✔
1147
      case 'concatenate':
380✔
1148
        return true;
53✔
1149
      default:
380✔
1150
        return false;
327✔
1151
    }
380✔
1152
  }
380✔
1153

1154
  private buildConditionalRollupAggregation(
17,691✔
1155
    rollupExpression: string,
628✔
1156
    fieldExpression: string,
628✔
1157
    targetField: FieldCore,
628✔
1158
    foreignAlias: string,
628✔
1159
    orderByClause?: string
628✔
1160
  ): string {
628✔
1161
    const fn = this.parseRollupFunction(rollupExpression);
628✔
1162
    return this.dialect.rollupAggregate(fn, fieldExpression, {
628✔
1163
      targetField,
628✔
1164
      rowPresenceExpr: `"${foreignAlias}"."${ID_FIELD_NAME}"`,
628✔
1165
      orderByField: orderByClause,
628✔
1166
      flattenNestedArray: fn === 'array_compact' && !!targetField.isConditionalLookup,
628✔
1167
    });
628✔
1168
  }
628✔
1169

1170
  private extractConditionalEqualityJoinPlan(
17,691✔
1171
    filter: IFilter | null | undefined,
302✔
1172
    table: TableDomain,
302✔
1173
    foreignTable: TableDomain,
302✔
1174
    mainAlias: string,
302✔
1175
    foreignAlias: string
302✔
1176
  ): {
1177
    joinKeys: Array<{ alias: string; hostExpr: string; foreignExpr: string }>;
1178
    residualFilter: IFilter | null;
1179
  } | null {
302✔
1180
    if (!filter?.filterSet?.length) return null;
302!
1181

1182
    const joinKeys: Array<{ alias: string; hostExpr: string; foreignExpr: string }> = [];
302✔
1183

1184
    type FilterNode = Exclude<IFilter, null>;
1185

1186
    const buildResidual = (
302✔
1187
      current: IFilter | null | undefined
302✔
1188
    ): { ok: boolean; residual: IFilter } => {
1189
      if (!current?.filterSet?.length) return { ok: false, residual: null };
302!
1190
      const conjunction = current.conjunction ?? 'and';
302!
1191
      if (conjunction !== 'and') return { ok: false, residual: null };
302!
1192

1193
      const residualEntries: Array<FilterNode | IFilterItem> = [];
302✔
1194

1195
      for (const entry of current.filterSet ?? []) {
302✔
1196
        if (!entry) continue;
487!
1197
        if ('fieldId' in entry) {
487✔
1198
          const item = entry as IFilterItem;
487✔
1199

1200
          if (item.operator === FilterOperatorIs.value && isFieldReferenceValue(item.value)) {
487✔
1201
            const hostRef = item.value;
232✔
1202
            if (hostRef.tableId && hostRef.tableId !== table.id) {
232✔
1203
              return { ok: false, residual: null };
×
1204
            }
×
1205
            const foreignField = foreignTable.getField(item.fieldId);
232✔
1206
            const hostField = table.getField(hostRef.fieldId);
232✔
1207
            if (!foreignField || !hostField) {
232!
1208
              return { ok: false, residual: null };
×
1209
            }
×
1210
            if (isDateLikeField(foreignField) || isDateLikeField(hostField)) {
232✔
1211
              return { ok: false, residual: null };
1✔
1212
            }
1✔
1213
            const caseInsensitive =
231✔
1214
              foreignField.dbFieldType === DbFieldType.Text &&
231✔
1215
              hostField.dbFieldType === DbFieldType.Text;
232✔
1216
            const alias = `__cr_key_${joinKeys.length}`;
232✔
1217
            const foreignExpr = caseInsensitive
232✔
1218
              ? `LOWER("${foreignAlias}"."${foreignField.dbFieldName}")`
194✔
1219
              : `"${foreignAlias}"."${foreignField.dbFieldName}"`;
232✔
1220
            const hostExpr = caseInsensitive
232✔
1221
              ? `LOWER("${mainAlias}"."${hostField.dbFieldName}")`
194✔
1222
              : `"${mainAlias}"."${hostField.dbFieldName}"`;
232✔
1223
            joinKeys.push({ alias, hostExpr, foreignExpr });
232✔
1224
            continue;
232✔
1225
          }
232✔
1226

1227
          if (isFieldReferenceValue(item.value)) {
458✔
1228
            return { ok: false, residual: null };
85✔
1229
          }
85✔
1230

1231
          if (!SUPPORTED_EQUALITY_RESIDUAL_OPERATORS.has(item.operator)) {
446!
1232
            return { ok: false, residual: null };
×
1233
          }
✔
1234

1235
          residualEntries.push(entry);
170✔
1236
          continue;
170✔
1237
        }
170!
1238

1239
        if ('filterSet' in entry) {
×
1240
          const nested = buildResidual(entry as IFilter);
×
1241
          if (!nested.ok) {
×
1242
            return { ok: false, residual: null };
×
1243
          }
×
1244
          const nestedResidual = nested.residual;
×
1245
          if (nestedResidual && 'filterSet' in nestedResidual && nestedResidual.filterSet?.length) {
×
1246
            residualEntries.push(nestedResidual as FilterNode);
×
1247
          }
×
1248
          continue;
×
1249
        }
×
1250

1251
        return { ok: false, residual: null };
×
1252
      }
✔
1253

1254
      if (!residualEntries.length) {
290✔
1255
        return { ok: true, residual: null };
109✔
1256
      }
109✔
1257

1258
      return {
107✔
1259
        ok: true,
107✔
1260
        residual: {
107✔
1261
          conjunction,
107✔
1262
          filterSet: residualEntries,
107✔
1263
        } as FilterNode,
107✔
1264
      };
107✔
1265
    };
107✔
1266

1267
    const { ok, residual } = buildResidual(filter);
302✔
1268
    if (!ok || !joinKeys.length) return null;
302✔
1269
    return { joinKeys, residualFilter: residual };
130✔
1270
  }
130✔
1271

1272
  private getConditionalEqualityFallback(aggregationFn: string, field: FieldCore): string | null {
17,691✔
1273
    switch (aggregationFn) {
130✔
1274
      case 'countall':
130✔
1275
      case 'count':
130✔
1276
      case 'sum':
130✔
1277
      case 'average':
130✔
1278
        return '0::double precision';
126✔
1279
      case 'max':
130✔
1280
      case 'min': {
130✔
1281
        const dbType = field.dbFieldType ?? DbFieldType.Text;
4!
1282
        return this.dialect.typedNullFor(dbType);
4✔
1283
      }
4✔
1284
      default:
130!
1285
        return null;
×
1286
    }
130✔
1287
  }
130✔
1288

1289
  private resolveConditionalComputedTargetExpression(
17,691✔
1290
    targetField: FieldCore,
671✔
1291
    foreignTable: TableDomain,
671✔
1292
    foreignAlias: string,
671✔
1293
    selectVisitor: FieldSelectVisitor
671✔
1294
  ): string {
671✔
1295
    if (targetField.type === FieldType.ConditionalRollup && !targetField.isLookup) {
671✔
1296
      const conditionalTarget = targetField as ConditionalRollupFieldCore;
6✔
1297
      this.generateConditionalRollupFieldCteForScope(foreignTable, conditionalTarget);
6✔
1298
      const nestedCteName = this.state.getFieldCteMap().get(conditionalTarget.id);
6✔
1299
      if (nestedCteName) {
6✔
1300
        return `((SELECT "conditional_rollup_${conditionalTarget.id}" FROM "${nestedCteName}" WHERE "${nestedCteName}"."main_record_id" = "${foreignAlias}"."${ID_FIELD_NAME}"))`;
6✔
1301
      }
6!
1302
      const fallback = conditionalTarget.accept(selectVisitor);
×
1303
      return typeof fallback === 'string' ? fallback : fallback.toSQL().sql;
6!
1304
    }
6✔
1305

1306
    if (targetField.isConditionalLookup) {
671✔
1307
      const options = targetField.getConditionalLookupOptions?.();
6✔
1308
      if (options) {
6✔
1309
        this.generateConditionalLookupFieldCteForScope(foreignTable, targetField, options);
6✔
1310
      }
6✔
1311
      const nestedCteName = this.state.getFieldCteMap().get(targetField.id);
6✔
1312
      if (nestedCteName) {
6✔
1313
        const column =
6✔
1314
          targetField.type === FieldType.ConditionalRollup
6✔
1315
            ? `conditional_rollup_${targetField.id}`
×
1316
            : `conditional_lookup_${targetField.id}`;
6✔
1317
        return `((SELECT "${column}" FROM "${nestedCteName}" WHERE "${nestedCteName}"."main_record_id" = "${foreignAlias}"."${ID_FIELD_NAME}"))`;
6✔
1318
      }
6✔
1319
    }
6✔
1320

1321
    const targetSelect = targetField.accept(selectVisitor);
659✔
1322
    return typeof targetSelect === 'string' ? targetSelect : targetSelect.toSQL().sql;
671!
1323
  }
671✔
1324

1325
  private generateConditionalRollupFieldCte(field: ConditionalRollupFieldCore): void {
17,691✔
1326
    this.generateConditionalRollupFieldCteForScope(this.table, field);
389✔
1327
  }
389✔
1328

1329
  private generateConditionalRollupFieldCteForScope(
17,691✔
1330
    table: TableDomain,
401✔
1331
    field: ConditionalRollupFieldCore
401✔
1332
  ): void {
401✔
1333
    if (field.hasError) return;
401✔
1334
    if (this.state.getFieldCteMap().has(field.id)) return;
401✔
1335
    if (this.conditionalRollupGenerationStack.has(field.id)) return;
401✔
1336

1337
    this.conditionalRollupGenerationStack.add(field.id);
380✔
1338
    try {
380✔
1339
      const {
380✔
1340
        foreignTableId,
380✔
1341
        lookupFieldId,
380✔
1342
        expression = 'countall({values})',
380✔
1343
        filter,
380✔
1344
        sort,
380✔
1345
        limit,
380✔
1346
      } = field.options;
380✔
1347
      if (!foreignTableId || !lookupFieldId) {
401!
1348
        return;
×
1349
      }
✔
1350

1351
      const foreignTable = this.tables.getTable(foreignTableId);
380✔
1352
      if (!foreignTable) {
401!
1353
        return;
×
1354
      }
✔
1355

1356
      const targetField = foreignTable.getField(lookupFieldId);
380✔
1357
      if (!targetField) {
401!
1358
        return;
×
1359
      }
✔
1360

1361
      const joinToMain = table === this.table;
380✔
1362

1363
      const cteName = `CTE_REF_${field.id}`;
380✔
1364
      const mainAlias = getTableAliasFromTable(table);
380✔
1365
      const foreignAlias = getTableAliasFromTable(foreignTable);
380✔
1366
      const foreignAliasUsed = foreignAlias === mainAlias ? `${foreignAlias}_ref` : foreignAlias;
401✔
1367

1368
      const qb = this.qb.client.queryBuilder();
401✔
1369
      const selectVisitor = new FieldSelectVisitor(
401✔
1370
        qb,
401✔
1371
        this.dbProvider,
401✔
1372
        foreignTable,
401✔
1373
        new ScopedSelectionState(this.state),
401✔
1374
        this.dialect,
401✔
1375
        foreignAliasUsed,
401✔
1376
        true
401✔
1377
      );
1378

1379
      const rawExpression = this.resolveConditionalComputedTargetExpression(
401✔
1380
        targetField,
401✔
1381
        foreignTable,
401✔
1382
        foreignAliasUsed,
401✔
1383
        selectVisitor
401✔
1384
      );
1385
      const formattingVisitor = new FieldFormattingVisitor(rawExpression, this.dialect);
401✔
1386
      const formattedExpression = targetField.accept(formattingVisitor);
401✔
1387

1388
      const aggregationFn = this.parseRollupFunction(expression);
401✔
1389
      const aggregationInputExpression = this.shouldUseFormattedExpressionForAggregation(
401✔
1390
        aggregationFn
401✔
1391
      )
1392
        ? formattedExpression
19✔
1393
        : rawExpression;
361✔
1394

1395
      const supportsOrdering = this.rollupFunctionSupportsOrdering(expression);
401✔
1396

1397
      let orderByClause: string | undefined;
401✔
1398
      if (supportsOrdering && sort?.fieldId) {
401✔
1399
        const sortField = foreignTable.getField(sort.fieldId);
19✔
1400
        if (sortField) {
19✔
1401
          let sortExpression = this.resolveConditionalComputedTargetExpression(
19✔
1402
            sortField,
19✔
1403
            foreignTable,
19✔
1404
            foreignAliasUsed,
19✔
1405
            selectVisitor
19✔
1406
          );
1407

1408
          const defaultForeignAlias = getTableAliasFromTable(foreignTable);
19✔
1409
          if (defaultForeignAlias !== foreignAliasUsed) {
19!
1410
            sortExpression = sortExpression.replaceAll(
×
1411
              `"${defaultForeignAlias}"`,
×
1412
              `"${foreignAliasUsed}"`
×
1413
            );
1414
          }
×
1415

1416
          const direction = sort.order === SortFunc.Desc ? 'DESC' : 'ASC';
19✔
1417
          orderByClause = `${sortExpression} ${direction}`;
19✔
1418
        }
19✔
1419
      }
19✔
1420

1421
      const aggregateExpression = this.buildConditionalRollupAggregation(
380✔
1422
        expression,
380✔
1423
        aggregationInputExpression,
380✔
1424
        targetField,
380✔
1425
        foreignAliasUsed,
380✔
1426
        supportsOrdering ? orderByClause : undefined
401✔
1427
      );
1428
      const castedAggregateExpression = this.castExpressionForDbType(aggregateExpression, field);
401✔
1429

1430
      const equalityEnabledFns = new Set(['countall', 'count', 'sum', 'average', 'max', 'min']);
401✔
1431
      const canUseEqualityPlan =
401✔
1432
        equalityEnabledFns.has(aggregationFn) &&
401✔
1433
        !supportsOrdering &&
302✔
1434
        !orderByClause &&
302✔
1435
        !sort?.fieldId;
302!
1436
      const equalityPlan = canUseEqualityPlan
401✔
1437
        ? this.extractConditionalEqualityJoinPlan(
302✔
1438
            filter,
302✔
1439
            table,
302✔
1440
            foreignTable,
302✔
1441
            mainAlias,
302✔
1442
            foreignAliasUsed
302✔
1443
          )
1444
        : null;
78✔
1445

1446
      if (equalityPlan?.joinKeys.length) {
401✔
1447
        const countsAlias = `__cr_counts_${field.id}`;
130✔
1448
        const countsQuery = this.qb.client
130✔
1449
          .queryBuilder()
130✔
1450
          .from(`${foreignTable.dbTableName} as ${foreignAliasUsed}`);
130✔
1451
        for (const cond of equalityPlan.joinKeys) {
130✔
1452
          countsQuery.select(this.qb.client.raw(`${cond.foreignExpr} as "${cond.alias}"`));
163✔
1453
          countsQuery.groupByRaw(cond.foreignExpr);
163✔
1454
        }
163✔
1455
        countsQuery.select(this.qb.client.raw(`${castedAggregateExpression} as "reference_value"`));
130✔
1456

1457
        if (equalityPlan.residualFilter) {
130✔
1458
          const fieldMap = foreignTable.fieldList.reduce(
21✔
1459
            (map, f) => {
21✔
1460
              map[f.id] = f as FieldCore;
79✔
1461
              return map;
79✔
1462
            },
79✔
1463
            {} as Record<string, FieldCore>
21✔
1464
          );
1465

1466
          const selectionMap = new Map<string, IFieldSelectName>();
21✔
1467
          for (const f of foreignTable.fields.ordered) {
21✔
1468
            selectionMap.set(f.id, `"${foreignAliasUsed}"."${f.dbFieldName}"`);
79✔
1469
          }
79✔
1470

1471
          const fieldReferenceSelectionMap = new Map<string, string>();
21✔
1472
          const fieldReferenceFieldMap = new Map<string, FieldCore>();
21✔
1473
          for (const mainField of table.fields.ordered) {
21✔
1474
            fieldReferenceSelectionMap.set(
105✔
1475
              mainField.id,
105✔
1476
              `"${mainAlias}"."${mainField.dbFieldName}"`
105✔
1477
            );
1478
            fieldReferenceFieldMap.set(mainField.id, mainField as FieldCore);
105✔
1479
          }
105✔
1480

1481
          this.dbProvider
21✔
1482
            .filterQuery(countsQuery, fieldMap, equalityPlan.residualFilter, undefined, {
21✔
1483
              selectionMap,
21✔
1484
              fieldReferenceSelectionMap,
21✔
1485
              fieldReferenceFieldMap,
21✔
1486
            })
21✔
1487
            .appendQueryBuilder();
21✔
1488
        }
21✔
1489

1490
        const equalityFallback = this.getConditionalEqualityFallback(aggregationFn, field);
130✔
1491
        this.qb.with(cteName, (cqb) => {
130✔
1492
          cqb.select(`${mainAlias}.${ID_FIELD_NAME} as main_record_id`);
130✔
1493
          const refValueSql =
130✔
1494
            equalityFallback != null
130✔
1495
              ? `COALESCE(${countsAlias}."reference_value", ${equalityFallback})`
130✔
1496
              : `${countsAlias}."reference_value"`;
130!
1497
          cqb.select(cqb.client.raw(`${refValueSql} as "conditional_rollup_${field.id}"`));
130✔
1498
          this.fromTableWithRestriction(cqb, table, mainAlias);
130✔
1499
          cqb.leftJoin(
130✔
1500
            this.qb.client.raw(`(${countsQuery.toQuery()}) as ${countsAlias}`),
130✔
1501
            (join) => {
130✔
1502
              for (const cond of equalityPlan.joinKeys) {
130✔
1503
                join.on(
163✔
1504
                  this.qb.client.raw(cond.hostExpr),
163✔
1505
                  '=',
163✔
1506
                  this.qb.client.raw(`${countsAlias}."${cond.alias}"`)
163✔
1507
                );
1508
              }
163✔
1509
            }
130✔
1510
          );
1511
        });
130✔
1512

1513
        if (joinToMain && !this.state.isCteJoined(cteName)) {
130✔
1514
          this.qb.leftJoin(cteName, `${mainAlias}.${ID_FIELD_NAME}`, `${cteName}.main_record_id`);
127✔
1515
          this.state.markCteJoined(cteName);
127✔
1516
        }
127✔
1517

1518
        this.state.setFieldCte(field.id, cteName);
130✔
1519
        return;
130✔
1520
      }
130✔
1521

1522
      const aggregateSourceQuery = this.qb.client
250✔
1523
        .queryBuilder()
250✔
1524
        .select('*')
250✔
1525
        .from(`${foreignTable.dbTableName} as ${foreignAliasUsed}`);
250✔
1526

1527
      if (filter) {
250✔
1528
        const fieldMap = foreignTable.fieldList.reduce(
250✔
1529
          (map, f) => {
250✔
1530
            map[f.id] = f as FieldCore;
1,063✔
1531
            return map;
1,063✔
1532
          },
1,063✔
1533
          {} as Record<string, FieldCore>
250✔
1534
        );
1535

1536
        const selectionMap = new Map<string, IFieldSelectName>();
250✔
1537
        for (const f of foreignTable.fields.ordered) {
250✔
1538
          selectionMap.set(f.id, `"${foreignAliasUsed}"."${f.dbFieldName}"`);
1,063✔
1539
        }
1,063✔
1540

1541
        const fieldReferenceSelectionMap = new Map<string, string>();
250✔
1542
        const fieldReferenceFieldMap = new Map<string, FieldCore>();
250✔
1543
        for (const mainField of table.fields.ordered) {
250✔
1544
          fieldReferenceSelectionMap.set(mainField.id, `"${mainAlias}"."${mainField.dbFieldName}"`);
1,634✔
1545
          fieldReferenceFieldMap.set(mainField.id, mainField as FieldCore);
1,634✔
1546
        }
1,634✔
1547

1548
        this.dbProvider
250✔
1549
          .filterQuery(aggregateSourceQuery, fieldMap, filter, undefined, {
250✔
1550
            selectionMap,
250✔
1551
            fieldReferenceSelectionMap,
250✔
1552
            fieldReferenceFieldMap,
250✔
1553
          })
250✔
1554
          .appendQueryBuilder();
250✔
1555
      }
250✔
1556

1557
      if (supportsOrdering && orderByClause) {
401✔
1558
        aggregateSourceQuery.orderByRaw(orderByClause);
19✔
1559
      }
19✔
1560

1561
      if (supportsOrdering) {
378✔
1562
        const resolvedLimit = normalizeConditionalLimit(limit);
53✔
1563
        aggregateSourceQuery.limit(resolvedLimit);
53✔
1564
      }
53✔
1565

1566
      const aggregateQuery = this.qb.client
250✔
1567
        .queryBuilder()
250✔
1568
        .from(aggregateSourceQuery.as(foreignAliasUsed));
250✔
1569

1570
      aggregateQuery.select(this.qb.client.raw(`${castedAggregateExpression} as reference_value`));
250✔
1571
      const aggregateSql = aggregateQuery.toQuery();
250✔
1572

1573
      this.qb.with(cteName, (cqb) => {
250✔
1574
        cqb
247✔
1575
          .select(`${mainAlias}.${ID_FIELD_NAME} as main_record_id`)
247✔
1576
          .select(cqb.client.raw(`(${aggregateSql}) as "conditional_rollup_${field.id}"`))
247✔
1577
          .modify((builder) => this.fromTableWithRestriction(builder, table, mainAlias));
247✔
1578
      });
247✔
1579

1580
      if (joinToMain && !this.state.isCteJoined(cteName)) {
401✔
1581
        this.qb.leftJoin(cteName, `${mainAlias}.${ID_FIELD_NAME}`, `${cteName}.main_record_id`);
241✔
1582
        this.state.markCteJoined(cteName);
241✔
1583
      }
241✔
1584

1585
      this.state.setFieldCte(field.id, cteName);
247✔
1586
    } finally {
401✔
1587
      this.conditionalRollupGenerationStack.delete(field.id);
380✔
1588
    }
380✔
1589
  }
401✔
1590

1591
  private generateConditionalLookupFieldCte(field: FieldCore, options: IConditionalLookupOptions) {
17,691✔
1592
    this.generateConditionalLookupFieldCteForScope(this.table, field, options);
259✔
1593
  }
259✔
1594

1595
  private generateConditionalLookupFieldCteForScope(
17,691✔
1596
    table: TableDomain,
273✔
1597
    field: FieldCore,
273✔
1598
    options: IConditionalLookupOptions
273✔
1599
  ): void {
273✔
1600
    if (field.hasError) return;
273✔
1601
    if (this.state.getFieldCteMap().has(field.id)) return;
273✔
1602
    if (this.conditionalLookupGenerationStack.has(field.id)) return;
273✔
1603

1604
    this.conditionalLookupGenerationStack.add(field.id);
253✔
1605
    try {
253✔
1606
      const { foreignTableId, lookupFieldId, filter, sort, limit } = options;
253✔
1607
      if (!foreignTableId || !lookupFieldId) {
273!
1608
        return;
×
1609
      }
✔
1610

1611
      const foreignTable = this.tables.getTable(foreignTableId);
253✔
1612
      if (!foreignTable) {
273!
1613
        return;
×
1614
      }
✔
1615

1616
      const targetField = foreignTable.getField(lookupFieldId);
253✔
1617
      if (!targetField) {
273!
1618
        return;
×
1619
      }
✔
1620

1621
      const joinToMain = table === this.table;
253✔
1622

1623
      const cteName = `CTE_CONDITIONAL_LOOKUP_${field.id}`;
253✔
1624
      const mainAlias = getTableAliasFromTable(table);
253✔
1625
      const foreignAlias = getTableAliasFromTable(foreignTable);
253✔
1626
      const foreignAliasUsed = foreignAlias === mainAlias ? `${foreignAlias}_ref` : foreignAlias;
273✔
1627

1628
      const qb = this.qb.client.queryBuilder();
273✔
1629
      const selectVisitor = new FieldSelectVisitor(
273✔
1630
        qb,
273✔
1631
        this.dbProvider,
273✔
1632
        foreignTable,
273✔
1633
        new ScopedSelectionState(this.state),
273✔
1634
        this.dialect,
273✔
1635
        foreignAliasUsed,
273✔
1636
        true
273✔
1637
      );
1638

1639
      const rawExpression = this.resolveConditionalComputedTargetExpression(
273✔
1640
        targetField,
273✔
1641
        foreignTable,
273✔
1642
        foreignAliasUsed,
273✔
1643
        selectVisitor
273✔
1644
      );
1645

1646
      let orderByClause: string | undefined;
273✔
1647
      if (sort?.fieldId) {
273✔
1648
        const sortField = foreignTable.getField(sort.fieldId);
19✔
1649
        if (sortField) {
19✔
1650
          let sortExpression = this.resolveConditionalComputedTargetExpression(
19✔
1651
            sortField,
19✔
1652
            foreignTable,
19✔
1653
            foreignAliasUsed,
19✔
1654
            selectVisitor
19✔
1655
          );
1656

1657
          const defaultForeignAlias = getTableAliasFromTable(foreignTable);
19✔
1658
          if (defaultForeignAlias !== foreignAliasUsed) {
19!
1659
            sortExpression = sortExpression.replaceAll(
×
1660
              `"${defaultForeignAlias}"`,
×
1661
              `"${foreignAliasUsed}"`
×
1662
            );
1663
          }
×
1664

1665
          const direction = sort.order === SortFunc.Desc ? 'DESC' : 'ASC';
19✔
1666
          orderByClause = `${sortExpression} ${direction}`;
19✔
1667
        }
19✔
1668
      }
19✔
1669

1670
      const aggregateExpression =
253✔
1671
        field.type === FieldType.ConditionalRollup
253✔
1672
          ? this.dialect.jsonAggregateNonNull(rawExpression, orderByClause)
5✔
1673
          : this.buildConditionalRollupAggregation(
248✔
1674
              'array_compact({values})',
248✔
1675
              rawExpression,
248✔
1676
              targetField,
248✔
1677
              foreignAliasUsed,
248✔
1678
              orderByClause
248✔
1679
            );
1680
      const castedAggregateExpression = this.castExpressionForDbType(aggregateExpression, field);
273✔
1681

1682
      const applyConditionalFilter = (targetQb: Knex.QueryBuilder) => {
273✔
1683
        if (!filter) return;
253!
1684

1685
        const fieldMap = foreignTable.fieldList.reduce(
253✔
1686
          (map, f) => {
253✔
1687
            map[f.id] = f as FieldCore;
1,203✔
1688
            return map;
1,203✔
1689
          },
1,203✔
1690
          {} as Record<string, FieldCore>
253✔
1691
        );
1692

1693
        const selectionMap = new Map<string, IFieldSelectName>();
253✔
1694
        for (const f of foreignTable.fields.ordered) {
253✔
1695
          selectionMap.set(f.id, `"${foreignAliasUsed}"."${f.dbFieldName}"`);
1,203✔
1696
        }
1,203✔
1697

1698
        const fieldReferenceSelectionMap = new Map<string, string>();
253✔
1699
        const fieldReferenceFieldMap = new Map<string, FieldCore>();
253✔
1700
        for (const mainField of table.fields.ordered) {
253✔
1701
          fieldReferenceSelectionMap.set(mainField.id, `"${mainAlias}"."${mainField.dbFieldName}"`);
1,683✔
1702
          fieldReferenceFieldMap.set(mainField.id, mainField as FieldCore);
1,683✔
1703
        }
1,683✔
1704

1705
        this.dbProvider
253✔
1706
          .filterQuery(targetQb, fieldMap, filter, undefined, {
253✔
1707
            selectionMap,
253✔
1708
            fieldReferenceSelectionMap,
253✔
1709
            fieldReferenceFieldMap,
253✔
1710
          })
253✔
1711
          .appendQueryBuilder();
253✔
1712
      };
253✔
1713

1714
      const aggregateSourceQuery = this.qb.client
273✔
1715
        .queryBuilder()
273✔
1716
        .select('*')
273✔
1717
        .from(`${foreignTable.dbTableName} as ${foreignAliasUsed}`);
273✔
1718

1719
      applyConditionalFilter(aggregateSourceQuery);
273✔
1720

1721
      if (orderByClause) {
273✔
1722
        aggregateSourceQuery.orderByRaw(orderByClause);
19✔
1723
      }
19✔
1724

1725
      const resolvedLimit = normalizeConditionalLimit(limit);
253✔
1726
      aggregateSourceQuery.limit(resolvedLimit);
253✔
1727

1728
      const aggregateQuery = this.qb.client
253✔
1729
        .queryBuilder()
253✔
1730
        .from(aggregateSourceQuery.as(foreignAliasUsed));
253✔
1731

1732
      aggregateQuery.select(this.qb.client.raw(`${castedAggregateExpression} as reference_value`));
253✔
1733

1734
      const aggregateSql = aggregateQuery.toQuery();
253✔
1735
      const lookupAlias = `conditional_lookup_${field.id}`;
253✔
1736
      const rollupAlias = `conditional_rollup_${field.id}`;
253✔
1737

1738
      this.qb.with(cteName, (cqb) => {
253✔
1739
        cqb.select(`${mainAlias}.${ID_FIELD_NAME} as main_record_id`);
250✔
1740
        cqb.select(cqb.client.raw(`(${aggregateSql}) as "${lookupAlias}"`));
250✔
1741
        if (field.type === FieldType.ConditionalRollup) {
250✔
1742
          cqb.select(cqb.client.raw(`(${aggregateSql}) as "${rollupAlias}"`));
5✔
1743
        }
5✔
1744
        this.fromTableWithRestriction(cqb, table, mainAlias);
250✔
1745
      });
250✔
1746

1747
      if (joinToMain && !this.state.isCteJoined(cteName)) {
273✔
1748
        this.qb.leftJoin(cteName, `${mainAlias}.${ID_FIELD_NAME}`, `${cteName}.main_record_id`);
241✔
1749
        this.state.markCteJoined(cteName);
241✔
1750
      }
241✔
1751

1752
      this.state.setFieldCte(field.id, cteName);
250✔
1753
    } finally {
268✔
1754
      this.conditionalLookupGenerationStack.delete(field.id);
253✔
1755
    }
253✔
1756
  }
273✔
1757

1758
  public build() {
17,691✔
1759
    const list = getOrderedFieldsByProjection(this.table, this.projection) as FieldCore[];
17,691✔
1760
    this.filteredIdSet = new Set(list.map((f) => f.id));
17,691✔
1761

1762
    // Ensure CTEs for any link fields that are dependencies of the projected fields.
17,691✔
1763
    // This allows selecting lookup/rollup values even when the link fields themselves
17,691✔
1764
    // are not part of the projection.
17,691✔
1765
    for (const field of list) {
17,691✔
1766
      const linkFields = field.getLinkFields(this.table);
73,351✔
1767
      for (const lf of linkFields) {
73,351✔
1768
        if (!lf) continue;
6,193!
1769
        if (!this.state.getFieldCteMap().has(lf.id)) {
6,193✔
1770
          this.generateLinkFieldCte(lf);
3,616✔
1771
        }
3,616✔
1772
      }
6,193✔
1773

1774
      if (field.isConditionalLookup) {
73,351✔
1775
        const options = field.getConditionalLookupOptions?.();
259✔
1776
        if (options) {
259✔
1777
          this.generateConditionalLookupFieldCte(field, options);
259✔
1778
        }
259✔
1779
      }
259✔
1780
    }
73,351✔
1781

1782
    for (const field of list) {
17,691✔
1783
      field.accept(this);
73,346✔
1784
    }
73,346✔
1785
  }
17,685✔
1786

1787
  private generateLinkFieldCte(linkField: LinkFieldCore): void {
17,691✔
1788
    // Avoid defining the same CTE multiple times in a single WITH clause
11,914✔
1789
    if (this.state.getFieldCteMap().has(linkField.id)) {
11,914!
1790
      return;
×
1791
    }
×
1792
    const foreignTable = this.tables.getLinkForeignTable(linkField);
11,914✔
1793
    // Skip CTE generation if foreign table is missing (e.g., deleted)
11,914✔
1794
    if (!foreignTable) {
11,914✔
1795
      return;
651✔
1796
    }
651✔
1797
    const cteName = FieldCteVisitor.generateCTENameForField(this.table, linkField);
11,263✔
1798
    const usesJunctionTable = getLinkUsesJunctionTable(linkField);
11,263✔
1799
    const options = linkField.options as ILinkFieldOptions;
11,263✔
1800
    const mainAlias = getTableAliasFromTable(this.table);
11,263✔
1801
    const foreignAlias = getTableAliasFromTable(foreignTable);
11,263✔
1802
    const foreignAliasUsed = foreignAlias === mainAlias ? `${foreignAlias}_f` : foreignAlias;
11,914✔
1803
    const { fkHostTableName, selfKeyName, foreignKeyName, relationship } = options;
11,914✔
1804

1805
    // Determine which lookup/rollup fields are actually needed from this link
11,914✔
1806
    let lookupFields = linkField.getLookupFields(this.table);
11,914✔
1807
    let rollupFields = linkField.getRollupFields(this.table);
11,914✔
1808
    if (this.filteredIdSet) {
11,914✔
1809
      lookupFields = lookupFields.filter((f) => this.filteredIdSet!.has(f.id));
11,263✔
1810
      rollupFields = rollupFields.filter((f) => this.filteredIdSet!.has(f.id));
11,263✔
1811
    }
11,263✔
1812

1813
    // Pre-generate nested CTEs limited to selected lookup/rollup dependencies
11,263✔
1814
    this.generateNestedForeignCtesIfNeeded(
11,263✔
1815
      this.table,
11,263✔
1816
      foreignTable,
11,263✔
1817
      linkField,
11,263✔
1818
      new Set(lookupFields.map((f) => f.id)),
11,263✔
1819
      new Set(rollupFields.map((f) => f.id))
11,263✔
1820
    );
1821

1822
    // Hard guarantee: if any main-table lookup targets a foreign-table lookup, ensure the
11,263✔
1823
    // foreign link CTE used by that target lookup is generated before referencing it.
11,263✔
1824
    for (const lk of lookupFields) {
11,914✔
1825
      const target = lk.getForeignLookupField(foreignTable);
3,090✔
1826
      const nestedLinkId = target ? getLinkFieldId(target.lookupOptions) : undefined;
3,090✔
1827
      if (nestedLinkId) {
3,090✔
1828
        const nestedLink = foreignTable.getField(nestedLinkId) as LinkFieldCore | undefined;
50✔
1829
        if (nestedLink && !this.state.getFieldCteMap().has(nestedLink.id)) {
50✔
1830
          this.generateLinkFieldCteForTable(foreignTable, nestedLink);
13✔
1831
        }
13✔
1832
      }
50✔
1833
    }
3,090✔
1834

1835
    // Collect all nested link dependencies that need to be JOINed
11,263✔
1836
    const nestedJoins = new Set<string>();
11,263✔
1837

1838
    // Helper: add dependent link fields from a target field
11,263✔
1839
    const addDepLinksFromTarget = (field: FieldCore) => {
11,263✔
1840
      const targetField = field.getForeignLookupField(foreignTable);
16,509✔
1841
      if (!targetField) return;
16,509✔
1842
      if (targetField.type === FieldType.ConditionalRollup && !targetField.isLookup) {
16,509✔
1843
        this.generateConditionalRollupFieldCteForScope(
3✔
1844
          foreignTable,
3✔
1845
          targetField as ConditionalRollupFieldCore
3✔
1846
        );
1847
      }
3✔
1848
      if (targetField.isConditionalLookup) {
16,509✔
1849
        const options = targetField.getConditionalLookupOptions?.();
4✔
1850
        if (options) {
4✔
1851
          this.generateConditionalLookupFieldCteForScope(foreignTable, targetField, options);
4✔
1852
        }
4✔
1853
      }
4✔
1854
      const depLinks = targetField.getLinkFields(foreignTable);
16,342✔
1855
      for (const lf of depLinks) {
16,509✔
1856
        if (!lf?.id) continue;
116!
1857
        if (!this.fieldCteMap.has(lf.id)) {
116✔
1858
          // Pre-generate nested CTE for foreign link field
51✔
1859
          this.generateLinkFieldCteForTable(foreignTable, lf);
51✔
1860
        }
51✔
1861
        nestedJoins.add(lf.id);
116✔
1862
      }
116✔
1863
    };
16,342✔
1864

1865
    // Check lookup fields: collect all dependent link fields
11,263✔
1866
    for (const lookupField of lookupFields) {
11,914✔
1867
      addDepLinksFromTarget(lookupField);
3,090✔
1868
    }
3,090✔
1869

1870
    // Check rollup fields: collect all dependent link fields
11,263✔
1871
    for (const rollupField of rollupFields) {
11,914✔
1872
      addDepLinksFromTarget(rollupField);
2,156✔
1873
    }
2,156✔
1874

1875
    addDepLinksFromTarget(linkField);
11,263✔
1876

1877
    this.qb
11,263✔
1878
      // eslint-disable-next-line sonarjs/cognitive-complexity
11,263✔
1879
      .with(cteName, (cqb) => {
11,263✔
1880
        // Create set of JOINed CTEs for this scope
11,270✔
1881
        const joinedCtesInScope = new Set(nestedJoins);
11,270✔
1882

1883
        const visitor = new FieldCteSelectionVisitor(
11,270✔
1884
          cqb,
11,270✔
1885
          this.dbProvider,
11,270✔
1886
          this.dialect,
11,270✔
1887
          this.table,
11,270✔
1888
          foreignTable,
11,270✔
1889
          this.state,
11,270✔
1890
          joinedCtesInScope,
11,270✔
1891
          usesJunctionTable || relationship === Relationship.OneMany ? false : true,
11,270✔
1892
          foreignAliasUsed,
11,270✔
1893
          linkField.id
11,270✔
1894
        );
1895
        const linkValue = linkField.accept(visitor);
11,270✔
1896

1897
        cqb.select(`${mainAlias}.${ID_FIELD_NAME} as main_record_id`);
11,270✔
1898
        // Ensure jsonb type on Postgres to avoid type mismatch (e.g., NULL defaults)
11,270✔
1899
        const linkValueExpr =
11,270✔
1900
          this.dbProvider.driver === DriverClient.Pg ? `${linkValue}::jsonb` : `${linkValue}`;
11,270!
1901
        cqb.select(cqb.client.raw(`${linkValueExpr} as link_value`));
11,270✔
1902

1903
        for (const lookupField of lookupFields) {
11,270✔
1904
          const visitor = new FieldCteSelectionVisitor(
3,091✔
1905
            cqb,
3,091✔
1906
            this.dbProvider,
3,091✔
1907
            this.dialect,
3,091✔
1908
            this.table,
3,091✔
1909
            foreignTable,
3,091✔
1910
            this.state,
3,091✔
1911
            joinedCtesInScope,
3,091✔
1912
            usesJunctionTable || relationship === Relationship.OneMany ? false : true,
3,091✔
1913
            foreignAliasUsed,
3,091✔
1914
            linkField.id
3,091✔
1915
          );
1916
          const lookupValue = lookupField.accept(visitor);
3,091✔
1917
          cqb.select(cqb.client.raw(`${lookupValue} as "lookup_${lookupField.id}"`));
3,091✔
1918
        }
3,091✔
1919

1920
        for (const rollupField of rollupFields) {
11,270✔
1921
          const visitor = new FieldCteSelectionVisitor(
2,157✔
1922
            cqb,
2,157✔
1923
            this.dbProvider,
2,157✔
1924
            this.dialect,
2,157✔
1925
            this.table,
2,157✔
1926
            foreignTable,
2,157✔
1927
            this.state,
2,157✔
1928
            joinedCtesInScope,
2,157✔
1929
            usesJunctionTable || relationship === Relationship.OneMany ? false : true,
2,157✔
1930
            foreignAliasUsed,
2,157✔
1931
            linkField.id
2,157✔
1932
          );
1933
          const rollupValue = rollupField.accept(visitor);
2,157✔
1934
          cqb.select(cqb.client.raw(`${rollupValue} as "rollup_${rollupField.id}"`));
2,157✔
1935
        }
2,157✔
1936

1937
        if (usesJunctionTable) {
11,270✔
1938
          this.fromTableWithRestriction(cqb, this.table, mainAlias);
2,663✔
1939
          cqb
2,663✔
1940
            .leftJoin(
2,663✔
1941
              `${fkHostTableName} as ${JUNCTION_ALIAS}`,
2,663✔
1942
              `${mainAlias}.__id`,
2,663✔
1943
              `${JUNCTION_ALIAS}.${selfKeyName}`
2,663✔
1944
            )
1945
            .leftJoin(
2,663✔
1946
              `${foreignTable.dbTableName} as ${foreignAliasUsed}`,
2,663✔
1947
              `${JUNCTION_ALIAS}.${foreignKeyName}`,
2,663✔
1948
              `${foreignAliasUsed}.__id`
2,663✔
1949
            );
1950

1951
          // Add LEFT JOINs to nested CTEs
2,663✔
1952
          for (const nestedLinkFieldId of nestedJoins) {
2,663✔
1953
            const nestedCteName = this.state.getFieldCteMap().get(nestedLinkFieldId)!;
39✔
1954
            cqb.leftJoin(
39✔
1955
              nestedCteName,
39✔
1956
              `${nestedCteName}.main_record_id`,
39✔
1957
              `${foreignAliasUsed}.__id`
39✔
1958
            );
1959
          }
39✔
1960

1961
          // Removed global application of all lookup/rollup filters: we now apply per-field filters only at selection time
2,663✔
1962

1963
          cqb.groupBy(`${mainAlias}.__id`);
2,663✔
1964

1965
          // For SQLite, add ORDER BY at query level since json_group_array doesn't support internal ordering
2,663✔
1966
          if (this.dbProvider.driver === DriverClient.Sqlite) {
2,663!
1967
            cqb.orderBy(`${JUNCTION_ALIAS}.__id`);
×
1968
          }
×
1969
        } else if (relationship === Relationship.OneMany) {
11,270✔
1970
          // For non-one-way OneMany relationships, foreign key is stored in the foreign table
3,137✔
1971
          // No junction table needed
3,137✔
1972

1973
          this.fromTableWithRestriction(cqb, this.table, mainAlias);
3,137✔
1974
          cqb.leftJoin(
3,137✔
1975
            `${foreignTable.dbTableName} as ${foreignAliasUsed}`,
3,137✔
1976
            `${mainAlias}.__id`,
3,137✔
1977
            `${foreignAliasUsed}.${selfKeyName}`
3,137✔
1978
          );
1979

1980
          // Add LEFT JOINs to nested CTEs
3,137✔
1981
          for (const nestedLinkFieldId of nestedJoins) {
3,137✔
1982
            const nestedCteName = this.state.getFieldCteMap().get(nestedLinkFieldId)!;
33✔
1983
            cqb.leftJoin(
33✔
1984
              nestedCteName,
33✔
1985
              `${nestedCteName}.main_record_id`,
33✔
1986
              `${foreignAliasUsed}.__id`
33✔
1987
            );
1988
          }
33✔
1989

1990
          // Removed global application of all lookup/rollup filters
3,137✔
1991

1992
          cqb.groupBy(`${mainAlias}.__id`);
3,137✔
1993

1994
          // For SQLite, add ORDER BY at query level (NULLS FIRST + stable tie-breaker)
3,137✔
1995
          if (this.dbProvider.driver === DriverClient.Sqlite) {
3,137!
1996
            if (linkField.getHasOrderColumn()) {
×
1997
              cqb.orderByRaw(
×
1998
                `(CASE WHEN ${foreignAliasUsed}.${selfKeyName}_order IS NULL THEN 0 ELSE 1 END) ASC`
×
1999
              );
2000
              cqb.orderBy(`${foreignAliasUsed}.${selfKeyName}_order`, 'asc');
×
2001
            }
×
2002
            // Always tie-break by record id for deterministic order
×
2003
            cqb.orderBy(`${foreignAliasUsed}.__id`, 'asc');
×
2004
          }
×
2005
        } else if (relationship === Relationship.ManyOne || relationship === Relationship.OneOne) {
8,607✔
2006
          // Direct join for many-to-one and one-to-one relationships
5,470✔
2007
          // No GROUP BY needed for single-value relationships
5,470✔
2008

2009
          // For OneOne and ManyOne relationships, the foreign key is always stored in fkHostTableName
5,470✔
2010
          // But we need to determine the correct join condition based on which table we're querying from
5,470✔
2011
          const isForeignKeyInMainTable = fkHostTableName === this.table.dbTableName;
5,470✔
2012

2013
          this.fromTableWithRestriction(cqb, this.table, mainAlias);
5,470✔
2014

2015
          if (isForeignKeyInMainTable) {
5,470✔
2016
            // Foreign key is stored in the main table (original field case)
5,290✔
2017
            // Join: main_table.foreign_key_column = foreign_table.__id
5,290✔
2018
            cqb.leftJoin(
5,290✔
2019
              `${foreignTable.dbTableName} as ${foreignAliasUsed}`,
5,290✔
2020
              `${mainAlias}.${foreignKeyName}`,
5,290✔
2021
              `${foreignAliasUsed}.__id`
5,290✔
2022
            );
2023
          } else {
5,470✔
2024
            // Foreign key is stored in the foreign table (symmetric field case)
180✔
2025
            // Join: foreign_table.foreign_key_column = main_table.__id
180✔
2026
            // Note: for symmetric fields, selfKeyName and foreignKeyName are swapped
180✔
2027
            cqb.leftJoin(
180✔
2028
              `${foreignTable.dbTableName} as ${foreignAliasUsed}`,
180✔
2029
              `${foreignAliasUsed}.${selfKeyName}`,
180✔
2030
              `${mainAlias}.__id`
180✔
2031
            );
2032
          }
180✔
2033

2034
          // Removed global application of all lookup/rollup filters
5,470✔
2035

2036
          // Add LEFT JOINs to nested CTEs for single-value relationships
5,470✔
2037
          for (const nestedLinkFieldId of nestedJoins) {
5,470✔
2038
            const nestedCteName = this.state.getFieldCteMap().get(nestedLinkFieldId)!;
33✔
2039
            cqb.leftJoin(
33✔
2040
              nestedCteName,
33✔
2041
              `${nestedCteName}.main_record_id`,
33✔
2042
              `${foreignAliasUsed}.__id`
33✔
2043
            );
2044
          }
33✔
2045
        }
5,470✔
2046
      });
11,270✔
2047

2048
    if (!this.state.isCteJoined(cteName)) {
11,263✔
2049
      this.qb.leftJoin(cteName, `${mainAlias}.${ID_FIELD_NAME}`, `${cteName}.main_record_id`);
11,263✔
2050
      this.state.markCteJoined(cteName);
11,263✔
2051
    }
11,263✔
2052

2053
    this.state.setFieldCte(linkField.id, cteName);
11,263✔
2054
  }
11,263✔
2055

2056
  /**
17,691✔
2057
   * Generate CTEs for foreign table's dependent link fields if any of the lookup/rollup targets
2058
   * on the current link field point to lookup fields in the foreign table.
2059
   * This ensures multi-layer lookup/rollup can reference precomputed values via nested CTEs.
2060
   */
17,691✔
2061
  private generateNestedForeignCtesIfNeeded(
17,691✔
2062
    mainTable: TableDomain,
11,465✔
2063
    foreignTable: TableDomain,
11,465✔
2064
    mainToForeignLinkField: LinkFieldCore,
11,465✔
2065
    limitLookupIds?: Set<string>,
11,465✔
2066
    limitRollupIds?: Set<string>
11,465✔
2067
  ): void {
11,465✔
2068
    const nestedLinkFields = new Map<string, LinkFieldCore>();
11,465✔
2069
    const ensureConditionalComputedCte = (table: TableDomain, targetField?: FieldCore) => {
11,465✔
2070
      if (!targetField) return;
5,382✔
2071
      if (targetField.type === FieldType.ConditionalRollup && !targetField.isLookup) {
5,382✔
2072
        this.generateConditionalRollupFieldCteForScope(
3✔
2073
          table,
3✔
2074
          targetField as ConditionalRollupFieldCore
3✔
2075
        );
2076
      }
3✔
2077
      if (targetField.isConditionalLookup) {
5,382✔
2078
        const options = targetField.getConditionalLookupOptions?.();
4✔
2079
        if (options) {
4✔
2080
          this.generateConditionalLookupFieldCteForScope(table, targetField, options);
4✔
2081
        }
4✔
2082
      }
4✔
2083
    };
5,382✔
2084

2085
    // Collect lookup fields on main table that depend on this link
11,465✔
2086
    let lookupFields = mainToForeignLinkField.getLookupFields(mainTable);
11,465✔
2087
    if (limitLookupIds) {
11,465✔
2088
      lookupFields = lookupFields.filter((f) => limitLookupIds.has(f.id));
11,263✔
2089
    }
11,263✔
2090
    for (const lookupField of lookupFields) {
11,465✔
2091
      const target = lookupField.getForeignLookupField(foreignTable);
3,198✔
2092
      if (target) {
3,198✔
2093
        ensureConditionalComputedCte(foreignTable, target);
3,135✔
2094
        if (target.type === FieldType.Link) {
3,135✔
2095
          const lf = target as LinkFieldCore;
80✔
2096
          if (!nestedLinkFields.has(lf.id)) nestedLinkFields.set(lf.id, lf);
80✔
2097
        }
80✔
2098
        for (const lf of target.getLinkFields(foreignTable)) {
3,135✔
2099
          if (!nestedLinkFields.has(lf.id)) nestedLinkFields.set(lf.id, lf);
47✔
2100
        }
47✔
2101
      } else {
3,198✔
2102
        const nestedId = lookupField.lookupOptions?.lookupFieldId;
63✔
2103
        const nestedField = nestedId ? foreignTable.getField(nestedId) : undefined;
63!
2104
        if (
63✔
2105
          nestedField &&
63!
2106
          nestedField.type === FieldType.Link &&
63!
2107
          !nestedLinkFields.has(nestedField.id)
×
2108
        ) {
63!
2109
          nestedLinkFields.set(nestedField.id, nestedField as LinkFieldCore);
×
2110
        }
×
2111
        ensureConditionalComputedCte(foreignTable, nestedField);
63✔
2112
      }
63✔
2113
    }
3,198✔
2114

2115
    // Collect rollup fields on main table that depend on this link
11,465✔
2116
    let rollupFields = mainToForeignLinkField.getRollupFields(mainTable);
11,465✔
2117
    if (limitRollupIds) {
11,465✔
2118
      rollupFields = rollupFields.filter((f) => limitRollupIds.has(f.id));
11,263✔
2119
    }
11,263✔
2120
    for (const rollupField of rollupFields) {
11,465✔
2121
      const target = rollupField.getForeignLookupField(foreignTable);
2,184✔
2122
      if (target) {
2,184✔
2123
        ensureConditionalComputedCte(foreignTable, target);
2,158✔
2124
        if (target.type === FieldType.Link) {
2,158!
2125
          const lf = target as LinkFieldCore;
×
2126
          if (!nestedLinkFields.has(lf.id)) nestedLinkFields.set(lf.id, lf);
×
2127
        }
×
2128
        for (const lf of target.getLinkFields(foreignTable)) {
2,158✔
2129
          if (!nestedLinkFields.has(lf.id)) nestedLinkFields.set(lf.id, lf);
22✔
2130
        }
22✔
2131
      } else {
2,184✔
2132
        const nestedId = rollupField.lookupOptions?.lookupFieldId;
26✔
2133
        const nestedField = nestedId ? foreignTable.getField(nestedId) : undefined;
26!
2134
        if (
26✔
2135
          nestedField &&
26!
2136
          nestedField.type === FieldType.Link &&
26!
2137
          !nestedLinkFields.has(nestedField.id)
×
2138
        ) {
26!
2139
          nestedLinkFields.set(nestedField.id, nestedField as LinkFieldCore);
×
2140
        }
×
2141
        ensureConditionalComputedCte(foreignTable, nestedField);
26✔
2142
      }
26✔
2143
    }
2,184✔
2144

2145
    // Generate CTEs for each nested link field on the foreign table if not already generated
11,465✔
2146
    for (const [nestedLinkFieldId, nestedLinkFieldCore] of nestedLinkFields) {
11,465✔
2147
      if (this.state.getFieldCteMap().has(nestedLinkFieldId)) continue;
138!
2148
      this.generateLinkFieldCteForTable(foreignTable, nestedLinkFieldCore);
138✔
2149
    }
138✔
2150
  }
11,465✔
2151

2152
  /**
17,691✔
2153
   * Generate CTE for a link field using the provided table as the "main" table context.
2154
   * This is used to build nested CTEs for foreign tables.
2155
   */
17,691✔
2156
  // eslint-disable-next-line sonarjs/cognitive-complexity
17,691✔
2157
  private generateLinkFieldCteForTable(table: TableDomain, linkField: LinkFieldCore): void {
17,691✔
2158
    const foreignTable = this.tables.getLinkForeignTable(linkField);
202✔
2159
    if (!foreignTable) {
202!
2160
      return;
×
2161
    }
×
2162
    const cteName = FieldCteVisitor.generateCTENameForField(table, linkField);
202✔
2163
    const usesJunctionTable = getLinkUsesJunctionTable(linkField);
202✔
2164
    const options = linkField.options as ILinkFieldOptions;
202✔
2165
    const mainAlias = getTableAliasFromTable(table);
202✔
2166
    const foreignAlias = getTableAliasFromTable(foreignTable);
202✔
2167
    const foreignAliasUsed = foreignAlias === mainAlias ? `${foreignAlias}_f` : foreignAlias;
202!
2168
    const { fkHostTableName, selfKeyName, foreignKeyName, relationship } = options;
202✔
2169

2170
    // Ensure deeper nested dependencies for this nested link are also generated
202✔
2171
    this.generateNestedForeignCtesIfNeeded(table, foreignTable, linkField);
202✔
2172

2173
    // Collect all nested link dependencies that need to be JOINed
202✔
2174
    const nestedJoins = new Set<string>();
202✔
2175
    const lookupFields = linkField.getLookupFields(table);
202✔
2176
    const rollupFields = linkField.getRollupFields(table);
202✔
2177
    if (this.filteredIdSet) {
202✔
2178
      // filteredIdSet belongs to the main table. For nested tables, we cannot filter
202✔
2179
      // by main-table projection IDs; keep all nested lookup/rollup columns to ensure correctness.
202✔
2180
    }
202✔
2181

2182
    // Check if any lookup/rollup fields depend on nested CTEs
202✔
2183
    for (const lookupField of lookupFields) {
202✔
2184
      const target = lookupField.getForeignLookupField(foreignTable);
108✔
2185
      if (target) {
108✔
2186
        if (target.type === FieldType.ConditionalRollup && !target.isLookup) {
106!
2187
          this.generateConditionalRollupFieldCteForScope(
×
2188
            foreignTable,
×
2189
            target as ConditionalRollupFieldCore
×
2190
          );
2191
        }
×
2192
        if (target.isConditionalLookup) {
106!
2193
          const options = target.getConditionalLookupOptions?.();
×
2194
          if (options) {
×
2195
            this.generateConditionalLookupFieldCteForScope(foreignTable, target, options);
×
2196
          }
×
2197
        }
×
2198
        if (target.type === FieldType.Link) {
106!
2199
          const lf = target as LinkFieldCore;
×
2200
          if (this.fieldCteMap.has(lf.id)) {
×
2201
            nestedJoins.add(lf.id);
×
2202
          }
×
2203
        }
×
2204
        const nestedLinkFieldId = getLinkFieldId(target.lookupOptions);
106✔
2205
        if (nestedLinkFieldId && this.fieldCteMap.has(nestedLinkFieldId)) {
106✔
2206
          nestedJoins.add(nestedLinkFieldId);
5✔
2207
        }
5✔
2208
      }
106✔
2209
    }
108✔
2210

2211
    for (const rollupField of rollupFields) {
202✔
2212
      const target = rollupField.getForeignLookupField(foreignTable);
28✔
2213
      if (target) {
28✔
2214
        if (target.type === FieldType.ConditionalRollup && !target.isLookup) {
28!
2215
          this.generateConditionalRollupFieldCteForScope(
×
2216
            foreignTable,
×
2217
            target as ConditionalRollupFieldCore
×
2218
          );
2219
        }
×
2220
        if (target.isConditionalLookup) {
28!
2221
          const options = target.getConditionalLookupOptions?.();
×
2222
          if (options) {
×
2223
            this.generateConditionalLookupFieldCteForScope(foreignTable, target, options);
×
2224
          }
×
2225
        }
×
2226
        if (target.type === FieldType.Link) {
28!
2227
          const lf = target as LinkFieldCore;
×
2228
          if (this.fieldCteMap.has(lf.id)) {
×
2229
            nestedJoins.add(lf.id);
×
2230
          }
×
2231
        }
×
2232
        const nestedLinkFieldId = getLinkFieldId(target.lookupOptions);
28✔
2233
        if (nestedLinkFieldId && this.fieldCteMap.has(nestedLinkFieldId)) {
28!
2234
          nestedJoins.add(nestedLinkFieldId);
×
2235
        }
×
2236
      }
28✔
2237
    }
28✔
2238

2239
    this.qb.with(cteName, (cqb) => {
202✔
2240
      // Create set of JOINed CTEs for this scope
202✔
2241
      const joinedCtesInScope = new Set(nestedJoins);
202✔
2242

2243
      const visitor = new FieldCteSelectionVisitor(
202✔
2244
        cqb,
202✔
2245
        this.dbProvider,
202✔
2246
        this.dialect,
202✔
2247
        table,
202✔
2248
        foreignTable,
202✔
2249
        this.state,
202✔
2250
        joinedCtesInScope,
202✔
2251
        usesJunctionTable || relationship === Relationship.OneMany ? false : true,
202✔
2252
        foreignAliasUsed,
202✔
2253
        linkField.id
202✔
2254
      );
2255
      const linkValue = linkField.accept(visitor);
202✔
2256

2257
      cqb.select(`${mainAlias}.${ID_FIELD_NAME} as main_record_id`);
202✔
2258
      // Ensure jsonb type on Postgres to avoid type mismatch (e.g., NULL defaults)
202✔
2259
      const linkValueExpr =
202✔
2260
        this.dbProvider.driver === DriverClient.Pg ? `${linkValue}::jsonb` : `${linkValue}`;
202!
2261
      cqb.select(cqb.client.raw(`${linkValueExpr} as link_value`));
202✔
2262

2263
      for (const lookupField of lookupFields) {
202✔
2264
        const visitor = new FieldCteSelectionVisitor(
108✔
2265
          cqb,
108✔
2266
          this.dbProvider,
108✔
2267
          this.dialect,
108✔
2268
          table,
108✔
2269
          foreignTable,
108✔
2270
          this.state,
108✔
2271
          joinedCtesInScope,
108✔
2272
          usesJunctionTable || relationship === Relationship.OneMany ? false : true,
108✔
2273
          foreignAliasUsed,
108✔
2274
          linkField.id
108✔
2275
        );
2276
        const lookupValue = lookupField.accept(visitor);
108✔
2277
        cqb.select(cqb.client.raw(`${lookupValue} as "lookup_${lookupField.id}"`));
108✔
2278
      }
108✔
2279

2280
      for (const rollupField of rollupFields) {
202✔
2281
        const visitor = new FieldCteSelectionVisitor(
28✔
2282
          cqb,
28✔
2283
          this.dbProvider,
28✔
2284
          this.dialect,
28✔
2285
          table,
28✔
2286
          foreignTable,
28✔
2287
          this.state,
28✔
2288
          joinedCtesInScope,
28✔
2289
          usesJunctionTable || relationship === Relationship.OneMany ? false : true,
28✔
2290
          foreignAliasUsed,
28✔
2291
          linkField.id
28✔
2292
        );
2293
        const rollupValue = rollupField.accept(visitor);
28✔
2294
        // Ensure the rollup CTE column has a type that matches the physical column
28✔
2295
        // to avoid Postgres UPDATE ... FROM assignment type mismatches (e.g., text vs numeric).
28✔
2296
        const value = typeof rollupValue === 'string' ? rollupValue : rollupValue.toQuery();
28!
2297
        const castedRollupValue = this.castExpressionForDbType(value, rollupField);
28✔
2298
        cqb.select(cqb.client.raw(`${castedRollupValue} as "rollup_${rollupField.id}"`));
28✔
2299
      }
28✔
2300

2301
      if (usesJunctionTable) {
202✔
2302
        this.fromTableWithRestriction(cqb, table, mainAlias);
73✔
2303
        cqb
73✔
2304
          .leftJoin(
73✔
2305
            `${fkHostTableName} as ${JUNCTION_ALIAS}`,
73✔
2306
            `${mainAlias}.__id`,
73✔
2307
            `${JUNCTION_ALIAS}.${selfKeyName}`
73✔
2308
          )
2309
          .leftJoin(
73✔
2310
            `${foreignTable.dbTableName} as ${foreignAliasUsed}`,
73✔
2311
            `${JUNCTION_ALIAS}.${foreignKeyName}`,
73✔
2312
            `${foreignAliasUsed}.__id`
73✔
2313
          );
2314

2315
        // Add LEFT JOINs to nested CTEs
73✔
2316
        for (const nestedLinkFieldId of nestedJoins) {
73✔
2317
          const nestedCteName = this.state.getFieldCteMap().get(nestedLinkFieldId)!;
5✔
2318
          cqb.leftJoin(
5✔
2319
            nestedCteName,
5✔
2320
            `${nestedCteName}.main_record_id`,
5✔
2321
            `${foreignAliasUsed}.__id`
5✔
2322
          );
2323
        }
5✔
2324

2325
        cqb.groupBy(`${mainAlias}.__id`);
73✔
2326

2327
        if (this.dbProvider.driver === DriverClient.Sqlite) {
73!
2328
          if (linkField.getHasOrderColumn()) {
×
2329
            const ordCol = `${JUNCTION_ALIAS}.${linkField.getOrderColumnName()}`;
×
2330
            cqb.orderByRaw(`(CASE WHEN ${ordCol} IS NULL THEN 0 ELSE 1 END) ASC`);
×
2331
            cqb.orderBy(ordCol, 'asc');
×
2332
          }
×
2333
          cqb.orderBy(`${JUNCTION_ALIAS}.__id`, 'asc');
×
2334
        }
×
2335
      } else if (relationship === Relationship.OneMany) {
202✔
2336
        this.fromTableWithRestriction(cqb, table, mainAlias);
77✔
2337
        cqb.leftJoin(
77✔
2338
          `${foreignTable.dbTableName} as ${foreignAliasUsed}`,
77✔
2339
          `${mainAlias}.__id`,
77✔
2340
          `${foreignAliasUsed}.${selfKeyName}`
77✔
2341
        );
2342

2343
        // Add LEFT JOINs to nested CTEs
77✔
2344
        for (const nestedLinkFieldId of nestedJoins) {
77!
2345
          const nestedCteName = this.state.getFieldCteMap().get(nestedLinkFieldId)!;
×
2346
          cqb.leftJoin(
×
2347
            nestedCteName,
×
2348
            `${nestedCteName}.main_record_id`,
×
2349
            `${foreignAliasUsed}.__id`
×
2350
          );
2351
        }
×
2352

2353
        cqb.groupBy(`${mainAlias}.__id`);
77✔
2354

2355
        if (this.dbProvider.driver === DriverClient.Sqlite) {
77!
2356
          if (linkField.getHasOrderColumn()) {
×
2357
            cqb.orderByRaw(
×
2358
              `(CASE WHEN ${foreignAliasUsed}.${selfKeyName}_order IS NULL THEN 0 ELSE 1 END) ASC`
×
2359
            );
2360
            cqb.orderBy(`${foreignAliasUsed}.${selfKeyName}_order`, 'asc');
×
2361
          }
×
2362
          cqb.orderBy(`${foreignAliasUsed}.__id`, 'asc');
×
2363
        }
×
2364
      } else if (relationship === Relationship.ManyOne || relationship === Relationship.OneOne) {
129✔
2365
        const isForeignKeyInMainTable = fkHostTableName === table.dbTableName;
52✔
2366
        this.fromTableWithRestriction(cqb, table, mainAlias);
52✔
2367

2368
        if (isForeignKeyInMainTable) {
52✔
2369
          cqb.leftJoin(
52✔
2370
            `${foreignTable.dbTableName} as ${foreignAliasUsed}`,
52✔
2371
            `${mainAlias}.${foreignKeyName}`,
52✔
2372
            `${foreignAliasUsed}.__id`
52✔
2373
          );
2374
        } else {
52!
2375
          cqb.leftJoin(
×
2376
            `${foreignTable.dbTableName} as ${foreignAliasUsed}`,
×
2377
            `${foreignAliasUsed}.${selfKeyName}`,
×
2378
            `${mainAlias}.__id`
×
2379
          );
2380
        }
×
2381

2382
        // Add LEFT JOINs to nested CTEs for single-value relationships
52✔
2383
        for (const nestedLinkFieldId of nestedJoins) {
52!
2384
          const nestedCteName = this.state.getFieldCteMap().get(nestedLinkFieldId)!;
×
2385
          cqb.leftJoin(
×
2386
            nestedCteName,
×
2387
            `${nestedCteName}.main_record_id`,
×
2388
            `${foreignAliasUsed}.__id`
×
2389
          );
2390
        }
×
2391
      }
52✔
2392
    });
202✔
2393

2394
    this.state.setFieldCte(linkField.id, cteName);
202✔
2395
  }
202✔
2396

2397
  visitNumberField(_field: NumberFieldCore): void {}
17,691✔
2398
  visitSingleLineTextField(_field: SingleLineTextFieldCore): void {}
17,691✔
2399
  visitLongTextField(_field: LongTextFieldCore): void {}
17,691✔
2400
  visitAttachmentField(_field: AttachmentFieldCore): void {}
17,691✔
2401
  visitCheckboxField(_field: CheckboxFieldCore): void {}
17,691✔
2402
  visitDateField(_field: DateFieldCore): void {}
17,691✔
2403
  visitRatingField(_field: RatingFieldCore): void {}
17,691✔
2404
  visitAutoNumberField(_field: AutoNumberFieldCore): void {}
17,691✔
2405
  visitLinkField(field: LinkFieldCore): void {
17,691✔
2406
    if (field.hasError) return;
11,281✔
2407
    const existingCteName = this.state.getCteName(field.id);
11,266✔
2408
    if (existingCteName) {
11,281✔
2409
      this.ensureLinkCteJoined(existingCteName);
2,968✔
2410
      return;
2,968✔
2411
    }
2,968✔
2412
    this.generateLinkFieldCte(field);
8,298✔
2413
  }
8,298✔
2414
  visitRollupField(_field: RollupFieldCore): void {}
17,691✔
2415
  visitConditionalRollupField(field: ConditionalRollupFieldCore): void {
17,691✔
2416
    this.generateConditionalRollupFieldCte(field);
389✔
2417
  }
389✔
2418
  visitSingleSelectField(_field: SingleSelectFieldCore): void {}
17,691✔
2419
  visitMultipleSelectField(_field: MultipleSelectFieldCore): void {}
17,691✔
2420
  visitFormulaField(_field: FormulaFieldCore): void {}
17,691✔
2421
  visitCreatedTimeField(_field: CreatedTimeFieldCore): void {}
17,691✔
2422
  visitLastModifiedTimeField(_field: LastModifiedTimeFieldCore): void {}
17,691✔
2423
  visitUserField(_field: UserFieldCore): void {}
17,691✔
2424
  visitCreatedByField(_field: CreatedByFieldCore): void {}
17,691✔
2425
  visitLastModifiedByField(_field: LastModifiedByFieldCore): void {}
17,691✔
2426
  visitButtonField(_field: ButtonFieldCore): void {}
17,691✔
2427

2428
  private ensureLinkCteJoined(cteName: string): void {
17,691✔
2429
    if (this.state.isCteJoined(cteName)) {
2,968✔
2430
      return;
2,968✔
2431
    }
2,968!
2432
    const mainAlias = getTableAliasFromTable(this.table);
×
2433
    this.qb.leftJoin(cteName, `${mainAlias}.${ID_FIELD_NAME}`, `${cteName}.main_record_id`);
×
2434
    this.state.markCteJoined(cteName);
×
2435
  }
×
2436
}
17,691✔
2437
const getLinkFieldId = (options: FieldCore['lookupOptions']): string | undefined => {
2✔
2438
  return options && isLinkLookupOptions(options) ? options.linkFieldId : undefined;
8,491✔
2439
};
8,491✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc