diff --git a/integration_tests/create_database.test.ts b/integration_tests/create_database.test.ts index 0d474ac5..83caf7e3 100644 --- a/integration_tests/create_database.test.ts +++ b/integration_tests/create_database.test.ts @@ -55,13 +55,13 @@ describe('Create a database, schema and insert data', () => { test('Query Person by name', async () => { const queryTemplate = {"name":"Tom", "@type":"Person" } const result = await client.getDocument({query:queryTemplate}); - expect(result).toStrictEqual({ '@id': 'Child/Tom', '@type': 'Child', age: "10", name: 'Tom' }); + expect(result).toStrictEqual({ '@id': 'Child/Tom', '@type': 'Child', age: 10, name: 'Tom' }); }) test('Query Person by ege', async () => { const queryTemplate = {"age":"40", "@type":"Person" } const result = await client.getDocument({query:queryTemplate}); - expect(result).toStrictEqual({"@id": "Parent/Tom%20Senior", "age":"40","name":"Tom Senior","@type":"Parent" , "has_child":"Child/Tom"}); + expect(result).toStrictEqual({"@id": "Parent/Tom%20Senior", "age":40,"name":"Tom Senior","@type":"Parent" , "has_child":"Child/Tom"}); }) const change_request = "change_request02"; @@ -77,7 +77,7 @@ describe('Create a database, schema and insert data', () => { }) test('Update Child Tom, link Parent', async () => { - const childTom = { '@id': 'Child/Tom', '@type': 'Child', age: "10", name: 'Tom' , has_parent:"Parent/Tom%20Senior"} + const childTom = { '@id': 'Child/Tom', '@type': 'Child', age: 10, name: 'Tom' , has_parent:"Parent/Tom%20Senior"} const result = await client.updateDocument(childTom); expect(result).toStrictEqual(["terminusdb:///data/Child/Tom" ]); }) @@ -113,7 +113,7 @@ describe('Create a database, schema and insert data', () => { expect(result).toStrictEqual({ '@id': 'Child/Tom', '@type': 'Child', - age: "10", + age: 10, name: 'Tom', has_parent: 'Parent/Tom%20Senior' }); diff --git a/integration_tests/woql_slice.test.ts b/integration_tests/woql_slice.test.ts new file mode 100644 index 00000000..f8d93223 --- /dev/null +++ b/integration_tests/woql_slice.test.ts @@ -0,0 +1,110 @@ +//@ts-check +import { describe, expect, test, beforeAll, afterAll } from "@jest/globals"; +import { WOQLClient, WOQL } from "../index.js"; +import { DbDetails } from "../dist/typescript/lib/typedef.js"; +import { Vars } from "../lib/woql.js"; + +let client: WOQLClient; +const db01 = "db__test_woql_slice"; + +beforeAll(() => { + client = new WOQLClient("http://127.0.0.1:6363", { + user: "admin", + organization: "admin", + key: process.env.TDB_ADMIN_PASS ?? "root" + }); + client.db(db01); +}); + +describe("Integration tests for WOQL slice operator", () => { + test("Create a database", async () => { + const dbObj: DbDetails = { + label: db01, + comment: "test slice operator", + schema: true + }; + const result = await client.createDatabase(db01, dbObj); + expect(result["@type"]).toEqual("api:DbCreateResponse"); + expect(result["api:status"]).toEqual("api:success"); + }); + + test("Basic slice: slice([A, B, C, D], 0, 2) returns [A, B]", async () => { + let v = Vars("Result"); + const query = WOQL.slice(["A", "B", "C", "D"], v.Result, 0, 2); + + const result = await client.query(query); + expect(result?.bindings).toHaveLength(1); + expect(result?.bindings[0].Result).toEqual([ + { "@type": "xsd:string", "@value": "A" }, + { "@type": "xsd:string", "@value": "B" } + ]); + }); + + test("Negative indices: slice([A, B, C, D], -2) returns [C, D]", async () => { + let v = Vars("Result"); + // Without end parameter - slice from -2 to end + const query = WOQL.slice(["A", "B", "C", "D"], v.Result, -2); + + const result = await client.query(query); + expect(result?.bindings).toHaveLength(1); + expect(result?.bindings[0].Result).toEqual([ + { "@type": "xsd:string", "@value": "C" }, + { "@type": "xsd:string", "@value": "D" } + ]); + }); + + test("Out-of-bounds clamped: slice([A, B, C], 1, 100) returns [B, C]", async () => { + let v = Vars("Result"); + const query = WOQL.slice(["A", "B", "C"], v.Result, 1, 100); + + const result = await client.query(query); + expect(result?.bindings).toHaveLength(1); + expect(result?.bindings[0].Result).toEqual([ + { "@type": "xsd:string", "@value": "B" }, + { "@type": "xsd:string", "@value": "C" } + ]); + }); + + test("Empty slice: slice([A, B, C], 2, 2) returns []", async () => { + let v = Vars("Result"); + const query = WOQL.slice(["A", "B", "C"], v.Result, 2, 2); + + const result = await client.query(query); + expect(result?.bindings).toHaveLength(1); + expect(result?.bindings[0].Result).toEqual([]); + }); + + test("Slice with numeric list: slice([10, 20, 30, 40], 1, 3) returns [20, 30]", async () => { + let v = Vars("Result"); + const query = WOQL.slice([10, 20, 30, 40], v.Result, 1, 3); + + const result = await client.query(query); + expect(result?.bindings).toHaveLength(1); + expect(result?.bindings[0].Result).toEqual([ + { "@type": "xsd:decimal", "@value": 20 }, + { "@type": "xsd:decimal", "@value": 30 } + ]); + }); + + test("Full range: slice([A, B, C, D], 0, 4) returns all elements", async () => { + let v = Vars("Result"); + const query = WOQL.slice(["A", "B", "C", "D"], v.Result, 0, 4); + + const result = await client.query(query); + expect(result?.bindings).toHaveLength(1); + expect(result?.bindings[0].Result).toEqual([ + { "@type": "xsd:string", "@value": "A" }, + { "@type": "xsd:string", "@value": "B" }, + { "@type": "xsd:string", "@value": "C" }, + { "@type": "xsd:string", "@value": "D" } + ]); + }); + + test("Delete a database", async () => { + const result = await client.deleteDatabase(db01); + expect(result).toStrictEqual({ + "@type": "api:DbDeleteResponse", + "api:status": "api:success" + }); + }); +}); diff --git a/lib/query/woqlQuery.js b/lib/query/woqlQuery.js index f0786b0d..ddde694a 100644 --- a/lib/query/woqlQuery.js +++ b/lib/query/woqlQuery.js @@ -1408,6 +1408,40 @@ WOQLQuery.prototype.length = function (inputVarList, resultVarName) { return this; }; +/** + * Extracts a contiguous subsequence from a list, following JavaScript's slice() semantics + * @param {string|array|Var} inputList - Either a variable representing a list or a list of + * variables or literals + * @param {string|Var} resultVarName - A variable in which the sliced list is stored + * @param {number|string|Var} start - The start index (0-based, supports negative indices) + * @param {number|string|Var} [end] - The end index (exclusive, optional - defaults to list length) + * @returns {WOQLQuery} A WOQLQuery which contains the Slice pattern matching expression + * @example + * let [result] = vars("result") + * slice(["a", "b", "c", "d"], result, 1, 3) // result = ["b", "c"] + * slice(["a", "b", "c", "d"], result, -2) // result = ["c", "d"] + */ +WOQLQuery.prototype.slice = function (inputList, resultVarName, start, end) { + if (this.cursor['@type']) this.wrapCursorWithAnd(); + this.cursor['@type'] = 'Slice'; + this.cursor.list = this.cleanDataValue(inputList); + this.cursor.result = this.cleanDataValue(resultVarName); + if (typeof start === 'number') { + this.cursor.start = this.cleanObject(start, 'xsd:integer'); + } else { + this.cursor.start = this.cleanDataValue(start); + } + // end is optional - only set if provided + if (end !== undefined) { + if (typeof end === 'number') { + this.cursor.end = this.cleanObject(end, 'xsd:integer'); + } else { + this.cursor.end = this.cleanDataValue(end); + } + } + return this; +}; + /** * * Logical negation of the contained subquery - if the subquery matches, the query diff --git a/lib/woql.js b/lib/woql.js index f709b613..1bbd6c92 100644 --- a/lib/woql.js +++ b/lib/woql.js @@ -875,6 +875,23 @@ WOQL.sum = function (subquery, total) { return new WOQLQuery().sum(subquery, total); }; +/** + * Extracts a contiguous subsequence from a list, following JavaScript's slice() semantics + * @param {string|array|Var} inputList - Either a variable representing a list or a list of + * variables or literals + * @param {string|Var} resultVarName - A variable in which the sliced list is stored + * @param {number|string|Var} start - The start index (0-based, supports negative indices) + * @param {number|string|Var} [end] - The end index (exclusive, optional - defaults to list length) + * @returns {WOQLQuery} A WOQLQuery which contains the Slice pattern matching expression + * @example + * let [result] = vars("result") + * slice(["a", "b", "c", "d"], result, 1, 3) // result = ["b", "c"] + * slice(["a", "b", "c", "d"], result, -2) // result = ["c", "d"] + */ +WOQL.slice = function (inputList, resultVarName, start, end) { + return new WOQLQuery().slice(inputList, resultVarName, start, end); +}; + /** * * Specifies an offset position in the results to start listing results from diff --git a/test/woqlJson/woqlSliceJson.js b/test/woqlJson/woqlSliceJson.js new file mode 100644 index 00000000..4bca64b2 --- /dev/null +++ b/test/woqlJson/woqlSliceJson.js @@ -0,0 +1,99 @@ +/** + * Expected JSON output for WOQL slice operator tests + */ + +const WOQL_SLICE_JSON = { + // Basic slice with all parameters + basicSlice: { + '@type': 'Slice', + list: { + '@type': 'DataValue', + list: [ + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'a' } }, + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'b' } }, + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'c' } }, + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'd' } }, + ], + }, + result: { '@type': 'DataValue', variable: 'Result' }, + start: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': 1 } }, + end: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': 3 } }, + }, + + // Slice with negative indices + negativeIndices: { + '@type': 'Slice', + list: { + '@type': 'DataValue', + list: [ + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'a' } }, + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'b' } }, + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'c' } }, + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'd' } }, + ], + }, + result: { '@type': 'DataValue', variable: 'Result' }, + start: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': -2 } }, + end: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': -1 } }, + }, + + // Slice without end parameter (optional) + withoutEnd: { + '@type': 'Slice', + list: { + '@type': 'DataValue', + list: [ + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'a' } }, + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'b' } }, + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'c' } }, + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'd' } }, + ], + }, + result: { '@type': 'DataValue', variable: 'Result' }, + start: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': 1 } }, + // Note: no 'end' property when end is omitted + }, + + // Slice with variable as list input + variableList: { + '@type': 'Slice', + list: { '@type': 'DataValue', variable: 'MyList' }, + result: { '@type': 'DataValue', variable: 'Result' }, + start: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': 0 } }, + end: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': 2 } }, + }, + + // Slice with variable indices + variableIndices: { + '@type': 'Slice', + list: { + '@type': 'DataValue', + list: [ + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'x' } }, + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'y' } }, + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'z' } }, + ], + }, + result: { '@type': 'DataValue', variable: 'Result' }, + start: { '@type': 'DataValue', variable: 'Start' }, + end: { '@type': 'DataValue', variable: 'End' }, + }, + + // Slice from start (index 0) + fromStart: { + '@type': 'Slice', + list: { + '@type': 'DataValue', + list: [ + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'a' } }, + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'b' } }, + { '@type': 'DataValue', data: { '@type': 'xsd:string', '@value': 'c' } }, + ], + }, + result: { '@type': 'DataValue', variable: 'Result' }, + start: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': 0 } }, + end: { '@type': 'Value', data: { '@type': 'xsd:integer', '@value': 2 } }, + }, +}; + +module.exports = WOQL_SLICE_JSON; diff --git a/test/woqlSlice.spec.js b/test/woqlSlice.spec.js new file mode 100644 index 00000000..662d21fd --- /dev/null +++ b/test/woqlSlice.spec.js @@ -0,0 +1,85 @@ +/** + * Unit tests for WOQL slice operator + * + * Tests the JavaScript client binding for slice(list, result, start, end?) + */ + +const { expect } = require('chai'); +const WOQL = require('../lib/woql'); +const WOQL_SLICE_JSON = require('./woqlJson/woqlSliceJson'); + +describe('WOQL slice operator', () => { + describe('Basic slicing', () => { + it('generates correct JSON for slice with start and end', () => { + const woqlObject = WOQL.slice(['a', 'b', 'c', 'd'], 'v:Result', 1, 3); + expect(woqlObject.json()).to.eql(WOQL_SLICE_JSON.basicSlice); + }); + }); + + describe('Negative indices', () => { + it('generates correct JSON for slice with negative indices', () => { + const woqlObject = WOQL.slice(['a', 'b', 'c', 'd'], 'v:Result', -2, -1); + expect(woqlObject.json()).to.eql(WOQL_SLICE_JSON.negativeIndices); + }); + }); + + describe('Optional end parameter', () => { + it('generates correct JSON when end is omitted', () => { + const woqlObject = WOQL.slice(['a', 'b', 'c', 'd'], 'v:Result', 1); + expect(woqlObject.json()).to.eql(WOQL_SLICE_JSON.withoutEnd); + }); + }); + + describe('Variable inputs', () => { + it('generates correct JSON with variable as list input', () => { + const woqlObject = WOQL.slice('v:MyList', 'v:Result', 0, 2); + expect(woqlObject.json()).to.eql(WOQL_SLICE_JSON.variableList); + }); + + it('generates correct JSON with variable indices', () => { + const woqlObject = WOQL.slice(['x', 'y', 'z'], 'v:Result', 'v:Start', 'v:End'); + expect(woqlObject.json()).to.eql(WOQL_SLICE_JSON.variableIndices); + }); + }); + + describe('Full range', () => { + it('generates correct JSON for slice from start', () => { + const woqlObject = WOQL.slice(['a', 'b', 'c'], 'v:Result', 0, 2); + expect(woqlObject.json()).to.eql(WOQL_SLICE_JSON.fromStart); + }); + }); + + describe('Method chaining', () => { + it('works with method chaining via WOQLQuery instance', () => { + const woqlObject = WOQL.slice(['a', 'b', 'c'], 'v:Result', 0, 2); + expect(woqlObject.json()['@type']).to.equal('Slice'); + }); + + it('chains with and() correctly', () => { + const woqlObject = WOQL.eq('v:MyList', ['x', 'y', 'z']) + .and() + .slice('v:MyList', 'v:Result', 1, 3); + + const json = woqlObject.json(); + expect(json['@type']).to.equal('And'); + expect(json.and).to.be.an('array').that.has.lengthOf(2); + expect(json.and[1]['@type']).to.equal('Slice'); + }); + }); + + describe('Edge cases', () => { + it('handles empty list', () => { + const woqlObject = WOQL.slice([], 'v:Result', 0, 1); + const json = woqlObject.json(); + expect(json['@type']).to.equal('Slice'); + expect(json.list.list).to.be.an('array').that.has.lengthOf(0); + }); + + it('handles single element slice', () => { + const woqlObject = WOQL.slice(['only'], 'v:Result', 0, 1); + const json = woqlObject.json(); + expect(json['@type']).to.equal('Slice'); + expect(json.list.list).to.be.an('array').that.has.lengthOf(1); + }); + }); +});