Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/ddb-sql-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,21 @@ export function astToSql(node, inLambda, inputType={}) {

//non-standard
case '_splitPath':
// Use the function-call form `parse_path(el, '/')` rather than the
// method-call form `el.parse_path('/')` when operating on a lambda
// parameter. DuckDB 1.4.x's binder fails to resolve a lambda parameter
// used as a method-call receiver inside a list_filter predicate that is
// itself nested in another lambda (e.g. getReferenceKey() inside a
// forEach) — see issue #18. The function-call form binds correctly on
// both 1.4.x and 1.5.x. The non-lambda case stays method-chained so
// flattenSql can join it onto the preceding navigation segment.
return inputType && inputType.isArray
? {
sql: `list_transform(el -> el.parse_path('/')[${firstArg.value}])`,
sql: `list_transform(el -> parse_path(el, '/')[${firstArg.value}])`,
outputType: {isArray: true, fhirType: "string"}
}
: {
sql: `${inLambda ? "el." : ""}parse_path('/')[${firstArg.value}]`,
sql: `parse_path(${inLambda ? "el, " : ""}'/')[${firstArg.value}]`,
outputType: {isArray: false, fhirType: "string"}
}

Expand Down
90 changes: 81 additions & 9 deletions tests/e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import fhirSchema from "../schemas/fhir-schema-r4.json";

let db;
let resourceFile;
let encounterFile;

const resource = {
"resourceType": "QuestionnaireResponse",
Expand All @@ -19,18 +20,28 @@ const resource = {
}]
};

beforeAll(done => {
// Encounter with a participant referencing a Practitioner — drives the issue #18
// test (a getReferenceKey() column inside a forEachOrNull).
const encounterResource = {
resourceType: "Encounter",
id: "enc1",
status: "finished",
participant: [{individual: {reference: "Practitioner/prac1"}}]
};

beforeAll(async () => {
db = openMemoryDb();
// Create temporary resource file
// Create temporary resource files
resourceFile = path.join(import.meta.dir, "e2e-test-resources.temp.json");
Bun.write(resourceFile, JSON.stringify([resource]));
done();
encounterFile = path.join(import.meta.dir, "e2e-encounter.temp.json");
await Bun.write(resourceFile, JSON.stringify([resource]));
await Bun.write(encounterFile, JSON.stringify([encounterResource]));
});

afterAll(done => {
// Clean up temporary file
if (fs.existsSync(resourceFile)) {
fs.unlinkSync(resourceFile);
// Clean up temporary files
for (const file of [resourceFile, encounterFile]) {
if (file && fs.existsSync(file)) fs.unlinkSync(file);
}
db.close(() => done());
});
Expand All @@ -55,7 +66,68 @@ describe("e2e tests", () => {
true, true
);

const result = await executeQuery(db, querySql);
const result = await executeQuery(db, querySql);
expect(new Set(result)).toEqual(new Set(expected));
});

test("forEachOrNull with getReferenceKey() column binds on DuckDB 1.4.x (issue #18)", async () => {
// getReferenceKey() lowers to a where() predicate whose _splitPath step uses the
// lambda parameter as a method-call receiver (el.parse_path(...)). When that
// predicate sits in a list_filter nested inside the forEachOrNull's
// list_transform, DuckDB 1.4.x's binder fails to resolve the parameter:
// Binder Error: Referenced column "el" not found in FROM clause!
// Emitting parse_path in function-call form binds correctly on 1.4.x and 1.5.x.
const viewDefinition = {
"resource": "Encounter",
"select": [
{"column": [{"name": "id", "path": "getResourceKey()"}]},
{
"forEachOrNull": "participant",
"column": [{
"name": "practitioner_id",
"path": "individual.getReferenceKey(Practitioner)"
}]
}
]
};

const expected = [{"id": "enc1", "practitioner_id": "prac1"}];
const querySql = templateToQuery(
viewDefinition, fhirSchema,
testQueryTemplate, [["test_file_path", encounterFile]],
true, true
);

const result = await executeQuery(db, querySql);
expect(new Set(result)).toEqual(new Set(expected));
});
});

test("forEach with getReferenceKey() column binds on DuckDB 1.4.x (issue #18)", async () => {
// Same nested-lambda codegen as the forEachOrNull case above; forEach only
// differs by omitting the .ifnull2([NULL]) null-row suffix. Guards both
// directives against regression of the parse_path binding fix.
const viewDefinition = {
"resource": "Encounter",
"select": [
{"column": [{"name": "id", "path": "getResourceKey()"}]},
{
"forEach": "participant",
"column": [{
"name": "practitioner_id",
"path": "individual.getReferenceKey(Practitioner)"
}]
}
]
};

const expected = [{"id": "enc1", "practitioner_id": "prac1"}];
const querySql = templateToQuery(
viewDefinition, fhirSchema,
testQueryTemplate, [["test_file_path", encounterFile]],
true, true
);

const result = await executeQuery(db, querySql);
expect(new Set(result)).toEqual(new Set(expected));
});
});