git » hoplite.git » commit b97f00c

min

author Alan Dipert
2021-06-15 05:40:22 UTC
committer Alan Dipert
2021-06-15 05:40:22 UTC
parent 6f3eb2a028e7896b29f0c8ce61fe8b1f19c06209

min

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"
+    );
+  });
 });