Skip to content

Commit ccfc3bd

Browse files
committed
Fix forEach .where() parsing with nested parentheses and .first()
Fixed two issues in PathParser.parseFhirPathWhere(): 1. Replaced greedy regex with balanced parenthesis counting - Old: /^(.+)\.where\((.*)\)$/ would match to the LAST ) - New: Counts opening/closing parens to find matching ) - Fixes: "component.where(code.coding.exists(...)).first()" 2. Convert .first() to [0] array indexing - .first() after .where() is now converted to [0] - Results in correct SQL: WHERE [key] = '0' This allows forEach paths like: component.where(code.coding.exists(system='...' and code=%const)).first() To correctly: - Extract condition: code.coding.exists(system='...' and code=%const) - Substitute constants: %const → actual value - Apply .first(): converted to [0] array index - Generate: OPENJSON with WHERE clause including [key] = '0'
1 parent bc49d95 commit ccfc3bd

File tree

1 file changed

+38
-6
lines changed

1 file changed

+38
-6
lines changed

src/queryGenerator/PathParser.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,50 @@ export class PathParser {
5050
path: string,
5151
context: TranspilerContext,
5252
): FhirPathWhereResult {
53-
const whereMatch = /^(.+)\.where\((.*)\)$/.exec(path);
54-
if (!whereMatch) {
53+
// Find .where( in the path.
54+
const whereIndex = path.indexOf(".where(");
55+
if (whereIndex === -1) {
5556
return { path, whereCondition: null };
5657
}
5758

58-
const basePath = whereMatch[1];
59-
const condition = whereMatch[2].trim();
59+
const basePath = path.substring(0, whereIndex);
60+
61+
// Find the matching closing parenthesis using balanced counting.
62+
let parenCount = 0;
63+
let conditionEnd = -1;
64+
const whereStart = whereIndex + 7; // Position after ".where(".
65+
66+
for (let i = whereStart; i < path.length; i++) {
67+
if (path[i] === "(") {
68+
parenCount++;
69+
} else if (path[i] === ")") {
70+
if (parenCount === 0) {
71+
conditionEnd = i;
72+
break;
73+
}
74+
parenCount--;
75+
}
76+
}
77+
78+
if (conditionEnd === -1) {
79+
throw new Error(`Unmatched parentheses in .where() function: ${path}`);
80+
}
81+
82+
const condition = path.substring(whereStart, conditionEnd).trim();
83+
let remainingPath = path.substring(conditionEnd + 1);
84+
85+
// Handle .first() by converting it to [0] array indexing.
86+
if (remainingPath === ".first()") {
87+
remainingPath = "[0]";
88+
}
89+
90+
// If there's a remaining path, append it to the base path.
91+
const fullPath = remainingPath ? `${basePath}${remainingPath}` : basePath;
6092

6193
// Handle .where(false) - filter out everything.
6294
if (condition === "false") {
6395
return {
64-
path: basePath,
96+
path: fullPath,
6597
whereCondition: "1 = 0", // Always false.
6698
};
6799
}
@@ -76,7 +108,7 @@ export class PathParser {
76108

77109
const sqlCondition = Transpiler.transpile(condition, itemContext);
78110
return {
79-
path: basePath,
111+
path: fullPath,
80112
whereCondition: sqlCondition,
81113
};
82114
} catch (error) {

0 commit comments

Comments
 (0)