From c948ad7ce9a26f2a84f36733ff7c512fb5ecda24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20H=C3=B6ij?= Date: Mon, 27 Apr 2026 22:22:06 +0200 Subject: [PATCH] Initial fix for the vars issue --- integration_tests/woql_arithmetic.test.ts | 13 ++ integration_tests/woql_triple_slice.test.ts | 2 +- .../woql_var_type_compat.test.ts | 208 ++++++++++++++++++ lib/query/woqlQuery.js | 20 +- lib/woql.js | 26 +-- 5 files changed, 245 insertions(+), 24 deletions(-) create mode 100644 integration_tests/woql_var_type_compat.test.ts diff --git a/integration_tests/woql_arithmetic.test.ts b/integration_tests/woql_arithmetic.test.ts index cbaf955..3ba18c1 100644 --- a/integration_tests/woql_arithmetic.test.ts +++ b/integration_tests/woql_arithmetic.test.ts @@ -64,6 +64,19 @@ describe('Tests for woql arithmetic', () => { expect(result?.bindings).toStrictEqual(expectedJson); }); + // test('Eval with variable in expression should unify, not internal server error', async () => { + // // evaluate(plus("v:a", 5), 10) should unify v:a with 5 + // // Currently this gives an internal server error due to Prolog is/2 + // // requiring a fully ground RHS expression. + // const query = WOQL.eval(WOQL.plus("v:a", 5), 10); + + // const result = await client.query(query); + // // Should not throw an internal server error. + // // Expected: v:a unifies with 5 (since 5 + 5 = 10) + // const expectedJson = [{"a": {"@type": "xsd:decimal", "@value": 5}}]; + // expect(result?.bindings).toStrictEqual(expectedJson); + // }); + }); afterAll(async () => { diff --git a/integration_tests/woql_triple_slice.test.ts b/integration_tests/woql_triple_slice.test.ts index 0d5b63c..a0419b2 100644 --- a/integration_tests/woql_triple_slice.test.ts +++ b/integration_tests/woql_triple_slice.test.ts @@ -57,7 +57,7 @@ beforeAll(async () => { afterAll(async () => { await cleanupDatabase(client, dbName); -}); +}, 30000); describe("triple_slice: forward range queries on integers", () => { diff --git a/integration_tests/woql_var_type_compat.test.ts b/integration_tests/woql_var_type_compat.test.ts new file mode 100644 index 0000000..b05e687 --- /dev/null +++ b/integration_tests/woql_var_type_compat.test.ts @@ -0,0 +1,208 @@ +//@ts-check +/** + * Integration tests for issue #378: Var arguments in read_document and length + * + * These tests verify that WOQL.vars() Var objects can be passed to + * read_document() and length() without TypeScript errors, and that the + * resulting queries compile and execute correctly against TerminusDB. + * + * Before the fix: TypeScript rejects Var arguments (compile-time failure). + * After the fix: Tests compile and pass at runtime. + */ +import { describe, expect, test, beforeAll, afterAll } from "@jest/globals"; +import { WOQL } from "../index.js"; +import { Vars } from "../lib/woql.js"; +import { + createTestClient, + setupTestBranch, + teardownTestBranch, +} from "./test_utils"; + +const branchName = "test_woql_var_type_compat"; +let client = createTestClient(); + +// Schema: a simple Item class with a name and a tags list +const itemSchema = [ + { + "@base": "terminusdb:///data/", + "@schema": "terminusdb:///schema#", + "@type": "@context", + }, + { + "@id": "Item", + "@key": { "@type": "Lexical", "@fields": ["name"] }, + "@type": "Class", + name: "xsd:string", + tags: { "@type": "Set", "@class": "xsd:string" }, + }, +]; + +beforeAll(async () => { + await setupTestBranch(client, branchName); + + // Add schema + await client.addDocument(itemSchema, { + graph_type: "schema", + full_replace: true, + }); + + // Insert test documents + await client.addDocument([ + { "@type": "Item", name: "Alpha", tags: ["fast", "reliable"] }, + { "@type": "Item", name: "Beta", tags: ["cheap"] }, + { "@type": "Item", name: "Gamma", tags: ["fast", "cheap", "new"] }, + ]); +}, 60000); + +afterAll(async () => { + await teardownTestBranch(client, branchName); +}); + +describe("Issue #378: Var type compatibility with read_document", () => { + test("read_document accepts Var objects from WOQL.vars() for IRI", async () => { + // Use Var objects (not "v:" strings) for read_document arguments + const [uri, doc] = WOQL.vars("uri", "doc"); + + // Construct query: find an Item by type, then read it as a document + const query = WOQL.and( + WOQL.triple(uri, "rdf:type", "@schema:Item"), + WOQL.read_document(uri, doc) + ); + + const result = await client.query(query); + + // Should return bindings for each Item document + expect(result?.bindings).toBeDefined(); + expect(result!.bindings.length).toBeGreaterThanOrEqual(1); + + // Each binding should have a doc that is an object with @type "Item" + const docs = result!.bindings.map( + (b: Record) => b.doc + ); + for (const d of docs) { + expect(d).toHaveProperty("@type", "Item"); + expect(d).toHaveProperty("name"); + } + }); + + test("read_document accepts Var objects from WOQL.Vars() for both IRI and output", async () => { + // Use WOQL.Vars (record-style) instead of destructured array + const v = Vars("itemUri", "itemDoc"); + + const query = WOQL.and( + WOQL.eq(v.itemUri, "Item/Alpha"), + WOQL.read_document(v.itemUri, v.itemDoc) + ); + + const result = await client.query(query); + + expect(result?.bindings).toHaveLength(1); + const doc = result!.bindings[0].itemDoc; + expect(doc).toHaveProperty("@type", "Item"); + expect(doc).toHaveProperty("name", "Alpha"); + }); + + test("read_document compiles correct JSON-LD with Var arguments", () => { + const [uri, doc] = WOQL.vars("uri", "doc"); + + const query = WOQL.read_document(uri, doc); + const json = query.json(); + + // Verify the compiled JSON-LD structure uses NodeValue/Value with variable + expect(json).toHaveProperty("@type", "ReadDocument"); + expect(json).toHaveProperty("identifier"); + expect(json.identifier).toHaveProperty("variable", "uri"); + expect(json).toHaveProperty("document"); + expect(json.document).toHaveProperty("variable", "doc"); + }); +}); + +describe("Issue #378: Var type compatibility with length", () => { + test("length accepts Var object for result variable", async () => { + const v = Vars("name", "names", "count"); + + // Collect all item names, then count them + const query = WOQL.and( + WOQL.collect( + v.name, + v.names, + WOQL.triple("v:itemId", "name", v.name) + ), + WOQL.length(v.names, v.count) + ); + + const result = await client.query(query); + + expect(result?.bindings).toHaveLength(1); + expect(result!.bindings[0].count["@value"]).toBe(3); + }); + + test("length accepts Var object for list argument via string variable", async () => { + const [count] = WOQL.vars("count"); + + // Use a string "v:list" for the list and a Var for the count + const query = WOQL.and( + WOQL.collect("v:name", "v:list", WOQL.triple("v:itemId", "name", "v:name")), + WOQL.length("v:list", count) + ); + + const result = await client.query(query); + + expect(result?.bindings).toHaveLength(1); + expect(result!.bindings[0].count["@value"]).toBe(3); + }); + + test("length compiles correct JSON-LD with Var arguments", () => { + const v = Vars("myList", "len"); + + const query = WOQL.length(v.myList, v.len); + const json = query.json(); + + // Verify the compiled JSON-LD structure + expect(json).toHaveProperty("@type", "Length"); + expect(json).toHaveProperty("list"); + expect(json).toHaveProperty("length"); + // The length field should be a variable reference + expect(json.length).toHaveProperty("variable", "len"); + }); +}); + +describe("Issue #378: Mixed Var and string variable usage", () => { + test("read_document works with Var for IRI and string for output", async () => { + const [uri] = WOQL.vars("uri"); + + const query = WOQL.and( + WOQL.eq(uri, "Item/Beta"), + WOQL.read_document(uri, "v:betaDoc") + ); + + const result = await client.query(query); + + expect(result?.bindings).toHaveLength(1); + expect(result!.bindings[0].betaDoc).toHaveProperty("@type", "Item"); + expect(result!.bindings[0].betaDoc).toHaveProperty("name", "Beta"); + }); + + test("read_document works with string for IRI and Var for output", async () => { + const [doc] = WOQL.vars("doc"); + + const query = WOQL.read_document("Item/Gamma", doc); + + const result = await client.query(query); + + expect(result?.bindings).toHaveLength(1); + expect(result!.bindings[0].doc).toHaveProperty("@type", "Item"); + expect(result!.bindings[0].doc).toHaveProperty("name", "Gamma"); + }); + + test("length works with literal list and Var result", async () => { + const [count] = WOQL.vars("count"); + + const query = WOQL.length(["a", "b", "c"], count); + + const result = await client.query(query); + + expect(result?.bindings).toHaveLength(1); + expect(result!.bindings[0].count["@value"]).toBe(3); + }); +}); diff --git a/lib/query/woqlQuery.js b/lib/query/woqlQuery.js index 33b2526..fdc11a4 100644 --- a/lib/query/woqlQuery.js +++ b/lib/query/woqlQuery.js @@ -188,8 +188,8 @@ class WOQLQuery extends WOQLCore { /** * Read a node identified by an IRI as a JSON-LD document - * @param {string} IRI - The document id or a variable to read - * @param {string} output - Variable which will be bound to the document. + * @param {string|Var} IRI - The document id or a variable to read + * @param {string|Var} output - Variable which will be bound to the document. * @return {WOQLQuery} WOQLQuery */ WOQLQuery.prototype.read_document = function (IRI, output) { @@ -204,7 +204,7 @@ WOQLQuery.prototype.read_document = function (IRI, output) { * Insert a document in the graph. * @param {object} docjson - The document to insert. Must either have an '@id' or * have a class specified key. - * @param {string} [IRI] - An optional identifier specifying the document location. + * @param {string|Var} [IRI] - An optional identifier specifying the document location. * @return {WOQLQuery} WOQLQuery */ @@ -222,7 +222,7 @@ WOQLQuery.prototype.insert_document = function (docjson, IRI) { * Update a document identified by an IRI * @param {object} docjson - The document to update. Must either have an '@id' or * have a class specified key. - * @param {string} [IRI] - An optional identifier specifying the document location. + * @param {string|Var} [IRI] - An optional identifier specifying the document location. * @return {WOQLQuery} WOQLQuery */ @@ -238,7 +238,7 @@ WOQLQuery.prototype.update_document = function (docjson, IRI) { /** * Delete a document from the graph. - * @param {string} IRI - The document id or a variable + * @param {string|Var} IRI - The document id or a variable * @return {WOQLQuery} WOQLQuery */ @@ -842,9 +842,9 @@ WOQLQuery.prototype.removed_quad = function (subject, predicate, object, graphRe /** * Returns true if ClassA subsumes ClassB, according to the current DB schema - * @param {string} classA - ClassA - * @param {string} classB - ClassB - * @returns {boolean} WOQLQuery + * @param {string|Var} classA - ClassA + * @param {string|Var} classB - ClassB + * @returns {WOQLQuery} WOQLQuery */ WOQLQuery.prototype.sub = function (classA, classB) { if (!classA || !classB) return this.parameterError('Subsumption takes two parameters, both URIs'); @@ -1803,7 +1803,7 @@ WOQLQuery.prototype.idgenerator = WOQLQuery.prototype.idgen; * Generates a random ID with a specified prefix * Uses cryptographically secure random base64 encoding to generate unique identifiers * @param {string} prefix - prefix for the generated ID - * @param {string} outputVar - variable that stores the generated ID + * @param {string|Var} outputVar - variable that stores the generated ID * @returns {WOQLQuery} A WOQLQuery which contains the random ID generation pattern * idgen_random("Person/", "v:person_id") */ @@ -2112,7 +2112,7 @@ WOQLQuery.prototype.regexp = WOQLQuery.prototype.re; /** * Calculates the length of the list in va and stores it in vb - * @param {string|array} inputVarList - Either a variable representing a list or a list of + * @param {string|Var|array} inputVarList - Either a variable representing a list or a list of * variables or literals * @param {string|Var} resultVarName - A variable in which the length of the list is stored or * the length of the list as a non-negative integer diff --git a/lib/woql.js b/lib/woql.js index 8f11e0c..62cebac 100644 --- a/lib/woql.js +++ b/lib/woql.js @@ -138,8 +138,8 @@ WOQL.read_object = function (IRI, output, formatObj) { /** * Read a node identified by an IRI as a JSON-LD document - * @param {string} IRI - The document id or a variable to read - * @param {string} output - Variable which will be bound to the document. + * @param {string|Var} IRI - The document id or a variable to read + * @param {string|Var} output - Variable which will be bound to the document. * @return {WOQLQuery} WOQLQuery * @example * let [person] = vars("Person") @@ -158,7 +158,7 @@ WOQL.read_document = function (IRI, output) { * Insert a document in the graph. * @param {object} docjson - The document to insert. Must either have an '@id' or * have a class specified key. - * @param {string} [IRI] - An optional identifier specifying the document location. + * @param {string|Var} [IRI] - An optional identifier specifying the document location. * @return {WOQLQuery} WOQLQuery * @example * const res = await client.query( @@ -174,8 +174,8 @@ WOQL.insert_document = function (docjson, IRI) { * Update a document identified by an IRI * @param {object} docjson - The document to update. Must either have an '@id' or * have a class specified key. - * @param {string} [IRI] - An optional identifier specifying the document location. - * @return {object} WOQLQuery + * @param {string|Var} [IRI] - An optional identifier specifying the document location. + * @return {WOQLQuery} WOQLQuery */ WOQL.update_document = function (docjson, IRI) { @@ -184,7 +184,7 @@ WOQL.update_document = function (docjson, IRI) { /** * Delete a document from the graph. - * @param {string} IRI - The document id or a variable + * @param {string|Var} IRI - The document id or a variable * @return {WOQLQuery} WOQLQuery */ @@ -417,8 +417,8 @@ WOQL.quad_previous = function (subject, predicate, object, previous, graphRef) { /** * Returns true if ClassA subsumes ClassB, according to the current DB schema - * @param {string} classA - ClassA - * @param {string} classB - ClassB + * @param {string|Var} classA - ClassA + * @param {string|Var} classB - ClassB * @returns {WOQLQuery} WOQLQuery */ WOQL.sub = function (classA, classB) { @@ -1038,7 +1038,7 @@ WOQL.idgenerator = function (prefix, inputVarList, resultVarName) { /** * Generates a random ID with a specified prefix * @param {string} prefix - prefix for the generated ID - * @param {string} resultVarName - variable that stores the generated ID + * @param {string|Var} resultVarName - variable that stores the generated ID * @returns {WOQLQuery} A WOQLQuery object containing the random ID generation function * @example * let [newid] = vars("newid") @@ -1052,7 +1052,7 @@ WOQL.idgen_random = function (prefix, resultVarName) { * Backward-compatible alias for idgen_random * @deprecated Use idgen_random instead * @param {string} prefix - prefix for the generated ID - * @param {string} resultVarName - variable that stores the generated ID + * @param {string|Var} resultVarName - variable that stores the generated ID * @returns {WOQLQuery} A WOQLQuery object containing the random ID generation function */ WOQL.random_idgen = function (prefix, resultVarName) { @@ -1221,7 +1221,7 @@ WOQL.start = function (start, subquery) { /** * * Specifies a maximum number of results that will be returned from the subquery - * @param {number|string} limit - A variable that refers to an non-negative integer or a + * @param {number|string|Var} limit - A variable that refers to an non-negative integer or a * non-negative integer * @param {WOQLQuery} [subquery] - A subquery whose results will be limited * @returns {WOQLQuery} A WOQLQuery whose results will be returned starting from @@ -1240,7 +1240,7 @@ WOQL.limit = function (limit, subquery) { * * Matches the regular expression defined in Patern against the Test string, to produce * the matched patterns in Matches - * @param {string} pattern - string or variable using normal PCRE regular expression syntax with + * @param {string|Var} pattern - string or variable using normal PCRE regular expression syntax with * the exception that special characters have to be escaped twice (to enable transport in JSONLD) * @param {string|Var} inputVarName - string or variable containing the string to be tested for * patterns with the regex @@ -1266,7 +1266,7 @@ WOQL.regexp = function (pattern, inputVarName, resultVarList) { /** * * Calculates the length of the list in va and stores it in vb - * @param {string|array} inputVarList - Either a variable representing a list or a list of + * @param {string|Var|array} inputVarList - Either a variable representing a list or a list of * variables or literals * @param {string|Var} resultVarName - A variable in which the length of the list is stored or * the length of the list as a non-negative integer