Skip to content
Merged
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
8 changes: 4 additions & 4 deletions integration_tests/create_database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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" ]);
})
Expand Down Expand Up @@ -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'
});
Expand Down
110 changes: 110 additions & 0 deletions integration_tests/woql_slice.test.ts
Original file line number Diff line number Diff line change
@@ -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"
});
});
});
34 changes: 34 additions & 0 deletions lib/query/woqlQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions lib/woql.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
99 changes: 99 additions & 0 deletions test/woqlJson/woqlSliceJson.js
Original file line number Diff line number Diff line change
@@ -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;
85 changes: 85 additions & 0 deletions test/woqlSlice.spec.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});