UNPKG

lmdb-oql

Version:

A high level object query language for indexed LMDB databases

377 lines (363 loc) 14.6 kB
import {ANY,selector} from "lmdb-index"; import cartesianProduct from "@anywhichway/cartesian-product"; import {operators} from "./src/operators.js"; const optimize = (conditions,classNames) => { // optimize each to level portion // to do sort top level portions return Object.entries(conditions).reduce((optimized,[alias,condition]) => { const optimalOrder = Object.entries(condition).sort(([key1,value1],[key2,value2]) => { if(value1===null) { return value1===value2 ? 0 : -1 } if(value2===null) { return value1===value2 ? 0 : 1 } const type1 = typeof(value1), type2 = typeof(value2); if(type1==="object") { return type2=="object" ? 0 : 1 } if(type1==="function") { return type2==="function" ? 0 : type2==="object" ? -1 : 1; } if(type1===type2) { return value1 > value2 ? 1 : value1 === value2 ? 0 : -1 } if(type1==="symbol") return -1; if(type2==="symbol") return 1; return type1 < type2 ? -1 : 1; }) optimized[alias] = optimalOrder.reduce((conditions,[key,value]) => { conditions[key] = value; return conditions; },{}) return optimized; },{}) } function compileClasses (db,...classes) { return classes.reduce((result,item) => { const [cls,name] = Array.isArray(item) ? item : [item], cname = cls.name, index = "@@" + cname; db.defineSchema(cls); result[name||cname] = { cname, *entries(property,value) { const type = typeof(value); if(type==="string" || type==="number" || type==="boolean") { for(const {key} of db.getRange([property,value,index])) { if(key.length===4) { const id = key[key.length-1], value = db.get(id); if(value!==undefined) { yield {key:id,value} } } } } else { for(const {key} of db.getRangeFromIndex({[property]:value},null,null,{cname})) { const value = db.get(key); if(value!==undefined) { yield {key,value} } } } } } return result; },{}); } function* where(db,conditions={},classes,select,coerce) { const results = {}; // {$t1: {name: {$t2: {name: (value) => value!=null}} or null is a function for testing const aliases = new Set(); for(const [leftalias,leftpattern] of Object.entries(conditions)) { const cname = classes[leftalias]?.cname, idprefix = cname ? cname + "@" : /.*\@/g, schema = db.getSchema(cname), idkey = schema?.idKey || "#"; aliases.add(leftalias); results[leftalias] ||= {}; let maxCount = 0; for(const [leftproperty,test] of Object.entries(leftpattern)) { const type = typeof(test); let generator if(test && type==="object") { // get all instances of <cname>@ generator = db.getRange({start:[idprefix]}); } else { // get ids of all instances. relies on index structure created by lmdb-index, could use getRangeWhere instead, but less efficient generator = db.getRangeFromIndex({[leftproperty]:test}) } for(let {value} of generator) { const id = value[schema.idKey]; if(!id.startsWith(idprefix) && !(typeof(idPrefix)==="object" && idprefix instanceof RegExp && !id.match(idprefix))) { break; } let leftvalue = value[leftproperty]; results[leftalias][id] ||= {count:0,value}; maxCount = results[leftalias][id].count += 1; if(test && type==="object") { for (const [rightalias, rightpattern] of Object.entries(test)) { results[rightalias] ||= {}; aliases.add(rightalias); let every = true; for (const [rightproperty, rightvalue] of Object.entries(rightpattern)) { const type = typeof (rightvalue); // fail if property value test fails if ((type === "function" && rightvalue.length === 1 && rightvalue(leftvalue) === undefined) || (type !== "function" && rightvalue !== leftvalue)) { every = false; break; } // gets objects with same property where property values match const test = type === "function" ? ANY : leftvalue; // get all values for property for (const { key, value } of classes[rightalias].entries(rightproperty, test)) { // fail if comparison function fails if (type === "function" && rightvalue.length > 1 && (rightvalue.callRight ? rightvalue.callRight(leftvalue, value[rightproperty]) : rightvalue(leftvalue, value[rightproperty])) === undefined) { delete results[rightalias][key]; break; } results[rightalias][key] ||= {value}; } } if (!every) { delete results[rightalias]; break; } } } } if(maxCount===0) { delete results[leftalias]; break; } } if(maxCount===0) { delete results[leftalias]; break; } for(const [id, {count}] of Object.entries(results[leftalias])) { if(count<maxCount) { delete results[leftalias][id]; } } } if(![...Object.values(aliases)].every((alias) => alias in results)) { return; } const names = Object.keys(results); for(const classValues of Object.values(results)) { for (const product of cartesianProduct(Object.keys(classValues))) { // get all combinations of instance ids for each class const join = {}; product.forEach((id, i) => { const name = names[i]; join[name] = results[name][id].value; }) const selected = select ? (select === IDS ? select.bind(db)(join) : selector(join,select)) : join; if (selected != undefined) { yield selected; } } } } function del() { const db = this; return { from(...classes) { classes = compileClasses(db,...classes); async function *_where(conditions={}) { for(const join of where(db,conditions,classes,IDS)) { for(const id of join) { await db.remove(id); yield id; } } } return { where(conditions={}) { let generator = _where(optimize(conditions,classes)); const exec = async () => { const items = []; for await (const item of generator) { items.push(item); } generator = _where(optimize(conditions,classes)); generator.exec = exec; return items; } generator.exec = exec; return generator; } } } } } function insert() { const db = this; return { into(...classes) { classes = compileClasses(db,...classes); async function* _values(values) { for(let [key,instances] of Object.entries(values)) { const {cname} = classes[key], schema = db.getSchema(cname); if(instances instanceof Array) { if(!(instances[0] instanceof Array) && schema.create([]) instanceof Array) { throw new TypeError("Expected array of arrays when inserting Array"); } } else { instances = [instances]; } for(let instance of instances) { if(!(instance instanceof schema.ctor)) { instance = schema.create(instance); } yield await db.put(null,instance); } } } return { values(values) { let generator = _values(values); const exec = async () => { const ids = []; for await(const id of generator) { ids.push(id); } generator = _values(values); generator.exec = exec; return ids; }; generator.exec = exec; return generator; } } } } } function select(select) { const db = this; return { from(...classes) { classes = compileClasses(db,...classes); function *_where(conditions={}) { for(const item of where(db,conditions,classes,select)) { yield item; } } return { where(conditions={}) { let generator = _where(optimize(conditions,classes)); const exec = () => { const items = []; for (const item of generator) { items.push(item); } generator = _where(optimize(conditions,classes)); generator.exec = exec; return items; } generator.exec = exec; return generator; } } } } } function update(...classes) { const db = this; classes = compileClasses(db,...classes); return { set(patches) { async function *_where(conditions={}) { for(const join of where(db,conditions,classes,IDS)) { for(const id of join) { for(const {cname} of Object.values(classes)) { const patch = patches[cname]; if(patch && id.startsWith(cname+"@")) { await db.patch(id,patch); } } yield id; } } } return { where(conditions={}) { let generator = _where(optimize(conditions,classes)); const exec = async () => { const items = []; for await(const item of generator) { items.push(item); } generator = _where(optimize(conditions,classes)); generator.exec = exec; return items; } generator.exec = exec; return generator; } } } } } import {withExtensions as lmdbExtend} from "lmdb-index"; const withExtensions = (db,extensions={}) => { return lmdbExtend(db,{delete:del,insert,select,update,...extensions}) } const functionalOperators = Object.entries(operators).reduce((operators,[key,f]) => { operators[key] = function(test) { let join; const op = (left,right) => { return join ? f(left,right) : f(left,{test}); } op.callRight = (left,right) => { join = true; let result; try { result = (test===undefined ? op(left, {test:right}) : op(right,{test})); join = false; } finally { join = false; } return result; } return op; } operators.$and = (...tests) => { const op = (left,right) => { return tests.every((test) => test(left,right)); } op.callRight = (left,right) => { return tests.every((test) => test.callRight(left,right)); } return op; } operators.$or = (...tests) => { const op = (left,right) => { return tests.some((test) => test(left,right)); } op.callRight = (left,right) => { return tests.every((test) => test.callRight(left,right)); } return op; } operators.$not = (test) => { const op = (left,right) => { return !test(left,right); } op.callRight = (left,right) => { return !test.callRight(left,right); } return op; } return operators; },{}); function IDS(value) { return Object.values(value).map((value) => { const schema = this.getSchema(value); return schema ? value[schema.idKey||"#"] : value["#"] }); } export {functionalOperators as operators,withExtensions,IDS}