Skip to content

Commit eb036e2

Browse files
committed
Fix .first() transpilation in forEach with .where() filtering
Previously, .first() was incorrectly converted to [0] array indexing, which applied index filtering BEFORE the WHERE condition. This caused queries to fail when the desired element was not at index 0. For example, in blood pressure observations where: - component[0] = diastolic (8462-4) - component[1] = systolic (8480-6) The query `component.where(code='8480-6').first()` would incorrectly check only index 0 (which has code 8462-4), returning no results. Changes: - Modified FhirPathWhereResult interface to include useFirst flag - Updated parseFhirPathWhere() to detect .first() and set flag instead of converting to [0] - Modified ForEachProcessor.buildSimpleApplyClause() to use TOP 1 with ORDER BY [key] when useFirst is true - This generates: SELECT TOP 1 * ... WHERE condition ORDER BY [key] - Correctly filters all elements first, then takes the first match The fix ensures .first() applies to filtered results, not to pre-filtered array positions.
1 parent 6a54a14 commit eb036e2

File tree

2 files changed

+24
-9
lines changed

2 files changed

+24
-9
lines changed

src/queryGenerator/ForEachProcessor.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -383,8 +383,11 @@ export class ForEachProcessor {
383383
const applyAlias = forEachContext.currentForEachAlias ?? "";
384384
const sourceExpression = forEachContext.forEachSource ?? "";
385385

386-
const { path: pathWithoutWhere, whereCondition } =
387-
this.pathParser.parseFhirPathWhere(rawPath ?? "", forEachContext);
386+
const {
387+
path: pathWithoutWhere,
388+
whereCondition,
389+
useFirst,
390+
} = this.pathParser.parseFhirPathWhere(rawPath ?? "", forEachContext);
388391
const { path: forEachPath, arrayIndex } =
389392
this.pathParser.parseArrayIndexing(pathWithoutWhere);
390393
const arrayPaths = this.pathParser.detectArrayFlatteningPaths(forEachPath);
@@ -407,6 +410,7 @@ export class ForEachProcessor {
407410
applyAlias,
408411
arrayIndex,
409412
whereCondition,
413+
useFirst,
410414
);
411415
}
412416

@@ -420,13 +424,20 @@ export class ForEachProcessor {
420424
applyAlias: string,
421425
arrayIndex: number | null,
422426
whereCondition: string | null,
427+
useFirst: boolean = false,
423428
): string {
424429
const whereClauses = this.buildWhereClauses(arrayIndex, whereCondition);
425430

426-
if (whereClauses.length > 0) {
431+
if (whereClauses.length > 0 || useFirst) {
432+
// Build SELECT with WHERE clause and/or TOP 1 for .first()
433+
const topClause = useFirst ? "TOP 1 " : "";
434+
const orderBy = useFirst ? " ORDER BY [key]" : "";
435+
const whereClause =
436+
whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
437+
427438
return `\n${applyType} (
428-
SELECT * FROM OPENJSON(${sourceExpression}, '$.${forEachPath}')
429-
WHERE ${whereClauses.join(" AND ")}
439+
SELECT ${topClause}* FROM OPENJSON(${sourceExpression}, '$.${forEachPath}')
440+
${whereClause}${orderBy}
430441
) AS ${applyAlias}`;
431442
}
432443

src/queryGenerator/PathParser.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Transpiler, TranspilerContext } from "../fhirpath/transpiler.js";
1010
export interface FhirPathWhereResult {
1111
path: string;
1212
whereCondition: string | null;
13+
useFirst: boolean;
1314
}
1415

1516
/**
@@ -94,7 +95,7 @@ export class PathParser {
9495
): FhirPathWhereResult {
9596
const whereIndex = path.indexOf(".where(");
9697
if (whereIndex === -1) {
97-
return { path, whereCondition: null };
98+
return { path, whereCondition: null, useFirst: false };
9899
}
99100

100101
const basePath = path.substring(0, whereIndex);
@@ -108,9 +109,10 @@ export class PathParser {
108109
const condition = path.substring(whereStart, conditionEnd).trim();
109110
let remainingPath = path.substring(conditionEnd + 1);
110111

111-
// Handle .first() by converting it to [0] array indexing.
112-
if (remainingPath === ".first()") {
113-
remainingPath = "[0]";
112+
// Detect .first() to apply TOP 1 in SQL generation.
113+
const useFirst = remainingPath === ".first()";
114+
if (useFirst) {
115+
remainingPath = ""; // Remove .first() from path - it will be handled via TOP 1
114116
}
115117

116118
const fullPath = remainingPath ? `${basePath}${remainingPath}` : basePath;
@@ -120,12 +122,14 @@ export class PathParser {
120122
return {
121123
path: fullPath,
122124
whereCondition: "1 = 0",
125+
useFirst: false,
123126
};
124127
}
125128

126129
return {
127130
path: fullPath,
128131
whereCondition: this.transpileWhereCondition(condition, context),
132+
useFirst,
129133
};
130134
}
131135

0 commit comments

Comments
 (0)