sof-mssql 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +346 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +85 -0
- package/dist/cli.js.map +1 -0
- package/dist/fhirpath/transpiler.d.ts +18 -0
- package/dist/fhirpath/transpiler.d.ts.map +1 -0
- package/dist/fhirpath/transpiler.js +82 -0
- package/dist/fhirpath/transpiler.js.map +1 -0
- package/dist/fhirpath/visitor.d.ts +153 -0
- package/dist/fhirpath/visitor.d.ts.map +1 -0
- package/dist/fhirpath/visitor.js +1295 -0
- package/dist/fhirpath/visitor.js.map +1 -0
- package/dist/generated/grammar/fhirpathLexer.d.ts +88 -0
- package/dist/generated/grammar/fhirpathLexer.d.ts.map +1 -0
- package/dist/generated/grammar/fhirpathLexer.js +598 -0
- package/dist/generated/grammar/fhirpathLexer.js.map +1 -0
- package/dist/generated/grammar/fhirpathListener.d.ts +589 -0
- package/dist/generated/grammar/fhirpathListener.d.ts.map +1 -0
- package/dist/generated/grammar/fhirpathListener.js +4 -0
- package/dist/generated/grammar/fhirpathListener.js.map +1 -0
- package/dist/generated/grammar/fhirpathParser.d.ts +470 -0
- package/dist/generated/grammar/fhirpathParser.d.ts.map +1 -0
- package/dist/generated/grammar/fhirpathParser.js +3022 -0
- package/dist/generated/grammar/fhirpathParser.js.map +1 -0
- package/dist/generated/grammar/fhirpathVisitor.d.ts +372 -0
- package/dist/generated/grammar/fhirpathVisitor.d.ts.map +1 -0
- package/dist/generated/grammar/fhirpathVisitor.js +4 -0
- package/dist/generated/grammar/fhirpathVisitor.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/load.d.ts +14 -0
- package/dist/load.d.ts.map +1 -0
- package/dist/load.js +115 -0
- package/dist/load.js.map +1 -0
- package/dist/loader/connection.d.ts +36 -0
- package/dist/loader/connection.d.ts.map +1 -0
- package/dist/loader/connection.js +106 -0
- package/dist/loader/connection.js.map +1 -0
- package/dist/loader/discovery.d.ts +38 -0
- package/dist/loader/discovery.d.ts.map +1 -0
- package/dist/loader/discovery.js +107 -0
- package/dist/loader/discovery.js.map +1 -0
- package/dist/loader/index.d.ts +24 -0
- package/dist/loader/index.d.ts.map +1 -0
- package/dist/loader/index.js +193 -0
- package/dist/loader/index.js.map +1 -0
- package/dist/loader/progress.d.ts +70 -0
- package/dist/loader/progress.d.ts.map +1 -0
- package/dist/loader/progress.js +206 -0
- package/dist/loader/progress.js.map +1 -0
- package/dist/loader/stream.d.ts +21 -0
- package/dist/loader/stream.d.ts.map +1 -0
- package/dist/loader/stream.js +103 -0
- package/dist/loader/stream.js.map +1 -0
- package/dist/loader/tables.d.ts +43 -0
- package/dist/loader/tables.d.ts.map +1 -0
- package/dist/loader/tables.js +88 -0
- package/dist/loader/tables.js.map +1 -0
- package/dist/loader/types.d.ts +134 -0
- package/dist/loader/types.d.ts.map +1 -0
- package/dist/loader/types.js +8 -0
- package/dist/loader/types.js.map +1 -0
- package/dist/parser.d.ts +60 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +226 -0
- package/dist/parser.js.map +1 -0
- package/dist/queryGenerator/ColumnExpressionGenerator.d.ts +52 -0
- package/dist/queryGenerator/ColumnExpressionGenerator.d.ts.map +1 -0
- package/dist/queryGenerator/ColumnExpressionGenerator.js +144 -0
- package/dist/queryGenerator/ColumnExpressionGenerator.js.map +1 -0
- package/dist/queryGenerator/ForEachProcessor.d.ts +127 -0
- package/dist/queryGenerator/ForEachProcessor.d.ts.map +1 -0
- package/dist/queryGenerator/ForEachProcessor.js +351 -0
- package/dist/queryGenerator/ForEachProcessor.js.map +1 -0
- package/dist/queryGenerator/PathParser.d.ts +64 -0
- package/dist/queryGenerator/PathParser.d.ts.map +1 -0
- package/dist/queryGenerator/PathParser.js +164 -0
- package/dist/queryGenerator/PathParser.js.map +1 -0
- package/dist/queryGenerator/SelectClauseBuilder.d.ts +63 -0
- package/dist/queryGenerator/SelectClauseBuilder.d.ts.map +1 -0
- package/dist/queryGenerator/SelectClauseBuilder.js +196 -0
- package/dist/queryGenerator/SelectClauseBuilder.js.map +1 -0
- package/dist/queryGenerator/SelectCombinationExpander.d.ts +42 -0
- package/dist/queryGenerator/SelectCombinationExpander.d.ts.map +1 -0
- package/dist/queryGenerator/SelectCombinationExpander.js +95 -0
- package/dist/queryGenerator/SelectCombinationExpander.js.map +1 -0
- package/dist/queryGenerator/WhereClauseBuilder.d.ts +20 -0
- package/dist/queryGenerator/WhereClauseBuilder.d.ts.map +1 -0
- package/dist/queryGenerator/WhereClauseBuilder.js +63 -0
- package/dist/queryGenerator/WhereClauseBuilder.js.map +1 -0
- package/dist/queryGenerator/index.d.ts +10 -0
- package/dist/queryGenerator/index.d.ts.map +1 -0
- package/dist/queryGenerator/index.js +19 -0
- package/dist/queryGenerator/index.js.map +1 -0
- package/dist/queryGenerator.d.ts +61 -0
- package/dist/queryGenerator.d.ts.map +1 -0
- package/dist/queryGenerator.js +187 -0
- package/dist/queryGenerator.js.map +1 -0
- package/dist/tests/sqlOnFhir.test.d.ts +11 -0
- package/dist/tests/sqlOnFhir.test.d.ts.map +1 -0
- package/dist/tests/sqlOnFhir.test.js +24 -0
- package/dist/tests/sqlOnFhir.test.js.map +1 -0
- package/dist/tests/utils/database.d.ts +38 -0
- package/dist/tests/utils/database.d.ts.map +1 -0
- package/dist/tests/utils/database.js +258 -0
- package/dist/tests/utils/database.js.map +1 -0
- package/dist/tests/utils/generator.d.ts +58 -0
- package/dist/tests/utils/generator.d.ts.map +1 -0
- package/dist/tests/utils/generator.js +195 -0
- package/dist/tests/utils/generator.js.map +1 -0
- package/dist/tests/utils/reporter.d.ts +83 -0
- package/dist/tests/utils/reporter.d.ts.map +1 -0
- package/dist/tests/utils/reporter.js +245 -0
- package/dist/tests/utils/reporter.js.map +1 -0
- package/dist/tests/utils/sqlOnFhir.d.ts +33 -0
- package/dist/tests/utils/sqlOnFhir.d.ts.map +1 -0
- package/dist/tests/utils/sqlOnFhir.js +281 -0
- package/dist/tests/utils/sqlOnFhir.js.map +1 -0
- package/dist/tests/utils/testContext.d.ts +18 -0
- package/dist/tests/utils/testContext.d.ts.map +1 -0
- package/dist/tests/utils/testContext.js +25 -0
- package/dist/tests/utils/testContext.js.map +1 -0
- package/dist/tests/utils/types.d.ts +31 -0
- package/dist/tests/utils/types.d.ts.map +1 -0
- package/dist/tests/utils/types.js +9 -0
- package/dist/tests/utils/types.js.map +1 -0
- package/dist/types.d.ts +288 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,1295 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* FHIRPath to T-SQL visitor implementation using ANTLR.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.FHIRPathToTSqlVisitor = void 0;
|
|
7
|
+
const AbstractParseTreeVisitor_1 = require("antlr4ts/tree/AbstractParseTreeVisitor");
|
|
8
|
+
const fhirpathParser_1 = require("../generated/grammar/fhirpathParser");
|
|
9
|
+
class FHIRPathToTSqlVisitor extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor {
|
|
10
|
+
context;
|
|
11
|
+
constructor(context) {
|
|
12
|
+
super();
|
|
13
|
+
this.context = context;
|
|
14
|
+
}
|
|
15
|
+
defaultResult() {
|
|
16
|
+
return "NULL";
|
|
17
|
+
}
|
|
18
|
+
visitEntireExpression(ctx) {
|
|
19
|
+
return this.visit(ctx.expression());
|
|
20
|
+
}
|
|
21
|
+
visitTermExpression(ctx) {
|
|
22
|
+
return this.visit(ctx.term());
|
|
23
|
+
}
|
|
24
|
+
visitInvocationExpression(ctx) {
|
|
25
|
+
const base = this.visit(ctx.expression());
|
|
26
|
+
const invocation = ctx.invocation();
|
|
27
|
+
if (invocation instanceof fhirpathParser_1.MemberInvocationContext) {
|
|
28
|
+
return this.handleMemberInvocation(base, invocation);
|
|
29
|
+
}
|
|
30
|
+
else if (invocation instanceof fhirpathParser_1.FunctionInvocationContext) {
|
|
31
|
+
return this.handleFunctionInvocation(base, invocation);
|
|
32
|
+
}
|
|
33
|
+
return this.defaultResult();
|
|
34
|
+
}
|
|
35
|
+
visitIndexerExpression(ctx) {
|
|
36
|
+
const base = this.visit(ctx.expression(0));
|
|
37
|
+
const index = this.visit(ctx.expression(1));
|
|
38
|
+
// Generate JSON path with array index
|
|
39
|
+
if (base.includes("JSON_VALUE")) {
|
|
40
|
+
const pathMatch = /JSON_VALUE\(([^,]+),\s*'([^']+)'\)/.exec(base);
|
|
41
|
+
if (pathMatch) {
|
|
42
|
+
const source = pathMatch[1];
|
|
43
|
+
const path = pathMatch[2];
|
|
44
|
+
return `JSON_VALUE(${source}, '${path}[${index}]')`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return `JSON_VALUE(${base}, '$[${index}]')`;
|
|
48
|
+
}
|
|
49
|
+
visitPolarityExpression(ctx) {
|
|
50
|
+
const operand = this.visit(ctx.expression());
|
|
51
|
+
const operator = ctx.text.charAt(0); // '+' or '-'
|
|
52
|
+
if (operator === "-") {
|
|
53
|
+
return `(-${operand})`;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
return `(+${operand})`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
visitMultiplicativeExpression(ctx) {
|
|
60
|
+
const left = this.visit(ctx.expression(0));
|
|
61
|
+
const right = this.visit(ctx.expression(1));
|
|
62
|
+
// Get the original expression texts from the parse tree to find the operator
|
|
63
|
+
const leftText = ctx.expression(0).text;
|
|
64
|
+
const rightText = ctx.expression(1).text;
|
|
65
|
+
const operator = this.getOperatorFromContext(ctx.text, leftText, rightText);
|
|
66
|
+
// Cast JSON_VALUE results to DECIMAL for numeric operations
|
|
67
|
+
const leftCasted = this.castForNumericOperation(left);
|
|
68
|
+
const rightCasted = this.castForNumericOperation(right);
|
|
69
|
+
switch (operator) {
|
|
70
|
+
case "*":
|
|
71
|
+
return `(${leftCasted} * ${rightCasted})`;
|
|
72
|
+
case "/":
|
|
73
|
+
case "div":
|
|
74
|
+
return `(${leftCasted} / ${rightCasted})`;
|
|
75
|
+
case "mod":
|
|
76
|
+
return `(${leftCasted} % ${rightCasted})`;
|
|
77
|
+
default:
|
|
78
|
+
return `(${leftCasted} * ${rightCasted})`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
visitAdditiveExpression(ctx) {
|
|
82
|
+
const left = this.visit(ctx.expression(0));
|
|
83
|
+
const right = this.visit(ctx.expression(1));
|
|
84
|
+
// Get the original expression texts from the parse tree to find the operator
|
|
85
|
+
const leftText = ctx.expression(0).text;
|
|
86
|
+
const rightText = ctx.expression(1).text;
|
|
87
|
+
const operator = this.getOperatorFromContext(ctx.text, leftText, rightText);
|
|
88
|
+
switch (operator) {
|
|
89
|
+
case "+":
|
|
90
|
+
case "-": {
|
|
91
|
+
// Cast JSON_VALUE results to DECIMAL for numeric operations
|
|
92
|
+
const leftCasted = this.castForNumericOperation(left);
|
|
93
|
+
const rightCasted = this.castForNumericOperation(right);
|
|
94
|
+
return operator === "+"
|
|
95
|
+
? `(${leftCasted} + ${rightCasted})`
|
|
96
|
+
: `(${leftCasted} - ${rightCasted})`;
|
|
97
|
+
}
|
|
98
|
+
case "&":
|
|
99
|
+
// String concatenation in FHIRPath, use CONCAT in SQL Server
|
|
100
|
+
return `CONCAT(${left}, ${right})`;
|
|
101
|
+
default: {
|
|
102
|
+
const leftCasted = this.castForNumericOperation(left);
|
|
103
|
+
const rightCasted = this.castForNumericOperation(right);
|
|
104
|
+
return `(${leftCasted} + ${rightCasted})`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
visitTypeExpression(ctx) {
|
|
109
|
+
const expression = this.visit(ctx.expression());
|
|
110
|
+
const typeSpec = this.visit(ctx.typeSpecifier());
|
|
111
|
+
const operator = this.getOperatorFromContext(ctx.text, expression, typeSpec);
|
|
112
|
+
if (operator === "is") {
|
|
113
|
+
// Type checking - simplified implementation
|
|
114
|
+
return `(${expression} IS NOT NULL)`;
|
|
115
|
+
}
|
|
116
|
+
else if (operator === "as") {
|
|
117
|
+
// Type casting - return the expression as-is for simplification
|
|
118
|
+
return expression;
|
|
119
|
+
}
|
|
120
|
+
return expression;
|
|
121
|
+
}
|
|
122
|
+
visitUnionExpression(ctx) {
|
|
123
|
+
const left = this.visit(ctx.expression(0));
|
|
124
|
+
const right = this.visit(ctx.expression(1));
|
|
125
|
+
// Union operation - in SQL Server, we'd need a more complex implementation
|
|
126
|
+
// For now, we'll use a simplified approach
|
|
127
|
+
return `COALESCE(${left}, ${right})`;
|
|
128
|
+
}
|
|
129
|
+
visitInequalityExpression(ctx) {
|
|
130
|
+
const left = this.visit(ctx.expression(0));
|
|
131
|
+
const right = this.visit(ctx.expression(1));
|
|
132
|
+
// Get the operator from the middle child (between the two expressions)
|
|
133
|
+
// The context has 3 children: expr0, operator, expr1
|
|
134
|
+
const operator = ctx.childCount >= 3 ? ctx.getChild(1).text : "";
|
|
135
|
+
switch (operator) {
|
|
136
|
+
case "<":
|
|
137
|
+
return `(${left} < ${right})`;
|
|
138
|
+
case "<=":
|
|
139
|
+
return `(${left} <= ${right})`;
|
|
140
|
+
case ">":
|
|
141
|
+
return `(${left} > ${right})`;
|
|
142
|
+
case ">=":
|
|
143
|
+
return `(${left} >= ${right})`;
|
|
144
|
+
default:
|
|
145
|
+
return `(${left} < ${right})`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
visitEqualityExpression(ctx) {
|
|
149
|
+
const left = this.visit(ctx.expression(0));
|
|
150
|
+
const right = this.visit(ctx.expression(1));
|
|
151
|
+
const operator = this.getOperatorFromContext(ctx.text, left, right);
|
|
152
|
+
switch (operator) {
|
|
153
|
+
case "=":
|
|
154
|
+
// Handle boolean comparisons - now that boolean literals return quoted strings
|
|
155
|
+
return `(${left} = ${right})`;
|
|
156
|
+
case "!=":
|
|
157
|
+
return `(${left} != ${right})`;
|
|
158
|
+
case "~":
|
|
159
|
+
// Equivalent/approximately equal
|
|
160
|
+
return `(${left} = ${right})`;
|
|
161
|
+
case "!~":
|
|
162
|
+
// Not equivalent
|
|
163
|
+
return `(${left} != ${right})`;
|
|
164
|
+
default:
|
|
165
|
+
return `(${left} = ${right})`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
visitMembershipExpression(ctx) {
|
|
169
|
+
const left = this.visit(ctx.expression(0));
|
|
170
|
+
const right = this.visit(ctx.expression(1));
|
|
171
|
+
const operator = this.getOperatorFromContext(ctx.text, left, right);
|
|
172
|
+
if (operator === "in") {
|
|
173
|
+
// Check if left is in the collection right
|
|
174
|
+
return `EXISTS (SELECT 1 FROM OPENJSON(${right}) WHERE value = ${left})`;
|
|
175
|
+
}
|
|
176
|
+
else if (operator === "contains") {
|
|
177
|
+
// Check if collection left contains right
|
|
178
|
+
return `EXISTS (SELECT 1 FROM OPENJSON(${left}) WHERE value = ${right})`;
|
|
179
|
+
}
|
|
180
|
+
return this.defaultResult();
|
|
181
|
+
}
|
|
182
|
+
visitAndExpression(ctx) {
|
|
183
|
+
const left = this.visit(ctx.expression(0));
|
|
184
|
+
const right = this.visit(ctx.expression(1));
|
|
185
|
+
return `(${left} AND ${right})`;
|
|
186
|
+
}
|
|
187
|
+
visitOrExpression(ctx) {
|
|
188
|
+
const left = this.visit(ctx.expression(0));
|
|
189
|
+
const right = this.visit(ctx.expression(1));
|
|
190
|
+
const operator = this.getOperatorFromContext(ctx.text, left, right);
|
|
191
|
+
if (operator === "or") {
|
|
192
|
+
return `(${left} OR ${right})`;
|
|
193
|
+
}
|
|
194
|
+
else if (operator === "xor") {
|
|
195
|
+
// Exclusive OR
|
|
196
|
+
return `((${left} AND NOT ${right}) OR (NOT ${left} AND ${right}))`;
|
|
197
|
+
}
|
|
198
|
+
return `(${left} OR ${right})`;
|
|
199
|
+
}
|
|
200
|
+
visitImpliesExpression(ctx) {
|
|
201
|
+
const left = this.visit(ctx.expression(0));
|
|
202
|
+
const right = this.visit(ctx.expression(1));
|
|
203
|
+
// A implies B is equivalent to (NOT A) OR B
|
|
204
|
+
return `((NOT ${left}) OR ${right})`;
|
|
205
|
+
}
|
|
206
|
+
// Literal visitors
|
|
207
|
+
visitNullLiteral(_ctx) {
|
|
208
|
+
return "NULL";
|
|
209
|
+
}
|
|
210
|
+
visitBooleanLiteral(ctx) {
|
|
211
|
+
const value = ctx.text.toLowerCase();
|
|
212
|
+
// Return quoted boolean for JSON comparisons
|
|
213
|
+
return value === "true" ? "'true'" : "'false'";
|
|
214
|
+
}
|
|
215
|
+
visitStringLiteral(ctx) {
|
|
216
|
+
// Remove surrounding quotes and escape internal quotes
|
|
217
|
+
const value = ctx.text.slice(1, -1).replace(/'/g, "''");
|
|
218
|
+
return `'${value}'`;
|
|
219
|
+
}
|
|
220
|
+
visitNumberLiteral(ctx) {
|
|
221
|
+
return ctx.text;
|
|
222
|
+
}
|
|
223
|
+
visitLongNumberLiteral(ctx) {
|
|
224
|
+
return ctx.text.replace(/L$/i, "");
|
|
225
|
+
}
|
|
226
|
+
visitDateLiteral(ctx) {
|
|
227
|
+
// Remove @ prefix and wrap in quotes for SQL
|
|
228
|
+
const value = ctx.text.substring(1);
|
|
229
|
+
return `'${value}'`;
|
|
230
|
+
}
|
|
231
|
+
visitDateTimeLiteral(ctx) {
|
|
232
|
+
// Remove @ prefix and wrap in quotes for SQL
|
|
233
|
+
const value = ctx.text.substring(1);
|
|
234
|
+
return `'${value}'`;
|
|
235
|
+
}
|
|
236
|
+
visitTimeLiteral(ctx) {
|
|
237
|
+
// Remove @T prefix and wrap in quotes for SQL
|
|
238
|
+
const value = ctx.text.substring(2);
|
|
239
|
+
return `'${value}'`;
|
|
240
|
+
}
|
|
241
|
+
visitQuantityLiteral(ctx) {
|
|
242
|
+
return this.visit(ctx.quantity());
|
|
243
|
+
}
|
|
244
|
+
// Invocation visitors
|
|
245
|
+
visitMemberInvocation(ctx) {
|
|
246
|
+
const memberName = this.visit(ctx.identifier());
|
|
247
|
+
// Handle special identifiers
|
|
248
|
+
if (memberName === "id") {
|
|
249
|
+
// Extract id from JSON, not from database row ID
|
|
250
|
+
return `JSON_VALUE(${this.context.resourceAlias}.json, '$.id')`;
|
|
251
|
+
}
|
|
252
|
+
// Known FHIR array fields should use JSON_QUERY
|
|
253
|
+
const knownArrayFields = [
|
|
254
|
+
"name",
|
|
255
|
+
"given",
|
|
256
|
+
"telecom",
|
|
257
|
+
"address",
|
|
258
|
+
"line",
|
|
259
|
+
"identifier",
|
|
260
|
+
"extension",
|
|
261
|
+
"contact",
|
|
262
|
+
"output",
|
|
263
|
+
"item",
|
|
264
|
+
"udiCarrier",
|
|
265
|
+
"coding",
|
|
266
|
+
"component",
|
|
267
|
+
];
|
|
268
|
+
// Regular JSON property access
|
|
269
|
+
if (this.context.iterationContext) {
|
|
270
|
+
// Check if the member is a known array field - use JSON_QUERY for arrays
|
|
271
|
+
if (knownArrayFields.includes(memberName)) {
|
|
272
|
+
return `JSON_QUERY(${this.context.iterationContext}, '$.${memberName}')`;
|
|
273
|
+
}
|
|
274
|
+
return `JSON_VALUE(${this.context.iterationContext}, '$.${memberName}')`;
|
|
275
|
+
}
|
|
276
|
+
// Use JSON_QUERY for known array fields, JSON_VALUE for others
|
|
277
|
+
if (knownArrayFields.includes(memberName)) {
|
|
278
|
+
return `JSON_QUERY(${this.context.resourceAlias}.json, '$.${memberName}')`;
|
|
279
|
+
}
|
|
280
|
+
return `JSON_VALUE(${this.context.resourceAlias}.json, '$.${memberName}')`;
|
|
281
|
+
}
|
|
282
|
+
visitFunctionInvocation(ctx) {
|
|
283
|
+
return this.visit(ctx.function());
|
|
284
|
+
}
|
|
285
|
+
visitThisInvocation(_ctx) {
|
|
286
|
+
// $this refers to the current item in an iteration context
|
|
287
|
+
if (this.context.iterationContext) {
|
|
288
|
+
return this.context.iterationContext;
|
|
289
|
+
}
|
|
290
|
+
return `${this.context.resourceAlias}.json`;
|
|
291
|
+
}
|
|
292
|
+
visitIndexInvocation(_ctx) {
|
|
293
|
+
// $index in forEach contexts - return current iteration index (0-based)
|
|
294
|
+
if (this.context.currentForEachAlias) {
|
|
295
|
+
// In a forEach context, use the [key] column from OPENJSON which gives the array index
|
|
296
|
+
return `${this.context.currentForEachAlias}.[key]`;
|
|
297
|
+
}
|
|
298
|
+
// Outside forEach context, default to 0
|
|
299
|
+
return "0";
|
|
300
|
+
}
|
|
301
|
+
visitTotalInvocation(_ctx) {
|
|
302
|
+
// $total in forEach contexts - return total count of items in current iteration
|
|
303
|
+
if (this.context.currentForEachAlias &&
|
|
304
|
+
this.context.forEachSource &&
|
|
305
|
+
this.context.forEachPath) {
|
|
306
|
+
// Calculate total count using JSON_VALUE with array length
|
|
307
|
+
// Use a subquery to count items in the JSON array
|
|
308
|
+
return `(
|
|
309
|
+
SELECT COUNT(*)
|
|
310
|
+
FROM OPENJSON(${this.context.forEachSource}, '${this.context.forEachPath}')
|
|
311
|
+
)`;
|
|
312
|
+
}
|
|
313
|
+
// Outside forEach context, default to 1
|
|
314
|
+
return "1";
|
|
315
|
+
}
|
|
316
|
+
// Term visitors
|
|
317
|
+
visitInvocationTerm(ctx) {
|
|
318
|
+
return this.visit(ctx.invocation());
|
|
319
|
+
}
|
|
320
|
+
visitLiteralTerm(ctx) {
|
|
321
|
+
return this.visit(ctx.literal());
|
|
322
|
+
}
|
|
323
|
+
visitExternalConstantTerm(ctx) {
|
|
324
|
+
return this.visit(ctx.externalConstant());
|
|
325
|
+
}
|
|
326
|
+
visitParenthesizedTerm(ctx) {
|
|
327
|
+
const expr = this.visit(ctx.expression());
|
|
328
|
+
return `(${expr})`;
|
|
329
|
+
}
|
|
330
|
+
visitExternalConstant(ctx) {
|
|
331
|
+
let constantName;
|
|
332
|
+
const identifier = ctx.identifier();
|
|
333
|
+
if (identifier) {
|
|
334
|
+
constantName = this.visit(identifier);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
// STRING case - remove quotes
|
|
338
|
+
constantName = ctx.STRING()?.text.slice(1, -1) ?? "";
|
|
339
|
+
}
|
|
340
|
+
// Check if the constant is defined in the context
|
|
341
|
+
if (this.context.constants &&
|
|
342
|
+
this.context.constants[constantName] !== undefined) {
|
|
343
|
+
return this.formatConstantValue(this.context.constants[constantName]);
|
|
344
|
+
}
|
|
345
|
+
// Constant not found - throw an error
|
|
346
|
+
throw new Error(`Constant '%${constantName}' is not defined in the ViewDefinition`);
|
|
347
|
+
}
|
|
348
|
+
visitFunction(ctx) {
|
|
349
|
+
const functionName = this.visit(ctx.identifier());
|
|
350
|
+
const paramList = ctx.paramList();
|
|
351
|
+
// Special handling for where() function - need raw expression, not transpiled
|
|
352
|
+
if (functionName === "where") {
|
|
353
|
+
if (!paramList || paramList.expression().length !== 1) {
|
|
354
|
+
throw new Error("where() function requires exactly one argument");
|
|
355
|
+
}
|
|
356
|
+
// Get the raw filter expression context (not transpiled yet)
|
|
357
|
+
const filterExprCtx = paramList.expression()[0];
|
|
358
|
+
// Transpile the filter expression with current context
|
|
359
|
+
const filterVisitor = new FHIRPathToTSqlVisitor(this.context);
|
|
360
|
+
// Return the condition directly - this is for root-level where() calls
|
|
361
|
+
return filterVisitor.visit(filterExprCtx);
|
|
362
|
+
}
|
|
363
|
+
const args = paramList ? this.getParameterList(paramList) : [];
|
|
364
|
+
return this.executeFunctionHandler(functionName, args);
|
|
365
|
+
}
|
|
366
|
+
visitQuantity(ctx) {
|
|
367
|
+
// For now, just return the number - unit handling would be more complex
|
|
368
|
+
return ctx.NUMBER().text;
|
|
369
|
+
}
|
|
370
|
+
visitIdentifier(ctx) {
|
|
371
|
+
const identifier = ctx.IDENTIFIER();
|
|
372
|
+
const delimitedIdentifier = ctx.DELIMITEDIDENTIFIER();
|
|
373
|
+
if (identifier) {
|
|
374
|
+
return identifier.text;
|
|
375
|
+
}
|
|
376
|
+
else if (delimitedIdentifier) {
|
|
377
|
+
// Remove backticks
|
|
378
|
+
return delimitedIdentifier.text.slice(1, -1);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
// One of the keyword identifiers
|
|
382
|
+
return ctx.text;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
visitQualifiedIdentifier(ctx) {
|
|
386
|
+
const parts = ctx.identifier().map((id) => this.visit(id));
|
|
387
|
+
return parts.join(".");
|
|
388
|
+
}
|
|
389
|
+
// Helper methods
|
|
390
|
+
handleMemberInvocation(base, memberCtx) {
|
|
391
|
+
const memberName = this.visit(memberCtx.identifier());
|
|
392
|
+
// Handle subquery results from .where() or .extension() functions
|
|
393
|
+
// Pattern: (SELECT TOP 1 value FROM OPENJSON(...) WHERE ...)
|
|
394
|
+
// OR: (SELECT TOP 1 JSON_VALUE(value, '$.field') FROM OPENJSON(...) WHERE ...)
|
|
395
|
+
if (base.startsWith("(SELECT TOP 1 ")) {
|
|
396
|
+
// Check if it already has JSON_VALUE in the SELECT
|
|
397
|
+
const jsonValueMatch = /\(SELECT TOP 1 JSON_VALUE\(value, '\$\.([^']+)'\)(.*)/.exec(base);
|
|
398
|
+
if (jsonValueMatch) {
|
|
399
|
+
// Already has JSON_VALUE, append to the path
|
|
400
|
+
// Convert: (SELECT TOP 1 JSON_VALUE(value, '$.field') FROM ...)
|
|
401
|
+
// To: (SELECT TOP 1 JSON_VALUE(value, '$.field.member') FROM ...)
|
|
402
|
+
const existingPath = jsonValueMatch[1];
|
|
403
|
+
const rest = jsonValueMatch[2];
|
|
404
|
+
return `(SELECT TOP 1 JSON_VALUE(value, '$.${existingPath}.${memberName}')${rest}`;
|
|
405
|
+
}
|
|
406
|
+
else if (base.startsWith("(SELECT TOP 1 value FROM OPENJSON")) {
|
|
407
|
+
// Simple value select, add JSON_VALUE
|
|
408
|
+
// Convert: (SELECT TOP 1 value FROM OPENJSON(...))
|
|
409
|
+
// To: (SELECT TOP 1 JSON_VALUE(value, '$.member') FROM OPENJSON(...))
|
|
410
|
+
const fromPart = base.substring(base.indexOf(" FROM "));
|
|
411
|
+
return `(SELECT TOP 1 JSON_VALUE(value, '$.${memberName}')${fromPart}`;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Handle JSON_VALUE expressions - check this BEFORE JSON_QUERY
|
|
415
|
+
// because the base might be JSON_VALUE(JSON_QUERY(...))
|
|
416
|
+
if (base.startsWith("JSON_VALUE")) {
|
|
417
|
+
return this.handleJsonValueMember(base, memberName);
|
|
418
|
+
}
|
|
419
|
+
// Handle JSON_QUERY expressions (arrays)
|
|
420
|
+
if (base.includes("JSON_QUERY")) {
|
|
421
|
+
return this.handleJsonQueryMember(base, memberName);
|
|
422
|
+
}
|
|
423
|
+
return `JSON_VALUE(${base}, '$.${memberName}')`;
|
|
424
|
+
}
|
|
425
|
+
handleJsonQueryMember(base, memberName) {
|
|
426
|
+
const pathMatch = /JSON_QUERY\(([^,]+),\s*'([^']+)'\)/.exec(base);
|
|
427
|
+
if (!pathMatch) {
|
|
428
|
+
return `JSON_VALUE(${base}, '$.${memberName}')`;
|
|
429
|
+
}
|
|
430
|
+
const source = pathMatch[1];
|
|
431
|
+
const existingPath = pathMatch[2];
|
|
432
|
+
const isForEachValue = /forEach_\d+\.value/.test(source);
|
|
433
|
+
const previousFieldIsArray = this.checkPreviousFieldIsArray(existingPath, isForEachValue);
|
|
434
|
+
const currentMemberIsArray = this.checkCurrentMemberIsArray(memberName, existingPath, isForEachValue);
|
|
435
|
+
const newPath = previousFieldIsArray
|
|
436
|
+
? `${existingPath}[0].${memberName}`
|
|
437
|
+
: `${existingPath}.${memberName}`;
|
|
438
|
+
return currentMemberIsArray
|
|
439
|
+
? `JSON_QUERY(${source}, '${newPath}')`
|
|
440
|
+
: `JSON_VALUE(${source}, '${newPath}')`;
|
|
441
|
+
}
|
|
442
|
+
checkPreviousFieldIsArray(existingPath, isForEachValue) {
|
|
443
|
+
const alwaysArrayFields = this.getAlwaysArrayFields();
|
|
444
|
+
const contextDependentFields = ["name"];
|
|
445
|
+
const indexPattern = /\[\d+]/;
|
|
446
|
+
const pathSegments = existingPath
|
|
447
|
+
.split(".")
|
|
448
|
+
.filter((s) => s !== "$" && !indexPattern.exec(s));
|
|
449
|
+
const lastSegment = pathSegments[pathSegments.length - 1];
|
|
450
|
+
const previousFieldIsAlwaysArray = !!lastSegment && alwaysArrayFields.includes(lastSegment);
|
|
451
|
+
const previousFieldIsContextArray = !!lastSegment &&
|
|
452
|
+
contextDependentFields.includes(lastSegment) &&
|
|
453
|
+
!isForEachValue;
|
|
454
|
+
return previousFieldIsAlwaysArray || previousFieldIsContextArray;
|
|
455
|
+
}
|
|
456
|
+
checkCurrentMemberIsArray(memberName, existingPath, isForEachValue) {
|
|
457
|
+
const alwaysArrayFields = this.getAlwaysArrayFields();
|
|
458
|
+
const contextDependentFields = ["name"];
|
|
459
|
+
const indexPattern = /\[\d+]/;
|
|
460
|
+
const pathSegments = existingPath
|
|
461
|
+
.split(".")
|
|
462
|
+
.filter((s) => s !== "$" && !indexPattern.exec(s));
|
|
463
|
+
return (alwaysArrayFields.includes(memberName) ||
|
|
464
|
+
(contextDependentFields.includes(memberName) &&
|
|
465
|
+
!isForEachValue &&
|
|
466
|
+
pathSegments.length === 0));
|
|
467
|
+
}
|
|
468
|
+
getAlwaysArrayFields() {
|
|
469
|
+
return [
|
|
470
|
+
"given",
|
|
471
|
+
"telecom",
|
|
472
|
+
"address",
|
|
473
|
+
"line",
|
|
474
|
+
"identifier",
|
|
475
|
+
"extension",
|
|
476
|
+
"contact",
|
|
477
|
+
"output",
|
|
478
|
+
"item",
|
|
479
|
+
"udiCarrier",
|
|
480
|
+
"coding",
|
|
481
|
+
"component",
|
|
482
|
+
];
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Checks if a member name represents a FHIR array field.
|
|
486
|
+
*/
|
|
487
|
+
isArrayField(memberName) {
|
|
488
|
+
const knownArrayFields = [
|
|
489
|
+
"name",
|
|
490
|
+
"given",
|
|
491
|
+
"telecom",
|
|
492
|
+
"address",
|
|
493
|
+
"line",
|
|
494
|
+
"identifier",
|
|
495
|
+
"extension",
|
|
496
|
+
"contact",
|
|
497
|
+
"output",
|
|
498
|
+
"item",
|
|
499
|
+
"udiCarrier",
|
|
500
|
+
"coding",
|
|
501
|
+
"component",
|
|
502
|
+
];
|
|
503
|
+
return knownArrayFields.includes(memberName);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Handles nested JSON_QUERY with array indexing.
|
|
507
|
+
*/
|
|
508
|
+
handleNestedQueryWithIndex(source, existingPath, memberName) {
|
|
509
|
+
const queryMatch = /^JSON_QUERY\(([^,]+),\s*'([^']+)'\)$/.exec(source);
|
|
510
|
+
const isArrayIndexPath = /^\$\[\d+]$/.test(existingPath);
|
|
511
|
+
if (queryMatch && isArrayIndexPath) {
|
|
512
|
+
const innerSource = queryMatch[1];
|
|
513
|
+
const arrayPath = queryMatch[2];
|
|
514
|
+
const indexMatch = /\[(\d+)]/.exec(existingPath);
|
|
515
|
+
const index = indexMatch?.[1] ?? "0";
|
|
516
|
+
const newPath = `${arrayPath}[${index}].${memberName}`;
|
|
517
|
+
return `JSON_VALUE(${innerSource}, '${newPath}')`;
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
handleJsonValueMember(base, memberName) {
|
|
522
|
+
const pathMatch = /^JSON_VALUE\((.*),\s*'([^']+)'\)$/.exec(base);
|
|
523
|
+
if (!pathMatch) {
|
|
524
|
+
return `JSON_VALUE(${base}, '$.${memberName}')`;
|
|
525
|
+
}
|
|
526
|
+
const source = pathMatch[1];
|
|
527
|
+
const existingPath = pathMatch[2];
|
|
528
|
+
// Check if the member being accessed is an array field
|
|
529
|
+
if (this.isArrayField(memberName)) {
|
|
530
|
+
const newPath = `${existingPath}.${memberName}`;
|
|
531
|
+
return `JSON_QUERY(${source}, '${newPath}')`;
|
|
532
|
+
}
|
|
533
|
+
// Handle nested JSON_QUERY with array indexing
|
|
534
|
+
const nestedResult = this.handleNestedQueryWithIndex(source, existingPath, memberName);
|
|
535
|
+
if (nestedResult) {
|
|
536
|
+
return nestedResult;
|
|
537
|
+
}
|
|
538
|
+
// Check if the path already has an array index
|
|
539
|
+
if (existingPath.includes("[") && existingPath.includes("]")) {
|
|
540
|
+
const newPath = `${existingPath}.${memberName}`;
|
|
541
|
+
return `JSON_VALUE(${source}, '${newPath}')`;
|
|
542
|
+
}
|
|
543
|
+
const pathParts = existingPath.split(".");
|
|
544
|
+
const shouldAddArrayIndex = this.shouldAddArrayIndexForField(pathParts, existingPath);
|
|
545
|
+
if (shouldAddArrayIndex) {
|
|
546
|
+
const newPath = `${pathParts[0]}.${pathParts[1]}[0].${memberName}`;
|
|
547
|
+
return `JSON_VALUE(${source}, '${newPath}')`;
|
|
548
|
+
}
|
|
549
|
+
const newPath = `${existingPath}.${memberName}`;
|
|
550
|
+
return `JSON_VALUE(${source}, '${newPath}')`;
|
|
551
|
+
}
|
|
552
|
+
shouldAddArrayIndexForField(pathParts, existingPath) {
|
|
553
|
+
// Special handling for FHIR array fields
|
|
554
|
+
// Only add [0] when NOT in a forEach iteration context
|
|
555
|
+
// In forEach, we're already at the element level, so arrays within elements are accessed directly
|
|
556
|
+
// Note: "name" is excluded because it's an array at Patient level but an object within Contact
|
|
557
|
+
const knownArrayFields = [
|
|
558
|
+
"telecom",
|
|
559
|
+
"address",
|
|
560
|
+
"identifier",
|
|
561
|
+
"extension",
|
|
562
|
+
"contact",
|
|
563
|
+
"link",
|
|
564
|
+
];
|
|
565
|
+
// Determine if we should add [0] for this array field
|
|
566
|
+
// We should NOT add [0] if:
|
|
567
|
+
// 1. We're in a forEach context AND
|
|
568
|
+
// 2. The field is actually the forEach collection itself (not a nested array)
|
|
569
|
+
//
|
|
570
|
+
// For example:
|
|
571
|
+
// - forEach on "contact", accessing "name.family": "name" is NOT an array in contact
|
|
572
|
+
// - forEach on "contact", accessing "telecom.system": "telecom" IS an array in contact, so add [0]
|
|
573
|
+
// - forEach on "name", accessing "family": we're iterating names, don't add [0] to name itself
|
|
574
|
+
if (pathParts.length < 2 || existingPath.includes("[")) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
const fieldName = pathParts[1];
|
|
578
|
+
// Check if this field is in the known array fields list
|
|
579
|
+
if (knownArrayFields.includes(fieldName)) {
|
|
580
|
+
// Don't add [0] if this is the forEach array itself
|
|
581
|
+
return !this.context.forEachPath?.endsWith(fieldName);
|
|
582
|
+
}
|
|
583
|
+
else if (fieldName === "name") {
|
|
584
|
+
// "name" is special: it's an array in Patient but an object in Contact
|
|
585
|
+
// Only add [0] for "name" when NOT in a forEach context
|
|
586
|
+
return !this.context.iterationContext;
|
|
587
|
+
}
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
handleFunctionInvocation(base, functionCtx) {
|
|
591
|
+
const functionName = this.visit(functionCtx.function().identifier());
|
|
592
|
+
const paramList = functionCtx.function().paramList();
|
|
593
|
+
// Special handling for first() function to match expected format
|
|
594
|
+
if (functionName === "first") {
|
|
595
|
+
return this.handleFirstFunctionInvocation(base);
|
|
596
|
+
}
|
|
597
|
+
// Special handling for where() function - need raw expression, not transpiled
|
|
598
|
+
if (functionName === "where") {
|
|
599
|
+
return this.handleWhereFunctionInvocation(base, functionCtx);
|
|
600
|
+
}
|
|
601
|
+
// Special handling for ofType() function - need raw type name, not transpiled
|
|
602
|
+
if (functionName === "ofType") {
|
|
603
|
+
return this.handleOfTypeFunctionInvocation(base, functionCtx);
|
|
604
|
+
}
|
|
605
|
+
// Special handling for getReferenceKey() function - need raw type name, not transpiled
|
|
606
|
+
if (functionName === "getReferenceKey") {
|
|
607
|
+
return this.handleGetReferenceKeyFunctionInvocation(base, functionCtx);
|
|
608
|
+
}
|
|
609
|
+
// Special handling for exists() function - need raw expression, not transpiled
|
|
610
|
+
if (functionName === "exists") {
|
|
611
|
+
return this.handleExistsFunctionInvocation(base, functionCtx);
|
|
612
|
+
}
|
|
613
|
+
const args = paramList ? this.getParameterList(paramList) : [];
|
|
614
|
+
// Create new context and delegate to function handler
|
|
615
|
+
const newContext = this.createNewIterationContext(base);
|
|
616
|
+
const visitor = new FHIRPathToTSqlVisitor(newContext);
|
|
617
|
+
return visitor.executeFunctionHandler(functionName, args);
|
|
618
|
+
}
|
|
619
|
+
handleOfTypeFunctionInvocation(base, functionCtx) {
|
|
620
|
+
const paramList = functionCtx.function().paramList();
|
|
621
|
+
if (!paramList || paramList.expression().length !== 1) {
|
|
622
|
+
throw new Error("ofType() function requires exactly one argument");
|
|
623
|
+
}
|
|
624
|
+
// Get the raw type expression - it should be an identifier
|
|
625
|
+
const typeExprCtx = paramList.expression()[0];
|
|
626
|
+
const typeName = typeExprCtx.text; // Get the raw text (e.g., "integer")
|
|
627
|
+
return this.applyPolymorphicFieldMapping(base, typeName);
|
|
628
|
+
}
|
|
629
|
+
handleGetReferenceKeyFunctionInvocation(base, functionCtx) {
|
|
630
|
+
const paramList = functionCtx.function().paramList();
|
|
631
|
+
// Get the optional resource type parameter
|
|
632
|
+
let resourceType = null;
|
|
633
|
+
if (paramList && paramList.expression().length > 0) {
|
|
634
|
+
// Get the raw type expression - it should be an identifier
|
|
635
|
+
const typeExprCtx = paramList.expression()[0];
|
|
636
|
+
resourceType = typeExprCtx.text; // Get the raw text (e.g., "Patient")
|
|
637
|
+
}
|
|
638
|
+
// Create new context and call the handler
|
|
639
|
+
const newContext = this.createNewIterationContext(base);
|
|
640
|
+
const visitor = new FHIRPathToTSqlVisitor(newContext);
|
|
641
|
+
return visitor.handleGetReferenceKeyFunctionWithType(resourceType);
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Maps polymorphic FHIR fields to their typed variants.
|
|
645
|
+
* Example: value.ofType(integer) → valueInteger
|
|
646
|
+
* Handles paths with array indices like "output[0].value" → "output[0].valueUrl"
|
|
647
|
+
*/
|
|
648
|
+
applyPolymorphicFieldMapping(base, typeName) {
|
|
649
|
+
// Handle SELECT subqueries from extension() function
|
|
650
|
+
// Pattern: (SELECT TOP 1 JSON_VALUE(value, '$.value') FROM ...)
|
|
651
|
+
if (base.startsWith("(SELECT TOP 1 JSON_VALUE(value, '$.")) {
|
|
652
|
+
const suffix = this.getTypeSuffix(typeName);
|
|
653
|
+
// Find the JSON_VALUE path part
|
|
654
|
+
const pathMatch = /JSON_VALUE\(value, '\$\.([^']+)'\)/.exec(base);
|
|
655
|
+
if (pathMatch) {
|
|
656
|
+
const path = pathMatch[1];
|
|
657
|
+
if (this.isPolymorphicField(path)) {
|
|
658
|
+
// Replace the polymorphic field with its typed variant
|
|
659
|
+
const newPath = `${path}${suffix}`;
|
|
660
|
+
return base.replace(`JSON_VALUE(value, '$.${path}')`, `JSON_VALUE(value, '$.${newPath}')`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return base;
|
|
664
|
+
}
|
|
665
|
+
// Check if base is a JSON_VALUE call for a polymorphic field
|
|
666
|
+
const match = /JSON_VALUE\(([^,]+),\s*'\$\.([^']+)'\)/.exec(base);
|
|
667
|
+
if (!match) {
|
|
668
|
+
return base; // Not a JSON_VALUE call, return unchanged
|
|
669
|
+
}
|
|
670
|
+
const source = match[1];
|
|
671
|
+
const path = match[2];
|
|
672
|
+
const suffix = this.getTypeSuffix(typeName);
|
|
673
|
+
// Check if this is a known polymorphic field pattern
|
|
674
|
+
if (this.isPolymorphicField(path)) {
|
|
675
|
+
// Extract the last segment and replace it with the typed variant
|
|
676
|
+
const lastDotIndex = path.lastIndexOf(".");
|
|
677
|
+
if (lastDotIndex === -1) {
|
|
678
|
+
// No dot, so the whole path is the polymorphic field
|
|
679
|
+
return `JSON_VALUE(${source}, '$.${path}${suffix}')`;
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
// Replace the last segment with its typed variant
|
|
683
|
+
const prefix = path.substring(0, lastDotIndex);
|
|
684
|
+
const lastSegment = path.substring(lastDotIndex + 1);
|
|
685
|
+
return `JSON_VALUE(${source}, '$.${prefix}.${lastSegment}${suffix}')`;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return base; // Not a polymorphic field, return unchanged
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Returns the type suffix for polymorphic field mapping.
|
|
692
|
+
*/
|
|
693
|
+
getTypeSuffix(typeName) {
|
|
694
|
+
const typeMap = {
|
|
695
|
+
integer: "Integer",
|
|
696
|
+
string: "String",
|
|
697
|
+
boolean: "Boolean",
|
|
698
|
+
decimal: "Decimal",
|
|
699
|
+
dateTime: "DateTime",
|
|
700
|
+
date: "Date",
|
|
701
|
+
time: "Time",
|
|
702
|
+
instant: "Instant",
|
|
703
|
+
uri: "Uri",
|
|
704
|
+
url: "Url",
|
|
705
|
+
canonical: "Canonical",
|
|
706
|
+
uuid: "Uuid",
|
|
707
|
+
oid: "Oid",
|
|
708
|
+
id: "Id",
|
|
709
|
+
code: "Code",
|
|
710
|
+
markdown: "Markdown",
|
|
711
|
+
base64Binary: "Base64Binary",
|
|
712
|
+
positiveInt: "PositiveInt",
|
|
713
|
+
unsignedInt: "UnsignedInt",
|
|
714
|
+
integer64: "Integer64",
|
|
715
|
+
// Complex types use PascalCase as they match FHIR type names
|
|
716
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
717
|
+
Period: "Period",
|
|
718
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
719
|
+
Range: "Range",
|
|
720
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
721
|
+
Quantity: "Quantity",
|
|
722
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
723
|
+
CodeableConcept: "CodeableConcept",
|
|
724
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
725
|
+
Reference: "Reference",
|
|
726
|
+
};
|
|
727
|
+
return typeMap[typeName] || typeName;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Checks if a path represents a polymorphic field (value[x], onset[x], effective[x], deceased[x], identified[x]).
|
|
731
|
+
* Handles paths with array indices like "output[0].value" or "item[1].onset".
|
|
732
|
+
*/
|
|
733
|
+
isPolymorphicField(path) {
|
|
734
|
+
// Extract the last segment after the last dot (or the whole path if no dot)
|
|
735
|
+
// This handles paths like "output[0].value" → "value" or "item[1].onset" → "onset"
|
|
736
|
+
const lastSegment = path.includes(".")
|
|
737
|
+
? (path.split(".").pop() ?? "")
|
|
738
|
+
: path;
|
|
739
|
+
return (lastSegment === "value" ||
|
|
740
|
+
lastSegment === "onset" ||
|
|
741
|
+
lastSegment === "effective" ||
|
|
742
|
+
lastSegment === "deceased" ||
|
|
743
|
+
lastSegment === "identified");
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Cast expression to DECIMAL for numeric operations if needed.
|
|
747
|
+
* JSON_VALUE returns NVARCHAR by default, which can't be used in arithmetic operations.
|
|
748
|
+
*/
|
|
749
|
+
castForNumericOperation(expression) {
|
|
750
|
+
// Check if expression contains JSON_VALUE and isn't already wrapped in CAST
|
|
751
|
+
if (expression.includes("JSON_VALUE") && !expression.includes("CAST(")) {
|
|
752
|
+
return `CAST(${expression} AS DECIMAL(18,6))`;
|
|
753
|
+
}
|
|
754
|
+
// Already has CAST or doesn't need it
|
|
755
|
+
return expression;
|
|
756
|
+
}
|
|
757
|
+
handleWhereFunctionInvocation(base, functionCtx) {
|
|
758
|
+
const paramList = functionCtx.function().paramList();
|
|
759
|
+
if (!paramList || paramList.expression().length !== 1) {
|
|
760
|
+
throw new Error("where() function requires exactly one argument");
|
|
761
|
+
}
|
|
762
|
+
const filterExprCtx = paramList.expression()[0];
|
|
763
|
+
// Special case: where() called at resource root level (no collection)
|
|
764
|
+
if (this.isResourceRootLevel(base)) {
|
|
765
|
+
const filterVisitor = new FHIRPathToTSqlVisitor(this.context);
|
|
766
|
+
return filterVisitor.visit(filterExprCtx);
|
|
767
|
+
}
|
|
768
|
+
// Extract source and path from the base expression
|
|
769
|
+
const { source, jsonPath } = this.extractSourceAndPath(base);
|
|
770
|
+
// Build and return the EXISTS clause with filtered collection
|
|
771
|
+
return this.buildWhereExistsClause(source, jsonPath, filterExprCtx);
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Checks if the base expression represents the resource root level (not a collection).
|
|
775
|
+
*/
|
|
776
|
+
isResourceRootLevel(base) {
|
|
777
|
+
return (base === `${this.context.resourceAlias}.json` ||
|
|
778
|
+
base === this.context.resourceAlias ||
|
|
779
|
+
(!base.includes("JSON_QUERY") &&
|
|
780
|
+
!base.includes("JSON_VALUE") &&
|
|
781
|
+
!base.includes("EXISTS") &&
|
|
782
|
+
!base.includes("SELECT")));
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Extracts the source and JSON path from a base expression.
|
|
786
|
+
*/
|
|
787
|
+
extractSourceAndPath(base) {
|
|
788
|
+
let source = `${this.context.resourceAlias}.json`;
|
|
789
|
+
let jsonPath = "$";
|
|
790
|
+
if (base.includes("JSON_QUERY")) {
|
|
791
|
+
const match = /JSON_QUERY\(([^,]+),\s*'([^']+)'\)/.exec(base);
|
|
792
|
+
if (match) {
|
|
793
|
+
source = match[1];
|
|
794
|
+
jsonPath = match[2];
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
else if (base.includes("JSON_VALUE")) {
|
|
798
|
+
const match = /JSON_VALUE\(([^,]+),\s*'([^']+)'\)/.exec(base);
|
|
799
|
+
if (match) {
|
|
800
|
+
source = match[1];
|
|
801
|
+
jsonPath = match[2];
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return { source, jsonPath };
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Builds a subquery for filtering a collection with a where condition.
|
|
808
|
+
* Returns a subquery that selects the filtered items, allowing further navigation.
|
|
809
|
+
*/
|
|
810
|
+
buildWhereExistsClause(source, jsonPath, filterExprCtx) {
|
|
811
|
+
const tableAlias = "whereItem";
|
|
812
|
+
// Create a new context for the filter condition where expressions refer to items in the collection
|
|
813
|
+
const itemContext = {
|
|
814
|
+
resourceAlias: tableAlias,
|
|
815
|
+
constants: this.context.constants,
|
|
816
|
+
iterationContext: `${tableAlias}.value`,
|
|
817
|
+
};
|
|
818
|
+
// Transpile the filter expression with the item context
|
|
819
|
+
const filterVisitor = new FHIRPathToTSqlVisitor(itemContext);
|
|
820
|
+
const condition = filterVisitor.visit(filterExprCtx);
|
|
821
|
+
// Return a subquery that selects the filtered collection
|
|
822
|
+
// This allows further navigation (e.g., .family) to work correctly
|
|
823
|
+
return `(SELECT TOP 1 value FROM OPENJSON(${source}, '${jsonPath}') AS ${tableAlias} WHERE ${condition})`;
|
|
824
|
+
}
|
|
825
|
+
handleExistsFunctionInvocation(base, functionCtx) {
|
|
826
|
+
const paramList = functionCtx.function().paramList();
|
|
827
|
+
// If no arguments, delegate to the standard handler
|
|
828
|
+
if (!paramList || paramList.expression().length === 0) {
|
|
829
|
+
const args = [];
|
|
830
|
+
return this.handleExistsFunction(args, base);
|
|
831
|
+
}
|
|
832
|
+
// Get the raw filter expression context (not transpiled yet)
|
|
833
|
+
const filterExprCtx = paramList.expression()[0];
|
|
834
|
+
// Call the handler with the base and filter expression context
|
|
835
|
+
return this.handleExistsFunction([], base, filterExprCtx);
|
|
836
|
+
}
|
|
837
|
+
handleFirstFunctionInvocation(base) {
|
|
838
|
+
// Check if the base is a JSON_QUERY call for an array
|
|
839
|
+
const queryMatch = /^JSON_QUERY\(([^,]+),\s*'([^']+)'\)$/.exec(base);
|
|
840
|
+
if (queryMatch) {
|
|
841
|
+
const source = queryMatch[1];
|
|
842
|
+
const path = queryMatch[2];
|
|
843
|
+
return `JSON_VALUE(${source}, '${path}[0]')`;
|
|
844
|
+
}
|
|
845
|
+
// Check if the base is a JSON_VALUE call
|
|
846
|
+
const simpleJsonMatch = /^JSON_VALUE\(([^,]+),\s*'([^']+)'\)$/.exec(base);
|
|
847
|
+
if (simpleJsonMatch) {
|
|
848
|
+
const source = simpleJsonMatch[1];
|
|
849
|
+
const path = simpleJsonMatch[2];
|
|
850
|
+
// Check if the path ends with an array field that needs [0] indexing
|
|
851
|
+
// For known array fields like "given", "family" etc, add [0]
|
|
852
|
+
const knownArrayFields = [
|
|
853
|
+
"given",
|
|
854
|
+
"line",
|
|
855
|
+
"coding",
|
|
856
|
+
"telecom",
|
|
857
|
+
"identifier",
|
|
858
|
+
];
|
|
859
|
+
const pathSegments = path.split(".");
|
|
860
|
+
const lastSegment = pathSegments[pathSegments.length - 1];
|
|
861
|
+
if (knownArrayFields.includes(lastSegment)) {
|
|
862
|
+
// This is an array field, add [0] to get first element
|
|
863
|
+
return `JSON_VALUE(${source}, '${path}[0]')`;
|
|
864
|
+
}
|
|
865
|
+
// For non-array fields, first() should return the value as-is since it's already a scalar
|
|
866
|
+
return base;
|
|
867
|
+
}
|
|
868
|
+
else if (!base.includes("JSON_VALUE") &&
|
|
869
|
+
!base.includes("JSON_QUERY") &&
|
|
870
|
+
!base.includes("EXISTS") &&
|
|
871
|
+
!base.includes("SELECT")) {
|
|
872
|
+
// Simple identifier like 'name'
|
|
873
|
+
return `JSON_VALUE(${this.context.resourceAlias}.json, '$.${base}[0]')`;
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
// For complex expressions that aren't JSON_QUERY or JSON_VALUE,
|
|
877
|
+
// we can't easily add [0] indexing, so return as-is
|
|
878
|
+
return base;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
createNewIterationContext(base) {
|
|
882
|
+
if (!base.includes("JSON_VALUE") &&
|
|
883
|
+
!base.includes("JSON_QUERY") &&
|
|
884
|
+
!base.includes("EXISTS") &&
|
|
885
|
+
!base.includes("SELECT")) {
|
|
886
|
+
// Simple identifier like 'name' - construct proper JSON path
|
|
887
|
+
return {
|
|
888
|
+
...this.context,
|
|
889
|
+
iterationContext: `JSON_QUERY(${this.context.resourceAlias}.json, '$.${base}')`,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
return {
|
|
894
|
+
...this.context,
|
|
895
|
+
iterationContext: base,
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
getParameterList(paramListCtx) {
|
|
900
|
+
return paramListCtx.expression().map((expr) => this.visit(expr));
|
|
901
|
+
}
|
|
902
|
+
getOperatorFromContext(fullText, left, right) {
|
|
903
|
+
const leftIndex = fullText.indexOf(left);
|
|
904
|
+
const rightIndex = fullText.lastIndexOf(right);
|
|
905
|
+
if (leftIndex === -1 || rightIndex === -1) {
|
|
906
|
+
return "";
|
|
907
|
+
}
|
|
908
|
+
const operatorPart = fullText
|
|
909
|
+
.substring(leftIndex + left.length, rightIndex)
|
|
910
|
+
.trim();
|
|
911
|
+
return this.extractOperatorFromText(operatorPart);
|
|
912
|
+
}
|
|
913
|
+
extractOperatorFromText(operatorPart) {
|
|
914
|
+
// Order matters: check longer operators first to avoid substring matches
|
|
915
|
+
const operators = [
|
|
916
|
+
"<=",
|
|
917
|
+
">=",
|
|
918
|
+
"!=",
|
|
919
|
+
"!~",
|
|
920
|
+
"implies",
|
|
921
|
+
"contains",
|
|
922
|
+
"and",
|
|
923
|
+
"or",
|
|
924
|
+
"xor",
|
|
925
|
+
"div",
|
|
926
|
+
"mod",
|
|
927
|
+
"in",
|
|
928
|
+
"is",
|
|
929
|
+
"as",
|
|
930
|
+
"<",
|
|
931
|
+
">",
|
|
932
|
+
"=",
|
|
933
|
+
"~",
|
|
934
|
+
"*",
|
|
935
|
+
"/",
|
|
936
|
+
"+",
|
|
937
|
+
"-",
|
|
938
|
+
"&",
|
|
939
|
+
];
|
|
940
|
+
for (const operator of operators) {
|
|
941
|
+
if (operatorPart.includes(operator)) {
|
|
942
|
+
return operator;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return "";
|
|
946
|
+
}
|
|
947
|
+
formatConstantValue(value) {
|
|
948
|
+
if (typeof value === "string") {
|
|
949
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
950
|
+
}
|
|
951
|
+
else if (typeof value === "number") {
|
|
952
|
+
return value.toString();
|
|
953
|
+
}
|
|
954
|
+
else if (typeof value === "boolean") {
|
|
955
|
+
// Format as string to match JSON_VALUE output for boolean fields
|
|
956
|
+
return value ? "'true'" : "'false'";
|
|
957
|
+
}
|
|
958
|
+
else if (value === null || value === undefined) {
|
|
959
|
+
return "NULL";
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
executeFunctionHandler(functionName, args) {
|
|
966
|
+
const functionMap = {
|
|
967
|
+
exists: (args) => this.handleExistsFunction(args),
|
|
968
|
+
empty: (args) => this.handleEmptyFunction(args),
|
|
969
|
+
first: (args) => this.handleFirstFunction(args),
|
|
970
|
+
last: (args) => this.handleLastFunction(args),
|
|
971
|
+
count: (args) => this.handleCountFunction(args),
|
|
972
|
+
join: (args) => this.handleJoinFunction(args),
|
|
973
|
+
where: (args) => this.handleWhereFunction(args),
|
|
974
|
+
select: (args) => this.handleSelectFunction(args),
|
|
975
|
+
getResourceKey: () => this.handleGetResourceKeyFunction(),
|
|
976
|
+
ofType: (args) => this.handleOfTypeFunction(args),
|
|
977
|
+
not: (args) => this.handleNotFunction(args),
|
|
978
|
+
extension: (args) => this.handleExtensionFunction(args),
|
|
979
|
+
lowBoundary: (args) => this.handleBoundaryFunction(functionName, args),
|
|
980
|
+
highBoundary: (args) => this.handleBoundaryFunction(functionName, args),
|
|
981
|
+
};
|
|
982
|
+
const handler = functionMap[functionName];
|
|
983
|
+
if (!handler) {
|
|
984
|
+
throw new Error(`Unsupported FHIRPath function: ${functionName}`);
|
|
985
|
+
}
|
|
986
|
+
return handler(args);
|
|
987
|
+
}
|
|
988
|
+
// Function handlers (simplified versions of the original implementations)
|
|
989
|
+
handleExistsFunction(args, base, filterExprCtx) {
|
|
990
|
+
// If we have a filter expression context, use it (this comes from handleExistsFunctionInvocation)
|
|
991
|
+
if (filterExprCtx) {
|
|
992
|
+
return this.handleExistsWithArgs("", base, filterExprCtx);
|
|
993
|
+
}
|
|
994
|
+
// Otherwise check args
|
|
995
|
+
if (args.length === 0) {
|
|
996
|
+
return this.handleExistsWithoutArgs(base);
|
|
997
|
+
}
|
|
998
|
+
return this.handleExistsWithArgs(args[0], base, filterExprCtx);
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Handles exists() function without arguments, using iteration context or base.
|
|
1002
|
+
*/
|
|
1003
|
+
handleExistsWithoutArgs(base) {
|
|
1004
|
+
if (base) {
|
|
1005
|
+
return this.handleExistsWithBase(base);
|
|
1006
|
+
}
|
|
1007
|
+
if (this.context.iterationContext) {
|
|
1008
|
+
return this.handleExistsWithIterationContext();
|
|
1009
|
+
}
|
|
1010
|
+
// No iteration context or base - check resource
|
|
1011
|
+
return `(${this.context.resourceAlias}.json IS NOT NULL)`;
|
|
1012
|
+
}
|
|
1013
|
+
handleExistsWithBase(base) {
|
|
1014
|
+
const trimmedBase = base.trim();
|
|
1015
|
+
// SELECT subquery from .where() function - wrap in EXISTS
|
|
1016
|
+
if (trimmedBase.startsWith("(SELECT")) {
|
|
1017
|
+
return `EXISTS ${base}`;
|
|
1018
|
+
}
|
|
1019
|
+
// Already a boolean expression - return as-is
|
|
1020
|
+
if (this.isBooleanExpression(base)) {
|
|
1021
|
+
return base;
|
|
1022
|
+
}
|
|
1023
|
+
// JSON_QUERY (array) - check if not null and not empty
|
|
1024
|
+
if (base.includes("JSON_QUERY")) {
|
|
1025
|
+
return `(${base} IS NOT NULL AND ${base} != '[]')`;
|
|
1026
|
+
}
|
|
1027
|
+
return `(${base} IS NOT NULL)`;
|
|
1028
|
+
}
|
|
1029
|
+
handleExistsWithIterationContext() {
|
|
1030
|
+
// This method is only called when iterationContext is defined
|
|
1031
|
+
const iterCtx = this.context.iterationContext;
|
|
1032
|
+
if (!iterCtx) {
|
|
1033
|
+
throw new Error("handleExistsWithIterationContext called without iteration context");
|
|
1034
|
+
}
|
|
1035
|
+
const trimmedIterCtx = iterCtx.trim();
|
|
1036
|
+
// Already an EXISTS clause - return as-is
|
|
1037
|
+
if (trimmedIterCtx.startsWith("EXISTS")) {
|
|
1038
|
+
return iterCtx;
|
|
1039
|
+
}
|
|
1040
|
+
// SELECT subquery - wrap in EXISTS
|
|
1041
|
+
if (trimmedIterCtx.startsWith("(SELECT")) {
|
|
1042
|
+
return `EXISTS ${iterCtx}`;
|
|
1043
|
+
}
|
|
1044
|
+
// Already a boolean expression - return as-is
|
|
1045
|
+
if (this.isBooleanExpression(trimmedIterCtx)) {
|
|
1046
|
+
return iterCtx;
|
|
1047
|
+
}
|
|
1048
|
+
// JSON_QUERY (array) - check if not null and not empty
|
|
1049
|
+
if (trimmedIterCtx.includes("JSON_QUERY")) {
|
|
1050
|
+
return `(${iterCtx} IS NOT NULL AND ${iterCtx} != '[]')`;
|
|
1051
|
+
}
|
|
1052
|
+
// Otherwise check if not null
|
|
1053
|
+
return `(${iterCtx} IS NOT NULL)`;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Builds an EXISTS clause with an OPENJSON subquery for filtering a collection.
|
|
1057
|
+
*/
|
|
1058
|
+
buildExistsWithFilter(base, filterExprCtx) {
|
|
1059
|
+
const { source, jsonPath } = this.extractSourceAndPath(base);
|
|
1060
|
+
const tableAlias = "existsItem";
|
|
1061
|
+
const itemContext = {
|
|
1062
|
+
resourceAlias: tableAlias,
|
|
1063
|
+
constants: this.context.constants,
|
|
1064
|
+
iterationContext: `${tableAlias}.value`,
|
|
1065
|
+
};
|
|
1066
|
+
const filterVisitor = new FHIRPathToTSqlVisitor(itemContext);
|
|
1067
|
+
const condition = filterVisitor.visit(filterExprCtx);
|
|
1068
|
+
return `EXISTS (SELECT 1 FROM OPENJSON(${source}, '${jsonPath}') AS ${tableAlias} WHERE ${condition})`;
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Handles exists() function with an argument expression.
|
|
1072
|
+
*/
|
|
1073
|
+
handleExistsWithArgs(arg, base, filterExprCtx) {
|
|
1074
|
+
// If we have a filter expression context and a base, create an OPENJSON subquery
|
|
1075
|
+
if (filterExprCtx && base) {
|
|
1076
|
+
return this.buildExistsWithFilter(base, filterExprCtx);
|
|
1077
|
+
}
|
|
1078
|
+
const trimmedArg = arg.trim();
|
|
1079
|
+
// If already an EXISTS clause, return as-is
|
|
1080
|
+
if (trimmedArg.startsWith("EXISTS")) {
|
|
1081
|
+
return arg;
|
|
1082
|
+
}
|
|
1083
|
+
// If the argument is a SELECT subquery (from .where() function), wrap in EXISTS
|
|
1084
|
+
if (trimmedArg.startsWith("(SELECT")) {
|
|
1085
|
+
return `EXISTS ${arg}`;
|
|
1086
|
+
}
|
|
1087
|
+
// If already a boolean expression, return as-is
|
|
1088
|
+
if (this.isBooleanExpression(trimmedArg)) {
|
|
1089
|
+
return arg;
|
|
1090
|
+
}
|
|
1091
|
+
// If the argument is a JSON_QUERY (array), check if it's not null and not empty
|
|
1092
|
+
if (trimmedArg.includes("JSON_QUERY")) {
|
|
1093
|
+
return `(${arg} IS NOT NULL AND ${arg} != '[]')`;
|
|
1094
|
+
}
|
|
1095
|
+
// Otherwise wrap in IS NOT NULL check
|
|
1096
|
+
return `(${arg} IS NOT NULL)`;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Checks if an expression is a boolean expression (contains comparison operators).
|
|
1100
|
+
*/
|
|
1101
|
+
isBooleanExpression(expr) {
|
|
1102
|
+
return (expr.includes(" = ") ||
|
|
1103
|
+
expr.includes(" != ") ||
|
|
1104
|
+
expr.includes(" < ") ||
|
|
1105
|
+
expr.includes(" > ") ||
|
|
1106
|
+
expr.includes(" <= ") ||
|
|
1107
|
+
expr.includes(" >= ") ||
|
|
1108
|
+
expr.includes(" AND ") ||
|
|
1109
|
+
expr.includes(" OR ") ||
|
|
1110
|
+
expr.startsWith("NOT ") ||
|
|
1111
|
+
expr.startsWith("(NOT "));
|
|
1112
|
+
}
|
|
1113
|
+
handleEmptyFunction(args) {
|
|
1114
|
+
// If we have arguments, we need to check if that expression is empty
|
|
1115
|
+
if (args.length > 0) {
|
|
1116
|
+
const expression = args[0];
|
|
1117
|
+
// If the expression is an EXISTS clause, we need to negate it
|
|
1118
|
+
if (expression.includes("EXISTS")) {
|
|
1119
|
+
return `(NOT ${expression})`;
|
|
1120
|
+
}
|
|
1121
|
+
return `(CASE
|
|
1122
|
+
WHEN ${expression} IS NULL THEN 1
|
|
1123
|
+
WHEN JSON_QUERY(${expression}) = '[]' THEN 1
|
|
1124
|
+
WHEN JSON_VALUE(${expression}) IS NULL THEN 1
|
|
1125
|
+
ELSE 0
|
|
1126
|
+
END = 1)`;
|
|
1127
|
+
}
|
|
1128
|
+
// No arguments - check current iteration context
|
|
1129
|
+
if (this.context.iterationContext) {
|
|
1130
|
+
// If the current iteration context is an EXISTS clause, negate it
|
|
1131
|
+
if (this.context.iterationContext.includes("EXISTS")) {
|
|
1132
|
+
return `(NOT ${this.context.iterationContext})`;
|
|
1133
|
+
}
|
|
1134
|
+
if (this.context.iterationContext.includes("JSON_QUERY")) {
|
|
1135
|
+
return `(CASE
|
|
1136
|
+
WHEN ${this.context.iterationContext} IS NULL THEN 1
|
|
1137
|
+
WHEN CAST(${this.context.iterationContext} AS NVARCHAR(MAX)) = '[]' THEN 1
|
|
1138
|
+
WHEN CAST(${this.context.iterationContext} AS NVARCHAR(MAX)) = 'null' THEN 1
|
|
1139
|
+
ELSE 0
|
|
1140
|
+
END = 1)`;
|
|
1141
|
+
}
|
|
1142
|
+
else if (this.context.iterationContext.includes("JSON_VALUE")) {
|
|
1143
|
+
return `(CASE WHEN ${this.context.iterationContext} IS NULL THEN 1 ELSE 0 END = 1)`;
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
return `(CASE
|
|
1147
|
+
WHEN JSON_QUERY(${this.context.iterationContext}) IS NULL THEN 1
|
|
1148
|
+
WHEN JSON_QUERY(${this.context.iterationContext}) = '[]' THEN 1
|
|
1149
|
+
ELSE 0
|
|
1150
|
+
END = 1)`;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
else {
|
|
1154
|
+
return `(CASE WHEN ${this.context.resourceAlias}.json IS NULL THEN 1 ELSE 0 END = 1)`;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
handleFirstFunction(_args) {
|
|
1158
|
+
if (this.context.iterationContext) {
|
|
1159
|
+
// Check if we have a JSON_QUERY expression for an array
|
|
1160
|
+
if (this.context.iterationContext.includes("JSON_QUERY")) {
|
|
1161
|
+
const match = /JSON_QUERY\(([^,]+),\s*'([^']+)'\)/.exec(this.context.iterationContext);
|
|
1162
|
+
if (match) {
|
|
1163
|
+
const source = match[1];
|
|
1164
|
+
const path = match[2];
|
|
1165
|
+
return `JSON_VALUE(${source}, '${path}[0]')`;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (this.context.iterationContext.includes("[0]")) {
|
|
1169
|
+
return this.context.iterationContext;
|
|
1170
|
+
}
|
|
1171
|
+
return `JSON_VALUE(${this.context.iterationContext}, '$[0]')`;
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
return `JSON_VALUE(${this.context.resourceAlias}.json, '$[0]')`;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
handleLastFunction(args) {
|
|
1178
|
+
const pathExpr = args.length > 0
|
|
1179
|
+
? args[0]
|
|
1180
|
+
: (this.context.iterationContext ??
|
|
1181
|
+
`${this.context.resourceAlias}.json`);
|
|
1182
|
+
return `JSON_VALUE(${pathExpr}, '$[last]')`;
|
|
1183
|
+
}
|
|
1184
|
+
handleCountFunction(args) {
|
|
1185
|
+
const countPath = args.length > 0
|
|
1186
|
+
? args[0]
|
|
1187
|
+
: (this.context.iterationContext ??
|
|
1188
|
+
`${this.context.resourceAlias}.json`);
|
|
1189
|
+
return `JSON_ARRAY_LENGTH(${countPath})`;
|
|
1190
|
+
}
|
|
1191
|
+
handleJoinFunction(args) {
|
|
1192
|
+
let separator = "''";
|
|
1193
|
+
if (args.length > 0) {
|
|
1194
|
+
separator = args[0];
|
|
1195
|
+
}
|
|
1196
|
+
const context = this.context.iterationContext ?? `${this.context.resourceAlias}.json`;
|
|
1197
|
+
// Check if context is a JSON_QUERY that accesses a nested array path (e.g., '$.name[0].given')
|
|
1198
|
+
// If so, we need to iterate over ALL parent array elements, not just [0]
|
|
1199
|
+
const nestedArrayMatch = /JSON_QUERY\(([^,]+),\s*'(\$\.[^']+)\[0]\.([^']+)'\)/.exec(context);
|
|
1200
|
+
if (nestedArrayMatch) {
|
|
1201
|
+
const source = nestedArrayMatch[1];
|
|
1202
|
+
const parentPath = nestedArrayMatch[2]; // e.g., '$.name'
|
|
1203
|
+
const childField = nestedArrayMatch[3]; // e.g., 'given'
|
|
1204
|
+
// Generate SQL that iterates over ALL parent array elements and gets ALL child array values
|
|
1205
|
+
return `ISNULL((SELECT STRING_AGG(ISNULL(childValue.value, ''), ${separator}) WITHIN GROUP (ORDER BY parentItem.[key], childValue.[key])
|
|
1206
|
+
FROM OPENJSON(${source}, '${parentPath}') AS parentItem
|
|
1207
|
+
CROSS APPLY OPENJSON(parentItem.value, '$.${childField}') AS childValue
|
|
1208
|
+
WHERE childValue.type IN (1, 2)), '')`;
|
|
1209
|
+
}
|
|
1210
|
+
// Standard join for simple arrays
|
|
1211
|
+
return `ISNULL((SELECT STRING_AGG(ISNULL(value, ''), ${separator}) WITHIN GROUP (ORDER BY [key])
|
|
1212
|
+
FROM OPENJSON(${context})
|
|
1213
|
+
WHERE type IN (1, 2)), '')`;
|
|
1214
|
+
}
|
|
1215
|
+
handleWhereFunction(_args) {
|
|
1216
|
+
// This should not be called anymore since where() is handled specially in handleFunctionInvocation
|
|
1217
|
+
throw new Error("where() function should be handled by handleWhereFunctionInvocation");
|
|
1218
|
+
}
|
|
1219
|
+
handleSelectFunction(args) {
|
|
1220
|
+
if (args.length !== 1) {
|
|
1221
|
+
throw new Error("select() function requires exactly one argument");
|
|
1222
|
+
}
|
|
1223
|
+
return args[0];
|
|
1224
|
+
}
|
|
1225
|
+
handleGetResourceKeyFunction() {
|
|
1226
|
+
// Returns resourceType/id as the resource key, extracting id from JSON
|
|
1227
|
+
return `CONCAT(${this.context.resourceAlias}.resource_type, '/', JSON_VALUE(${this.context.resourceAlias}.json, '$.id'))`;
|
|
1228
|
+
}
|
|
1229
|
+
handleOfTypeFunction(_args) {
|
|
1230
|
+
// This should not be called anymore since ofType() is handled specially in handleFunctionInvocation
|
|
1231
|
+
throw new Error("ofType() function should be handled by handleOfTypeFunctionInvocation");
|
|
1232
|
+
}
|
|
1233
|
+
handleGetReferenceKeyFunctionWithType(resourceType) {
|
|
1234
|
+
// Extract the .reference field from a Reference object
|
|
1235
|
+
// Optional type parameter filters by resource type
|
|
1236
|
+
if (this.context.iterationContext) {
|
|
1237
|
+
// If we're in an iteration context, the context points to the Reference object
|
|
1238
|
+
// We need to extract the .reference field
|
|
1239
|
+
const refSource = this.context.iterationContext;
|
|
1240
|
+
let referenceExpr;
|
|
1241
|
+
// Check if it's a JSON_VALUE call - extract just the reference field
|
|
1242
|
+
if (refSource.includes("JSON_VALUE")) {
|
|
1243
|
+
// Replace the current path with .reference
|
|
1244
|
+
const match = /JSON_VALUE\(([^,]+),\s*'([^']+)'\)/.exec(refSource);
|
|
1245
|
+
if (match) {
|
|
1246
|
+
const source = match[1];
|
|
1247
|
+
const path = match[2];
|
|
1248
|
+
referenceExpr = `JSON_VALUE(${source}, '${path}.reference')`;
|
|
1249
|
+
}
|
|
1250
|
+
else {
|
|
1251
|
+
// Fallback
|
|
1252
|
+
referenceExpr = `JSON_VALUE(${refSource}, '$.reference')`;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
else {
|
|
1256
|
+
// For simple iteration context like "forEach_0.value"
|
|
1257
|
+
referenceExpr = `JSON_VALUE(${refSource}, '$.reference')`;
|
|
1258
|
+
}
|
|
1259
|
+
// If a resource type is specified, only return the reference if it matches
|
|
1260
|
+
if (resourceType) {
|
|
1261
|
+
return `IIF(LEFT(${referenceExpr}, ${resourceType.length + 1}) = '${resourceType}/', ${referenceExpr}, NULL)`;
|
|
1262
|
+
}
|
|
1263
|
+
return referenceExpr;
|
|
1264
|
+
}
|
|
1265
|
+
// No iteration context - shouldn't happen for getReferenceKey
|
|
1266
|
+
throw new Error("getReferenceKey() requires a Reference object context");
|
|
1267
|
+
}
|
|
1268
|
+
handleNotFunction(args) {
|
|
1269
|
+
if (args.length > 0) {
|
|
1270
|
+
return `NOT (${args[0]})`;
|
|
1271
|
+
}
|
|
1272
|
+
if (this.context.iterationContext) {
|
|
1273
|
+
return `NOT (${this.context.iterationContext})`;
|
|
1274
|
+
}
|
|
1275
|
+
return "NOT (1=1)";
|
|
1276
|
+
}
|
|
1277
|
+
handleExtensionFunction(args) {
|
|
1278
|
+
if (args.length !== 1) {
|
|
1279
|
+
throw new Error("extension() function requires exactly one argument");
|
|
1280
|
+
}
|
|
1281
|
+
// extension('url') is equivalent to .extension.where(url = 'url')
|
|
1282
|
+
// Returns the filtered extension object(s) as a JSON_QUERY result
|
|
1283
|
+
const extensionUrl = args[0];
|
|
1284
|
+
const base = this.context.iterationContext ?? `${this.context.resourceAlias}.json`;
|
|
1285
|
+
// Generate SQL that filters the extension array by URL
|
|
1286
|
+
// Returns the first matching extension as a JSON value
|
|
1287
|
+
return `(SELECT TOP 1 value FROM OPENJSON(${base}, '$.extension') WHERE JSON_VALUE(value, '$.url') = ${extensionUrl})`;
|
|
1288
|
+
}
|
|
1289
|
+
handleBoundaryFunction(_functionName, _args) {
|
|
1290
|
+
// Simplified implementation - return the value as-is
|
|
1291
|
+
return (this.context.iterationContext ?? `${this.context.resourceAlias}.json`);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
exports.FHIRPathToTSqlVisitor = FHIRPathToTSqlVisitor;
|
|
1295
|
+
//# sourceMappingURL=visitor.js.map
|