Skip to content

Commit 61fed00

Browse files
committed
feat(woql): Add vars_unique() for generating unique variable names fixes #261
Implements a new WOQL.vars_unique() function that creates variables with guaranteed unique names using an incrementing counter. This solves the problem where vars() generates generic variables that can collide across scopes. ## Implementation: **VarUnique Constructor (woqlDoc.js):** - Global counter for unique variable generation - Appends incrementing counter to base name (e.g., 'x' -> 'x_1', 'x_2') - Stores baseName, name, and counter properties - Generates proper JSON with unique variable names **WOQL.vars_unique() Function (woql.js):** - Maps input names to VarUnique instances - Works identically to vars() but with uniqueness guarantee - Useful with select() to firewall local variables from other scopes **Convert Function Update:** - Handles VarUnique instances like Var instances - Ensures proper JSON serialization ## Benefits: ✅ **Guaranteed Uniqueness:** Variables are unique across all scopes, even with same input names ✅ **Scope Isolation:** Works with select() to prevent variable collision ✅ **Backward Compatible:** Original vars() unchanged and works exactly as before ✅ **Well Documented:** Comprehensive JSDoc with usage examples ## Testing: **6 New Comprehensive Tests:** 1. ✅ Creates VarUnique instances 2. ✅ Unique names within single call 3. ✅ Unique names across multiple calls 4. ✅ Incrementing counter validation 5. ✅ Correct JSON generation 6. ✅ Original vars() unchanged **All 156 tests passing** ## Usage Examples: ```javascript // Basic usage const [a, b, c] = WOQL.vars_unique('a', 'b', 'c') // Creates: a_1, b_2, c_3 // Guaranteed uniqueness even with same names const [x1] = WOQL.vars_unique('x') // x_4 const [x2] = WOQL.vars_unique('x') // x_5 // Scope isolation with select() const [localVar] = WOQL.vars_unique('x') WOQL.select(localVar, WOQL.triple(localVar, 'rdf:type', 'Person')) // localVar is unique and won't conflict with other 'x' variables ```
1 parent ed94e33 commit 61fed00

File tree

3 files changed

+154
-3
lines changed

3 files changed

+154
-3
lines changed

lib/query/woqlDoc.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ function convert(obj) {
4040
'@type': 'Value',
4141
variable: obj.name,
4242
};
43+
// eslint-disable-next-line no-use-before-define
44+
} if (obj instanceof VarUnique) {
45+
return {
46+
'@type': 'Value',
47+
variable: obj.name,
48+
};
4349
} if (typeof (obj) === 'object' && !Array.isArray(obj)) {
4450
const pairs = [];
4551
// eslint-disable-next-line no-restricted-syntax
@@ -80,6 +86,30 @@ function Var(name) {
8086
};
8187
}
8288

89+
/**
90+
* Global counter for generating unique variable names
91+
*/
92+
let uniqueVarCounter = 0;
93+
94+
/**
95+
* Creates a unique variable by appending an incrementing counter to the name.
96+
* This ensures variables are unique across all instantiations, even with the same input name.
97+
* @param {string} name - Base name for the variable
98+
* @returns {VarUnique} - A unique variable object
99+
*/
100+
function VarUnique(name) {
101+
uniqueVarCounter += 1;
102+
this.name = `${name}_${uniqueVarCounter}`;
103+
this.baseName = name;
104+
this.counter = uniqueVarCounter;
105+
this.json = function () {
106+
return {
107+
'@type': 'Value',
108+
variable: this.name,
109+
};
110+
};
111+
}
112+
83113
/**
84114
* @param {object} name
85115
* @returns {object}
@@ -105,4 +135,6 @@ function Vars(...args) {
105135
return varObj;
106136
}
107137

108-
module.exports = { Vars, Var, Doc };
138+
module.exports = {
139+
Vars, Var, VarUnique, Doc,
140+
};

lib/woql.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
// I HAVE TO REVIEW THE Inheritance and the prototype chain
55
const WOQLQuery = require('./query/woqlBuilder');
66
const WOQLLibrary = require('./query/woqlLibrary');
7-
const { Vars, Var, Doc } = require('./query/woqlDoc');
7+
const {
8+
Vars, Var, VarUnique, Doc,
9+
} = require('./query/woqlDoc');
810
// eslint-disable-next-line no-unused-vars
911
const typedef = require('./typedef');
1012
// eslint-disable-next-line no-unused-vars
@@ -1352,6 +1354,33 @@ WOQL.vars = function (...varNames) {
13521354
return varNames.map((item) => new Var(item));
13531355
};
13541356

1357+
/**
1358+
* Generates unique javascript variables for use as WOQL variables within a query.
1359+
* Each call to vars_unique() creates variables with unique names by appending an
1360+
* incrementing counter, ensuring that variables are fully unique across all scopes.
1361+
* This is particularly useful with select() to firewall local variables from other
1362+
* scopes, even when requesting the same variable name multiple times.
1363+
*
1364+
* @param {...string} varNames - Base names for the variables
1365+
* @returns {array<VarUnique>} an array of unique javascript variables which can be
1366+
* dereferenced using the array destructuring operation
1367+
* @example
1368+
* const [a, b, c] = WOQL.vars_unique("a", "b", "c")
1369+
* // Creates variables like "a_1", "b_2", "c_3"
1370+
* const [a2, b2] = WOQL.vars_unique("a", "b")
1371+
* // Creates variables like "a_4", "b_5" - guaranteed unique even with same input names
1372+
*
1373+
* @example
1374+
* // Using with select() to create isolated scopes
1375+
* const [localVar] = WOQL.vars_unique("x")
1376+
* WOQL.select(localVar, WOQL.triple(localVar, "rdf:type", "Person"))
1377+
* // localVar is "x_1" and won't conflict with any other "x" variables
1378+
*/
1379+
1380+
WOQL.vars_unique = function (...varNames) {
1381+
return varNames.map((item) => new VarUnique(item));
1382+
};
1383+
13551384
/**
13561385
* Produces an encoded form of a document that can be used by a WOQL operation
13571386
* such as `WOQL.insert_document`.

test/woql.spec.js

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const { expect } = require('chai');
22

33
const WOQL = require('../lib/woql');
4-
const { Var, Vars } = require('../lib/query/woqlDoc');
4+
const { Var, VarUnique, Vars } = require('../lib/query/woqlDoc');
55

66
const idGenJson = require('./woqlJson/woqlIdgenJson');
77
const woqlStarJson = require('./woqlJson/woqlStarJson');
@@ -376,6 +376,96 @@ describe('woql queries', () => {
376376
expect(varsArr[0]).to.be.instanceof(Var);
377377
});
378378

379+
it('check the vars_unique method creates VarUnique instances', () => {
380+
const varsArr = WOQL.vars_unique('A', 'B', 'C');
381+
382+
expect(varsArr[0]).to.be.instanceof(VarUnique);
383+
expect(varsArr[1]).to.be.instanceof(VarUnique);
384+
expect(varsArr[2]).to.be.instanceof(VarUnique);
385+
});
386+
387+
it('check vars_unique creates unique variable names within a single call', () => {
388+
const [a, b, c] = WOQL.vars_unique('A', 'B', 'C');
389+
390+
// Each variable should have a unique name
391+
expect(a.name).to.not.equal(b.name);
392+
expect(b.name).to.not.equal(c.name);
393+
expect(a.name).to.not.equal(c.name);
394+
395+
// Each should contain the base name
396+
expect(a.name).to.include('A');
397+
expect(b.name).to.include('B');
398+
expect(c.name).to.include('C');
399+
});
400+
401+
it('check vars_unique creates unique variable names across multiple calls', () => {
402+
const [a1] = WOQL.vars_unique('X');
403+
const [a2] = WOQL.vars_unique('X');
404+
const [a3] = WOQL.vars_unique('X');
405+
406+
// Variables with the same base name should still be unique
407+
expect(a1.name).to.not.equal(a2.name);
408+
expect(a2.name).to.not.equal(a3.name);
409+
expect(a1.name).to.not.equal(a3.name);
410+
411+
// All should contain the base name 'X'
412+
expect(a1.name).to.include('X');
413+
expect(a2.name).to.include('X');
414+
expect(a3.name).to.include('X');
415+
});
416+
417+
it('check vars_unique appends incrementing counter', () => {
418+
// Get the current counter value by creating a variable
419+
const [v1] = WOQL.vars_unique('test');
420+
const counter1 = v1.counter;
421+
422+
// Next variable should have counter + 1
423+
const [v2] = WOQL.vars_unique('test');
424+
expect(v2.counter).to.equal(counter1 + 1);
425+
426+
// And so on
427+
const [v3] = WOQL.vars_unique('test');
428+
expect(v3.counter).to.equal(counter1 + 2);
429+
});
430+
431+
it('check vars_unique generates correct JSON with unique variable names', () => {
432+
const [a, b] = WOQL.vars_unique('myvar', 'myvar');
433+
434+
const jsonA = a.json();
435+
const jsonB = b.json();
436+
437+
expect(jsonA).to.have.property('@type', 'Value');
438+
expect(jsonA).to.have.property('variable');
439+
expect(jsonB).to.have.property('@type', 'Value');
440+
expect(jsonB).to.have.property('variable');
441+
442+
// Variable names in JSON should be different even with same base name
443+
expect(jsonA.variable).to.not.equal(jsonB.variable);
444+
expect(jsonA.variable).to.include('myvar');
445+
expect(jsonB.variable).to.include('myvar');
446+
});
447+
448+
it('check vars still works exactly as before (no changes)', () => {
449+
const [x, y, z] = WOQL.vars('X', 'Y', 'Z');
450+
451+
// Should create Var instances, not VarUnique
452+
expect(x).to.be.instanceof(Var);
453+
expect(x).to.not.be.instanceof(VarUnique);
454+
455+
// Names should be exactly as provided
456+
expect(x.name).to.equal('X');
457+
expect(y.name).to.equal('Y');
458+
expect(z.name).to.equal('Z');
459+
460+
// Should not have counter property
461+
expect(x).to.not.have.property('counter');
462+
463+
// Multiple calls with same names should produce identical variable names
464+
const [x2] = WOQL.vars('X');
465+
expect(x2.name).to.equal('X');
466+
expect(x2.name).to.equal(x.name);
467+
});
468+
379469
it('check type_of(Var,Var)', () => {
380470
const TypeOf = WOQL.type_of('v:X', 'v:Y').json()
381471
expect(TypeOf).to.deep.eql({

0 commit comments

Comments
 (0)