├── .coveralls.yml ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── index.js ├── lib ├── HyperdbReadTransform.js ├── JoinStream.js ├── Variable.js ├── constants.js ├── planner.js ├── prefixes.js └── utils.js ├── licenses ├── HYPER_GRAPH_DB_LICENSE └── LEVELGRAPH_LICENSE ├── package-lock.json ├── package.json ├── readme.md └── test ├── basic.spec.js ├── data ├── simplefoaf.ttl └── sparqlIn11Minutes.ttl ├── fixture ├── foaf.js └── homes_in_paris.js ├── join-stream.spec.js ├── planner.spec.js ├── prefixes.spec.js ├── queries ├── sparqlIn11Minutes1.rq ├── sparqlIn11Minutes11.rq ├── sparqlIn11Minutes2.rq ├── sparqlIn11Minutes3.rq ├── sparqlIn11Minutes4.rq ├── sparqlIn11Minutes5.rq ├── sparqlIn11Minutes6.rq ├── sparqlIn11Minutes7.rq ├── sparqlIn11Minutes8.rq ├── sparqlIn11Minutes9.rq └── union.rq ├── sparql.spec.js ├── triple-store.spec.js ├── utils.spec.js └── variable.spec.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: UOjQpeYk2baG2Ehj54CukhLdKBIN96Bya 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | *.log 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - 7 5 | - 8 6 | - 9 7 | - 10 8 | script: 9 | - npm run lint 10 | - npm run test 11 | jobs: 12 | include: 13 | - stage: coverage 14 | node_js: 8 15 | script: npm run travis 16 | after_script: 17 | - npm run report-coverage 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const hyperdb = require('hyperdb') 2 | const stream = require('readable-stream') 3 | const thunky = require('thunky') 4 | const pump = require('pump') 5 | const inherits = require('inherits') 6 | const events = require('events') 7 | const SparqlIterator = require('sparql-iterator') 8 | 9 | const constants = require('./lib/constants') 10 | const utils = require('./lib/utils') 11 | const prefixes = require('./lib/prefixes') 12 | const Variable = require('./lib/Variable') 13 | const HyperdbReadTransform = require('./lib/HyperdbReadTransform') 14 | const JoinStream = require('./lib/JoinStream') 15 | const planner = require('./lib/planner') 16 | const pkg = require('./package.json') 17 | 18 | const Transform = stream.Transform 19 | const PassThrough = stream.PassThrough 20 | 21 | function Graph (storage, key, opts) { 22 | if (!(this instanceof Graph)) return new Graph(storage, key, opts) 23 | events.EventEmitter.call(this) 24 | if (typeof key === 'string') key = Buffer.from(key, 'hex') 25 | 26 | if (!Buffer.isBuffer(key) && !opts) { 27 | opts = key 28 | key = null 29 | } 30 | 31 | opts = opts || {} 32 | this.db = (storage instanceof hyperdb) ? storage : hyperdb(storage, key, opts) 33 | this._prefixes = Object.assign({}, opts.prefixes || constants.DEFAULT_PREFIXES) 34 | this._basename = opts.name || constants.DEFAULT_BASE 35 | this._prefixes._ = this._basename 36 | this._indexType = opts.index === 'tri' ? 'tri' : 'hex' 37 | this._indexes = opts.index === 'tri' 38 | ? constants.HEXSTORE_INDEXES_REDUCED 39 | : constants.HEXSTORE_INDEXES 40 | this._indexKeys = Object.keys(this._indexes) 41 | this.isReady = false 42 | this.ready = thunky(this._ready.bind(this)) 43 | this.db.on('error', (e) => { 44 | this.emit('error', e) 45 | }) 46 | this.db.ready(() => { 47 | this.ready(() => { 48 | this.emit('ready') 49 | }) 50 | }) 51 | } 52 | 53 | inherits(Graph, events.EventEmitter) 54 | 55 | Graph.prototype._ready = function (cb) { 56 | this.isReady = true 57 | if (utils.isNewDatabase(this.db)) { 58 | this._onNew(cb) 59 | } else { 60 | this._onInit(cb) 61 | } 62 | } 63 | 64 | Graph.prototype._onNew = function (cb) { 65 | this._version = pkg.version 66 | const metadata = [ 67 | ['@version', pkg.version], 68 | ['@index', this._indexType], 69 | ['@name', this._basename] 70 | ] 71 | Object.keys(this._prefixes).forEach((key) => { 72 | if (key !== '_') { 73 | metadata.push([prefixes.toKey(key), this._prefixes[key]]) 74 | } 75 | }) 76 | utils.put(this.db, metadata, cb) 77 | } 78 | 79 | Graph.prototype._onInit = function (cb) { 80 | // get and set graph version 81 | this._version = null 82 | this._basename = null 83 | this._indexType = null 84 | 85 | let missing = 4 86 | let error = null 87 | // get and set version 88 | this.graphVersion((err, version) => { 89 | if (err) error = err 90 | this._version = version 91 | maybeDone() 92 | }) 93 | // get and set graph name 94 | this.name((err, name) => { 95 | if (err) error = err 96 | this._basename = name || constants.DEFAULT_BASE 97 | // modify prefixes to ensure correct namespacing 98 | this._prefixes._ = this._basename 99 | maybeDone() 100 | }) 101 | // get and set graph indexation 102 | this.indexType((err, index) => { 103 | if (err) error = err 104 | this._indexType = index || 'hex' 105 | this._indexes = index === 'tri' 106 | ? constants.HEXSTORE_INDEXES_REDUCED 107 | : constants.HEXSTORE_INDEXES 108 | this._indexKeys = Object.keys(this._indexes) 109 | maybeDone() 110 | }) 111 | // get and set prefixes 112 | this.prefixes((err, prefixes) => { 113 | if (err) error = err 114 | this._prefixes = Object.assign({ _: this._basename }, prefixes) 115 | maybeDone() 116 | }) 117 | function maybeDone () { 118 | missing-- 119 | if (!missing) { 120 | cb(error) 121 | } 122 | } 123 | } 124 | 125 | Graph.prototype.v = (name) => new Variable(name) 126 | 127 | function returnValueAsString (cb) { 128 | return (err, nodes) => { 129 | if (err) return cb(err) 130 | if (!nodes || nodes.length === 0) return cb(null, null) 131 | cb(null, nodes[0].value.toString()) 132 | } 133 | } 134 | 135 | Graph.prototype.graphVersion = function (cb) { 136 | if (this._version) return cb(null, this._version) 137 | this.db.get('@version', returnValueAsString(cb)) 138 | } 139 | 140 | Graph.prototype.name = function (cb) { 141 | if (this._basename) return cb(null, this._basename) 142 | this.db.get('@name', returnValueAsString(cb)) 143 | } 144 | 145 | Graph.prototype.indexType = function (cb) { 146 | if (this._indexType) return cb(null, this._indexType) 147 | this.db.get('@index', returnValueAsString(cb)) 148 | } 149 | 150 | Graph.prototype.prefixes = function (callback) { 151 | // should cache this somehow 152 | const prefixStream = this.db.createReadStream(constants.PREFIX_KEY) 153 | utils.collect(prefixStream, (err, data) => { 154 | if (err) return callback(err) 155 | var names = data.reduce((p, nodes) => { 156 | var data = prefixes.fromNodes(nodes) 157 | p[data.prefix] = data.uri 158 | return p 159 | }, {}) 160 | callback(null, names) 161 | }) 162 | } 163 | 164 | Graph.prototype.addPrefix = function (prefix, uri, cb) { 165 | this.db.put(prefixes.toKey(prefix), uri, cb) 166 | } 167 | 168 | Graph.prototype.getStream = function (triple, opts) { 169 | const stream = this.db.createReadStream(this._createQuery(triple, { encode: (!opts || opts.encode === undefined) ? true : opts.encode })) 170 | return stream.pipe(new HyperdbReadTransform(this.db, this._basename, opts)) 171 | } 172 | 173 | Graph.prototype.get = function (triple, opts, callback) { 174 | if (typeof opts === 'function') return this.get(triple, undefined, opts) 175 | this.ready(() => { 176 | utils.collect(this.getStream(triple, opts), callback) 177 | }) 178 | } 179 | function doAction (action) { 180 | return function (triples, callback) { 181 | if (!triples) return callback(new Error('Must pass triple')) 182 | this.ready(() => { 183 | let entries = (!triples.reduce) ? [triples] : triples 184 | entries = entries.reduce((prev, triple) => { 185 | return prev.concat(this._generateBatch(triple, action)) 186 | }, []) 187 | this.db.batch(entries.reverse(), callback) 188 | }) 189 | } 190 | } 191 | 192 | function doActionStream (action) { 193 | return function () { 194 | const self = this 195 | const transform = new Transform({ 196 | objectMode: true, 197 | transform (triples, encoding, done) { 198 | if (!triples) return done() 199 | let entries = (!triples.reduce) ? [triples] : triples 200 | entries = entries.reduce((prev, triple) => { 201 | return prev.concat(self._generateBatch(triple, action)) 202 | }, []) 203 | this.push(entries.reverse()) 204 | done() 205 | } 206 | }) 207 | const writeStream = this.db.createWriteStream() 208 | transform.pipe(writeStream) 209 | return transform 210 | } 211 | } 212 | 213 | Graph.prototype.put = doAction('put') 214 | Graph.prototype.putStream = doActionStream('put') 215 | 216 | // this is not implemented in hyperdb yet 217 | // for now we just put a null value in the db 218 | Graph.prototype.del = doAction('del') 219 | Graph.prototype.delStream = doActionStream('del') 220 | 221 | Graph.prototype.searchStream = function (query, options) { 222 | const result = new PassThrough({ objectMode: true }) 223 | const defaults = { solution: {} } 224 | if (!query || query.length === 0) { 225 | result.end() 226 | return result 227 | } else if (!Array.isArray(query)) { 228 | query = [ query ] 229 | } 230 | const plannedQuery = planner(query, this._prefixes) 231 | var streams = plannedQuery.map((triple, i) => { 232 | const limit = (options && i === plannedQuery.length - 1) ? options.limit : undefined 233 | return new JoinStream({ 234 | triple: utils.filterTriple(triple), 235 | filter: triple.filter, 236 | db: this, 237 | limit 238 | }) 239 | }) 240 | 241 | streams[0].start = true 242 | streams[0].end(defaults.solution) 243 | 244 | streams.push(result) 245 | pump(streams) 246 | return result 247 | } 248 | 249 | Graph.prototype.search = function (query, options, callback) { 250 | if (typeof options === 'function') { 251 | callback = options 252 | options = undefined 253 | } 254 | this.ready(() => { 255 | utils.collect(this.searchStream(query, options), callback) 256 | }) 257 | } 258 | 259 | Graph.prototype.queryStream = function (query) { 260 | return new SparqlIterator(query, { hypergraph: this }) 261 | } 262 | 263 | Graph.prototype.query = function (query, callback) { 264 | this.ready(() => { 265 | utils.collect(this.queryStream(query), callback) 266 | }) 267 | } 268 | 269 | Graph.prototype.close = function (callback) { 270 | callback() 271 | } 272 | 273 | /* PRIVATE FUNCTIONS */ 274 | 275 | Graph.prototype._generateBatch = function (triple, action) { 276 | if (!action) action = 'put' 277 | var data = null 278 | if (action === 'put') { 279 | data = JSON.stringify(utils.extraDataMask(triple)) 280 | } 281 | return this._encodeKeys(triple).map(key => ({ 282 | type: action, 283 | key: key, 284 | value: data 285 | })) 286 | } 287 | 288 | Graph.prototype._encodeKeys = function (triple) { 289 | const encodedTriple = utils.encodeTriple(triple, this._prefixes) 290 | return this._indexKeys.map(key => utils.encodeKey(key, encodedTriple)) 291 | } 292 | 293 | Graph.prototype._createQuery = function (pattern, options) { 294 | var types = utils.typesFromPattern(pattern) 295 | var preferedIndex = options && options.index 296 | var index = this._findIndex(types, preferedIndex) 297 | const encodedTriple = utils.encodeTriple(pattern, options.encode ? this._prefixes : { _: this._basename }) 298 | var key = utils.encodeKey(index, encodedTriple) 299 | return key 300 | } 301 | 302 | Graph.prototype._possibleIndexes = function (types) { 303 | var result = this._indexKeys.filter((key) => { 304 | var matches = 0 305 | return this._indexes[key].every(function (e, i) { 306 | if (types.indexOf(e) >= 0) { 307 | matches++ 308 | return true 309 | } 310 | if (matches === types.length) { 311 | return true 312 | } 313 | }) 314 | }) 315 | result.sort() 316 | return result 317 | } 318 | 319 | Graph.prototype._findIndex = function (types, preferedIndex) { 320 | var result = this._possibleIndexes(types) 321 | if (preferedIndex && result.some(r => r === preferedIndex)) { 322 | return preferedIndex 323 | } 324 | return result[0] 325 | } 326 | 327 | module.exports = Graph 328 | -------------------------------------------------------------------------------- /lib/HyperdbReadTransform.js: -------------------------------------------------------------------------------- 1 | const Transform = require('readable-stream').Transform 2 | const inherits = require('inherits') 3 | const utils = require('./utils') 4 | 5 | function HyperdbReadTransform (db, basename, options) { 6 | if (!(this instanceof HyperdbReadTransform)) { 7 | return new HyperdbReadTransform(db, basename, options) 8 | } 9 | var opts = options || {} 10 | this.db = db 11 | this._prefixes = { _: basename } 12 | this._count = 0 13 | this._filter = opts.filter 14 | this._offset = opts.offset || 0 15 | this._limit = opts.limit && opts.limit + this._offset 16 | Transform.call(this, Object.assign(opts, { objectMode: true })) 17 | this._sources = [] 18 | this.once('pipe', (source) => { 19 | source.on('error', e => this.emit('error', e)) 20 | this._sources.push(source) 21 | }) 22 | } 23 | 24 | inherits(HyperdbReadTransform, Transform) 25 | 26 | HyperdbReadTransform.prototype._transform = function transform (nodes, encoding, done) { 27 | var value = nodes[0].value && JSON.parse(nodes[0].value.toString()) 28 | if (value === null) return done() 29 | value = Object.assign(value, utils.decodeKey(nodes[0].key, this._prefixes)) 30 | if (!this._filter || this._filter(value)) { 31 | if (this._count >= this._offset) { 32 | this.push(value) 33 | } 34 | this._count++ 35 | if (this._limit && this._count >= this._limit) { 36 | this.end() 37 | } 38 | } 39 | done() 40 | } 41 | 42 | HyperdbReadTransform.prototype._flush = function (done) { 43 | this._sources.forEach(source => source.destroy()) 44 | done() 45 | } 46 | 47 | module.exports = HyperdbReadTransform 48 | -------------------------------------------------------------------------------- /lib/JoinStream.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2013-2017 Matteo Collina and LevelGraph Contributors 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 20 | OTHER DEALINGS IN THE SOFTWARE. 21 | */ 22 | 23 | const Transform = require('readable-stream').Transform 24 | const inherits = require('inherits') 25 | const utils = require('./utils') 26 | const queryMask = utils.queryMask 27 | const maskUpdater = utils.maskUpdater 28 | const matcher = utils.matcher 29 | 30 | function JoinStream (options) { 31 | if (!(this instanceof JoinStream)) { 32 | return new JoinStream(options) 33 | } 34 | options.objectMode = true 35 | Transform.call(this, options) 36 | 37 | this.triple = options.triple 38 | this.matcher = matcher(options.triple) 39 | this.mask = queryMask(options.triple) 40 | this.maskUpdater = maskUpdater(options.triple) 41 | this.limit = options.limit 42 | this._limitCounter = 0 43 | this.db = options.db 44 | this._ended = false 45 | this.filter = options.filter 46 | this.offset = options.offset 47 | 48 | this.once('pipe', (source) => { 49 | source.on('error', (err) => { 50 | this.emit('error', err) 51 | }) 52 | }) 53 | 54 | this._onErrorStream = (err) => { 55 | this.emit('error', err) 56 | } 57 | 58 | this._onDataStream = (triple) => { 59 | var newsolution = this.matcher(this._lastSolution, triple) 60 | 61 | if (this._ended || !newsolution) { 62 | return 63 | } 64 | 65 | this.push(newsolution) 66 | this._limitCounter += 1 67 | if (this.limit && this._limitCounter === this.limit) { 68 | this._readStream.destroy() 69 | this._ended = true 70 | this.push(null) 71 | } 72 | } 73 | 74 | this._options = { 75 | filter: this.filter, 76 | offset: this.offset, 77 | encode: options.encode ? !!options.encode : false 78 | } 79 | } 80 | 81 | inherits(JoinStream, Transform) 82 | 83 | JoinStream.prototype._transform = function transform (solution, encoding, done) { 84 | if (this._ended) { 85 | return done() 86 | } 87 | var newMask = this.maskUpdater(solution, this.mask) 88 | 89 | this._lastSolution = solution 90 | this._readStream = this.db.getStream(newMask, this._options) 91 | 92 | this._readStream.on('data', this._onDataStream) 93 | this._readStream.on('error', this._onErrorStream) 94 | this._readStream.on('end', done) 95 | } 96 | 97 | module.exports = JoinStream 98 | -------------------------------------------------------------------------------- /lib/Variable.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2013-2017 Matteo Collina and LevelGraph Contributors 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 20 | OTHER DEALINGS IN THE SOFTWARE. 21 | */ 22 | 23 | function Variable (name) { 24 | if (!(this instanceof Variable)) return new Variable(name) 25 | this.name = name 26 | } 27 | 28 | Variable.prototype.bind = function (solution, value) { 29 | if (!this.isBindable(solution, value)) return null 30 | var newsolution = Object.assign({}, solution) 31 | newsolution[this.name] = value 32 | return newsolution 33 | } 34 | 35 | Variable.prototype.isBound = function (solution) { 36 | return solution[this.name] !== undefined 37 | } 38 | 39 | Variable.prototype.isBindable = function (solution, value) { 40 | return !solution[this.name] || solution[this.name] === value 41 | } 42 | 43 | module.exports = Variable 44 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | const HEXSTORE_INDEXES = { 2 | spo: ['subject', 'predicate', 'object'], 3 | pos: ['predicate', 'object', 'subject'], 4 | osp: ['object', 'subject', 'predicate'], 5 | sop: ['subject', 'object', 'predicate'], // [optional] 6 | pso: ['predicate', 'subject', 'object'], // [optional] 7 | ops: ['object', 'predicate', 'subject'] // [optional] 8 | } 9 | const HEXSTORE_INDEXES_REDUCED = { 10 | spo: HEXSTORE_INDEXES.spo, 11 | pos: HEXSTORE_INDEXES.pos, 12 | osp: HEXSTORE_INDEXES.osp 13 | } 14 | const PREFIX_KEY = '@prefix/' 15 | const DEFAULT_BASE = 'hg://' 16 | const DEFAULT_PREFIXES = { 17 | foaf: 'http://xmlns.com/foaf/0.1/' 18 | } 19 | 20 | module.exports = { 21 | PREFIX_KEY, 22 | DEFAULT_BASE, 23 | DEFAULT_PREFIXES, 24 | HEXSTORE_INDEXES, 25 | HEXSTORE_INDEXES_REDUCED 26 | } 27 | -------------------------------------------------------------------------------- /lib/planner.js: -------------------------------------------------------------------------------- 1 | 2 | const utils = require('./utils') 3 | 4 | function planner (query, prefixMap) { 5 | if (query.length === 1) return [utils.encodeTriple(query[0], prefixMap)] 6 | // first group queries based on number of variables. 7 | const solved = new Set() 8 | var grouped = query.reduce((ordered, q) => { 9 | const variables = utils.variableNames(q) 10 | if (ordered[variables.length]) { 11 | ordered[variables.length].push({ 12 | query: q, 13 | names: variables 14 | }) 15 | } else { 16 | ordered[variables.length] = [{ 17 | query: q, 18 | names: variables 19 | }] 20 | } 21 | if (variables.length === 1) { 22 | solved.add(variables[0]) 23 | } 24 | return ordered 25 | }, []) 26 | // then order vars > 1 by if they occur in 27 | const orderedQueries = grouped[1] ? grouped[1].map(v => utils.encodeTriple(v.query, prefixMap)) : [] 28 | 29 | for (let i = 2; i < grouped.length; i++) { 30 | if (grouped[i] === undefined) continue 31 | while (grouped[i].length > 0) { 32 | // get the next easiest to solve 33 | // or the one that makes the rest easiest to solve 34 | grouped[i].sort((a, b) => { 35 | // number of unsolved variables 36 | let unsolvedA = a.names.filter(name => !solved.has(name)) 37 | let unsolvedB = b.names.filter(name => !solved.has(name)) 38 | if (unsolvedA.length < unsolvedB.length) return -1 39 | if (unsolvedA.length > unsolvedB.length) return 1 40 | // calculate how many unsolved vars it has in common with others in the group 41 | // should this be a vector? many vars is better than solving 1 lots. 42 | let sharedUnsolvedA = 0 43 | let sharedUnsolvedB = 0 44 | grouped[i].forEach(v => { 45 | v.names.forEach((name) => { 46 | if (solved.has(name)) return 47 | if (v !== a && a.names.includes(name)) sharedUnsolvedA++ 48 | if (v !== b && b.names.includes(name)) sharedUnsolvedB++ 49 | }) 50 | }) 51 | if (sharedUnsolvedA > sharedUnsolvedB) return -1 52 | if (sharedUnsolvedA < sharedUnsolvedB) return 1 53 | return 0 54 | }) 55 | const next = grouped[i].shift() 56 | orderedQueries.push(utils.encodeTriple(next.query, prefixMap)) 57 | next.names.forEach(n => solved.add(n)) 58 | } 59 | } 60 | return orderedQueries 61 | }; 62 | 63 | module.exports = planner 64 | -------------------------------------------------------------------------------- /lib/prefixes.js: -------------------------------------------------------------------------------- 1 | var constants = require('./constants') 2 | 3 | const PREFIX_KEY = constants.PREFIX_KEY 4 | const PREFIX_REGEX = /^(\w+):/ 5 | 6 | function toKey (prefix) { 7 | return PREFIX_KEY + prefix 8 | } 9 | function fromKey (key) { 10 | return key.replace(PREFIX_KEY, '') 11 | } 12 | 13 | function fromNodes (nodes) { 14 | return { 15 | prefix: fromKey(nodes[0].key), 16 | uri: nodes[0].value.toString() 17 | } 18 | } 19 | 20 | function toPrefixed (uri, prefixes) { 21 | if (!prefixes) return uri 22 | const prefix = Object.keys(prefixes).find(v => uri.startsWith(prefixes[v])) 23 | if (!prefix) return uri 24 | return uri.replace(prefixes[prefix], prefix + ':') 25 | } 26 | 27 | function fromPrefixed (uri, prefixes) { 28 | if (!prefixes) return uri 29 | const match = uri.match(PREFIX_REGEX) 30 | if (match && prefixes[match[1]]) { 31 | return uri.replace(PREFIX_REGEX, prefixes[match[1]]) 32 | } 33 | return uri 34 | } 35 | 36 | module.exports = { 37 | toKey, 38 | fromKey, 39 | fromNodes, 40 | toPrefixed, 41 | fromPrefixed 42 | } 43 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const Variable = require('./Variable') 2 | const prefixes = require('./prefixes') 3 | const constants = require('./constants') 4 | 5 | const spo = constants.HEXSTORE_INDEXES.spo 6 | const tripleAliasMap = { 7 | s: 'subject', 8 | p: 'predicate', 9 | o: 'object' 10 | } 11 | 12 | function isNewDatabase (db) { 13 | return !(db.feeds.length > 1 || db.feeds[0].length > 1) 14 | } 15 | 16 | function put (db, data, callback) { 17 | var i = 0 18 | 19 | next() 20 | 21 | function next (err) { 22 | if (err) return callback(err) 23 | var v = data[i++] 24 | db.put(v[0], v[1], (i < data.length) ? next : callback) 25 | } 26 | } 27 | 28 | function collect (stream, cb) { 29 | var res = [] 30 | stream.on('data', res.push.bind(res)) 31 | stream.once('error', cb) 32 | stream.once('end', cb.bind(null, null, res)) 33 | } 34 | 35 | function filterTriple (triple) { 36 | const filtered = {} 37 | spo.forEach((key) => { 38 | if (triple.hasOwnProperty(key)) { 39 | filtered[key] = triple[key] 40 | } 41 | }) 42 | return filtered 43 | } 44 | 45 | function escapeKeyValue (value, prefixMap) { 46 | if (typeof value === 'string' || value instanceof String) { 47 | return prefixes.toPrefixed(value, prefixMap).replace(/(\/)/g, '%2F') 48 | } 49 | return value 50 | } 51 | 52 | function unescapeKeyValue (value, prefixMap) { 53 | return prefixes.fromPrefixed(value.replace(/%2F/g, '/'), prefixMap) 54 | } 55 | 56 | function encodeTriple (triple, prefixMap) { 57 | spo.forEach((key) => { 58 | if (triple.hasOwnProperty(key)) { 59 | triple[key] = escapeKeyValue(triple[key], prefixMap) 60 | } 61 | }) 62 | return triple 63 | } 64 | 65 | function encodeKey (key, triple) { 66 | var result = key 67 | var def = constants.HEXSTORE_INDEXES[key] 68 | var i = 0 69 | var value = triple[def[i]] 70 | // need to handle this smarter 71 | while (value) { 72 | result += '/' + value 73 | i += 1 74 | value = triple[def[i]] 75 | } 76 | if (i < 3) { 77 | result += '/' 78 | } 79 | return result 80 | } 81 | 82 | function decodeKey (key, prefixMap) { 83 | const values = key.split('/') 84 | if (values.length < 4) throw new Error('Key is not in triple form') 85 | const order = values[0] 86 | const triple = {} 87 | for (var i = 0; i < 3; i++) { 88 | const k = tripleAliasMap[order[i]] 89 | triple[k] = unescapeKeyValue(values[i + 1], prefixMap) 90 | } 91 | return triple 92 | } 93 | 94 | function typesFromPattern (pattern) { 95 | return Object.keys(pattern).filter((key) => { 96 | switch (key) { 97 | case 'subject': 98 | return !!pattern.subject 99 | case 'predicate': 100 | return !!pattern.predicate 101 | case 'object': 102 | return !!pattern.object 103 | default: 104 | return false 105 | } 106 | }) 107 | } 108 | 109 | function hasKey (key) { 110 | return spo.indexOf(key) >= 0 111 | } 112 | 113 | function keyIsNotAObject (tripleKey) { 114 | return typeof tripleKey !== 'object' 115 | } 116 | 117 | function keyIsAVariable (tripleKey) { 118 | return tripleKey instanceof Variable 119 | } 120 | 121 | function extraDataMask (obj) { 122 | return Object.keys(obj) 123 | .reduce((prev, key) => { 124 | if (!keyIsAVariable(obj[key]) && !hasKey(key)) prev[key] = obj[key] 125 | return prev 126 | }, {}) 127 | } 128 | 129 | function objectMask (criteria, obj) { 130 | return Object.keys(obj) 131 | .filter(hasKey) 132 | .filter(key => criteria(obj[key])) 133 | .reduce((prev, key) => { 134 | prev[key] = obj[key] 135 | return prev 136 | }, {}) 137 | }; 138 | 139 | function variableNames (obj) { 140 | return Object.keys(obj) 141 | .filter(key => hasKey(key) && keyIsAVariable(obj[key])) 142 | .map(key => obj[key].name) 143 | }; 144 | 145 | function queryMask (object) { 146 | return objectMask(keyIsNotAObject, object) 147 | }; 148 | 149 | function variablesMask (object) { 150 | return objectMask(keyIsAVariable, object) 151 | }; 152 | 153 | function maskUpdater (pattern) { 154 | const variables = variablesMask(pattern) 155 | return (solution, mask) => { 156 | const maskCopy = Object.assign({}, mask) 157 | return Object.keys(variables) 158 | .reduce((newMask, key) => { 159 | const variable = variables[key] 160 | if (variable.isBound(solution)) { 161 | newMask[key] = solution[variable.name] 162 | } 163 | return newMask 164 | }, maskCopy) 165 | } 166 | } 167 | 168 | function matcher (pattern) { 169 | const variables = variablesMask(pattern) 170 | 171 | return (solution, triple) => { 172 | return Object.keys(variables).reduce((newSolution, key) => { 173 | if (newSolution) { 174 | return variables[key].bind(newSolution, triple[key]) 175 | } 176 | return newSolution 177 | }, solution) 178 | } 179 | } 180 | 181 | function materializer (pattern, data) { 182 | return Object.keys(pattern).reduce((result, key) => { 183 | if (pattern[key] instanceof Variable) { 184 | result[key] = data[pattern[key].name] 185 | } else { 186 | result[key] = pattern[key] 187 | } 188 | return result 189 | }, {}) 190 | } 191 | 192 | module.exports = { 193 | isNewDatabase, 194 | put, 195 | escapeKeyValue, 196 | unescapeKeyValue, 197 | encodeTriple, 198 | encodeKey, 199 | decodeKey, 200 | typesFromPattern, 201 | filterTriple, 202 | collect, 203 | extraDataMask, 204 | queryMask, 205 | variablesMask, 206 | variableNames, 207 | maskUpdater, 208 | matcher, 209 | materializer 210 | } 211 | -------------------------------------------------------------------------------- /licenses/HYPER_GRAPH_DB_LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Benjamin Forster and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /licenses/LEVELGRAPH_LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-2017 Matteo Collina and LevelGraph Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyper-graph-db", 3 | "version": "0.3.5", 4 | "description": "A distributed graph database built upon hyperdb.", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/e-e-e/hyper-graph-db.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/e-e-e/hyper-graph-db/issues" 12 | }, 13 | "dependencies": { 14 | "hyperdb": "^3.5.0", 15 | "inherits": "^2.0.3", 16 | "lru": "^3.1.0", 17 | "pump": "^3.0.0", 18 | "readable-stream": "^3.0.1", 19 | "sparql-iterator": "^2.0.5", 20 | "thunky": "^1.0.2" 21 | }, 22 | "devDependencies": { 23 | "chai": "^4.1.2", 24 | "coveralls": "^3.0.0", 25 | "istanbul": "^0.4.5", 26 | "mocha": "^5.0.0", 27 | "n3": "^0.8.5", 28 | "random-access-memory": "^3.0.0", 29 | "standard": "^12.0.0", 30 | "tmp": "0.0.33" 31 | }, 32 | "scripts": { 33 | "test": "mocha test/**.spec.js", 34 | "tdd": "mocha test/**.spec.js -w", 35 | "lint": "standard", 36 | "lint:fix": "standard --fix", 37 | "travis": "NODE_ENV=test istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec", 38 | "report-coverage": "cat ./coverage/lcov.info | coveralls" 39 | }, 40 | "author": "Benjamin Forster", 41 | "license": "MIT" 42 | } 43 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # hyper-graph-db 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/e-e-e/hyper-graph-db.svg)](https://greenkeeper.io/) 4 | 5 | [![Build Status](https://travis-ci.org/e-e-e/hyper-graph-db.svg?branch=master)](https://travis-ci.org/e-e-e/hyper-graph-db) [![Coverage Status](https://coveralls.io/repos/github/e-e-e/hyper-graph-db/badge.svg?branch=master)](https://coveralls.io/github/e-e-e/hyper-graph-db?branch=master) 6 | 7 | hyper-graph-db is a graph database on top of [hyperdb](https://github.com/mafintosh/hyperdb). It interface and test specs have been adapted from [LevelGraph](https://github.com/levelgraph/levelgraph). 8 | 9 | Like LevelGraph, **hyper-graph-db** follows the **Hexastore** approach as presented in the article: [Hexastore: sextuple indexing for semantic web data management C Weiss, P Karras, A Bernstein - Proceedings of the VLDB Endowment, 2008](http://www.vldb.org/pvldb/1/1453965.pdf). As such hyper-graph-db uses six indices for every triple in order to access them as fast as it is possible. 10 | 11 | ## install 12 | 13 | ``` 14 | npm install hyper-graph-db 15 | ``` 16 | 17 | This requires node v6.x.x or greater. 18 | 19 | ## basic usage 20 | 21 | ```js 22 | var hypergraph = require('hyper-graph-db') 23 | 24 | var db = hypergraph('./my.db', { valueEncoding: 'utf-8' }) 25 | 26 | var triple = { subject: 'a', predicate: 'b', object: 'c' } 27 | 28 | db.put(triple, function (err) { 29 | if (err) throw err 30 | db.get({ subject: 'a' }, function(err, list) { 31 | console.log(list) 32 | }); 33 | }) 34 | ``` 35 | 36 | ## API 37 | 38 | #### `var db = hypergraph(storage, [key], [options])` 39 | 40 | Returns an instance of hyper-graph-db. Arguments are passed directly to hyperdb, look at its constructor [API](https://github.com/mafintosh/hyperdb#var-db--hyperdbstorage-key-options) for configuration options. 41 | 42 | Storage argument can be a Hyperdb instance, a filepath, or a storage object. For an example of storage type objects look at [dat-storage](https://github.com/datproject/dat-storage). 43 | 44 | Extra Options: 45 | ```js 46 | { 47 | index: 'hex' || 'tri', // 6 or 3 indices, default 'hex' 48 | name: string, // name that prefixes blank nodes 49 | prefixes: { // an object representing RDF namespace prefixes 50 | [sorthand]: string, 51 | }, 52 | } 53 | ``` 54 | 55 | The prefix option can be used to further reduce db size, as it will auto replace namespaced values with their prefered prefix. 56 | 57 | For example: `{ vcard: 'http://www.w3.org/2006/vcard/ns#' }` will store `http://www.w3.org/2006/vcard/ns#given-name` as `vcard:given-name`. 58 | 59 | **Note:** `index`, `name`, and `prefixes` can only be set when a graph db is first created. When loading an existing graph these values are also loaded from the db. 60 | 61 | #### `db.on('ready')` 62 | 63 | *This event is passed on from underlying hyperdb instance.* 64 | 65 | Emitted exactly once: when the db is fully ready and all static properties have 66 | been set. You do not need to wait for this when calling any async functions. 67 | 68 | #### `db.on('error', err)` 69 | 70 | *This event is passed on from underlying hyperdb instance.* 71 | 72 | Emitted if there was a critical error before `db` is ready. 73 | 74 | #### `db.put(triple, [callback])` 75 | 76 | Inserts **Hexastore** formated entries for triple into the graph database. 77 | 78 | #### `var stream = db.putStream(triple)` 79 | 80 | Returns a writable stream. 81 | 82 | #### `db.get(triple, [options], callback)` 83 | 84 | Returns all entries that match the triple. This allows for partial pattern-matching. For example `{ subject: 'a' })`, will return all triples with subject equal to 'a'. 85 | 86 | Options: 87 | ```js 88 | { 89 | limit: number, // limit number of triples returned 90 | offset: number, // offset returned 91 | filter: function (triple) { return bool }, // filter the results 92 | } 93 | ``` 94 | 95 | #### `db.del(triple, [callback])` 96 | 97 | Remove triples indices from the graph database. 98 | 99 | #### `var stream = db.delStream(triple)` 100 | 101 | Returns a writable stream for removing entries. 102 | 103 | #### `var stream = db.getStream(triple, [options])` 104 | 105 | Returns a readable stream of all matching triples. 106 | 107 | Allowed options: 108 | ```js 109 | { 110 | limit: number, // limit number of triples returned 111 | offset: number, // offset returned 112 | filter: function (triple) { return bool }, // filter the results 113 | } 114 | ``` 115 | 116 | #### `db.query(query, callback)` 117 | 118 | Allows for querying the graph with [SPARQL](https://www.w3.org/TR/sparql11-protocol/) queries. 119 | Returns all entries that match the query. 120 | 121 | SPARQL queries are implemented using [sparql-iterator](https://github.com/e-e-e/sparql-iterator) - a fork of [Linked Data Fragments Client](https://github.com/LinkedDataFragments/Client.js). 122 | 123 | #### `var stream = db.queryStream(query)` 124 | 125 | Returns a stream of results from the SPARQL query. 126 | 127 | #### `db.search(patterns, [options], callback)` 128 | 129 | Allows for Basic Graph Patterns searches where all patterns must match. 130 | Expects patterns to be an array of triple options of the form: 131 | 132 | ```js 133 | { 134 | subject: String || Variable, // required 135 | predicate: String || Variable, // required 136 | object: String || Variable, // required 137 | filter: Function, // optional 138 | } 139 | ``` 140 | 141 | Allowed options: 142 | ```js 143 | { 144 | limit: number, // limit number of results returned 145 | } 146 | ``` 147 | 148 | filter: function (triple) { return bool }, 149 | 150 | ```js 151 | db.put([{ 152 | subject: 'matteo', 153 | predicate: 'friend', 154 | object: 'daniele' 155 | }, { 156 | subject: 'daniele', 157 | predicate: 'friend', 158 | object: 'matteo' 159 | }, { 160 | subject: 'daniele', 161 | predicate: 'friend', 162 | object: 'marco' 163 | }, { 164 | subject: 'lucio', 165 | predicate: 'friend', 166 | object: 'matteo' 167 | }, { 168 | subject: 'lucio', 169 | predicate: 'friend', 170 | object: 'marco' 171 | }, { 172 | subject: 'marco', 173 | predicate: 'friend', 174 | object: 'davide' 175 | }], () => { 176 | 177 | const stream = db.search([{ 178 | subject: 'matteo', 179 | predicate: 'friend', 180 | object: db.v('x') 181 | }, { 182 | subject: db.v('x'), 183 | predicate: 'friend', 184 | object: db.v('y') 185 | }, { 186 | subject: db.v('y'), 187 | predicate: 'friend', 188 | object: 'davide' 189 | }], (err, results) => { 190 | if (err) throw err 191 | console.log(results) 192 | }) 193 | }) 194 | ``` 195 | 196 | #### `var stream = db.searchStream(queries)` 197 | 198 | Returns search results as a stream. 199 | 200 | #### `db.graphVersion()` 201 | 202 | Returns the version of hyper-graph-db that created the db. 203 | 204 | #### `db.indexType()` 205 | 206 | Returns the type of index which the graph is configured to use: `hex` or `tri`. 207 | 208 | #### `db.name()` 209 | 210 | Returns the name used for blank nodes when searching the db. 211 | 212 | #### `db.prefixes()` 213 | 214 | Returns an object representing the RDF prefixes used by the db. 215 | 216 | -------------------------------------------------------------------------------- /test/basic.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const expect = require('chai').expect 3 | const ram = require('random-access-memory') 4 | const tmp = require('tmp') 5 | const path = require('path') 6 | const hyperdb = require('hyperdb') 7 | const pkg = require('../package.json') 8 | 9 | const hypergraph = require('../index') 10 | const constants = require('../lib/constants') 11 | const prefixes = require('../lib/prefixes') 12 | 13 | function ramStore (filename) { 14 | // filename will be one of: data, bitfield, tree, signatures, key, secret_key 15 | // the data file will contain all your data concattenated. 16 | // just store all files in ram by returning a random-access-memory instance 17 | return ram() 18 | } 19 | 20 | describe('hypergraph', function () { 21 | let db 22 | describe('constructor storage argument', () => { 23 | context('with instance of hyperdb', () => { 24 | it('sets graph database to hyperdb passed into the constructor', () => { 25 | var hyperdbInstance = hyperdb(ramStore) 26 | db = hypergraph(hyperdbInstance) 27 | expect(db.db).to.eql(hyperdbInstance) 28 | }) 29 | }) 30 | }) 31 | context('when newly created it adds metadata to db', () => { 32 | it('includes graph version', (done) => { 33 | db = hypergraph(ramStore) 34 | db.on('ready', () => { 35 | db.db.get('@version', (err, node) => { 36 | expect(err).to.not.be.a('error') 37 | expect(node[0].value.toString()).to.match(/\d+.\d+.\d+.*/) 38 | done() 39 | }) 40 | }) 41 | }) 42 | it('includes index type (default)', (done) => { 43 | db = hypergraph(ramStore) 44 | db.on('ready', () => { 45 | db.db.get('@index', (err, node) => { 46 | expect(err).to.not.be.a('error') 47 | expect(node[0].value.toString()).to.eql('hex') 48 | done() 49 | }) 50 | }) 51 | }) 52 | it('includes index type (option.index = tri)', (done) => { 53 | db = hypergraph(ramStore, { index: 'tri' }) 54 | db.on('ready', () => { 55 | db.db.get('@index', (err, node) => { 56 | expect(err).to.not.be.a('error') 57 | expect(node[0].value.toString()).to.eql('tri') 58 | done() 59 | }) 60 | }) 61 | }) 62 | it('includes name (default)', (done) => { 63 | db = hypergraph(ramStore) 64 | db.on('ready', () => { 65 | db.db.get('@name', (err, node) => { 66 | expect(err).to.not.be.a('error') 67 | expect(node[0].value.toString()).to.eql(constants.DEFAULT_BASE) 68 | done() 69 | }) 70 | }) 71 | }) 72 | it('includes name (option.name)', (done) => { 73 | db = hypergraph(ramStore, { name: 'this://' }) 74 | db.on('ready', () => { 75 | db.db.get('@name', (err, node) => { 76 | expect(err).to.not.be.a('error') 77 | expect(node[0].value.toString()).to.eql('this://') 78 | done() 79 | }) 80 | }) 81 | }) 82 | it('includes default prefixes', (done) => { 83 | db = hypergraph(ramStore) 84 | db.on('ready', () => { 85 | const stream = db.db.createReadStream('@prefix/') 86 | var count = 0 87 | stream.on('data', (nodes) => { 88 | const prefix = prefixes.fromKey(nodes[0].key) 89 | count++ 90 | expect(nodes[0].value.toString()).to.eql(constants.DEFAULT_PREFIXES[prefix]) 91 | }) 92 | stream.on('error', done) 93 | stream.on('end', () => { 94 | expect(count).to.eql(Object.keys(constants.DEFAULT_PREFIXES).length) 95 | done() 96 | }) 97 | }) 98 | }) 99 | it('includes only specified prefixes (options.prefixes)', (done) => { 100 | var customPrefixes = { 101 | schema: 'http://schema.org/', 102 | library: 'http://purl.org/library/', 103 | void: 'http://rdfs.org/ns/void#', 104 | dct: 'http://purl.org/dc/terms/', 105 | rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', 106 | madsrdf: 'http://www.loc.gov/mads/rdf/v1#', 107 | discovery: 'http://worldcat.org/vocab/discovery/', 108 | bgn: 'http://bibliograph.net/', 109 | pto: 'http://www.productontology.org/id/', 110 | dc: 'http://purl.org/dc/elements/1.1/' 111 | } 112 | db = hypergraph(ramStore, { prefixes: customPrefixes }) 113 | db.on('ready', () => { 114 | const stream = db.db.createReadStream('@prefix/') 115 | var count = 0 116 | stream.on('data', (nodes) => { 117 | const prefix = prefixes.fromKey(nodes[0].key) 118 | count++ 119 | expect(nodes[0].value.toString()).to.eql(customPrefixes[prefix]) 120 | }) 121 | stream.on('error', done) 122 | stream.on('end', () => { 123 | expect(count).to.eql(Object.keys(customPrefixes).length) 124 | done() 125 | }) 126 | }) 127 | }) 128 | }) 129 | context('when loading db that already exists', () => { 130 | it('does not add new metadata', (done) => { 131 | tmp.dir({ unsafeCleanup: true }, function (err, dir, cleanupCallback) { 132 | if (err) return done(err) 133 | const dbDir = path.join(dir, 'test.db') 134 | // create new hyperdb 135 | const hyper = hyperdb(dbDir) 136 | hyper.on('ready', () => { 137 | hyper.put('test', 'data', (err) => { 138 | if (err) return finish(err) 139 | openExistingDBAsGraphDB() 140 | }) 141 | }) 142 | hyper.on('error', finish) 143 | 144 | function openExistingDBAsGraphDB () { 145 | db = hypergraph(dbDir) 146 | db.on('ready', () => { 147 | db.db.get('@version', (err, nodes) => { 148 | expect(nodes).to.eql([]) 149 | finish(err) 150 | }) 151 | }) 152 | db.on('error', finish) 153 | } 154 | function finish (e) { 155 | cleanupCallback() 156 | done(e) 157 | } 158 | }) 159 | }) 160 | 161 | context('with graph already containing index, version, name and prefixes', () => { 162 | const options = { 163 | index: 'tri', 164 | name: 'baseName', 165 | prefixes: { 166 | test: 'http://hyperreadings.info/test#', 167 | xsd: 'http://www.w3.org/2001/XMLSchema#' 168 | } 169 | } 170 | let cleanup = () => {} 171 | let graphDir 172 | before((done) => { 173 | tmp.dir({ unsafeCleanup: true }, (err, dir, cleanupCallback) => { 174 | if (err) return done(err) 175 | cleanup = cleanupCallback 176 | graphDir = dir 177 | const graph = hypergraph(graphDir, options) 178 | graph.on('ready', () => { done() }) 179 | graph.on('error', done) 180 | }) 181 | }) 182 | after(() => { 183 | cleanup() 184 | }) 185 | it('contains version that was used to create the db', (done) => { 186 | const graph = hypergraph(graphDir) 187 | graph.on('ready', () => { 188 | graph.graphVersion((err, version) => { 189 | expect(err).to.eql(null) 190 | expect(version).to.eql(pkg.version) 191 | done() 192 | }) 193 | }) 194 | }) 195 | it('overrides options with metadata set in hyperdb (index)', (done) => { 196 | const graph = hypergraph(graphDir, { index: 'hex' }) 197 | graph.on('ready', () => { 198 | graph.indexType((err, index) => { 199 | expect(err).to.eql(null) 200 | expect(index).to.eql(options.index) 201 | done() 202 | }) 203 | }) 204 | }) 205 | it('overrides options with metadata set in hyperdb (name)', (done) => { 206 | const graph = hypergraph(graphDir, { index: 'hex', name: 'overrideMe' }) 207 | graph.on('ready', () => { 208 | graph.name((err, name) => { 209 | expect(err).to.eql(null) 210 | expect(name).to.eql(options.name) 211 | done() 212 | }) 213 | }) 214 | }) 215 | it('overrides options with metadata set in hyperdb (prefix)', (done) => { 216 | const prefixes = { 217 | thing: 'http://some.co/thing#' 218 | } 219 | const graph = hypergraph(graphDir, { index: 'hex', name: 'overrideMe', prefixes }) 220 | graph.on('ready', () => { 221 | graph.prefixes((err, prefixes) => { 222 | expect(err).to.eql(null) 223 | expect(prefixes).to.deep.eql(options.prefixes) 224 | done() 225 | }) 226 | }) 227 | }) 228 | }) 229 | }) 230 | }) 231 | -------------------------------------------------------------------------------- /test/data/simplefoaf.ttl: -------------------------------------------------------------------------------- 1 | @prefix foaf: . 2 | 3 | _:a foaf:givenname "Alice" . 4 | _:a foaf:family_name "Hacker" . 5 | 6 | _:b foaf:firstname "Bob" . 7 | _:b foaf:surname "Hacker" . 8 | -------------------------------------------------------------------------------- /test/data/sparqlIn11Minutes.ttl: -------------------------------------------------------------------------------- 1 | @prefix vcard: . 2 | @prefix sn: . 3 | 4 | sn:emp1 vcard:given-name "Heidi" . 5 | sn:emp1 vcard:family-name "Smith" . 6 | sn:emp1 vcard:title "CEO" . 7 | sn:emp1 sn:hireDate "2015-01-13" . 8 | sn:emp1 sn:completedOrientation "2015-01-30" . 9 | 10 | sn:emp2 vcard:given-name "John" . 11 | sn:emp2 vcard:family-name "Smith" . 12 | sn:emp2 sn:hireDate "2015-01-28" . 13 | sn:emp2 vcard:title "Engineer" . 14 | sn:emp2 sn:completedOrientation "2015-01-30" . 15 | sn:emp2 sn:completedOrientation "2015-03-15" . 16 | 17 | sn:emp3 vcard:given-name "Francis" . 18 | sn:emp3 vcard:family-name "Jones" . 19 | sn:emp3 sn:hireDate "2015-02-13" . 20 | sn:emp3 vcard:title "Vice President" . 21 | 22 | sn:emp4 vcard:given-name "Jane" . 23 | sn:emp4 vcard:family-name "Berger" . 24 | sn:emp4 sn:hireDate "1000-03-10" . 25 | sn:emp4 vcard:title "Sales" . 26 | -------------------------------------------------------------------------------- /test/fixture/foaf.js: -------------------------------------------------------------------------------- 1 | module.exports = [{ 2 | subject: 'matteo', 3 | predicate: 'friend', 4 | object: 'daniele' 5 | }, { 6 | subject: 'daniele', 7 | predicate: 'friend', 8 | object: 'matteo' 9 | }, { 10 | subject: 'daniele', 11 | predicate: 'friend', 12 | object: 'marco' 13 | }, { 14 | subject: 'lucio', 15 | predicate: 'friend', 16 | object: 'matteo' 17 | }, { 18 | subject: 'lucio', 19 | predicate: 'friend', 20 | object: 'marco' 21 | }, { 22 | subject: 'marco', 23 | predicate: 'friend', 24 | object: 'davide' 25 | }, { 26 | subject: 'marco', 27 | predicate: 'age', 28 | object: 32 29 | }, { 30 | subject: 'daniele', 31 | predicate: 'age', 32 | object: 25 33 | }, { 34 | subject: 'lucio', 35 | predicate: 'age', 36 | object: 15 37 | }, { 38 | subject: 'davide', 39 | predicate: 'age', 40 | object: 70 41 | }] 42 | -------------------------------------------------------------------------------- /test/fixture/homes_in_paris.js: -------------------------------------------------------------------------------- 1 | module.exports = [{ 2 | subject: 'https://my-profile.eu/people/deiu/card#me', 3 | predicate: 'http://xmlns.com/foaf/0.1/name', 4 | object: '"Andrei Vlad Sambra"' 5 | }, { 6 | subject: 'http://bblfish.net/people/henry/card#me', 7 | predicate: 'http://xmlns.com/foaf/0.1/name', 8 | object: '"Henry Story"' 9 | }, { 10 | subject: 'http://presbrey.mit.edu/foaf#presbrey', 11 | predicate: 'http://xmlns.com/foaf/0.1/name', 12 | object: '"Joe Presbrey"' 13 | }, { 14 | subject: 'http://manu.sporny.org#person', 15 | predicate: 'http://xmlns.com/foaf/0.1/name', 16 | object: '"Manu Sporny"' 17 | }, { 18 | subject: 'http://melvincarvalho.com/#me', 19 | predicate: 'http://xmlns.com/foaf/0.1/name', 20 | object: '"Melvin Carvalho"' 21 | }, { 22 | subject: 'http://manu.sporny.org#person', 23 | predicate: 'http://xmlns.com/foaf/0.1/knows', 24 | object: 'http://bblfish.net/people/henry/card#me' 25 | }, { 26 | subject: 'http://presbrey.mit.edu/foaf#presbrey', 27 | predicate: 'http://xmlns.com/foaf/0.1/based_near', 28 | object: 'http://dbpedia.org/resource/Cambridge' 29 | }, { 30 | subject: 'http://melvincarvalho.com/#me', 31 | predicate: 'http://xmlns.com/foaf/0.1/based_near', 32 | object: 'http://dbpedia.org/resource/Honolulu' 33 | }, { 34 | subject: 'http://bblfish.net/people/henry/card#me', 35 | predicate: 'http://xmlns.com/foaf/0.1/based_near', 36 | object: 'http://dbpedia.org/resource/Paris' 37 | }, { 38 | subject: 'https://my-profile.eu/people/deiu/card#me', 39 | predicate: 'http://xmlns.com/foaf/0.1/based_near', 40 | object: 'http://dbpedia.org/resource/Paris' 41 | }, { 42 | subject: 'http://manu.sporny.org#person', 43 | predicate: 'http://xmlns.com/foaf/0.1/homepage', 44 | object: 'http://manu.sporny.org/' 45 | }, { 46 | subject: 'http://manu.sporny.org#person', 47 | predicate: 'http://xmlns.com/foaf/0.1/knows', 48 | object: 'http://melvincarvalho.com/#me' 49 | }, { 50 | subject: 'http://manu.sporny.org#person', 51 | predicate: 'http://xmlns.com/foaf/0.1/knows', 52 | object: 'http://presbrey.mit.edu/foaf#presbrey' 53 | }, { 54 | subject: 'http://manu.sporny.org#person', 55 | predicate: 'http://xmlns.com/foaf/0.1/knows', 56 | object: 'https://my-profile.eu/people/deiu/card#me' 57 | }] 58 | -------------------------------------------------------------------------------- /test/join-stream.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | const expect = require('chai').expect 4 | const ram = require('random-access-memory') 5 | const hypergraph = require('../index') 6 | const fixture = require('./fixture/foaf') 7 | 8 | function ramStore (filename) { 9 | // filename will be one of: data, bitfield, tree, signatures, key, secret_key 10 | // the data file will contain all your data concattenated. 11 | // just store all files in ram by returning a random-access-memory instance 12 | return ram() 13 | } 14 | 15 | describe('JoinStream', () => { 16 | let db 17 | beforeEach((done) => { 18 | db = hypergraph(ramStore) 19 | db.put(fixture, done) 20 | }) 21 | 22 | afterEach((done) => { 23 | db.close(done) 24 | }) 25 | 26 | it('should do a join with one results', (done) => { 27 | db.search([{ 28 | subject: db.v('x'), 29 | predicate: 'friend', 30 | object: 'daniele' 31 | }], (err, results) => { 32 | expect(results).to.have.property('length', 1) 33 | expect(results[0]).to.have.property('x', 'matteo') 34 | done(err) 35 | }) 36 | }) 37 | 38 | it('should support non-array search parameter', (done) => { 39 | db.search({ 40 | subject: db.v('x'), 41 | predicate: 'friend', 42 | object: 'daniele' 43 | }, (err, results) => { 44 | expect(results).to.have.property('length', 1) 45 | expect(results[0]).to.have.property('x', 'matteo') 46 | done(err) 47 | }) 48 | }) 49 | 50 | it('should do a join with two results', (done) => { 51 | db.search([{ 52 | subject: db.v('x'), 53 | predicate: 'friend', 54 | object: 'marco' 55 | }, { 56 | subject: db.v('x'), 57 | predicate: 'friend', 58 | object: 'matteo' 59 | }], (err, results) => { 60 | expect(results).to.have.property('length', 2) 61 | expect(results[0]).to.have.property('x', 'daniele') 62 | expect(results[1]).to.have.property('x', 'lucio') 63 | done(err) 64 | }) 65 | }) 66 | 67 | it('should do a join with three conditions', (done) => { 68 | db.search([{ 69 | subject: db.v('x'), 70 | predicate: 'friend', 71 | object: db.v('y') 72 | }, { 73 | subject: db.v('x'), 74 | predicate: 'friend', 75 | object: 'matteo' 76 | }, { 77 | subject: 'lucio', 78 | predicate: 'friend', 79 | object: db.v('y') 80 | }], (err, results) => { 81 | expect(results).to.have.property('length', 4) 82 | done(err) 83 | }) 84 | }) 85 | 86 | it('should return the two solutions through the searchStream interface', (done) => { 87 | var solutions = [{ x: 'daniele' }, { x: 'lucio' }] 88 | var stream = db.searchStream([{ 89 | subject: db.v('x'), 90 | predicate: 'friend', 91 | object: 'marco' 92 | }, { 93 | subject: db.v('x'), 94 | predicate: 'friend', 95 | object: 'matteo' 96 | }]) 97 | 98 | stream.on('data', (data) => { 99 | expect(data).to.eql(solutions.shift()) 100 | }) 101 | 102 | stream.on('end', done) 103 | }) 104 | 105 | it('should allow to find mutual friends', (done) => { 106 | var solutions = [{ x: 'daniele', y: 'matteo' }, { x: 'matteo', y: 'daniele' }] 107 | var stream = db.searchStream([{ 108 | subject: db.v('x'), 109 | predicate: 'friend', 110 | object: db.v('y') 111 | }, { 112 | subject: db.v('y'), 113 | predicate: 'friend', 114 | object: db.v('x') 115 | }]) 116 | 117 | stream.on('data', (data) => { 118 | var solutionIndex = -1 119 | 120 | solutions.forEach((solution, i) => { 121 | var found = Object.keys(solutions).every((v) => { 122 | return solution[v] === data[v] 123 | }) 124 | if (found) { 125 | solutionIndex = i 126 | } 127 | }) 128 | 129 | if (solutionIndex !== -1) { 130 | solutions.splice(solutionIndex, 1) 131 | } 132 | }) 133 | 134 | stream.on('end', () => { 135 | expect(solutions).to.have.property('length', 0) 136 | done() 137 | }) 138 | }) 139 | 140 | it('should allow to intersect common friends', (done) => { 141 | var solutions = [{ x: 'matteo' }, { x: 'marco' }] 142 | var stream = db.searchStream([{ 143 | subject: 'lucio', 144 | predicate: 'friend', 145 | object: db.v('x') 146 | }, { 147 | subject: 'daniele', 148 | predicate: 'friend', 149 | object: db.v('x') 150 | }]) 151 | 152 | stream.on('data', (data) => { 153 | expect(data).to.eql(solutions.shift()) 154 | }) 155 | 156 | stream.on('end', () => { 157 | expect(solutions).to.have.property('length', 0) 158 | done() 159 | }) 160 | }) 161 | 162 | it('should support the friend of a friend scenario', (done) => { 163 | var solutions = [{ x: 'daniele', y: 'marco' }] 164 | var stream = db.searchStream([{ 165 | subject: 'matteo', 166 | predicate: 'friend', 167 | object: db.v('x') 168 | }, { 169 | subject: db.v('x'), 170 | predicate: 'friend', 171 | object: db.v('y') 172 | }, { 173 | subject: db.v('y'), 174 | predicate: 'friend', 175 | object: 'davide' 176 | }]) 177 | 178 | stream.on('data', (data) => { 179 | expect(data).to.eql(solutions.shift()) 180 | }) 181 | 182 | stream.on('end', () => { 183 | expect(solutions).to.have.property('length', 0) 184 | done() 185 | }) 186 | }) 187 | 188 | xit('should return triples from a join aka materialized API', (done) => { 189 | db.search([{ 190 | subject: db.v('x'), 191 | predicate: 'friend', 192 | object: 'marco' 193 | }, { 194 | subject: db.v('x'), 195 | predicate: 'friend', 196 | object: 'matteo' 197 | }], { 198 | materialized: { 199 | subject: db.v('x'), 200 | predicate: 'newpredicate', 201 | object: 'abcde' 202 | } 203 | }, (err, results) => { 204 | expect(results).to.eql([{ 205 | subject: 'daniele', 206 | predicate: 'newpredicate', 207 | object: 'abcde' 208 | }, { 209 | subject: 'lucio', 210 | predicate: 'newpredicate', 211 | object: 'abcde' 212 | }]) 213 | done(err) 214 | }) 215 | }) 216 | 217 | it('should support a friend-of-a-friend-of-a-friend scenario', (done) => { 218 | var solutions = [ 219 | { x: 'daniele', y: 'matteo', z: 'daniele' }, 220 | { x: 'daniele', y: 'marco', z: 'davide' } 221 | ] 222 | 223 | var stream = db.searchStream([{ 224 | subject: 'matteo', 225 | predicate: 'friend', 226 | object: db.v('x') 227 | }, { 228 | subject: db.v('x'), 229 | predicate: 'friend', 230 | object: db.v('y') 231 | }, { 232 | subject: db.v('y'), 233 | predicate: 'friend', 234 | object: db.v('z') 235 | }]) 236 | stream.on('data', (data) => { 237 | expect(data).to.eql(solutions.shift()) 238 | }) 239 | 240 | stream.on('end', () => { 241 | expect(solutions).to.have.property('length', 0) 242 | done() 243 | }) 244 | }) 245 | 246 | xit('should emit triples from the stream interface aka materialized API', (done) => { 247 | var triples = [{ 248 | subject: 'daniele', 249 | predicate: 'newpredicate', 250 | object: 'abcde' 251 | }] 252 | var stream = db.searchStream([{ 253 | subject: 'matteo', 254 | predicate: 'friend', 255 | object: db.v('x') 256 | }, { 257 | subject: db.v('x'), 258 | predicate: 'friend', 259 | object: db.v('y') 260 | }, { 261 | subject: db.v('y'), 262 | predicate: 'friend', 263 | object: 'davide' 264 | }], { 265 | materialized: { 266 | subject: db.v('x'), 267 | predicate: 'newpredicate', 268 | object: 'abcde' 269 | } 270 | }) 271 | 272 | stream.on('data', (data) => { 273 | expect(data).to.eql(triples.shift()) 274 | }) 275 | 276 | stream.on('end', () => { 277 | expect(triples).to.have.property('length', 0) 278 | done() 279 | }) 280 | }) 281 | 282 | it('should support filtering inside a condition', (done) => { 283 | db.search([{ 284 | subject: db.v('x'), 285 | predicate: 'friend', 286 | object: 'daniele', 287 | filter: triple => triple.subject !== 'matteo' 288 | }], (err, results) => { 289 | expect(results).to.have.length(0) 290 | done(err) 291 | }) 292 | }) 293 | 294 | it('should support filtering inside a second-level condition', (done) => { 295 | db.search([{ 296 | subject: 'matteo', 297 | predicate: 'friend', 298 | object: db.v('y') 299 | }, { 300 | subject: db.v('y'), 301 | predicate: 'friend', 302 | object: db.v('x'), 303 | filter: (triple) => triple.object !== 'matteo' 304 | }], (err, results) => { 305 | expect(results).to.eql([{ 306 | 'y': 'daniele', 307 | 'x': 'marco' 308 | }]) 309 | done(err) 310 | }) 311 | }) 312 | 313 | xit('should support solution filtering', (done) => { 314 | db.search([{ 315 | subject: 'matteo', 316 | predicate: 'friend', 317 | object: db.v('y') 318 | }, { 319 | subject: db.v('y'), 320 | predicate: 'friend', 321 | object: db.v('x') 322 | }], { 323 | filter: (context, callback) => { 324 | if (context.x !== 'matteo') { 325 | callback(null, context) 326 | } else { 327 | callback(null) 328 | } 329 | } 330 | }, (err, results) => { 331 | expect(results).to.eql([{ 332 | 'y': 'daniele', 333 | 'x': 'marco' 334 | }]) 335 | done(err) 336 | }) 337 | }) 338 | 339 | xit('should support solution filtering w/ 2 args', (done) => { 340 | // Who's a friend of matteo and aged 25. 341 | db.search([{ 342 | subject: db.v('s'), 343 | predicate: 'age', 344 | object: db.v('age') 345 | }, { 346 | subject: db.v('s'), 347 | predicate: 'friend', 348 | object: 'matteo' 349 | }], { 350 | filter: (context, callback) => { 351 | if (context.age === 25) { 352 | callback(null, context) // confirm 353 | } else { 354 | callback(null) // refute 355 | } 356 | } 357 | }, (err, results) => { 358 | expect(results).to.eql([{ 359 | 'age': 25, 360 | 's': 'daniele' 361 | }]) 362 | done(err) 363 | }) 364 | }) 365 | 366 | it('should return only one solution with limit 1', (done) => { 367 | db.search([{ 368 | subject: db.v('x'), 369 | predicate: 'friend', 370 | object: 'marco' 371 | }, { 372 | subject: db.v('x'), 373 | predicate: 'friend', 374 | object: 'matteo' 375 | }], { limit: 1 }, (err, results) => { 376 | expect(results).to.have.property('length', 1) 377 | expect(results[0]).to.have.property('x', 'daniele') 378 | done(err) 379 | }) 380 | }) 381 | 382 | it('should return only one solution with limit 1 (bis)', (done) => { 383 | db.search([{ 384 | subject: 'lucio', 385 | predicate: 'friend', 386 | object: db.v('x') 387 | }, { 388 | subject: 'daniele', 389 | predicate: 'friend', 390 | object: db.v('x') 391 | }], { limit: 1 }, (err, results) => { 392 | expect(results).to.have.property('length', 1) 393 | expect(results[0]).to.have.property('x', 'matteo') 394 | done(err) 395 | }) 396 | }) 397 | 398 | xit('should return skip the first solution with offset 1', (done) => { 399 | db.search([{ 400 | subject: db.v('x'), 401 | predicate: 'friend', 402 | object: 'marco' 403 | }, { 404 | subject: db.v('x'), 405 | predicate: 'friend', 406 | object: 'matteo' 407 | }], { offset: 1 }, (err, results) => { 408 | expect(results).to.have.property('length', 1) 409 | expect(results[0]).to.have.property('x', 'lucio') 410 | done(err) 411 | }) 412 | }) 413 | 414 | it('should find homes in paris', (done) => { 415 | var paris = 'http://dbpedia.org/resource/Paris' 416 | var parisians = [ 417 | { 418 | webid: 'https://my-profile.eu/people/deiu/card#me', 419 | name: '"Andrei Vlad Sambra"' 420 | }, { 421 | webid: 'http://bblfish.net/people/henry/card#me', 422 | name: '"Henry Story"' 423 | } 424 | ] 425 | 426 | db.put(require('./fixture/homes_in_paris'), () => { 427 | db.search([{ 428 | subject: 'http://manu.sporny.org#person', 429 | predicate: 'http://xmlns.com/foaf/0.1/knows', 430 | object: db.v('webid') 431 | }, { 432 | subject: db.v('webid'), 433 | predicate: 'http://xmlns.com/foaf/0.1/based_near', 434 | object: paris 435 | }, { 436 | subject: db.v('webid'), 437 | predicate: 'http://xmlns.com/foaf/0.1/name', 438 | object: db.v('name') 439 | } 440 | ], (err, solution) => { 441 | expect(solution).to.eql(parisians) 442 | done(err) 443 | }) 444 | }) 445 | }) 446 | }) 447 | -------------------------------------------------------------------------------- /test/planner.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const expect = require('chai').expect 3 | const planner = require('../lib/planner') 4 | const v = require('../lib/Variable') 5 | 6 | describe('query planner', () => { 7 | var query, expected 8 | 9 | it('should return single entries with no changes', () => { 10 | query = [ { predicate: 'friend' } ] 11 | expect(planner(query)).to.eql(query) 12 | }) 13 | 14 | it('should order queries based on size', () => { 15 | query = [{ 16 | subject: v('x'), 17 | predicate: 'friend', 18 | object: v('c') 19 | }, { 20 | subject: v('x'), 21 | predicate: 'abc', 22 | object: 'xyz' 23 | }] 24 | 25 | expected = [{ 26 | subject: v('x'), 27 | predicate: 'abc', 28 | object: 'xyz' 29 | }, { 30 | subject: v('x'), 31 | predicate: 'friend', 32 | object: v('c') 33 | }] 34 | 35 | expect(planner(query)).to.eql(expected) 36 | }) 37 | 38 | it('should return queries in the same order if they have the same variables', () => { 39 | query = [{ 40 | subject: v('x'), 41 | predicate: 'friend', 42 | object: v('c') 43 | }, { 44 | subject: v('c'), 45 | predicate: 'friend', 46 | object: v('x') 47 | }] 48 | 49 | expected = [{ 50 | subject: v('x'), 51 | predicate: 'friend', 52 | object: v('c') 53 | }, { 54 | subject: v('c'), 55 | predicate: 'friend', 56 | object: v('x') 57 | }] 58 | 59 | expect(planner(query)).to.eql(expected) 60 | }) 61 | 62 | it('should sort three condition queries', () => { 63 | query = [{ 64 | subject: v('b'), 65 | predicate: 'friend', 66 | object: v('c') 67 | }, { 68 | subject: v('a'), 69 | predicate: 'friend', 70 | object: v('b') 71 | }, { 72 | subject: 'bob', 73 | predicate: 'father', 74 | object: v('a') 75 | }] 76 | 77 | expected = [{ 78 | subject: 'bob', 79 | predicate: 'father', 80 | object: v('a') 81 | }, { 82 | subject: v('a'), 83 | predicate: 'friend', 84 | object: v('b') 85 | }, { 86 | subject: v('b'), 87 | predicate: 'friend', 88 | object: v('c') 89 | }] 90 | expect(planner(query)).to.eql(expected) 91 | }) 92 | 93 | it('should sort same number of vars in order of solving simplicity', () => { 94 | query = [{ 95 | subject: v('x'), 96 | predicate: 'friend', 97 | object: v('c') 98 | }, { 99 | subject: v('y'), 100 | predicate: 'friend', 101 | object: v('z') 102 | }, { 103 | subject: v('c'), 104 | predicate: 'friend', 105 | object: v('y') 106 | }] 107 | 108 | expected = [ 109 | query[2], 110 | query[0], 111 | query[1] 112 | ] 113 | expect(planner(query)).to.eql(expected) 114 | }) 115 | 116 | it('should sort queries with same # vars based on overlapping vars (bis)', () => { 117 | query = [{ 118 | subject: v('1'), 119 | predicate: 'friend', 120 | object: v('2') 121 | }, { 122 | subject: v('2'), 123 | predicate: 'friend', 124 | object: v('3') 125 | }, { 126 | subject: v('3'), 127 | predicate: 'friend', 128 | object: v('4') 129 | }, { 130 | subject: v('3'), 131 | predicate: 'has', 132 | object: v('4') 133 | }] 134 | 135 | expected = [ 136 | query[3], 137 | query[2], 138 | query[1], 139 | query[0] 140 | ] 141 | expect(planner(query)).to.eql(expected) 142 | }) 143 | }) 144 | 145 | // it('should put the variables from the previous condition in the same order', () => { 146 | // query = [{ 147 | // subject: v('x0'), 148 | // predicate: 'friend', 149 | // object: 'davide' 150 | // }, { 151 | // subject: v('x1'), 152 | // predicate: 'friend', 153 | // object: v('x0') 154 | // }, { 155 | // subject: v('x1'), 156 | // predicate: 'friend', 157 | // object: v('x2') 158 | // }] 159 | 160 | // expected = [{ 161 | // subject: v('x0'), 162 | // predicate: 'friend', 163 | // object: 'davide', 164 | // stream: JoinStream, 165 | // index: 'pos' 166 | // }, { 167 | // subject: v('x1'), 168 | // predicate: 'friend', 169 | // object: v('x0'), 170 | // stream: SortJoinStream, 171 | // index: 'pos' 172 | // }, { 173 | // subject: v('x1'), 174 | // predicate: 'friend', 175 | // object: v('x2'), 176 | // stream: SortJoinStream, 177 | // index: 'pso' 178 | // }] 179 | 180 | // planner(query, (err, result) => { 181 | // expect(result).to.eql(expected) 182 | // done(err) 183 | // }) 184 | // }) 185 | 186 | // it('should use a SortJoinStream for another three-conditions query', (done) => { 187 | // query = [{ 188 | // subject: 'matteo', 189 | // predicate: 'friend', 190 | // object: v('x') 191 | // }, { 192 | // subject: v('x'), 193 | // predicate: 'friend', 194 | // object: v('y') 195 | // }, { 196 | // subject: v('y'), 197 | // predicate: 'friend', 198 | // object: 'daniele' 199 | // }] 200 | 201 | // expected = [{ 202 | // subject: 'matteo', 203 | // predicate: 'friend', 204 | // object: v('x'), 205 | // stream: JoinStream, 206 | // index: 'pso' 207 | // }, { 208 | // subject: v('x'), 209 | // predicate: 'friend', 210 | // object: v('y'), 211 | // stream: SortJoinStream, 212 | // index: 'pso' 213 | // }, { 214 | // subject: v('y'), 215 | // predicate: 'friend', 216 | // object: 'daniele', 217 | // stream: SortJoinStream, 218 | // index: 'pos' 219 | // }] 220 | 221 | // planner(query, (err, result) => { 222 | // expect(result).to.eql(expected) 223 | // done(err) 224 | // }) 225 | // }) 226 | 227 | // it('should use a SortJoinStream for the friend-of-a-friend-of-a-friend scenario', (done) => { 228 | // query = [{ 229 | // subject: 'matteo', 230 | // predicate: 'friend', 231 | // object: v('x') 232 | // }, { 233 | // subject: v('x'), 234 | // predicate: 'friend', 235 | // object: v('y') 236 | // }, { 237 | // subject: v('y'), 238 | // predicate: 'friend', 239 | // object: v('z') 240 | // }] 241 | 242 | // expected = [{ 243 | // subject: 'matteo', 244 | // predicate: 'friend', 245 | // object: v('x'), 246 | // stream: JoinStream, 247 | // index: 'pso' 248 | // }, { 249 | // subject: v('x'), 250 | // predicate: 'friend', 251 | // object: v('y'), 252 | // stream: SortJoinStream, 253 | // index: 'pso' 254 | // }, { 255 | // subject: v('y'), 256 | // predicate: 'friend', 257 | // object: v('z'), 258 | // stream: SortJoinStream, 259 | // index: 'pso' 260 | // }] 261 | 262 | // planner(query, (err, result) => { 263 | // expect(result).to.eql(expected) 264 | // done(err) 265 | // }) 266 | // }) 267 | 268 | // it('should pick the correct indexes with multiple predicates going out the same subject', (done) => { 269 | // query = [{ 270 | // subject: v('a'), 271 | // predicate: 'friend', 272 | // object: 'marco' 273 | // }, { 274 | // subject: v('a'), 275 | // predicate: 'friend', 276 | // object: v('x1') 277 | // }, { 278 | // subject: v('x1'), 279 | // predicate: 'friend', 280 | // object: v('a') 281 | // }] 282 | 283 | // expected = [{ 284 | // subject: v('a'), 285 | // predicate: 'friend', 286 | // object: 'marco', 287 | // stream: JoinStream, 288 | // index: 'pos' 289 | // }, { 290 | // subject: v('a'), 291 | // predicate: 'friend', 292 | // object: v('x1'), 293 | // stream: SortJoinStream, 294 | // index: 'pso' 295 | // }, { 296 | // subject: v('x1'), 297 | // predicate: 'friend', 298 | // object: v('a'), 299 | // stream: SortJoinStream, 300 | // index: 'pos' 301 | // }] 302 | 303 | // planner(query, (err, result) => { 304 | // expect(result).to.eql(expected) 305 | // done(err) 306 | // }) 307 | // }) 308 | 309 | // describe('without approximateSize', () => { 310 | // beforeEach(() => { 311 | // db = { 312 | // db: { 313 | // } 314 | // } 315 | // }) 316 | 317 | // it('should order two conditions based on their size', (done) => { 318 | // query = [{ 319 | // subject: 'matteo', 320 | // predicate: 'friend', 321 | // object: v('a') 322 | // }, { 323 | // subject: v('b'), 324 | // predicate: 'friend', 325 | // object: v('c') 326 | // }] 327 | 328 | // expected = [{ 329 | // subject: 'matteo', 330 | // predicate: 'friend', 331 | // object: v('a'), 332 | // stream: JoinStream 333 | // }, { 334 | // subject: v('b'), 335 | // predicate: 'friend', 336 | // object: v('c'), 337 | // stream: JoinStream 338 | // }] 339 | 340 | // planner(query, (err, result) => { 341 | // expect(result).to.eql(expected) 342 | // done(err) 343 | // }) 344 | // }) 345 | 346 | // it('should order two conditions based on their size (bis)', (done) => { 347 | // query = [{ 348 | // subject: v('b'), 349 | // predicate: 'friend', 350 | // object: v('c') 351 | // }, { 352 | // subject: 'matteo', 353 | // predicate: 'friend', 354 | // object: v('a') 355 | // }] 356 | 357 | // expected = [{ 358 | // subject: 'matteo', 359 | // predicate: 'friend', 360 | // object: v('a'), 361 | // stream: JoinStream 362 | // }, { 363 | // subject: v('b'), 364 | // predicate: 'friend', 365 | // object: v('c'), 366 | // stream: JoinStream 367 | // }] 368 | 369 | // planner(query, (err, result) => { 370 | // expect(result).to.eql(expected) 371 | // done(err) 372 | // }) 373 | // }) 374 | // }) 375 | // }) 376 | -------------------------------------------------------------------------------- /test/prefixes.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const expect = require('chai').expect 3 | const prefixes = require('../lib/prefixes') 4 | 5 | describe('prefix utilities', () => { 6 | describe('toKey', () => { 7 | it('converts a prefix to a key', () => { 8 | expect(prefixes.toKey('this')).to.eql('@prefix/this') 9 | }) 10 | }) 11 | 12 | describe('fromKey', () => { 13 | it('converts a key to its prefix', () => { 14 | expect(prefixes.fromKey('@prefix/this')).to.eql('this') 15 | }) 16 | }) 17 | 18 | describe('fromNodes', () => { 19 | it('converts hyperdb nodes to prefix/uri object', () => { 20 | const dummyNodes = [{ key: '@prefix/this', value: Buffer.from('http://this.example.com') }] 21 | expect(prefixes.fromNodes(dummyNodes)).to.eql({ uri: 'http://this.example.com', prefix: 'this' }) 22 | }) 23 | it('ignores conflicts', () => { 24 | const dummyNodes = [ 25 | { key: '@prefix/this', value: Buffer.from('http://this.example.com') }, 26 | { key: '@prefix/this', value: Buffer.from('http://conflict.example.com') } 27 | ] 28 | expect(prefixes.fromNodes(dummyNodes)).to.eql({ uri: 'http://this.example.com', prefix: 'this' }) 29 | }) 30 | }) 31 | 32 | describe('toPrefixed', () => { 33 | it('returns unmodified string if prefix is undefined', () => { 34 | const str = 'http://test.com/this/ok#wow' 35 | const prefixed = prefixes.toPrefixed(str) 36 | expect(prefixed).to.eql(str) 37 | }) 38 | it('returns unmodified string if prefix is not set', () => { 39 | const str = 'http://test.com/this/ok#wow' 40 | const prefixed = prefixes.toPrefixed(str, { 'wow': 'http://wow.com/' }) 41 | expect(prefixed).to.eql(str) 42 | }) 43 | it('returns prefix string if prefix is present', () => { 44 | const str = 'http://wow.com/now/this#and_hashed' 45 | const prefixed = prefixes.toPrefixed(str, { 'wow': 'http://wow.com/' }) 46 | expect(prefixed).to.eql('wow:now/this#and_hashed') 47 | }) 48 | }) 49 | 50 | describe('fromPrefixed', () => { 51 | it('returns unmodified string if prefix is undefined', () => { 52 | const str = 'some:thing' 53 | const prefixed = prefixes.fromPrefixed(str) 54 | expect(prefixed).to.eql(str) 55 | }) 56 | it('returns unmodified string if prefix is not set', () => { 57 | const str = 'some:thing' 58 | const prefixed = prefixes.fromPrefixed(str, { 'wow': 'http://wow.com/' }) 59 | expect(prefixed).to.eql(str) 60 | }) 61 | it('returns prefix string if prefix is present', () => { 62 | const str = 'wow:now' 63 | const prefixed = prefixes.fromPrefixed(str, { 'wow': 'http://wow.com/' }) 64 | expect(prefixed).to.eql('http://wow.com/now') 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /test/queries/sparqlIn11Minutes1.rq: -------------------------------------------------------------------------------- 1 | PREFIX vcard: 2 | 3 | SELECT ?person 4 | WHERE 5 | { 6 | ?person vcard:family-name "Smith" . 7 | } 8 | -------------------------------------------------------------------------------- /test/queries/sparqlIn11Minutes11.rq: -------------------------------------------------------------------------------- 1 | PREFIX vcard: 2 | PREFIX sn: 3 | PREFIX foaf: 4 | PREFIX rdf: 5 | 6 | CONSTRUCT { 7 | ?person rdf:type foaf:Person . 8 | ?person foaf:givenName ?givenName . 9 | ?person foaf:familyName ?familyName . 10 | ?person foaf:name ?fullName . 11 | } 12 | WHERE { 13 | ?person vcard:given-name ?givenName . 14 | ?person vcard:family-name ?familyName . 15 | BIND(concat(?givenName," ",?familyName) AS ?fullName) 16 | } 17 | -------------------------------------------------------------------------------- /test/queries/sparqlIn11Minutes2.rq: -------------------------------------------------------------------------------- 1 | PREFIX vcard: 2 | 3 | SELECT ?person ?givenName 4 | WHERE 5 | { 6 | ?person vcard:family-name "Smith" . 7 | ?person vcard:given-name ?givenName . 8 | } 9 | -------------------------------------------------------------------------------- /test/queries/sparqlIn11Minutes3.rq: -------------------------------------------------------------------------------- 1 | PREFIX vcard: 2 | PREFIX sn: 3 | 4 | SELECT ?givenName ?familyName ?hireDate 5 | WHERE 6 | { 7 | ?person vcard:given-name ?givenName . 8 | ?person vcard:family-name ?familyName . 9 | ?person sn:hireDate ?hireDate . 10 | } 11 | -------------------------------------------------------------------------------- /test/queries/sparqlIn11Minutes4.rq: -------------------------------------------------------------------------------- 1 | PREFIX vcard: 2 | PREFIX sn: 3 | 4 | SELECT ?givenName ?familyName ?hireDate 5 | WHERE 6 | { 7 | ?person vcard:given-name ?givenName . 8 | ?person vcard:family-name ?familyName . 9 | ?person sn:hireDate ?hireDate . 10 | FILTER(?hireDate < "2015-03-01") 11 | } 12 | -------------------------------------------------------------------------------- /test/queries/sparqlIn11Minutes5.rq: -------------------------------------------------------------------------------- 1 | PREFIX vcard: 2 | PREFIX sn: 3 | 4 | SELECT ?givenName ?familyName ?oDate 5 | WHERE 6 | { 7 | ?person vcard:given-name ?givenName . 8 | ?person vcard:family-name ?familyName . 9 | ?person sn:completedOrientation ?oDate . 10 | } 11 | -------------------------------------------------------------------------------- /test/queries/sparqlIn11Minutes6.rq: -------------------------------------------------------------------------------- 1 | PREFIX vcard: 2 | PREFIX sn: 3 | 4 | SELECT ?givenName ?familyName ?oDate 5 | WHERE 6 | { 7 | ?person vcard:given-name ?givenName . 8 | ?person vcard:family-name ?familyName . 9 | OPTIONAL { ?person sn:completedOrientation ?oDate . } 10 | } 11 | -------------------------------------------------------------------------------- /test/queries/sparqlIn11Minutes7.rq: -------------------------------------------------------------------------------- 1 | PREFIX vcard: 2 | PREFIX sn: 3 | 4 | SELECT ?givenName ?familyName 5 | WHERE 6 | { 7 | ?person vcard:given-name ?givenName . 8 | ?person vcard:family-name ?familyName . 9 | NOT EXISTS { ?person sn:completedOrientation ?oDate . } 10 | } 11 | -------------------------------------------------------------------------------- /test/queries/sparqlIn11Minutes8.rq: -------------------------------------------------------------------------------- 1 | PREFIX vcard: 2 | PREFIX sn: 3 | 4 | SELECT ?givenName ?familyName ?someVariable WHERE { ?person 5 | vcard:given-name ?givenName . ?person vcard:family-name ?familyName . 6 | BIND("some value" AS ?someVariable) } 7 | -------------------------------------------------------------------------------- /test/queries/sparqlIn11Minutes9.rq: -------------------------------------------------------------------------------- 1 | PREFIX vcard: 2 | PREFIX sn: 3 | 4 | SELECT ?givenName ?familyName ?fullName 5 | WHERE { 6 | ?person vcard:given-name ?givenName . 7 | ?person vcard:family-name ?familyName . 8 | BIND(concat(?givenName," ",?familyName) AS ?fullName) 9 | } 10 | -------------------------------------------------------------------------------- /test/queries/union.rq: -------------------------------------------------------------------------------- 1 | PREFIX foaf: 2 | PREFIX vcard: 3 | 4 | CONSTRUCT { ?x vcard:N _:v . 5 | _:v vcard:givenName ?gname . 6 | _:v vcard:familyName ?fname } 7 | WHERE 8 | { 9 | { ?x foaf:firstname ?gname } UNION { ?x foaf:givenname ?gname } . 10 | { ?x foaf:surname ?fname } UNION { ?x foaf:family_name ?fname } . 11 | } 12 | -------------------------------------------------------------------------------- /test/sparql.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 3 | var hypergraph = require('../index') 4 | var ram = require('random-access-memory') 5 | var N3 = require('n3') 6 | var fs = require('fs') 7 | var path = require('path') 8 | var chai = require('chai') 9 | var expect = chai.expect 10 | 11 | function ramStore () { return ram() } 12 | 13 | function importTurtleFile (graph, file, callback) { 14 | var parser = N3.StreamParser() 15 | var writer = graph.putStream() 16 | N3.Parser._resetBlankNodeIds() 17 | fs.createReadStream(file).pipe(parser).pipe(writer) 18 | writer.on('end', callback) 19 | writer.on('error', callback) 20 | } 21 | 22 | function testQuery (graph, queryFile, expected, done) { 23 | var query = fs.readFileSync(queryFile) 24 | var s = graph.queryStream(query.toString()) 25 | s.on('data', (d) => { 26 | expect(d).to.deep.equal(expected.shift()) 27 | }) 28 | s.on('end', () => { 29 | expect(expected).to.have.length(0) 30 | done() 31 | }) 32 | s.on('error', done) 33 | } 34 | 35 | describe('hypergraph.queryStream', () => { 36 | context('with simple foaf data', () => { 37 | var graph 38 | before((done) => { 39 | graph = hypergraph(ramStore) 40 | graph.on('ready', () => { 41 | importTurtleFile(graph, path.join(__dirname, './data/simplefoaf.ttl'), done) 42 | }) 43 | }) 44 | 45 | it('performs CONSTRUCT query type with UNION operator', (done) => { 46 | console.time('query') 47 | var expected = [ 48 | { subject: 'hg://b0_b', 49 | predicate: 'http://www.w3.org/2001/vcard-rdf/3.0#N', 50 | object: '_:b0' }, 51 | { subject: '_:b0', 52 | predicate: 'http://www.w3.org/2001/vcard-rdf/3.0#givenName', 53 | object: '"Bob"' }, 54 | { subject: '_:b0', 55 | predicate: 'http://www.w3.org/2001/vcard-rdf/3.0#familyName', 56 | object: '"Hacker"' }, 57 | { subject: 'hg://b0_a', 58 | predicate: 'http://www.w3.org/2001/vcard-rdf/3.0#N', 59 | object: '_:b1' }, 60 | { subject: '_:b1', 61 | predicate: 'http://www.w3.org/2001/vcard-rdf/3.0#givenName', 62 | object: '"Alice"' }, 63 | { subject: '_:b1', 64 | predicate: 'http://www.w3.org/2001/vcard-rdf/3.0#familyName', 65 | object: '"Hacker"' } 66 | ] 67 | var query = path.join(__dirname, './queries/union.rq') 68 | testQuery(graph, query, expected, done) 69 | }) 70 | }) 71 | describe('with data from sparql in 11 minutes', () => { 72 | var graph 73 | beforeEach((done) => { 74 | graph = hypergraph(ramStore) 75 | graph.on('ready', () => { 76 | importTurtleFile(graph, path.join(__dirname, './data/sparqlIn11Minutes.ttl'), done) 77 | }) 78 | }) 79 | 80 | it('executes singular query selecting singular variable', (done) => { 81 | var expected = [ 82 | { '?person': 'http://www.snee.com/hr/emp1' }, 83 | { '?person': 'http://www.snee.com/hr/emp2' } 84 | ] 85 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes1.rq`) 86 | testQuery(graph, query, expected, done) 87 | }) 88 | 89 | it('executes two queries selecting two variables', (done) => { 90 | var expected = [ 91 | { '?person': 'http://www.snee.com/hr/emp1', 92 | '?givenName': '"Heidi"' }, 93 | { '?person': 'http://www.snee.com/hr/emp2', 94 | '?givenName': '"John"' } 95 | ] 96 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes2.rq`) 97 | testQuery(graph, query, expected, done) 98 | }) 99 | 100 | it('executes three queries selecting three variables', (done) => { 101 | var expected = [ 102 | { '?givenName': '"Heidi"', 103 | '?familyName': '"Smith"', 104 | '?hireDate': '"2015-01-13"' }, 105 | { '?givenName': '"Jane"', 106 | '?familyName': '"Berger"', 107 | '?hireDate': '"1000-03-10"' }, 108 | { '?givenName': '"John"', 109 | '?familyName': '"Smith"', 110 | '?hireDate': '"2015-01-28"' }, 111 | { '?givenName': '"Francis"', 112 | '?familyName': '"Jones"', 113 | '?hireDate': '"2015-02-13"' } 114 | ] 115 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes3.rq`) 116 | testQuery(graph, query, expected, done) 117 | }) 118 | 119 | it('executes filters based on a variable (String comparison not Date)', (done) => { 120 | var expected = [ 121 | { '?givenName': '"Jane"', 122 | '?familyName': '"Berger"', 123 | '?hireDate': '"1000-03-10"' } 124 | ] 125 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes4.rq`) 126 | testQuery(graph, query, expected, done) 127 | }) 128 | 129 | it('executes three queries selecting three variables (again)', (done) => { 130 | var expected = [ 131 | { '?givenName': '"John"', 132 | '?familyName': '"Smith"', 133 | '?oDate': '"2015-03-15"' }, 134 | { '?givenName': '"Heidi"', 135 | '?familyName': '"Smith"', 136 | '?oDate': '"2015-01-30"' }, 137 | { '?givenName': '"John"', 138 | '?familyName': '"Smith"', 139 | '?oDate': '"2015-01-30"' } 140 | ] 141 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes5.rq`) 142 | testQuery(graph, query, expected, done) 143 | }) 144 | 145 | it('executes three queries selecting three variables (with OPTIONAL)', (done) => { 146 | var expected = [ 147 | { '?givenName': '"Jane"', 148 | '?familyName': '"Berger"', 149 | '?oDate': null }, 150 | { '?givenName': '"Heidi"', 151 | '?familyName': '"Smith"', 152 | '?oDate': '"2015-01-30"' }, 153 | { '?givenName': '"John"', 154 | '?familyName': '"Smith"', 155 | '?oDate': '"2015-03-15"' }, 156 | { '?givenName': '"John"', 157 | '?familyName': '"Smith"', 158 | '?oDate': '"2015-01-30"' }, 159 | { '?givenName': '"Francis"', 160 | '?familyName': '"Jones"', 161 | '?oDate': null } 162 | ] 163 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes6.rq`) 164 | testQuery(graph, query, expected, done) 165 | }) 166 | 167 | // NOT EXISTS is not implemented in Sparql iterator 168 | xit('executes three queries selecting three variables (with NOT EXISTS)', (done) => { 169 | var expected = [] 170 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes7.rq`) 171 | testQuery(graph, query, expected, done) 172 | }) 173 | 174 | xit('executes two queries selecting three variables (with BIND)', (done) => { 175 | var expected = [] 176 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes8.rq`) 177 | testQuery(graph, query, expected, done) 178 | }) 179 | 180 | xit('executes two queries selecting three variables (with BIND and CONCAT)', (done) => { 181 | var expected = [] 182 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes9.rq`) 183 | testQuery(graph, query, expected, done) 184 | }) 185 | 186 | xit('executes two queries constructing new triples (with BIND and CONCAT)', (done) => { 187 | var expected = [] 188 | var query = path.join(__dirname, `./queries/sparqlIn11Minutes11.rq`) 189 | testQuery(graph, query, expected, done) 190 | }) 191 | }) 192 | }) 193 | -------------------------------------------------------------------------------- /test/triple-store.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const expect = require('chai').expect 3 | const ram = require('random-access-memory') 4 | const hypergraph = require('../index') 5 | 6 | function ramStore (filename) { 7 | // filename will be one of: data, bitfield, tree, signatures, key, secret_key 8 | // the data file will contain all your data concattenated. 9 | // just store all files in ram by returning a random-access-memory instance 10 | return ram() 11 | } 12 | 13 | describe('a basic triple store', function () { 14 | let db 15 | 16 | beforeEach(function () { 17 | db = hypergraph(ramStore) 18 | }) 19 | 20 | afterEach(function (done) { 21 | db.close(done) 22 | }) 23 | 24 | it('should put a triple', function (done) { 25 | var triple = { subject: 'a', predicate: 'b', object: 'c' } 26 | db.put(triple, done) 27 | }) 28 | 29 | describe('with a triple inserted', function () { 30 | var triple 31 | 32 | beforeEach(function (done) { 33 | triple = { subject: 'a', predicate: 'b', object: 'c' } 34 | db.put(triple, done) 35 | }) 36 | 37 | it('should get it specifiying the subject', function (done) { 38 | db.get({ subject: 'a' }, (err, list) => { 39 | expect(list).to.eql([triple]) 40 | done(err) 41 | }) 42 | }) 43 | 44 | it('should get it specifiying the object', function (done) { 45 | db.get({ object: 'c' }, (err, list) => { 46 | expect(list).to.eql([triple]) 47 | done(err) 48 | }) 49 | }) 50 | 51 | it('should get it specifiying the predicate', function (done) { 52 | db.get({ predicate: 'b' }, (err, list) => { 53 | expect(list).to.eql([triple]) 54 | done(err) 55 | }) 56 | }) 57 | 58 | it('should get it specifiying the subject and the predicate', function (done) { 59 | db.get({ subject: 'a', predicate: 'b' }, (err, list) => { 60 | expect(list).to.eql([triple]) 61 | done(err) 62 | }) 63 | }) 64 | 65 | it('should get it specifiying the subject and the object', function (done) { 66 | db.get({ subject: 'a', object: 'c' }, (err, list) => { 67 | expect(list).to.eql([triple]) 68 | done(err) 69 | }) 70 | }) 71 | 72 | it('should get it specifiying the predicate and the object', function (done) { 73 | db.get({ predicate: 'b', object: 'c' }, (err, list) => { 74 | expect(list).to.eql([triple]) 75 | done(err) 76 | }) 77 | }) 78 | 79 | it('should get it specifiying the subject and falsy params', function (done) { 80 | db.get({ subject: 'a', predicate: false, object: null }, (err, list) => { 81 | expect(list).to.eql([triple]) 82 | done(err) 83 | }) 84 | }); 85 | 86 | ['subject', 'predicate', 'object'].forEach(function (type) { 87 | it('should get nothing if nothing matches an only ' + type + ' query', 88 | function (done) { 89 | var query = {} 90 | query[type] = 'notfound' 91 | db.get(query, (err, list) => { 92 | expect(list).to.eql([]) 93 | done(err) 94 | }) 95 | }) 96 | }) 97 | 98 | it('should return the triple through the getStream interface', function (done) { 99 | var stream = db.getStream({ predicate: 'b' }) 100 | stream.on('data', function (data) { 101 | expect(data).to.eql(triple) 102 | }) 103 | stream.on('end', done) 104 | }) 105 | 106 | it('should return the triple through the getStream interface with falsy params', function (done) { 107 | var stream = db.getStream({ subject: null, predicate: 'b', object: false }) 108 | stream.on('data', function (data) { 109 | expect(data).to.eql(triple) 110 | }) 111 | stream.on('end', done) 112 | }) 113 | 114 | it('should get the triple if limit 1 is used', function (done) { 115 | db.get({}, { limit: 1 }, (err, list) => { 116 | expect(list).to.eql([triple]) 117 | done(err) 118 | }) 119 | }) 120 | 121 | it('should get the triple if limit 0 is used', function (done) { 122 | db.get({}, { limit: 0 }, (err, list) => { 123 | expect(list).to.eql([triple]) 124 | done(err) 125 | }) 126 | }) 127 | 128 | it('should get the triple if offset 0 is used', function (done) { 129 | db.get({}, { offset: 0 }, (err, list) => { 130 | expect(list).to.eql([triple]) 131 | done(err) 132 | }) 133 | }) 134 | 135 | it('should not get the triple if offset 1 is used', function (done) { 136 | db.get({}, { offset: 1 }, (err, list) => { 137 | expect(list).to.eql([]) 138 | done(err) 139 | }) 140 | }) 141 | }) 142 | 143 | it('should put an array of triples', function (done) { 144 | var t1 = { subject: 'a', predicate: 'b', object: 'c' } 145 | var t2 = { subject: 'a', predicate: 'b', object: 'd' } 146 | db.put([t1, t2], done) 147 | }) 148 | 149 | it('should get only triples with exact match of subjects', function (done) { 150 | var t1 = { subject: 'a1', predicate: 'b', object: 'c' } 151 | var t2 = { subject: 'a', predicate: 'b', object: 'd' } 152 | db.put([t1, t2], function () { 153 | db.get({ subject: 'a' }, function (err, matched) { 154 | expect(matched.length).to.eql(1) 155 | expect(matched[0]).to.eql(t2) 156 | done(err) 157 | }) 158 | }) 159 | }) 160 | 161 | describe('with special characters', function () { 162 | it('should support string contain ::', function (done) { 163 | var t1 = { subject: 'a', predicate: 'b', object: 'c' } 164 | var t2 = { subject: 'a::a::a', predicate: 'b', object: 'c' } 165 | db.put([t1, t2], function () { 166 | db.get({ subject: 'a' }, (err, values) => { 167 | expect(values).to.have.lengthOf(1) 168 | done(err) 169 | }) 170 | }) 171 | }) 172 | it('should support string contain \\::', function (done) { 173 | var t1 = { subject: 'a', predicate: 'b', object: 'c' } 174 | var t2 = { subject: 'a\\::a', predicate: 'b', object: 'c' } 175 | db.put([t1, t2], function () { 176 | db.get({ subject: 'a' }, (err, values) => { 177 | expect(values).to.have.lengthOf(1) 178 | done(err) 179 | }) 180 | }) 181 | }) 182 | it('should support string end with :', function (done) { 183 | var t1 = { subject: 'a', predicate: 'b', object: 'c' } 184 | var t2 = { subject: 'a:', predicate: 'b', object: 'c' } 185 | db.put([t1, t2], function () { 186 | db.get({ subject: 'a:' }, (err, values) => { 187 | expect(values).to.have.lengthOf(1) 188 | expect(values[0].subject).to.equal('a:') 189 | done(err) 190 | }) 191 | }) 192 | }) 193 | it('should support string end with \\', function (done) { 194 | var t1 = { subject: 'a', predicate: 'b', object: 'c' } 195 | var t2 = { subject: 'a\\', predicate: 'b', object: 'c' } 196 | db.put([t1, t2], function () { 197 | db.get({ subject: 'a\\' }, (err, values) => { 198 | expect(values).to.have.lengthOf(1) 199 | expect(values[0].subject).to.equal('a\\') 200 | done(err) 201 | }) 202 | }) 203 | }) 204 | }) 205 | 206 | it('should put a triple with an object to false', function (done) { 207 | var t = { subject: 'a', predicate: 'b', object: false } 208 | db.put(t, function () { 209 | // accessing underlying db instance 210 | db.db.get('spo/a/b/false', done) 211 | }) 212 | }) 213 | 214 | describe('with two triple inserted with the same predicate', function () { 215 | var triple1, 216 | triple2 217 | 218 | beforeEach(function (done) { 219 | triple1 = { subject: 'a1', predicate: 'b', object: 'c' } 220 | triple2 = { subject: 'a2', predicate: 'b', object: 'd' } 221 | db.put([triple1, triple2], done) 222 | }) 223 | 224 | it('should get one by specifiying the subject', function (done) { 225 | db.get({ subject: 'a1' }, (err, list) => { 226 | expect(list).to.eql([triple1]) 227 | done(err) 228 | }) 229 | }) 230 | 231 | it('should get one by specifiying the subject and a falsy predicate', function (done) { 232 | db.get({ subject: 'a1', predicate: null }, (err, list) => { 233 | expect(list).to.eql([triple1]) 234 | done(err) 235 | }) 236 | }) 237 | 238 | it('should get two by specifiying the predicate', function (done) { 239 | db.get({ predicate: 'b' }, (err, list) => { 240 | expect(list).to.eql([triple1, triple2]) 241 | done(err) 242 | }) 243 | }) 244 | 245 | it('should get two by specifiying the predicate and a falsy subject', function (done) { 246 | db.get({ subject: null, predicate: 'b' }, (err, list) => { 247 | expect(list).to.eql([triple1, triple2]) 248 | done(err) 249 | }) 250 | }) 251 | 252 | it('should remove one and still return the other', function (done) { 253 | db.del(triple2, function () { 254 | db.get({ predicate: 'b' }, (err, list) => { 255 | expect(list).to.eql([triple1]) 256 | done(err) 257 | }) 258 | }) 259 | }) 260 | 261 | it('should return both triples through the getStream interface', function (done) { 262 | var triples = [triple1, triple2] 263 | var stream = db.getStream({ predicate: 'b' }) 264 | stream.on('data', function (data) { 265 | expect(data).to.eql(triples.shift()) 266 | }) 267 | 268 | stream.on('end', done) 269 | }) 270 | 271 | it('should return only one triple with limit 1', function (done) { 272 | db.get({ predicate: 'b' }, { limit: 1 }, (err, list) => { 273 | expect(list).to.eql([triple1]) 274 | done(err) 275 | }) 276 | }) 277 | 278 | it('should return two triples with limit 2', function (done) { 279 | db.get({ predicate: 'b' }, { limit: 2 }, (err, list) => { 280 | expect(list).to.eql([triple1, triple2]) 281 | done(err) 282 | }) 283 | }) 284 | 285 | it('should return three triples with limit 3', function (done) { 286 | db.get({ predicate: 'b' }, { limit: 3 }, (err, list) => { 287 | expect(list).to.eql([triple1, triple2]) 288 | done(err) 289 | }) 290 | }) 291 | 292 | it('should support limit over streams', function (done) { 293 | var triples = [triple1] 294 | var stream = db.getStream({ predicate: 'b' }, { limit: 1 }) 295 | stream.on('data', function (data) { 296 | expect(data).to.eql(triples.shift()) 297 | }) 298 | 299 | stream.on('end', done) 300 | }) 301 | 302 | it('should return only one triple with offset 1', function (done) { 303 | db.get({ predicate: 'b' }, { offset: 1 }, (err, list) => { 304 | expect(list).to.eql([triple2]) 305 | done(err) 306 | }) 307 | }) 308 | 309 | it('should return only no triples with offset 2', function (done) { 310 | db.get({ predicate: 'b' }, { offset: 2 }, (err, list) => { 311 | expect(list).to.eql([]) 312 | done(err) 313 | }) 314 | }) 315 | 316 | it('should support offset over streams', function (done) { 317 | var triples = [triple2] 318 | var stream = db.getStream({ predicate: 'b' }, { offset: 1 }) 319 | stream.on('data', function (data) { 320 | expect(data).to.eql(triples.shift()) 321 | }) 322 | 323 | stream.on('end', done) 324 | }) 325 | 326 | xit('should return the triples in reverse order with reverse true', function (done) { 327 | db.get({ predicate: 'b', reverse: true }, (err, list) => { 328 | expect(list).to.eql([triple2, triple1]) 329 | done(err) 330 | }) 331 | }) 332 | 333 | xit('should return the last triple with reverse true and limit 1', function (done) { 334 | db.get({ predicate: 'b', reverse: true, limit: 1 }, (err, list) => { 335 | expect(list).to.eql([triple2]) 336 | done(err) 337 | }) 338 | }) 339 | 340 | xit('should support reverse over streams', function (done) { 341 | var triples = [triple2, triple1] 342 | var stream = db.getStream({ predicate: 'b', reverse: true }) 343 | stream.on('data', function (data) { 344 | expect(data).to.eql(triples.shift()) 345 | }) 346 | 347 | stream.on('end', done) 348 | }) 349 | }) 350 | 351 | describe('with two triple inserted with the same predicate and same object', function () { 352 | var triple1 353 | var triple2 354 | 355 | beforeEach(function (done) { 356 | triple1 = { subject: 'a', predicate: 'b', object: 'c' } 357 | triple2 = { subject: 'a2', predicate: 'b', object: 'c' } 358 | db.put([triple1, triple2], done) 359 | }) 360 | 361 | it('should get one by specifiying the subject', function (done) { 362 | db.get({ subject: 'a' }, (err, list) => { 363 | expect(list).to.eql([triple1]) 364 | done(err) 365 | }) 366 | }) 367 | 368 | it('should get one by specifiying the exact triple', function (done) { 369 | db.get({ subject: 'a', predicate: 'b', object: 'c' }, (err, list) => { 370 | expect(list).to.eql([triple1]) 371 | done(err) 372 | }) 373 | }) 374 | 375 | it('should get one by specifiying the subject and a falsy predicate', function (done) { 376 | db.get({ subject: 'a', predicate: null }, (err, list) => { 377 | expect(list).to.eql([triple1]) 378 | done(err) 379 | }) 380 | }) 381 | 382 | it('should get two by specifiying the predicate', function (done) { 383 | db.get({ predicate: 'b' }, (err, list) => { 384 | expect(list).to.eql([triple1, triple2]) 385 | done(err) 386 | }) 387 | }) 388 | 389 | it('should get two by specifiying the predicate and a falsy subject', function (done) { 390 | db.get({ subject: null, predicate: 'b' }, (err, list) => { 391 | expect(list).to.eql([triple1, triple2]) 392 | done(err) 393 | }) 394 | }) 395 | 396 | it('should remove one and still return the other', function (done) { 397 | db.del(triple2, function () { 398 | db.get({ predicate: 'b' }, (err, list) => { 399 | expect(list).to.eql([triple1]) 400 | done(err) 401 | }) 402 | }) 403 | }) 404 | 405 | it('should return both triples through the getStream interface', function (done) { 406 | var triples = [triple1, triple2] 407 | var stream = db.getStream({ predicate: 'b' }) 408 | stream.on('data', function (data) { 409 | expect(data).to.eql(triples.shift()) 410 | }) 411 | 412 | stream.on('end', done) 413 | }) 414 | 415 | it('should return only one triple with limit 1', function (done) { 416 | db.get({ predicate: 'b' }, { limit: 1 }, (err, list) => { 417 | expect(list).to.eql([triple1]) 418 | done(err) 419 | }) 420 | }) 421 | 422 | it('should return two triples with limit 2', function (done) { 423 | db.get({ predicate: 'b' }, { limit: 2 }, (err, list) => { 424 | expect(list).to.eql([triple1, triple2]) 425 | done(err) 426 | }) 427 | }) 428 | 429 | it('should return three triples with limit 3', function (done) { 430 | db.get({ predicate: 'b' }, { limit: 3 }, (err, list) => { 431 | expect(list).to.eql([triple1, triple2]) 432 | done(err) 433 | }) 434 | }) 435 | 436 | it('should support limit over streams', function (done) { 437 | var triples = [triple1] 438 | var stream = db.getStream({ predicate: 'b' }, { limit: 1 }) 439 | stream.on('data', function (data) { 440 | expect(data).to.eql(triples.shift()) 441 | }) 442 | 443 | stream.on('end', done) 444 | }) 445 | 446 | it('should return only one triple with offset 1', function (done) { 447 | db.get({ predicate: 'b' }, { offset: 1 }, (err, list) => { 448 | expect(list).to.eql([triple2]) 449 | done(err) 450 | }) 451 | }) 452 | 453 | it('should return only no triples with offset 2', function (done) { 454 | db.get({ predicate: 'b' }, { offset: 2 }, (err, list) => { 455 | expect(list).to.eql([]) 456 | done(err) 457 | }) 458 | }) 459 | 460 | it('should support offset over streams', function (done) { 461 | var triples = [triple2] 462 | var stream = db.getStream({ predicate: 'b' }, { offset: 1 }) 463 | stream.on('data', function (data) { 464 | expect(data).to.eql(triples.shift()) 465 | }) 466 | 467 | stream.on('end', done) 468 | }) 469 | 470 | xit('should return the triples in reverse order with reverse true', function (done) { 471 | db.get({ predicate: 'b', reverse: true }, (err, list) => { 472 | expect(list).to.eql([triple2, triple1]) 473 | done(err) 474 | }) 475 | }) 476 | 477 | xit('should return the last triple with reverse true and limit 1', function (done) { 478 | db.get({ predicate: 'b', reverse: true, limit: 1 }, (err, list) => { 479 | expect(list).to.eql([triple2]) 480 | done(err) 481 | }) 482 | }) 483 | 484 | xit('should support reverse over streams', function (done) { 485 | var triples = [triple2, triple1] 486 | var stream = db.getStream({ predicate: 'b', reverse: true }) 487 | 488 | stream.on('data', function (data) { 489 | expect(data).to.eql(triples.shift()) 490 | }) 491 | 492 | stream.on('end', done) 493 | }) 494 | }) 495 | 496 | xdescribe('with 10 triples inserted', function () { 497 | beforeEach(function (done) { 498 | var triples = [] 499 | for (var i = 0; i < 10; i++) { 500 | triples[i] = { subject: 's', predicate: 'p', object: 'o' + i } 501 | } 502 | db.put(triples, done) 503 | }) 504 | 505 | if (!process.browser) { 506 | it('should return the approximate size', function (done) { 507 | db.approximateSize({ predicate: 'b' }, function (err, size) { 508 | expect(size).to.be.a('number') 509 | done(err) 510 | }) 511 | }) 512 | } 513 | }) 514 | 515 | it('should put triples using a stream', function (done) { 516 | var t1 = { subject: 'a', predicate: 'b', object: 'c' } 517 | var t2 = { subject: 'a', predicate: 'b', object: 'd' } 518 | var stream = db.putStream() 519 | stream.on('end', done) 520 | 521 | stream.write(t1) 522 | stream.end(t2) 523 | }) 524 | 525 | it('should store the triples written using a stream', function (done) { 526 | var t1 = { subject: 'a', predicate: 'b', object: 'c' } 527 | var t2 = { subject: 'a', predicate: 'b', object: 'd' } 528 | var stream = db.putStream() 529 | 530 | stream.write(t1) 531 | stream.end(t2) 532 | 533 | stream.on('end', function () { 534 | var triples = [t1, t2] 535 | var readStream = db.getStream({ predicate: 'b' }) 536 | 537 | readStream.on('data', function (data) { 538 | expect(data).to.eql(triples.shift()) 539 | }) 540 | 541 | readStream.on('end', done) 542 | }) 543 | }) 544 | 545 | it('should del the triples using a stream', function (done) { 546 | var t1 = { subject: 'a', predicate: 'b', object: 'c' } 547 | var t2 = { subject: 'a', predicate: 'b', object: 'd' } 548 | var stream = db.putStream() 549 | 550 | stream.write(t1) 551 | stream.end(t2) 552 | 553 | stream.on('end', function () { 554 | var delStream = db.delStream() 555 | delStream.write(t1) 556 | delStream.end(t2) 557 | 558 | delStream.on('end', function () { 559 | var readStream = db.getStream({ predicate: 'b' }) 560 | 561 | var results = [] 562 | readStream.on('data', function (data) { 563 | results.push(data) 564 | }) 565 | 566 | readStream.on('end', function () { 567 | expect(results).to.have.property('length', 0) 568 | done() 569 | }) 570 | }) 571 | }) 572 | }) 573 | 574 | it('should support filtering', function (done) { 575 | var triple1 = { subject: 'a', predicate: 'b', object: 'd' } 576 | var triple2 = { subject: 'a', predicate: 'b', object: 'c' } 577 | 578 | db.put([triple1, triple2], function () { 579 | function filter (triple) { 580 | return triple.object === 'd' 581 | } 582 | 583 | db.get({ subject: 'a', predicate: 'b' }, { filter: filter }, (err, results) => { 584 | expect(results).to.eql([triple1]) 585 | done(err) 586 | }) 587 | }) 588 | }) 589 | }) 590 | 591 | describe('deferred open support', function () { 592 | var db 593 | 594 | afterEach(function (done) { 595 | db.close(done) 596 | }) 597 | 598 | it('should support deferred search', function (done) { 599 | db = hypergraph(ramStore) 600 | db.search([{ predicate: 'likes' }], function () { 601 | done() 602 | }) 603 | }) 604 | }) 605 | 606 | describe('generateBatch', function () { 607 | var db 608 | context('without index option', () => { 609 | beforeEach(function () { 610 | db = hypergraph(ramStore) 611 | }) 612 | 613 | afterEach(function (done) { 614 | db.close(done) 615 | }) 616 | it('should generate a batch from a triple with length 6', function () { 617 | var triple = { subject: 'a', predicate: 'b', object: 'c', extra: 'data' } 618 | var ops = db._generateBatch(triple) 619 | expect(ops).to.have.property('length', 6) 620 | ops.forEach(function (op) { 621 | expect(op).to.have.property('type', 'put') 622 | expect(JSON.parse(op.value)).to.eql({ extra: 'data' }) 623 | }) 624 | }) 625 | 626 | it('should generate a batch of type', function () { 627 | var triple = { subject: 'a', predicate: 'b', object: 'c' } 628 | var ops = db._generateBatch(triple, 'del') 629 | expect(ops).to.have.property('length', 6) 630 | ops.forEach(function (op) { 631 | expect(op).to.have.property('type', 'del') 632 | expect(JSON.parse(op.value)).to.eql(null) 633 | }) 634 | }) 635 | }) 636 | context('with index option set to small', () => { 637 | beforeEach(function () { 638 | db = hypergraph(ramStore, null, { index: 'tri' }) 639 | }) 640 | 641 | afterEach(function (done) { 642 | db.close(done) 643 | }) 644 | it('should generate a batch from a triple with length 3', function () { 645 | var triple = { subject: 'a', predicate: 'b', object: 'c', other: 'stuff' } 646 | var ops = db._generateBatch(triple) 647 | expect(ops).to.have.property('length', 3) 648 | ops.forEach(function (op) { 649 | expect(op).to.have.property('type', 'put') 650 | expect(JSON.parse(op.value)).to.eql({ other: 'stuff' }) 651 | }) 652 | }) 653 | 654 | it('should generate a batch of type', function () { 655 | var triple = { subject: 'a', predicate: 'b', object: 'c', other: 'stuff' } 656 | var ops = db._generateBatch(triple, 'del') 657 | expect(ops).to.have.property('length', 3) 658 | ops.forEach(function (op) { 659 | expect(op).to.have.property('type', 'del') 660 | expect(JSON.parse(op.value)).to.eql(null) 661 | }) 662 | }) 663 | }) 664 | }) 665 | -------------------------------------------------------------------------------- /test/utils.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const expect = require('chai').expect 3 | const utils = require('../lib/utils') 4 | 5 | describe('util functions', () => { 6 | describe('encodeTriple', () => { 7 | it('does nothing if escape is not needed', () => { 8 | var encoded = utils.encodeTriple({ subject: 'a-subject', object: 'a-object', predicate: 'a-predicate', data: 'some data' }) 9 | expect(encoded).to.eql({ 10 | subject: 'a-subject', 11 | object: 'a-object', 12 | predicate: 'a-predicate', 13 | data: 'some data' 14 | }) 15 | }) 16 | it('escapes all forward slashes', () => { 17 | var encoded = utils.encodeTriple({ subject: 'a/subject', object: 'a-object', predicate: 'a/predicate', data: 'some/data' }) 18 | expect(encoded).to.eql({ 19 | subject: 'a%2Fsubject', 20 | object: 'a-object', 21 | predicate: 'a%2Fpredicate', 22 | data: 'some/data' 23 | }) 24 | }) 25 | it('escapes partial triples', () => { 26 | var encoded = utils.encodeTriple({ predicate: 'a/predicate', data: 'some/data' }) 27 | expect(encoded).to.eql({ 28 | predicate: 'a%2Fpredicate', 29 | data: 'some/data' 30 | }) 31 | }) 32 | context('with prefixes passed as argument', () => { 33 | it('addes known prefixes', () => { 34 | var triple = { subject: 'http://nothing.info/forever#maybe', object: 'http://everything.org/tomorrow', data: 'some/data' } 35 | var encoded = utils.encodeTriple(triple, { 36 | nothing: 'http://nothing.info/forever', 37 | inf: 'http://everything.org/' 38 | }) 39 | expect(encoded).to.eql({ 40 | subject: 'nothing:#maybe', 41 | object: 'inf:tomorrow', 42 | data: 'some/data' 43 | }) 44 | }) 45 | }) 46 | }) 47 | 48 | describe('encodeKey', () => { 49 | it('generates a unique index key for a triple (spo)', () => { 50 | var key = utils.encodeKey('spo', { subject: 'a-subject', object: 'a-object', predicate: 'a-predicate' }) 51 | expect(key).to.eql('spo/a-subject/a-predicate/a-object') 52 | }) 53 | it('generates a unique index key for a triple (spo)', () => { 54 | var key = utils.encodeKey('sop', { subject: 'a-subject', object: 'a-object', predicate: 'a-predicate' }) 55 | expect(key).to.eql('sop/a-subject/a-object/a-predicate') 56 | }) 57 | it('generates a unique index key for a triple (osp)', () => { 58 | var key = utils.encodeKey('osp', { subject: 'a-subject', object: 'a-object', predicate: 'a-predicate' }) 59 | expect(key).to.eql('osp/a-object/a-subject/a-predicate') 60 | }) 61 | 62 | it('escapes forward slashes in the triple', () => { 63 | var key = utils.encodeKey('spo', { subject: 'a%2Fsubject' }) 64 | expect(key).to.eql('spo/a%2Fsubject/') 65 | }) 66 | }) 67 | 68 | describe('decodeKey', () => { 69 | it('generates a triple from a index key (spo)', () => { 70 | var triple = utils.decodeKey('spo/a-subject/a-predicate/a-object') 71 | expect(triple).to.eql({ subject: 'a-subject', object: 'a-object', predicate: 'a-predicate' }) 72 | }) 73 | it('generates a triple from a index key (spo)', () => { 74 | var triple = utils.decodeKey('sop/a-subject/a-object/a-predicate') 75 | expect(triple).to.eql({ subject: 'a-subject', object: 'a-object', predicate: 'a-predicate' }) 76 | }) 77 | it('generates a triple from a index key (osp)', () => { 78 | var triple = utils.decodeKey('osp/a-object/a-subject/a-predicate') 79 | expect(triple).to.eql({ subject: 'a-subject', object: 'a-object', predicate: 'a-predicate' }) 80 | }) 81 | 82 | it('unescapes escaped /‘s from index key (spo)', () => { 83 | var triple = utils.decodeKey('spo/a%2Fsubject/a%2Fpredicate/a%2Fobject') 84 | expect(triple).to.eql({ subject: 'a/subject', object: 'a/object', predicate: 'a/predicate' }) 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /test/variable.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | const expect = require('chai').expect 3 | var Variable = require('../lib/Variable') 4 | 5 | describe('Variable', () => { 6 | it('should have a name', () => { 7 | var v = new Variable('x') 8 | expect(v).to.have.property('name', 'x') 9 | }) 10 | 11 | it('should have a name (bis)', () => { 12 | var v = new Variable('y') 13 | expect(v).to.have.property('name', 'y') 14 | }) 15 | 16 | describe('#isBound', () => { 17 | var instance 18 | 19 | beforeEach(() => { 20 | instance = new Variable('x') 21 | }) 22 | 23 | it('should return true if there is a key in the solution', () => { 24 | expect(instance.isBound({ x: 'hello' })).to.equal(true) 25 | }) 26 | 27 | it('should return false if there is no key in the solution', () => { 28 | expect(instance.isBound({})).to.equal(false) 29 | }) 30 | 31 | it('should return false if there is another key in the solution', () => { 32 | expect(instance.isBound({ hello: 'world' })).to.equal(false) 33 | }) 34 | }) 35 | 36 | describe('#bind', () => { 37 | var instance 38 | 39 | beforeEach(() => { 40 | instance = new Variable('x') 41 | }) 42 | 43 | it('should return a different object', () => { 44 | var solution = {} 45 | expect(instance.bind(solution, 'hello')).to.not.be.equal(solution) 46 | }) 47 | 48 | it('should set an element in the solution', () => { 49 | var solution = {} 50 | expect(instance.bind(solution, 'hello')).to.be.deep.equal({ x: 'hello' }) 51 | }) 52 | 53 | it('should copy values', () => { 54 | var solution = { y: 'world' } 55 | expect(instance.bind(solution, 'hello')).to.be.deep.equal({ x: 'hello', y: 'world' }) 56 | }) 57 | }) 58 | 59 | describe('#isBindable', () => { 60 | var instance 61 | 62 | beforeEach(() => { 63 | instance = new Variable('x') 64 | }) 65 | 66 | it('should bind to the same value', () => { 67 | expect(instance.isBindable({ x: 'hello' }, 'hello')).to.equal(true) 68 | }) 69 | 70 | it('should not bind to a different value', () => { 71 | expect(instance.isBindable({ x: 'hello' }, 'hello2')).to.equal(false) 72 | }) 73 | 74 | it('should bind if the key is not present', () => { 75 | expect(instance.isBindable({}, 'hello')).to.equal(true) 76 | }) 77 | }) 78 | }) 79 | --------------------------------------------------------------------------------