Skip to content

Commit 5cba47b

Browse files
authored
Merge pull request #354 from hoijnet/issue/2284-set-operators
TerminusDB Issue/2284 set operators
2 parents 7608547 + 62f59cd commit 5cba47b

File tree

14 files changed

+336
-29
lines changed

14 files changed

+336
-29
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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_set_operations';
8+
9+
beforeAll(() => {
10+
client = new WOQLClient("http://127.0.0.1:6363", { user: 'admin', organization: 'admin', key: process.env.TDB_ADMIN_PASS ?? 'root' });
11+
client.db(db01);
12+
});
13+
14+
describe('Tests for WOQL set operations', () => {
15+
test('Create a database', async () => {
16+
const dbObj: DbDetails = { label: db01, comment: 'test woql set operations', schema: true };
17+
const result = await client.createDatabase(db01, dbObj);
18+
expect(result["@type"]).toEqual("api:DbCreateResponse");
19+
expect(result["api:status"]).toEqual("api:success");
20+
});
21+
22+
describe('set_difference', () => {
23+
test('computes difference between two lists', async () => {
24+
const query = WOQL.and(
25+
WOQL.eq("v:ListA", [1, 2, 3, 4]),
26+
WOQL.eq("v:ListB", [2, 4]),
27+
WOQL.set_difference("v:ListA", "v:ListB", "v:Diff")
28+
);
29+
30+
const result = await client.query(query);
31+
expect(result?.bindings).toHaveLength(1);
32+
const diff = result?.bindings[0].Diff.map((v: any) => v['@value']);
33+
expect(diff).toEqual([1, 3]);
34+
});
35+
36+
test('returns empty list when first list is subset of second', async () => {
37+
const query = WOQL.and(
38+
WOQL.eq("v:ListA", [1, 2]),
39+
WOQL.eq("v:ListB", [1, 2, 3]),
40+
WOQL.set_difference("v:ListA", "v:ListB", "v:Diff")
41+
);
42+
43+
const result = await client.query(query);
44+
expect(result?.bindings).toHaveLength(1);
45+
expect(result?.bindings[0].Diff).toEqual([]);
46+
});
47+
48+
test('handles empty lists', async () => {
49+
const query = WOQL.and(
50+
WOQL.eq("v:ListA", []),
51+
WOQL.eq("v:ListB", [1]),
52+
WOQL.set_difference("v:ListA", "v:ListB", "v:Diff")
53+
);
54+
55+
const result = await client.query(query);
56+
expect(result?.bindings).toHaveLength(1);
57+
expect(result?.bindings[0].Diff).toEqual([]);
58+
});
59+
});
60+
61+
describe('set_intersection', () => {
62+
test('computes intersection of two lists', async () => {
63+
const query = WOQL.and(
64+
WOQL.eq("v:ListA", [1, 2, 3]),
65+
WOQL.eq("v:ListB", [2, 3, 4]),
66+
WOQL.set_intersection("v:ListA", "v:ListB", "v:Common")
67+
);
68+
69+
const result = await client.query(query);
70+
expect(result?.bindings).toHaveLength(1);
71+
const common = result?.bindings[0].Common.map((v: any) => v['@value']);
72+
expect(common).toEqual([2, 3]);
73+
});
74+
75+
test('returns empty list when no common elements', async () => {
76+
const query = WOQL.and(
77+
WOQL.eq("v:ListA", [1, 2]),
78+
WOQL.eq("v:ListB", [3, 4]),
79+
WOQL.set_intersection("v:ListA", "v:ListB", "v:Common")
80+
);
81+
82+
const result = await client.query(query);
83+
expect(result?.bindings).toHaveLength(1);
84+
expect(result?.bindings[0].Common).toEqual([]);
85+
});
86+
});
87+
88+
describe('set_union', () => {
89+
test('computes union of two lists', async () => {
90+
const query = WOQL.and(
91+
WOQL.eq("v:ListA", [1, 2]),
92+
WOQL.eq("v:ListB", [2, 3]),
93+
WOQL.set_union("v:ListA", "v:ListB", "v:All")
94+
);
95+
96+
const result = await client.query(query);
97+
expect(result?.bindings).toHaveLength(1);
98+
const all = result?.bindings[0].All.map((v: any) => v['@value']);
99+
expect(all).toEqual([1, 2, 3]);
100+
});
101+
102+
test('removes duplicates', async () => {
103+
const query = WOQL.and(
104+
WOQL.eq("v:ListA", [1, 1, 2]),
105+
WOQL.eq("v:ListB", [2, 2]),
106+
WOQL.set_union("v:ListA", "v:ListB", "v:All")
107+
);
108+
109+
const result = await client.query(query);
110+
expect(result?.bindings).toHaveLength(1);
111+
const all = result?.bindings[0].All.map((v: any) => v['@value']);
112+
expect(all).toEqual([1, 2]);
113+
});
114+
});
115+
116+
describe('set_member', () => {
117+
test('checks membership in a set', async () => {
118+
const query = WOQL.and(
119+
WOQL.eq("v:MySet", [1, 2, 3]),
120+
WOQL.set_member(2, "v:MySet")
121+
);
122+
123+
const result = await client.query(query);
124+
expect(result?.bindings).toHaveLength(1);
125+
});
126+
127+
test('fails for non-member', async () => {
128+
const query = WOQL.and(
129+
WOQL.eq("v:MySet", [1, 2, 3]),
130+
WOQL.set_member(5, "v:MySet")
131+
);
132+
133+
const result = await client.query(query);
134+
expect(result?.bindings).toHaveLength(0);
135+
});
136+
});
137+
138+
describe('list_to_set', () => {
139+
test('converts list to set removing duplicates and sorting', async () => {
140+
const query = WOQL.and(
141+
WOQL.eq("v:MyList", [3, 1, 2, 1]),
142+
WOQL.list_to_set("v:MyList", "v:MySet")
143+
);
144+
145+
const result = await client.query(query);
146+
expect(result?.bindings).toHaveLength(1);
147+
const mySet = result?.bindings[0].MySet.map((v: any) => v['@value']);
148+
expect(mySet).toEqual([1, 2, 3]);
149+
});
150+
});
151+
152+
describe('performance test', () => {
153+
test('handles large set operations efficiently', async () => {
154+
// Create two large arrays with some overlap
155+
const listA = Array.from({ length: 1000 }, (_, i) => i);
156+
const listB = Array.from({ length: 1000 }, (_, i) => i + 500);
157+
158+
const query = WOQL.and(
159+
WOQL.eq("v:ListA", listA),
160+
WOQL.eq("v:ListB", listB),
161+
WOQL.set_difference("v:ListA", "v:ListB", "v:Diff")
162+
);
163+
164+
const startTime = Date.now();
165+
const result = await client.query(query);
166+
const elapsed = Date.now() - startTime;
167+
168+
expect(result?.bindings).toHaveLength(1);
169+
expect(result?.bindings[0].Diff.length).toEqual(500);
170+
171+
// Should complete in under 1 second with O(n log n) algorithm
172+
expect(elapsed).toBeLessThan(1000);
173+
});
174+
});
175+
176+
test('Delete a database', async () => {
177+
const result = await client.deleteDatabase(db01);
178+
expect(result).toStrictEqual({ '@type': 'api:DbDeleteResponse', 'api:status': 'api:success' });
179+
});
180+
});

lib/query/woqlBuilder.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,11 @@ WOQLQuery.prototype.nuke = function (graphRef) {
171171
};
172172

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

183181
WOQLQuery.prototype.node = function (node, type) {

lib/query/woqlCore.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,12 @@ WOQLQuery.prototype.cleanObject = function (o, t) {
403403
} else if (typeof o === 'boolean') {
404404
t = t || 'xsd:boolean';
405405
obj.data = this.jlt(o, t);
406+
} else if (Array.isArray(o)) {
407+
const res = [];
408+
for (let i = 0; i < o.length; i++) {
409+
res.push(this.cleanObject(o[i]));
410+
}
411+
obj.list = res;
406412
} else if (typeof o === 'object' && o) {
407413
if (typeof o['@value'] !== 'undefined') obj.data = o;
408414
else return o;

lib/query/woqlDoc.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ function convert(obj) {
9393

9494
/**
9595
* @param {string} name The variable name
96-
* @returns
96+
* @returns {Var}
9797
*/
9898
function Var(name) {
9999
this.name = name;
@@ -108,7 +108,7 @@ function Var(name) {
108108
let uniqueVarCounter = 0;
109109
/**
110110
* @param {string} name The variable name
111-
* @returns
111+
* @returns {VarUnique}
112112
*/
113113
function VarUnique(name) {
114114
uniqueVarCounter += 1;

lib/query/woqlQuery.js

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ class WOQLQuery extends WOQLCore {
8888
* @param {typedef.GraphRef} [graphRef] Resource String identifying the graph which will
8989
* be used for subsequent chained schema calls
9090
* @returns {WOQLQuery} A WOQLQuery which contains the partial Graph pattern matching expression
91-
* @example
9291
*/
9392
graph(graphRef) { return this; }
9493

@@ -127,31 +126,27 @@ class WOQLQuery extends WOQLCore {
127126
/**
128127
* @param {boolean} tf
129128
* @returns {object}
130-
* @example
131129
*/
132130
boolean(tf) { return {}; }
133131

134132
/**
135133
* @param {string} s
136134
* @returns {object}
137-
* @example
138135
*/
139136
string(s) { return {}; }
140137

141138
/**
142139
* @param {any} s
143140
* @param {string} t
144141
* @returns {object}
145-
* @example
142+
146143
*/
147144
literal(s, t) { return {}; }
148145

149146
/**
150-
* @param {string} s
151-
* @returns {object}
152-
* @example
153-
*/
154-
147+
* @param {string} s
148+
* @returns {object}
149+
*/
155150
iri(s) { return {}; }
156151

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

627622
/**
628623
* Matches if a is equal to b
629-
* @param {string|Var} varName - literal, variable or id
630-
* @param {string|Var} varValue - literal, variable or id
624+
* @param {string|number|boolean|array|Var} varName - literal, variable, array, or id
625+
* @param {string|number|boolean|array|Var} varValue - literal, variable, array, or id
631626
* @returns {WOQLQuery}
632627
*/
633628
WOQLQuery.prototype.eq = function (varName, varValue) {
@@ -1281,6 +1276,82 @@ WOQLQuery.prototype.member = function (element, list) {
12811276
return this;
12821277
};
12831278

1279+
/**
1280+
* Computes the set difference between two lists (elements in listA but not in listB)
1281+
* @param {string|Var|array} listA - First list or variable
1282+
* @param {string|Var|array} listB - Second list or variable
1283+
* @param {string|Var} result - Variable to store the result
1284+
* @returns {WOQLQuery} A WOQLQuery which contains the SetDifference expression
1285+
*/
1286+
WOQLQuery.prototype.set_difference = function (listA, listB, result) {
1287+
if (this.cursor['@type']) this.wrapCursorWithAnd();
1288+
this.cursor['@type'] = 'SetDifference';
1289+
this.cursor.list_a = this.valueList(listA);
1290+
this.cursor.list_b = this.valueList(listB);
1291+
this.cursor.result = this.valueList(result);
1292+
return this;
1293+
};
1294+
1295+
/**
1296+
* Computes the set intersection of two lists (elements in both listA and listB)
1297+
* @param {string|Var|array} listA - First list or variable
1298+
* @param {string|Var|array} listB - Second list or variable
1299+
* @param {string|Var} result - Variable to store the result
1300+
* @returns {WOQLQuery} A WOQLQuery which contains the SetIntersection expression
1301+
*/
1302+
WOQLQuery.prototype.set_intersection = function (listA, listB, result) {
1303+
if (this.cursor['@type']) this.wrapCursorWithAnd();
1304+
this.cursor['@type'] = 'SetIntersection';
1305+
this.cursor.list_a = this.valueList(listA);
1306+
this.cursor.list_b = this.valueList(listB);
1307+
this.cursor.result = this.valueList(result);
1308+
return this;
1309+
};
1310+
1311+
/**
1312+
* Computes the set union of two lists (all unique elements from both lists)
1313+
* @param {string|Var|array} listA - First list or variable
1314+
* @param {string|Var|array} listB - Second list or variable
1315+
* @param {string|Var} result - Variable to store the result
1316+
* @returns {WOQLQuery} A WOQLQuery which contains the SetUnion expression
1317+
*/
1318+
WOQLQuery.prototype.set_union = function (listA, listB, result) {
1319+
if (this.cursor['@type']) this.wrapCursorWithAnd();
1320+
this.cursor['@type'] = 'SetUnion';
1321+
this.cursor.list_a = this.valueList(listA);
1322+
this.cursor.list_b = this.valueList(listB);
1323+
this.cursor.result = this.valueList(result);
1324+
return this;
1325+
};
1326+
1327+
/**
1328+
* Checks if an element is a member of a set (efficient O(log n) lookup)
1329+
* @param {string|Var|any} element - Element to check
1330+
* @param {string|Var|array} set - Set (list) to check membership in
1331+
* @returns {WOQLQuery} A WOQLQuery which contains the SetMember expression
1332+
*/
1333+
WOQLQuery.prototype.set_member = function (element, set) {
1334+
if (this.cursor['@type']) this.wrapCursorWithAnd();
1335+
this.cursor['@type'] = 'SetMember';
1336+
this.cursor.element = this.cleanObject(element);
1337+
this.cursor.set = this.valueList(set);
1338+
return this;
1339+
};
1340+
1341+
/**
1342+
* Converts a list to a set (removes duplicates and sorts)
1343+
* @param {string|Var|array} list - Input list or variable
1344+
* @param {string|Var} set - Variable to store the resulting set
1345+
* @returns {WOQLQuery} A WOQLQuery which contains the ListToSet expression
1346+
*/
1347+
WOQLQuery.prototype.list_to_set = function (list, set) {
1348+
if (this.cursor['@type']) this.wrapCursorWithAnd();
1349+
this.cursor['@type'] = 'ListToSet';
1350+
this.cursor.list = this.valueList(list);
1351+
this.cursor.set = this.valueList(set);
1352+
return this;
1353+
};
1354+
12841355
/**
12851356
* takes a variable number of string arguments and concatenates them into a single string
12861357
* @param {array|string|Var} varList - a variable representing a list or a list of variables or

lib/viewer/frameRule.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
const TerminusRule = require('./terminusRule');
1010

1111
/**
12-
* @typedef {import('../typedef').Frame} Frame
12+
* @typedef {Object} Frame
1313
*/
1414

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

2727
/**
2828
* Returns an array of rules that match the paased frame
29-
* @param {[FrameRule]} rules - array of rules to be tested
29+
* @param {Array<FrameRule>} rules - array of rules to be tested
3030
* @param {Frame | object} frame - document frame, object frame, or property frame to be tested
3131
* @param {function} [onmatch] - optional function to be called with args (frame, rule)
3232
* on each match

lib/viewer/objectFrame.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ const WOQL = require('../woql');
3636
* doc.loadJSON(json_frames, cls) //console.log(this.document)
3737
* doc.loadDataFrames(json_frames, cls)
3838
* doc.loadClassFrames(json_frames, cls)
39-
* @example
40-
*
4139
* @description Represents a frame for programmatic access to object frame,
4240
* anywhere within a document
4341
* Recursive data structure where this.children contains an indexed array of object frames
@@ -49,7 +47,7 @@ const WOQL = require('../woql');
4947
* @param classframe - an array of frames representing a class
5048
* @param archetypes list of class frames
5149
* @param parent parent object
52-
* @returns
50+
* @returns {ObjectFrame}
5351
*/
5452

5553
function ObjectFrame(cls, jsonld, classframes, parent) {

0 commit comments

Comments
 (0)