|
| 1 | +//@ts-check |
| 2 | +import { describe, expect, test, beforeAll, afterAll } from "@jest/globals"; |
| 3 | +import { WOQLClient, WOQL } from "../index.js"; |
| 4 | +import { DbDetails } from "../dist/typescript/lib/typedef.js"; |
| 5 | + |
| 6 | +let client: WOQLClient; |
| 7 | +const db01 = "db__test_woql_dot_operator"; |
| 8 | + |
| 9 | +// Reusable WOQL query that uses dot operator with path results |
| 10 | +let pathQueryWithDot: any; |
| 11 | + |
| 12 | +// Schema for Person with knows relationship |
| 13 | +const personSchema = [ |
| 14 | + { |
| 15 | + "@base": "terminusdb:///data/", |
| 16 | + "@schema": "terminusdb:///schema#", |
| 17 | + "@type": "@context" |
| 18 | + }, |
| 19 | + { |
| 20 | + "@id": "Person", |
| 21 | + "@key": { |
| 22 | + "@fields": ["name"], |
| 23 | + "@type": "Lexical" |
| 24 | + }, |
| 25 | + "@type": "Class", |
| 26 | + name: { |
| 27 | + "@class": "xsd:string", |
| 28 | + "@type": "Optional" |
| 29 | + }, |
| 30 | + knows: { |
| 31 | + "@class": "Person", |
| 32 | + "@type": "Set" |
| 33 | + } |
| 34 | + } |
| 35 | +]; |
| 36 | + |
| 37 | +beforeAll(async () => { |
| 38 | + client = new WOQLClient("http://127.0.0.1:6363", { |
| 39 | + user: "admin", |
| 40 | + organization: "admin", |
| 41 | + key: process.env.TDB_ADMIN_PASS ?? "root" |
| 42 | + }); |
| 43 | + client.db(db01); |
| 44 | + |
| 45 | + // Create database with schema |
| 46 | + const dbObj: DbDetails = { |
| 47 | + label: db01, |
| 48 | + comment: "Test Dot operator", |
| 49 | + schema: true |
| 50 | + }; |
| 51 | + await client.createDatabase(db01, dbObj); |
| 52 | + |
| 53 | + // Add schema |
| 54 | + await client.addDocument(personSchema, { |
| 55 | + graph_type: "schema", |
| 56 | + full_replace: true |
| 57 | + }); |
| 58 | + |
| 59 | + // Insert test data: Alice -> Bob -> Charlie |
| 60 | + // Note: IDs match the name field due to lexical key |
| 61 | + const people = [ |
| 62 | + { |
| 63 | + "@id": "Person/Alice", |
| 64 | + "@type": "Person", |
| 65 | + name: "Alice", |
| 66 | + knows: ["Person/Bob", "Person/Ada"] // Forward reference - will be resolved |
| 67 | + }, |
| 68 | + { |
| 69 | + "@id": "Person/Bob", |
| 70 | + "@type": "Person", |
| 71 | + name: "Bob", |
| 72 | + knows: ["Person/Charlie", "Person/Ada"] |
| 73 | + }, |
| 74 | + { |
| 75 | + "@id": "Person/Ada", |
| 76 | + "@type": "Person", |
| 77 | + name: "Ada", |
| 78 | + knows: ["Person/Charlie"] |
| 79 | + }, |
| 80 | + { |
| 81 | + "@id": "Person/Charlie", |
| 82 | + "@type": "Person", |
| 83 | + name: "Charlie", |
| 84 | + knows: [] |
| 85 | + } |
| 86 | + ]; |
| 87 | + |
| 88 | + await client.addDocument(people, { graph_type: "instance" }); |
| 89 | + |
| 90 | + pathQueryWithDot = WOQL.and( |
| 91 | + WOQL.path("Person/Alice", "knows+", "v:destination", "v:path_edges"), |
| 92 | + WOQL.member("v:edge", "v:path_edges"), |
| 93 | + WOQL.dot("v:edge", "subject", "v:from"), |
| 94 | + WOQL.dot("v:edge", "predicate", "v:via"), |
| 95 | + WOQL.dot("v:edge", "object", "v:to") |
| 96 | + ); |
| 97 | +}); |
| 98 | + |
| 99 | +afterAll(async () => { |
| 100 | + await client.deleteDatabase(db01); |
| 101 | +}); |
| 102 | + |
| 103 | +describe("Tests for WOQL Dot operator", () => { |
| 104 | + test("Dot operator field should be DataValue with xsd:string", () => { |
| 105 | + // This test verifies the CORRECT client behavior: field wrapped as xsd:string |
| 106 | + // Both JS and Python clients implement it this way |
| 107 | + |
| 108 | + const query = WOQL.dot("v:dict", "field_name", "v:output"); |
| 109 | + |
| 110 | + // @ts-ignore - Testing internal JSON structure |
| 111 | + const json = query.json(); |
| 112 | + |
| 113 | + // The field should be wrapped as DataValue with xsd:string (correct client behavior) |
| 114 | + // @ts-ignore - Testing internal JSON structure |
| 115 | + expect(json.field).toBeDefined(); |
| 116 | + // @ts-ignore - Testing internal JSON structure |
| 117 | + expect(json.field["@type"]).toBe("DataValue"); |
| 118 | + // @ts-ignore - Testing internal JSON structure |
| 119 | + expect(json.field.data).toBeDefined(); |
| 120 | + // @ts-ignore - Testing internal JSON structure |
| 121 | + expect(json.field.data["@type"]).toBe("xsd:string"); |
| 122 | + // @ts-ignore - Testing internal JSON structure |
| 123 | + expect(json.field.data["@value"]).toBe("field_name"); |
| 124 | + }); |
| 125 | + |
| 126 | + test("Dot operator generates valid WOQL JSON structure", () => { |
| 127 | + // Test that dot operator generates valid WOQL JSON that can be serialized |
| 128 | + // The dot operator is typically used with path query results to extract |
| 129 | + // edge metadata fields like 'source', 'target', etc. |
| 130 | + |
| 131 | + // Example: Extract source and target from path edges |
| 132 | + const query = WOQL.and( |
| 133 | + WOQL.triple("v:a", "knows", "v:b"), |
| 134 | + WOQL.path("v:a", "knows+", "v:end", "v:path_edges"), |
| 135 | + WOQL.dot("v:path_edges", "source", "v:source_node"), |
| 136 | + WOQL.dot("v:path_edges", "target", "v:target_node") |
| 137 | + ); |
| 138 | + |
| 139 | + // @ts-ignore - Testing internal JSON structure |
| 140 | + const json = query.json(); |
| 141 | + |
| 142 | + // Verify the structure is valid |
| 143 | + // @ts-ignore - Testing internal JSON structure |
| 144 | + expect(json["@type"]).toBe("And"); |
| 145 | + // @ts-ignore - Testing internal JSON structure |
| 146 | + expect(json.and).toBeDefined(); |
| 147 | + // @ts-ignore - Testing internal JSON structure |
| 148 | + expect(Array.isArray(json.and)).toBe(true); |
| 149 | + // @ts-ignore - Testing internal JSON structure |
| 150 | + expect(json.and.length).toBe(4); |
| 151 | + |
| 152 | + // Find the dot operations |
| 153 | + // @ts-ignore - Testing internal JSON structure |
| 154 | + const dotOps = json.and.filter((op: any) => op["@type"] === "Dot"); |
| 155 | + expect(dotOps.length).toBe(2); |
| 156 | + |
| 157 | + // Verify field is wrapped as DataValue with xsd:string in both dot operations |
| 158 | + dotOps.forEach((dotOp: any) => { |
| 159 | + expect(dotOp.field).toBeDefined(); |
| 160 | + expect(dotOp.field["@type"]).toBe("DataValue"); |
| 161 | + expect(dotOp.field.data).toBeDefined(); |
| 162 | + expect(dotOp.field.data["@type"]).toBe("xsd:string"); |
| 163 | + const fieldValue = dotOp.field.data["@value"]; |
| 164 | + expect(fieldValue === "source" || fieldValue === "target").toBe(true); |
| 165 | + }); |
| 166 | + }); |
| 167 | + |
| 168 | + test("Dot operator compiles without server error", async () => { |
| 169 | + // End-to-end test: verify dot operator compiles successfully |
| 170 | + // The server compiler now correctly handles xsd:string typed literals |
| 171 | + // |
| 172 | + // Before the fix, ANY query with dot operator would fail with: |
| 173 | + // "Not well formed WOQL JSON-LD" error during compilation |
| 174 | + // |
| 175 | + // After the fix, queries with dot operator compile successfully |
| 176 | + |
| 177 | + // Use the reusable pathQueryWithDot that contains multiple dot operators |
| 178 | + // This query may return 0 results (depends on path structure), but the key |
| 179 | + // test is that it compiles without error - proving the bug fix works |
| 180 | + const result = await client.query(pathQueryWithDot); |
| 181 | + |
| 182 | + if (!result.bindings) { |
| 183 | + console.error( |
| 184 | + `=== Dot Operator Compilation Test Error ===` + |
| 185 | + `\n Query compiled successfully (no server compilation error)` + |
| 186 | + `\n However, 0 bindings returned` |
| 187 | + ); |
| 188 | + } |
| 189 | + expect(result.bindings).toBeDefined(); |
| 190 | + |
| 191 | + // The key verification: we got here without a compilation error |
| 192 | + // Before the fix, this would throw: "Not well formed WOQL JSON-LD" |
| 193 | + expect(result.bindings.length).toBeGreaterThanOrEqual(0); |
| 194 | + }); |
| 195 | + |
| 196 | + test("Dot operator extracts values from queried documents", async () => { |
| 197 | + const query = WOQL.and( |
| 198 | + // Find Alice's person ID |
| 199 | + WOQL.eq("v:person_id", "Person/Alice"), |
| 200 | + WOQL.triple("v:person_id", "rdf:type", "@schema:Person"), |
| 201 | + WOQL.triple("v:person_id", "name", "v:name_value"), |
| 202 | + // Read the document as JSON object |
| 203 | + // @ts-ignore - read_document exists but may not be in TypeScript definitions |
| 204 | + WOQL.read_document("v:person_id", "v:person_doc"), |
| 205 | + // Extract the name field using dot operator |
| 206 | + WOQL.opt().dot("v:person_doc", "name", "v:extracted_name") |
| 207 | + ); |
| 208 | + |
| 209 | + const result = await client.query(query); |
| 210 | + |
| 211 | + expect(result.bindings?.length).toBeGreaterThan(0); |
| 212 | + if (!result.bindings || result.bindings.length === 0) { |
| 213 | + console.error( |
| 214 | + `=== Dot Operator Extraction Test Error ===` + |
| 215 | + `\n Query compiled successfully (no server compilation error)` + |
| 216 | + `\n However, 0 bindings returned` |
| 217 | + ); |
| 218 | + } else { |
| 219 | + const binding = result.bindings[0]; |
| 220 | + expect(binding?.name_value?.["@value"]).toBe("Alice"); |
| 221 | + expect(binding?.extracted_name).toBe("Alice"); |
| 222 | + } |
| 223 | + }); |
| 224 | + |
| 225 | + test("Dot operator extracts values from path edges", async () => { |
| 226 | + const fullQuery = WOQL.and( |
| 227 | + WOQL.path("Person/Alice", "knows+", "v:known_person", "v:path_edges"), |
| 228 | + WOQL.member("v:edge", "v:path_edges"), |
| 229 | + WOQL.opt().dot("v:edge", "subject", "v:extracted_subject"), |
| 230 | + WOQL.opt().dot("v:edge", "woql:subject", "v:extracted_woql_subject"), |
| 231 | + WOQL.opt().dot( |
| 232 | + "v:edge", |
| 233 | + "http://terminusdb.com/schema/woql#subject", |
| 234 | + "v:extracted_terminus_subject" |
| 235 | + ) |
| 236 | + ); |
| 237 | + |
| 238 | + const fullResult = await client.query(fullQuery); |
| 239 | + |
| 240 | + expect(fullResult.bindings?.length).toBeGreaterThan(0); |
| 241 | + if (!fullResult.bindings || fullResult.bindings.length === 0) { |
| 242 | + console.error( |
| 243 | + `=== Dot Operator Extraction Path Edge Error ===` + |
| 244 | + `\n Query compiled successfully (no server compilation error)` + |
| 245 | + `\n However, 0 bindings returned` |
| 246 | + ); |
| 247 | + } else { |
| 248 | + const binding = fullResult.bindings[0]; |
| 249 | + expect(binding?.extracted_subject).toBe("Person/Alice"); |
| 250 | + expect(binding?.extracted_woql_subject).toBe("Person/Alice"); |
| 251 | + expect(binding?.extracted_terminus_subject).toBe("Person/Alice"); |
| 252 | + } |
| 253 | + }); |
| 254 | + |
| 255 | + test("Reusable query JSON structure verification", () => { |
| 256 | + // Verify the reusable pathQueryWithDot has correct JSON structure |
| 257 | + // with DataValue wrapped fields (xsd:string) |
| 258 | + |
| 259 | + // @ts-ignore - Testing internal JSON structure |
| 260 | + const json = pathQueryWithDot.json(); |
| 261 | + |
| 262 | + // @ts-ignore - Testing internal JSON structure |
| 263 | + expect(json["@type"]).toBe("And"); |
| 264 | + // @ts-ignore - Testing internal JSON structure |
| 265 | + expect(json.and).toBeDefined(); |
| 266 | + |
| 267 | + // Find all dot operations in the query |
| 268 | + // @ts-ignore - Testing internal JSON structure |
| 269 | + const dotOps = json.and.filter((op: any) => op["@type"] === "Dot"); |
| 270 | + |
| 271 | + expect(dotOps.length).toBe(3); // subject, predicate, object |
| 272 | + |
| 273 | + // Verify ALL dot operations have DataValue wrapped xsd:string fields |
| 274 | + dotOps.forEach((dotOp: any, i: number) => { |
| 275 | + const fieldValue = dotOp.field.data["@value"]; |
| 276 | + |
| 277 | + expect(dotOp.field["@type"]).toBe("DataValue"); |
| 278 | + expect(dotOp.field.data["@type"]).toBe("xsd:string"); |
| 279 | + expect(["subject", "predicate", "object"].includes(fieldValue)).toBe( |
| 280 | + true |
| 281 | + ); |
| 282 | + }); |
| 283 | + }); |
| 284 | + |
| 285 | + test("Field value structure - DataValue with xsd:string", () => { |
| 286 | + // This test verifies the client produces the CORRECT structure: |
| 287 | + // DataValue wrapped with xsd:string type (same as Python client) |
| 288 | + |
| 289 | + const actualQuery = WOQL.dot("v:dict", "field_name", "v:output"); |
| 290 | + // @ts-ignore - Testing internal JSON structure |
| 291 | + const actualJson = actualQuery.json(); |
| 292 | + // @ts-ignore - Testing internal JSON structure |
| 293 | + const actualField = actualJson.field; |
| 294 | + |
| 295 | + // Verify the field is wrapped as DataValue with xsd:string |
| 296 | + // This matches the Python client implementation |
| 297 | + expect(actualField).toBeDefined(); |
| 298 | + expect(actualField["@type"]).toBe("DataValue"); |
| 299 | + expect(actualField.data).toBeDefined(); |
| 300 | + expect(actualField.data["@type"]).toBe("xsd:string"); |
| 301 | + expect(actualField.data["@value"]).toBe("field_name"); |
| 302 | + }); |
| 303 | +}); |
0 commit comments