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.
Files changed (135) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +346 -0
  3. package/dist/cli.d.ts +7 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +85 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/fhirpath/transpiler.d.ts +18 -0
  8. package/dist/fhirpath/transpiler.d.ts.map +1 -0
  9. package/dist/fhirpath/transpiler.js +82 -0
  10. package/dist/fhirpath/transpiler.js.map +1 -0
  11. package/dist/fhirpath/visitor.d.ts +153 -0
  12. package/dist/fhirpath/visitor.d.ts.map +1 -0
  13. package/dist/fhirpath/visitor.js +1295 -0
  14. package/dist/fhirpath/visitor.js.map +1 -0
  15. package/dist/generated/grammar/fhirpathLexer.d.ts +88 -0
  16. package/dist/generated/grammar/fhirpathLexer.d.ts.map +1 -0
  17. package/dist/generated/grammar/fhirpathLexer.js +598 -0
  18. package/dist/generated/grammar/fhirpathLexer.js.map +1 -0
  19. package/dist/generated/grammar/fhirpathListener.d.ts +589 -0
  20. package/dist/generated/grammar/fhirpathListener.d.ts.map +1 -0
  21. package/dist/generated/grammar/fhirpathListener.js +4 -0
  22. package/dist/generated/grammar/fhirpathListener.js.map +1 -0
  23. package/dist/generated/grammar/fhirpathParser.d.ts +470 -0
  24. package/dist/generated/grammar/fhirpathParser.d.ts.map +1 -0
  25. package/dist/generated/grammar/fhirpathParser.js +3022 -0
  26. package/dist/generated/grammar/fhirpathParser.js.map +1 -0
  27. package/dist/generated/grammar/fhirpathVisitor.d.ts +372 -0
  28. package/dist/generated/grammar/fhirpathVisitor.d.ts.map +1 -0
  29. package/dist/generated/grammar/fhirpathVisitor.js +4 -0
  30. package/dist/generated/grammar/fhirpathVisitor.js.map +1 -0
  31. package/dist/index.d.ts +28 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +42 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/load.d.ts +14 -0
  36. package/dist/load.d.ts.map +1 -0
  37. package/dist/load.js +115 -0
  38. package/dist/load.js.map +1 -0
  39. package/dist/loader/connection.d.ts +36 -0
  40. package/dist/loader/connection.d.ts.map +1 -0
  41. package/dist/loader/connection.js +106 -0
  42. package/dist/loader/connection.js.map +1 -0
  43. package/dist/loader/discovery.d.ts +38 -0
  44. package/dist/loader/discovery.d.ts.map +1 -0
  45. package/dist/loader/discovery.js +107 -0
  46. package/dist/loader/discovery.js.map +1 -0
  47. package/dist/loader/index.d.ts +24 -0
  48. package/dist/loader/index.d.ts.map +1 -0
  49. package/dist/loader/index.js +193 -0
  50. package/dist/loader/index.js.map +1 -0
  51. package/dist/loader/progress.d.ts +70 -0
  52. package/dist/loader/progress.d.ts.map +1 -0
  53. package/dist/loader/progress.js +206 -0
  54. package/dist/loader/progress.js.map +1 -0
  55. package/dist/loader/stream.d.ts +21 -0
  56. package/dist/loader/stream.d.ts.map +1 -0
  57. package/dist/loader/stream.js +103 -0
  58. package/dist/loader/stream.js.map +1 -0
  59. package/dist/loader/tables.d.ts +43 -0
  60. package/dist/loader/tables.d.ts.map +1 -0
  61. package/dist/loader/tables.js +88 -0
  62. package/dist/loader/tables.js.map +1 -0
  63. package/dist/loader/types.d.ts +134 -0
  64. package/dist/loader/types.d.ts.map +1 -0
  65. package/dist/loader/types.js +8 -0
  66. package/dist/loader/types.js.map +1 -0
  67. package/dist/parser.d.ts +60 -0
  68. package/dist/parser.d.ts.map +1 -0
  69. package/dist/parser.js +226 -0
  70. package/dist/parser.js.map +1 -0
  71. package/dist/queryGenerator/ColumnExpressionGenerator.d.ts +52 -0
  72. package/dist/queryGenerator/ColumnExpressionGenerator.d.ts.map +1 -0
  73. package/dist/queryGenerator/ColumnExpressionGenerator.js +144 -0
  74. package/dist/queryGenerator/ColumnExpressionGenerator.js.map +1 -0
  75. package/dist/queryGenerator/ForEachProcessor.d.ts +127 -0
  76. package/dist/queryGenerator/ForEachProcessor.d.ts.map +1 -0
  77. package/dist/queryGenerator/ForEachProcessor.js +351 -0
  78. package/dist/queryGenerator/ForEachProcessor.js.map +1 -0
  79. package/dist/queryGenerator/PathParser.d.ts +64 -0
  80. package/dist/queryGenerator/PathParser.d.ts.map +1 -0
  81. package/dist/queryGenerator/PathParser.js +164 -0
  82. package/dist/queryGenerator/PathParser.js.map +1 -0
  83. package/dist/queryGenerator/SelectClauseBuilder.d.ts +63 -0
  84. package/dist/queryGenerator/SelectClauseBuilder.d.ts.map +1 -0
  85. package/dist/queryGenerator/SelectClauseBuilder.js +196 -0
  86. package/dist/queryGenerator/SelectClauseBuilder.js.map +1 -0
  87. package/dist/queryGenerator/SelectCombinationExpander.d.ts +42 -0
  88. package/dist/queryGenerator/SelectCombinationExpander.d.ts.map +1 -0
  89. package/dist/queryGenerator/SelectCombinationExpander.js +95 -0
  90. package/dist/queryGenerator/SelectCombinationExpander.js.map +1 -0
  91. package/dist/queryGenerator/WhereClauseBuilder.d.ts +20 -0
  92. package/dist/queryGenerator/WhereClauseBuilder.d.ts.map +1 -0
  93. package/dist/queryGenerator/WhereClauseBuilder.js +63 -0
  94. package/dist/queryGenerator/WhereClauseBuilder.js.map +1 -0
  95. package/dist/queryGenerator/index.d.ts +10 -0
  96. package/dist/queryGenerator/index.d.ts.map +1 -0
  97. package/dist/queryGenerator/index.js +19 -0
  98. package/dist/queryGenerator/index.js.map +1 -0
  99. package/dist/queryGenerator.d.ts +61 -0
  100. package/dist/queryGenerator.d.ts.map +1 -0
  101. package/dist/queryGenerator.js +187 -0
  102. package/dist/queryGenerator.js.map +1 -0
  103. package/dist/tests/sqlOnFhir.test.d.ts +11 -0
  104. package/dist/tests/sqlOnFhir.test.d.ts.map +1 -0
  105. package/dist/tests/sqlOnFhir.test.js +24 -0
  106. package/dist/tests/sqlOnFhir.test.js.map +1 -0
  107. package/dist/tests/utils/database.d.ts +38 -0
  108. package/dist/tests/utils/database.d.ts.map +1 -0
  109. package/dist/tests/utils/database.js +258 -0
  110. package/dist/tests/utils/database.js.map +1 -0
  111. package/dist/tests/utils/generator.d.ts +58 -0
  112. package/dist/tests/utils/generator.d.ts.map +1 -0
  113. package/dist/tests/utils/generator.js +195 -0
  114. package/dist/tests/utils/generator.js.map +1 -0
  115. package/dist/tests/utils/reporter.d.ts +83 -0
  116. package/dist/tests/utils/reporter.d.ts.map +1 -0
  117. package/dist/tests/utils/reporter.js +245 -0
  118. package/dist/tests/utils/reporter.js.map +1 -0
  119. package/dist/tests/utils/sqlOnFhir.d.ts +33 -0
  120. package/dist/tests/utils/sqlOnFhir.d.ts.map +1 -0
  121. package/dist/tests/utils/sqlOnFhir.js +281 -0
  122. package/dist/tests/utils/sqlOnFhir.js.map +1 -0
  123. package/dist/tests/utils/testContext.d.ts +18 -0
  124. package/dist/tests/utils/testContext.d.ts.map +1 -0
  125. package/dist/tests/utils/testContext.js +25 -0
  126. package/dist/tests/utils/testContext.js.map +1 -0
  127. package/dist/tests/utils/types.d.ts +31 -0
  128. package/dist/tests/utils/types.d.ts.map +1 -0
  129. package/dist/tests/utils/types.js +9 -0
  130. package/dist/tests/utils/types.js.map +1 -0
  131. package/dist/types.d.ts +288 -0
  132. package/dist/types.d.ts.map +1 -0
  133. package/dist/types.js +6 -0
  134. package/dist/types.js.map +1 -0
  135. 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