author | Alan Dipert
<alan@dipert.org> 2021-06-15 05:40:22 UTC |
committer | Alan Dipert
<alan@dipert.org> 2021-06-15 05:40:22 UTC |
parent | 6f3eb2a028e7896b29f0c8ce61fe8b1f19c06209 |
datalog.mjs | +49 | -5 |
tests.mjs | +39 | -30 |
diff --git a/datalog.mjs b/datalog.mjs index 785495c..4779b5e 100644 --- a/datalog.mjs +++ b/datalog.mjs @@ -86,6 +86,12 @@ class StupidTupleSet { toArray() { return [...this.tuples]; } + equals(other) { + if (!(other instanceof StupidTupleSet)) return false; + if (this.complement(other).size()) return false; + if (other.complement(this).size()) return false; + return true; + } } class Relation { @@ -102,6 +108,9 @@ class Relation { has(rel) { return this.rels.some(v => relEqual(v, rel)); } + size() { + return this.rels.length; + } [Symbol.iterator]() { return this.rels[Symbol.iterator](); } @@ -170,8 +179,31 @@ function initRel(use, vals) { return new Relation(varNames(use)).add(rx); } -function select(rel, vars) { - return new StupidTupleSet([...rel].map(rx => vars.map(v => rx[v]))); +function findAll(pred, xs) { + return xs.reduce((xs, y, i) => pred(y) ? [...xs, i] : xs, []); +} + +function select(rel, find, by) { + let varIdxs = findAll(isVar, find), + aggIdxs = findAll(x => !isVar(x), find); + if (!rel.size()) { + return rel; + } else if (!aggIdxs.length) { + return new StupidTupleSet([...rel].map(rx => find.map(v => rx[v.description]))); + } else if (!varIdxs.length) { + return new StupidTupleSet(find.map(([op, sym]) => { + if (op === "min") { + return [ + [...rel].map(rx => rx[sym.description]).sort((a, b) => a - b)[0] + ]; + } else { + throw new Error(`Unsupported op: ${op}`); + } + })); + } else { + // TODO agg + vars + throw new Error(`TODO grouping`) + } } function parseVals(use, vals) { @@ -257,7 +289,11 @@ function filterByPredicate(rel, predClause) { }, new Relation(rel.vars)); } -function query({find = [], use = [], where = []}, ...vals) { +// find = :find +// by = :with +// use = :in +// where = :where +function query({find = [], by = [], use = [], where = []}, ...vals) { let parsed = parseVals(use, vals); if (parsed.multipleSources) { let rels = [initRel(parsed.valueNames, parsed.values)]; @@ -267,7 +303,11 @@ function query({find = [], use = [], where = []}, ...vals) { .map(c => scan(parsed.namedSources[sourceName], c.slice(1))) .forEach(rel => rels.push(rel)); } - return select(rels.reduce(join), varNames(find)); + return select( + rels.reduce(join), + find, + by + ); } else { let rel = [ initRel(parsed.valueNames, parsed.values), @@ -279,7 +319,11 @@ function query({find = [], use = [], where = []}, ...vals) { if (preds.length) { rel = preds.reduce(filterByPredicate, rel); } - return select(rel, varNames(find)); + return select( + rel, + find, + by + ); } } diff --git a/tests.mjs b/tests.mjs index 5a61927..3dd941d 100644 --- a/tests.mjs +++ b/tests.mjs @@ -1,8 +1,15 @@ import { _, query, StupidTupleSet, ANY } from './datalog.mjs'; const { module, test } = QUnit; -// QUnit.assert.queryEquals = function(expect, q, ...args) { -// }; +QUnit.assert.setEqual = function(actual, expected, message = "okay") { + expected = new StupidTupleSet(expected); + this.pushResult({ + result: actual.equals(expected), + actual: [...actual], + expected: [...expected], + message: message + }); +} module('datalog', () => { @@ -22,28 +29,17 @@ module('datalog', () => { ]); test('basic query', assert => { - assert.deepEqual( - query({ - find: [_.e], - where: [ - [_.e, "age", 42] - ] - }, db1).toArray(), + assert.setEqual( + query({find: [_.e], where: [[_.e, "age", 42]]}, db1), [["fred"], ["ethel"]], "implicit database" ); - assert.deepEqual( - query({ - find: [_.e], - use: [[_.db]], - where: [ - [_.db, _.e, "age", 42] - ] - }, db1).toArray(), + assert.setEqual( + query({find: [_.e], use: [[_.db]], where: [[_.db, _.e, "age", 42]]}, db1), [["fred"], ["ethel"]], "explicit database" ); - assert.deepEqual( + assert.setEqual( query({ find: [_.sex], use: [[_.db1, _.db2], _.age], @@ -51,32 +47,29 @@ module('datalog', () => { [_.db1, _.e, "age", _.age], [_.db2, _.e, "sex", _.sex] ] - }, db1, db2, 21).toArray(), + }, db1, db2, 21), [["female"]], "multiple databases with variable" ); - assert.deepEqual( - query({ - find: [_.e], - where: [[_.e]] - }, db2).toArray(), + assert.setEqual( + query({find: [_.e], where: [[_.e]]}, db2), [["ethel"], ["fred"], ["sally"]], "partial tuple match" ); }); test('predicates', assert => { - assert.deepEqual( + assert.setEqual( query({ find: [_.e], where: [ [_.e, "age", _.age], [['<', _.age, 30]] ] - }, db1).toArray(), + }, db1), [["sally"]], "no variable" ); - assert.deepEqual( + assert.setEqual( query({ find: [_.e], use: [_.maxAge], @@ -84,11 +77,11 @@ module('datalog', () => { [_.e, "age", _.age], [['<', _.age, _.maxAge]] ] - }, db1, ANY.VALUE).toArray(), + }, db1, ANY.VALUE), [["sally"], ["fred"], ["ethel"]], "ANY.VALUE variable" ); - assert.deepEqual( + assert.setEqual( query({ find: [_.e], use: [_.minAge, _.maxAge], @@ -97,9 +90,25 @@ module('datalog', () => { [['>', _.age, _.minAge]], [['<=', _.age, _.maxAge]] ] - }, db1, 30, 50).toArray(), + }, db1, 30, 50), [["fred"], ["ethel"]], "multiple variable" ); }); + test('aggregation', assert => { + let monsters = new StupidTupleSet([ + ["Cerberus", 3], + ["Medusa", 1], + ["Cyclops", 1], + ["Chimera", 1] + ]); + assert.setEqual( + query({ + find: [['min', _.heads]], + where: [[_._, _.heads]] + }, monsters), + [[1]], + "min" + ); + }); });