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
180 changes: 180 additions & 0 deletions integration_tests/woql_set_operations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//@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';

let client: WOQLClient;
const db01 = 'db__test_woql_set_operations';

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('Tests for WOQL set operations', () => {
test('Create a database', async () => {
const dbObj: DbDetails = { label: db01, comment: 'test woql set operations', schema: true };
const result = await client.createDatabase(db01, dbObj);
expect(result["@type"]).toEqual("api:DbCreateResponse");
expect(result["api:status"]).toEqual("api:success");
});

describe('set_difference', () => {
test('computes difference between two lists', async () => {
const query = WOQL.and(
WOQL.eq("v:ListA", [1, 2, 3, 4]),
WOQL.eq("v:ListB", [2, 4]),
WOQL.set_difference("v:ListA", "v:ListB", "v:Diff")
);

const result = await client.query(query);
expect(result?.bindings).toHaveLength(1);
const diff = result?.bindings[0].Diff.map((v: any) => v['@value']);
expect(diff).toEqual([1, 3]);
});

test('returns empty list when first list is subset of second', async () => {
const query = WOQL.and(
WOQL.eq("v:ListA", [1, 2]),
WOQL.eq("v:ListB", [1, 2, 3]),
WOQL.set_difference("v:ListA", "v:ListB", "v:Diff")
);

const result = await client.query(query);
expect(result?.bindings).toHaveLength(1);
expect(result?.bindings[0].Diff).toEqual([]);
});

test('handles empty lists', async () => {
const query = WOQL.and(
WOQL.eq("v:ListA", []),
WOQL.eq("v:ListB", [1]),
WOQL.set_difference("v:ListA", "v:ListB", "v:Diff")
);

const result = await client.query(query);
expect(result?.bindings).toHaveLength(1);
expect(result?.bindings[0].Diff).toEqual([]);
});
});

describe('set_intersection', () => {
test('computes intersection of two lists', async () => {
const query = WOQL.and(
WOQL.eq("v:ListA", [1, 2, 3]),
WOQL.eq("v:ListB", [2, 3, 4]),
WOQL.set_intersection("v:ListA", "v:ListB", "v:Common")
);

const result = await client.query(query);
expect(result?.bindings).toHaveLength(1);
const common = result?.bindings[0].Common.map((v: any) => v['@value']);
expect(common).toEqual([2, 3]);
});

test('returns empty list when no common elements', async () => {
const query = WOQL.and(
WOQL.eq("v:ListA", [1, 2]),
WOQL.eq("v:ListB", [3, 4]),
WOQL.set_intersection("v:ListA", "v:ListB", "v:Common")
);

const result = await client.query(query);
expect(result?.bindings).toHaveLength(1);
expect(result?.bindings[0].Common).toEqual([]);
});
});

describe('set_union', () => {
test('computes union of two lists', async () => {
const query = WOQL.and(
WOQL.eq("v:ListA", [1, 2]),
WOQL.eq("v:ListB", [2, 3]),
WOQL.set_union("v:ListA", "v:ListB", "v:All")
);

const result = await client.query(query);
expect(result?.bindings).toHaveLength(1);
const all = result?.bindings[0].All.map((v: any) => v['@value']);
expect(all).toEqual([1, 2, 3]);
});

test('removes duplicates', async () => {
const query = WOQL.and(
WOQL.eq("v:ListA", [1, 1, 2]),
WOQL.eq("v:ListB", [2, 2]),
WOQL.set_union("v:ListA", "v:ListB", "v:All")
);

const result = await client.query(query);
expect(result?.bindings).toHaveLength(1);
const all = result?.bindings[0].All.map((v: any) => v['@value']);
expect(all).toEqual([1, 2]);
});
});

describe('set_member', () => {
test('checks membership in a set', async () => {
const query = WOQL.and(
WOQL.eq("v:MySet", [1, 2, 3]),
WOQL.set_member(2, "v:MySet")
);

const result = await client.query(query);
expect(result?.bindings).toHaveLength(1);
});

test('fails for non-member', async () => {
const query = WOQL.and(
WOQL.eq("v:MySet", [1, 2, 3]),
WOQL.set_member(5, "v:MySet")
);

const result = await client.query(query);
expect(result?.bindings).toHaveLength(0);
});
});

describe('list_to_set', () => {
test('converts list to set removing duplicates and sorting', async () => {
const query = WOQL.and(
WOQL.eq("v:MyList", [3, 1, 2, 1]),
WOQL.list_to_set("v:MyList", "v:MySet")
);

const result = await client.query(query);
expect(result?.bindings).toHaveLength(1);
const mySet = result?.bindings[0].MySet.map((v: any) => v['@value']);
expect(mySet).toEqual([1, 2, 3]);
});
});

describe('performance test', () => {
test('handles large set operations efficiently', async () => {
// Create two large arrays with some overlap
const listA = Array.from({ length: 1000 }, (_, i) => i);
const listB = Array.from({ length: 1000 }, (_, i) => i + 500);

const query = WOQL.and(
WOQL.eq("v:ListA", listA),
WOQL.eq("v:ListB", listB),
WOQL.set_difference("v:ListA", "v:ListB", "v:Diff")
);

const startTime = Date.now();
const result = await client.query(query);
const elapsed = Date.now() - startTime;

expect(result?.bindings).toHaveLength(1);
expect(result?.bindings[0].Diff.length).toEqual(500);

// Should complete in under 1 second with O(n log n) algorithm
expect(elapsed).toBeLessThan(1000);
});
});

test('Delete a database', async () => {
const result = await client.deleteDatabase(db01);
expect(result).toStrictEqual({ '@type': 'api:DbDeleteResponse', 'api:status': 'api:success' });
});
});
2 changes: 0 additions & 2 deletions lib/query/woqlBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,11 @@ WOQLQuery.prototype.nuke = function (graphRef) {
};

/**
*
* @param {string|Var} node - The IRI of a node or a variable containing an IRI which will
* be the subject of the builder functions
* @param {typedef.FuntionType} [type] - Optional type of builder function to build
* (default is triple)
* @returns {WOQLQuery} - A WOQLQuery which contains the partial Node pattern matching expression
* @example
*/

WOQLQuery.prototype.node = function (node, type) {
Expand Down
6 changes: 6 additions & 0 deletions lib/query/woqlCore.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,12 @@ WOQLQuery.prototype.cleanObject = function (o, t) {
} else if (typeof o === 'boolean') {
t = t || 'xsd:boolean';
obj.data = this.jlt(o, t);
} else if (Array.isArray(o)) {
const res = [];
for (let i = 0; i < o.length; i++) {
res.push(this.cleanObject(o[i]));
}
obj.list = res;
} else if (typeof o === 'object' && o) {
if (typeof o['@value'] !== 'undefined') obj.data = o;
else return o;
Expand Down
4 changes: 2 additions & 2 deletions lib/query/woqlDoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ function convert(obj) {

/**
* @param {string} name The variable name
* @returns
* @returns {Var}
*/
function Var(name) {
this.name = name;
Expand All @@ -108,7 +108,7 @@ function Var(name) {
let uniqueVarCounter = 0;
/**
* @param {string} name The variable name
* @returns
* @returns {VarUnique}
*/
function VarUnique(name) {
uniqueVarCounter += 1;
Expand Down
93 changes: 82 additions & 11 deletions lib/query/woqlQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ class WOQLQuery extends WOQLCore {
* @param {typedef.GraphRef} [graphRef] Resource String identifying the graph which will
* be used for subsequent chained schema calls
* @returns {WOQLQuery} A WOQLQuery which contains the partial Graph pattern matching expression
* @example
*/
graph(graphRef) { return this; }

Expand Down Expand Up @@ -127,31 +126,27 @@ class WOQLQuery extends WOQLCore {
/**
* @param {boolean} tf
* @returns {object}
* @example
*/
boolean(tf) { return {}; }

/**
* @param {string} s
* @returns {object}
* @example
*/
string(s) { return {}; }

/**
* @param {any} s
* @param {string} t
* @returns {object}
* @example

*/
literal(s, t) { return {}; }

/**
* @param {string} s
* @returns {object}
* @example
*/

* @param {string} s
* @returns {object}
*/
iri(s) { return {}; }

// eslint-disable-next-line no-underscore-dangle
Expand Down Expand Up @@ -626,8 +621,8 @@ WOQLQuery.prototype.subsumption = WOQLQuery.prototype.sub;

/**
* Matches if a is equal to b
* @param {string|Var} varName - literal, variable or id
* @param {string|Var} varValue - literal, variable or id
* @param {string|number|boolean|array|Var} varName - literal, variable, array, or id
* @param {string|number|boolean|array|Var} varValue - literal, variable, array, or id
* @returns {WOQLQuery}
*/
WOQLQuery.prototype.eq = function (varName, varValue) {
Expand Down Expand Up @@ -1281,6 +1276,82 @@ WOQLQuery.prototype.member = function (element, list) {
return this;
};

/**
* Computes the set difference between two lists (elements in listA but not in listB)
* @param {string|Var|array} listA - First list or variable
* @param {string|Var|array} listB - Second list or variable
* @param {string|Var} result - Variable to store the result
* @returns {WOQLQuery} A WOQLQuery which contains the SetDifference expression
*/
WOQLQuery.prototype.set_difference = function (listA, listB, result) {
if (this.cursor['@type']) this.wrapCursorWithAnd();
this.cursor['@type'] = 'SetDifference';
this.cursor.list_a = this.valueList(listA);
this.cursor.list_b = this.valueList(listB);
this.cursor.result = this.valueList(result);
return this;
};

/**
* Computes the set intersection of two lists (elements in both listA and listB)
* @param {string|Var|array} listA - First list or variable
* @param {string|Var|array} listB - Second list or variable
* @param {string|Var} result - Variable to store the result
* @returns {WOQLQuery} A WOQLQuery which contains the SetIntersection expression
*/
WOQLQuery.prototype.set_intersection = function (listA, listB, result) {
if (this.cursor['@type']) this.wrapCursorWithAnd();
this.cursor['@type'] = 'SetIntersection';
this.cursor.list_a = this.valueList(listA);
this.cursor.list_b = this.valueList(listB);
this.cursor.result = this.valueList(result);
return this;
};

/**
* Computes the set union of two lists (all unique elements from both lists)
* @param {string|Var|array} listA - First list or variable
* @param {string|Var|array} listB - Second list or variable
* @param {string|Var} result - Variable to store the result
* @returns {WOQLQuery} A WOQLQuery which contains the SetUnion expression
*/
WOQLQuery.prototype.set_union = function (listA, listB, result) {
if (this.cursor['@type']) this.wrapCursorWithAnd();
this.cursor['@type'] = 'SetUnion';
this.cursor.list_a = this.valueList(listA);
this.cursor.list_b = this.valueList(listB);
this.cursor.result = this.valueList(result);
return this;
};

/**
* Checks if an element is a member of a set (efficient O(log n) lookup)
* @param {string|Var|any} element - Element to check
* @param {string|Var|array} set - Set (list) to check membership in
* @returns {WOQLQuery} A WOQLQuery which contains the SetMember expression
*/
WOQLQuery.prototype.set_member = function (element, set) {
if (this.cursor['@type']) this.wrapCursorWithAnd();
this.cursor['@type'] = 'SetMember';
this.cursor.element = this.cleanObject(element);
this.cursor.set = this.valueList(set);
return this;
};

/**
* Converts a list to a set (removes duplicates and sorts)
* @param {string|Var|array} list - Input list or variable
* @param {string|Var} set - Variable to store the resulting set
* @returns {WOQLQuery} A WOQLQuery which contains the ListToSet expression
*/
WOQLQuery.prototype.list_to_set = function (list, set) {
if (this.cursor['@type']) this.wrapCursorWithAnd();
this.cursor['@type'] = 'ListToSet';
this.cursor.list = this.valueList(list);
this.cursor.set = this.valueList(set);
return this;
};

/**
* takes a variable number of string arguments and concatenates them into a single string
* @param {array|string|Var} varList - a variable representing a list or a list of variables or
Expand Down
4 changes: 2 additions & 2 deletions lib/viewer/frameRule.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
const TerminusRule = require('./terminusRule');

/**
* @typedef {import('../typedef').Frame} Frame
* @typedef {Object} Frame
*/

/**
Expand All @@ -26,7 +26,7 @@ Object.setPrototypeOf(FrameRule.prototype, TerminusRule.TerminusRule.prototype);

/**
* Returns an array of rules that match the paased frame
* @param {[FrameRule]} rules - array of rules to be tested
* @param {Array<FrameRule>} rules - array of rules to be tested
* @param {Frame | object} frame - document frame, object frame, or property frame to be tested
* @param {function} [onmatch] - optional function to be called with args (frame, rule)
* on each match
Expand Down
4 changes: 1 addition & 3 deletions lib/viewer/objectFrame.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ const WOQL = require('../woql');
* doc.loadJSON(json_frames, cls) //console.log(this.document)
* doc.loadDataFrames(json_frames, cls)
* doc.loadClassFrames(json_frames, cls)
* @example
*
* @description Represents a frame for programmatic access to object frame,
* anywhere within a document
* Recursive data structure where this.children contains an indexed array of object frames
Expand All @@ -49,7 +47,7 @@ const WOQL = require('../woql');
* @param classframe - an array of frames representing a class
* @param archetypes list of class frames
* @param parent parent object
* @returns
* @returns {ObjectFrame}
*/

function ObjectFrame(cls, jsonld, classframes, parent) {
Expand Down
Loading