Code Monkey home page Code Monkey logo

quadstore's Introduction

Logo

QUADSTORE

Quadstore is a LevelDB-backed RDF graph database / triplestore for JavaScript runtimes (browsers, Node.js, Deno, Bun, ...) written in TypeScript.

Table of contents

Example of basic usage

import { MemoryLevel } from 'memory-level';
import { DataFactory } from 'rdf-data-factory';
import { Quadstore } from 'quadstore';
import { Engine } from 'quadstore-comunica';

// Any implementation of AbstractLevel can be used.
const backend = new MemoryLevel();

// Implementation of the RDF/JS DataFactory interface
const df = new DataFactory();           

// Store and query engine are separate modules
const store = new Quadstore({backend, dataFactory: df});
const engine = new Engine(store);

// Open the store
await store.open();

// Put a single quad into the store using Quadstore's API
await store.put(df.quad(                      
  df.namedNode('http://example.com/subject'),
  df.namedNode('http://example.com/predicate'),
  df.namedNode('http://example.com/object'),
  df.defaultGraph(),
));

// Retrieves all quads using Quadstore's API  
const { items } = await store.get({});

// Retrieves all quads using RDF/JS Stream interfaces
const quadsStream = store.match(undefined, undefined, undefined, undefined);
quadsStream.on('data', quad => console.log(quad));

// Queries the store via RDF/JS Query interfaces
const bindingsStream = await engine.queryBindings('SELECT * {?s ?p ?o}');
bindingsStream.on('data', binding => console.log(binding));

Status

Active, under development.

Changelog

See CHANGELOG.md.

Roadmap

We're currently working on the following features:

  • optimizing SPARQL performance by pushing filters down from the engine to the persistence layer

We're also evaluating the following features for future developments:

Notes

Usage

Parsing and serializing RDF

quadstore is compatible with all parsers and serializers implementing the relevant RDF/JS interfaces, such as n3 and @rdfjs/formats. See https://rdf.js.org for an open list of such libraries.

For example, here is how to use n3 in order to parse a Turtle file into an instance of Quadstore in a streaming fashion, with full backpressure handling, using classic-level as the backend:

import { Quadstore } from 'quadstore';
import { ClassicLevel } from 'classic-level';
import { DataFactory, StreamParser } from 'n3';
const store = new Quadstore({
  backend: new ClassicLevel('/path/to/db'),
  dataFactory: DataFactory,
});
await store.open();
const reader = fs.createReadStream('/path/to/file.ttl');
const parser = new StreamParser({ format: 'text/turtle' });
await store.putStream(reader.pipe(parser), { batchSize: 100 });
await store.close();

quadstore does not include any RDF parsing and/or serialization capability by choice as no subset of formats would meet the requirements of every use case and shipping support for all mainstream RDF formats would result in exceedingly high bundle sizes.

Storage backends

quadstore can work with any storage backend that implements the AbstractLevel interface. An incomplete list of available backends is available at level/awesome#stores.

Our test suite focuses on the following backends:

Data model and return values

Except for those related to the RDF/JS stream interfaces, quadstore's API is promise-based and all methods return objects that include both the actual query results and the relevant metadata.

Objects returned by quadstore's APIs have the type property set to one of the following values:

  • "VOID" - when there's no data returned by the database, such as with the put method;
  • "QUADS" - when a query returns a collection of quads;
  • "APPROXIMATE_SIZE" - when a query returns an approximate count of how many matching items are present.

For those methods that return objects with the type property set to "QUADS", quadstore provides query results either in streaming mode or in non-streaming mode.

Streaming methods such as getStream return objects with the iterator property set to an instance of AsyncIterator, an implementation of a subset of the stream.Readable interface.

Non-streaming methods such as get return objects with the items property set to an array of quads.

Quads are returned as and expected to be instances of the RDF/JS Quad interface as produced by the implementation of the RDF/JS DataFactory interface passed to the Quadstore constructor.

Matching patterns, such as those used in the get and getStream methods, are expected to be maps of term names to instances of the RDF/JS Term interface.

Quadstore class

const Quadstore = require('quadstore').Quadstore;
const store = new Quadstore(opts);

Instantiates a new store. Supported properties for the opts argument are:

opts.backend

The opts.backend option must be an instance of a leveldb backend. See storage backends.

opts.dataFactory

The dataFactory option must be an implementation of the RDF/JS DataFactory interface. Some of the available implementations:

opts.indexes

The opts.indexes option allows users to configure which indexes will be used by the store. If not set, the store will default to the following indexes:

[
  ['subject', 'predicate', 'object', 'graph'],
  ['object', 'graph', 'subject', 'predicate'],
  ['graph', 'subject', 'predicate', 'object'],
  ['object', 'subject', 'predicate', 'graph'],
  ['predicate', 'object', 'graph', 'subject'],
  ['graph', 'predicate', 'object', 'subject'],
]; 

This option, if present, must be set to an array of term arrays, each of which must represent one of the 24 possible permutations of the four terms subject, predicate, object and graph. Partial indexes are not supported.

The store will automatically select which index(es) to use for a given query based on the available indexes and the query itself. If no suitable index is found for a given query, the store will throw an error.

opts.prefixes

Also, Quadstore can be configured with a prefixes object that defines a reversible mapping of IRIs to abbreviated forms, with the intention of reducing the storage cost where common HTTP prefixes are known in advance.

The prefixes object defines a bijection using two functions expandTerm and compactIri, both of which take a string parameter and return a string, as in the following example:

opts.prefixes = {
  expandTerm: term => term.replace(/^ex:/, 'http://example.com/'),
  compactIri: iri => iri.replace(/^http:\/\/example\.com\//, 'ex:'),
}

This will replace the IRI http://example.com/a with ex:a in storage.

Access to the backend

The backend of a quadstore can be accessed with the db property, to perform additional storage operations independently of quads.

In order to perform write operations atomically with quad storage, the put, multiPut, del, multiDel, patch and multiPatch methods accept a preWrite option which defines a procedure to augment the batch, as in the following example:

await store.put(dataFactory.quad(/* ... */), {
  preWrite: batch => batch.put('my.key', Buffer.from('my.value'))
});

Quadstore.prototype.open()

This method opens the store and throws if the open operation fails for any reason.

Quadstore.prototype.close()

This method closes the store and throws if the open operation fails for any reason.

Quadstore.prototype.get()

const pattern = {graph: dataFactory.namedNode('ex://g')};
const { items } = await store.get(pattern);

Returns an array of all quads within the store matching the specified terms.

This method also accepts an optional opts parameter with the following optional properties:

  • opts.order: array of term names (e.g. ['object']) that represents the desired ordering criteria of returned quads. Equivalent to the ORDER BY clause in SQL.
  • opts.reverse: boolean value that indicates whether to return quads in ascending or descending order. Equivalent to ASC / DESC modifiers in SQL.
  • opts.limit: limit the number of returned quads to the specified value. Equivalent to LIMIT clause in SQL.

Range matching

quadstore supports range-based matching in addition to value-based matching. Ranges can be defined using the gt, gte, lt, lte properties:

const pattern = {
  object: {
    termType: 'Range',
    gt: dataFactory.literal('7', 'http://www.w3.org/2001/XMLSchema#integer')
  }
};
const { items } = await store.get(matchTerms);

Values for literal terms with the following numeric datatypes are matched against their numerical values rather than their literal representations:

http://www.w3.org/2001/XMLSchema#integer
http://www.w3.org/2001/XMLSchema#decimal
http://www.w3.org/2001/XMLSchema#double
http://www.w3.org/2001/XMLSchema#nonPositiveInteger
http://www.w3.org/2001/XMLSchema#negativeInteger
http://www.w3.org/2001/XMLSchema#long
http://www.w3.org/2001/XMLSchema#int
http://www.w3.org/2001/XMLSchema#short
http://www.w3.org/2001/XMLSchema#byte
http://www.w3.org/2001/XMLSchema#nonNegativeInteger
http://www.w3.org/2001/XMLSchema#unsignedLong
http://www.w3.org/2001/XMLSchema#unsignedInt
http://www.w3.org/2001/XMLSchema#unsignedShort
http://www.w3.org/2001/XMLSchema#unsignedByte
http://www.w3.org/2001/XMLSchema#positiveInteger

This is also the case for terms with the following date/time datatypes:

http://www.w3.org/2001/XMLSchema#dateTime

Quadstore.prototype.put()

await store.put(dataFactory.quad(/* ... */));

Stores a new quad. Does not throw or return an error if the quad already exists.

This method also accepts an optional opts parameter with the following properties:

Quadstore.prototype.multiPut()

await store.multiPut([
  dataFactory.quad(/* ... */),
  dataFactory.quad(/* ... */),
]);

Stores new quads. Does not throw or return an error if quads already exists.

This method also accepts an optional opts parameter with the following properties:

Quadstore.prototype.del()

This method deletes a single quad. It Does not throw or return an error if the specified quad is not present in the store.

await store.del(dataFactory.quad(/* ... */));

This method also accepts an optional opts parameter with the following properties:

  • opts.preWrite: this can be set to a function which accepts a chainedBatch and performs additional backend operations atomically with the put operation. See Access to the backend for more information.

Quadstore.prototype.multiDel()

This method deletes multiple quads. It Does not throw or return an error if the specified quads are not present in the store.

await store.multiDel([
  dataFactory.quad(/* ... */),
  dataFactory.quad(/* ... */),
]);

This method also accepts an optional opts parameter with the following properties:

  • opts.preWrite: this can be set to a function which accepts a chainedBatch and performs additional backend operations atomically with the put operation. See Access to the backend for more information.

Quadstore.prototype.patch()

This method deletes one quad and inserts another quad in a single operation. It Does not throw or return an error if the specified quads are not present in the store (delete) or already present in the store (update).

await store.patch(
  dataFactory.quad(/* ... */),  // will be deleted
  dataFactory.quad(/* ... */),  // will be inserted
);

This method also accepts an optional opts parameter with the following properties:

  • opts.preWrite: this can be set to a function which accepts a chainedBatch and performs additional backend operations atomically with the put operation. See Access to the backend for more information.

Quadstore.prototype.multiPatch()

This method deletes and inserts quads in a single operation. It Does not throw or return an error if the specified quads are not present in the store (delete) or already present in the store (update).

// will be deleted
const oldQuads = [ 
    dataFactory.quad(/* ... */),
    dataFactory.quad(/* ... */),
];

// will be inserted
const newQuads = [ // will be inserted
    dataFactory.quad(/* ... */),
    dataFactory.quad(/* ... */),
    dataFactory.quad(/* ... */),        
];

await store.multiPatch(oldQuads, newQuads);

This method also accepts an optional opts parameter with the following properties:

  • opts.preWrite: this can be set to a function which accepts a chainedBatch and performs additional backend operations atomically with the put operation. See Access to the backend for more information.

Quadstore.prototype.getStream()

const pattern = {graph: dataFactory.namedNode('ex://g')};
const { iterator } = await store.getStream(pattern);

Just as QuadStore.prototype.get(), this method supports range matching and the order, reverse and limit options.

Quadstore.prototype.putStream()

await store.putStream(readableStream);

Imports all quads coming through the specified stream.Readable into the store.

This method also accepts an optional opts parameter with the following properties:

Quadstore.prototype.delStream()

await store.delStream(readableStream);

Deletes all quads coming through the specified stream.Readable from the store.

Quadstore.prototype.match()

const subject = dataFactory.namedNode('http://example.com/subject');
const graph = dataFactory.namedNode('http://example.com/graph');
store.match(subject, null, null, graph)
  .on('error', (err) => {})
  .on('data', (quad) => {
    // Quad is produced using dataFactory.quad()
  })
  .on('end', () => {});

Implementation of the RDF/JS Source#match method. Supports range-based matching.

Quadstore.prototype.import()

const readableStream; // A stream.Readable of Quad() instances
store.import(readableStream)
  .on('error', (err) => {})
  .on('end', () => {});

Implementation of the RDF/JS Sink#import method.

Quadstore.prototype.remove()

const readableStream; // A stream.Readable of Quad() instances
store.remove(readableStream)
  .on('error', (err) => {})
  .on('end', () => {});

Implementation of the RDF/JS Store#remove method.

Quadstore.prototype.removeMatches()

const subject = dataFactory.namedNode('http://example.com/subject');
const graph = dataFactory.namedNode('http://example.com/graph');
store.removeMatches(subject, null, null, graph)
  .on('error', (err) => {})
  .on('end', () => {});

Implementation of the RDF/JS Sink#removeMatches method.

Blank nodes and quad scoping

Blank nodes are defined as existential variables in that they merely indicate the existence of an entity rather than act as references to the entity itself.

While the semantics of blank nodes can be rather confusing, one of the most practical consequences of their definition is that two blank nodes having the same label may not refer to the same entity unless both nodes come from the same logical set of quads.

As an example, here's two JSON-LD documents converted to N-Quads using the
JSON-LD playground:

{
  "@id": "http://example.com/bob",
  "foaf:knows": {
    "foaf:name": "Alice"
  }
}
<http://example.com/bob> <foaf:knows> _:b0 .
_:b0 <foaf:name> "Alice" .
{
  "@id": "http://example.com/alice",
  "foaf:knows": {
    "foaf:name": "Bob"
  }
}
<http://example.com/alice> <foaf:knows> _:b0 .
_:b0 <foaf:name> "Bob" .

The N-Quads equivalent for both of these documents contains a blank node with the b0 label. However, although the label is the same, these blank nodes indicate the existence of two different entities. Intuitively, we can say that a blank node is scoped to the logical grouping of quads that contains it, be it a single quad, a document or a stream.

As quadstore treats all write operations as if they were happening within the same scope, importing these two sets of quads would result in a collision of two unrelated blank nodes, leading to a corrupted dataset.

A good way to address these issues is to skolemize skolemize all blank nodes into IRIs / named nodes. However, this is not always possible and / or practical.

The initScope() method returns a Scope instance which can be passed to the put, multiPut and putStream methods. When doing so, quadstore will replace each occurrence of a given blank node with a different blank node having a randomly-generated label, preventing blank node collisions.

Each Scope instance keeps an internal cache of mappings between previously encountered blank nodes and their replacements, so that it is able to always return the same replacement blank node for a given label. Each new mapping is atomically persisted to the store together with its originating quad, leading each scope to be incrementally persisted to the store consistently with each successful put and multiPut operation. This allows scopes to be re-used even across process restarts via the loadScope() method.

Quadstore.prototype.initScope()

Initializes a new, empty scope.

const scope = await store.initScope();
await store.put(quad, { scope });
await store.multiPut(quads, { scope });
await store.putStream(stream, { scope });

Quadstore.prototype.loadScope()

Each Scope instance has an .id property that acts as its unique identifier. The loadScope() method can be used to re-hydrate a scope through its .id:

const scope = await store.initScope();
/* store scope.id somewhere */
/* read the previously-stored scope.id */
const scope = await store.loadScope(scopeId);

Quadstore.prototype.deleteScope()

Deletes all mappings of a given scope from the store.

const scope = await store.initScope();
/* ... */
await store.deleteScope(scope.id);

Quadstore.prototype.deleteAllScopes()

Deletes all mappings of all scopes from the store.

await store.deleteAllScopes();

SPARQL

SPARQL queries can be executed against a Quadstore instance using any query engine capable of querying across RDF/JS data sources.

An example of one such engine is quadstore-comunica, an engine built as a custom distribution and configuration of Comunica that implements the RDF/JS Query spec.:

Comunica is a knowledge graph querying framework. [...] Comunica is a meta query engine using which query engines can be created. It does this by providing a set of modules that can be wired together in a flexible manner. [...] Its primary goal is executing SPARQL queries over one or more interfaces.

In time, quadstore-comunica will be extended with custom query modules that will optimize query performance by pushing some matching and ordering operations down to quadstore itself.

import { MemoryLevel } from 'memory-level';
import { DataFactory } from 'rdf-data-factory';
import { Quadstore } from 'quadstore';
import { Engine } from 'quadstore-comunica';

const backend = new MemoryLevel();
const df = new DataFactory();
const store = new Quadstore({backend, dataFactory: df});
const engine = new Engine(store);

await store.open();

const bindingsStream = await engine.queryBindings('SELECT * {?s ?p ?o}');

More information on quadstore-comunica's repository.

Browser usage

The browser-level backend for levelDB offers support for browser-side persistent storage via IndexedDB.

quadstore can be bundled for browser-side usage via Webpack, preferably using version 5.x. The reference quadstore-browser is meant to help in getting to a working Webpack configuration and also hosts a pre-built bundle with everything that is required to use quadstore in browsers.

Deno usage

quadstore can be used with the Deno runtime via the skypack.dev CDN:

import { DataFactory } from 'https://cdn.skypack.dev/[email protected]';
import { Quadstore } from 'https://cdn.skypack.dev/[email protected]';
import { MemoryLevel } from 'https://cdn.skypack.dev/[email protected]';
import { Engine } from 'https://cdn.skypack.dev/[email protected]';

const backend = new MemoryLevel();
const dataFactory = new DataFactory();
const store = new Quadstore({ backend, dataFactory });
const engine = new Engine(store);

await store.open();
await store.put(dataFactory.quad(
        dataFactory.namedNode('ex://s'),
        dataFactory.namedNode('ex://p'),
        dataFactory.namedNode('ex://o'),
));
const stream = await engine.queryBindings('SELECT * WHERE { ?s ?p ?o }');
stream.on('data', (bindings) => console.log(bindings));

Example usage:

deno run quadstore-test.ts

Performance

Performance is evaluated at tracked at https://github.com/jacoscaz/quadstore-perf

LICENSE

MIT. See LICENSE.md.

quadstore's People

Contributors

antonioru avatar elf-pavlik avatar gsvarovsky avatar jacoscaz avatar peeja avatar teomurgi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

quadstore's Issues

Chaining delete with query

From #32

Finally, being able to use the query interface with filters and joins to delete would be nice. Maybe using the chaining syntax? Maybe using .get() instead of toArray would make a .del() fit nicely at the end of the chain?

SPARQL queries

Implement RdfStore.prototype.sparql(query) method using ldf-client and the /ldf endpoint of #51 .

Allow del with matchPattern

Hi there,

I ran into something I found counter-intuitive. I was trying out db.patch and then switched to db.del and couldn't understand why the delete didn't occur until I realised (reading the doc) that del doesn't support the matchPattern syntax. It would be quite nice for consistency (and functionality) if del acted like the first argument of patch (array means actual deletes, and single object means matchPattern).

Also it might be useful to have a delExisting method like in LD-PATCH and possibly a putNew (like addNew) method?

Finally, being able to use the query interface with filters and joins to delete would be nice. Maybe using the chaining syntax? Maybe using .get() instead of toArray would make a .del() fit nicely at the end of the chain?

Cheers,

Jun

Fix behaviour of getApproximateSize()

There are two issues with QuadStore.prototype.getApproximateSize():

  • LevelDB's .approximateSize() returns the number of bytes, not records. How to convert? I've read somewhere that levelgraph has tried using approximateSize/128 as a reference number. Perhaps we could keep track of the current avg. size of our quads? Would probably need to be re-computed every now and then, though.
  • LevelDB's .approximateSize() seems to return 0 for small(-er) datasets.

RDF/JS + LDF HTTP API

  • GET /ldf
    • LDF-compatible endpoint
  • GET /match
    • Query params: subject, predicate, object, graph, offset, limit
    • Returns a application/trig or application/n-quads stream
  • POST /insert
    • Accepts a application/trig or application/n-quads stream
  • POST /remove
    • Accepts a application/trig or application/n-quads stream
  • Add basic tests for all four endpoints

index-browser.js doesn't export RdfStore

I didn't make PR adding that export wanting to check first if you really need separate browser entry point and quadstore.umd-bundle.js in the repo.

Looking at https://github.com/beautifulinteractions/node-quadstore#browser I would find it sufficient to recommend level-js module, provide snippet from the <script> tag in that section and point people to webpack, rollup, browserify documentation for how they can build their application. Similar to:

https://github.com/rdfjs/N3.js#installation

N3.js seamlessly works in browsers via webpack or browserify. If you're unfamiliar with these tools, you can read webpack: Creating a Bundle โ€“ getting started or Introduction to browserify.
You will need to create a "UMD bundle" and supply a name (e.g. with the -s N3 option in browserify).

Mocha suite fails randomly when using leveldown

When using leveldown as a levelup backend, unit tests fail randomly with no apparent reason. Any specific unit test might succeed during one run and fail during the next one. The issue disappears if we replace leveldown with memdown.

Range queries

Hi there,

I need to do range queries such as { subject: 'foo*' } which would get all the quads with a subject starting with foo. I can already do them today with a query and filter, but of course it would be much faster if using the underlying leveldb index. I guess something like the 10 lines of code of level-range would be great to have in the quadstore API!

Longer term, I guess it might be possible to do a pluggable solution with the existing indexing plugins for leveldb and surfacing some of this in the query API of quadstore which would be useful for quads with large text objects for instance.

Cheers,

Jun

Extending quadstore

What if someone requires additional indexes for context-specific queries? How can quadstore support extensions?

Additional methods

registerIndex(String indexName, Function keyGenerator)
  • indexName - name of index to be created
  • keyGenerator - function that generates an index key from a quad
queryIndex(String indexName, Object opts) and queryIndexStream(String indexName, Object opts)
  • indexName - name of index to be queried
  • opts - query options
    • opts.start - leveldb's gte minus indexName
    • opts.end - leveldb's lte minus indexName

Other stuff

  • make store._delimiter and store._boundary public

Get rid of asynctools

asynctools was an experiment that should have never made it into quadstore's dependencies. Get rid of it.

The word `context` is confusing in the intro

First, thanks for building this project!

Second, I found the use of the word context confusing in the introduction

Especially: (subject, predicate, object, context)

Most other quadstores/libraries express that as (subject, predicate, object, graph) (in my limited experience).

Change it would a) match prevailing convention and b) avoid tripping folks up coming from JSON-LD land where @context is something completely different.

Cheers!
๐ŸŽฉ

More advanced joins

Hi there,

Currently I believe it's only possible to join two queries by an identical property (i.e. predicate to predicate)

It would be great to be able to join queries by joining on different fields. This kind of API would do I guess:

// QuadStore
const matchTermsA = {graph: 'g'};
const matchTermsB = {subject: 's'};
quadStore.query(matchTermsA)
    .join(quadStore.query(matchTermsB), [{ 'subject': 'graph' }]) // this would join (rather arbitrarily) a.graph == b.subject)  
    .get((err, quads) => {});

Allowing to still pass strings would keep backward compatibility, and allowing objects would enable joining by different properties.

What do you think?

Jun

features needed for implementing Linked Data Fragment - Datasource interface

This issue captures conversation started in #3 (comment)

I think node-quadstore could provide a nice addition to the data sources already available for Linked Data Fragments server.

https://github.com/LinkedDataFragments/Server.js#configure-the-data-sources

Support for new sources is possible by implementing the Datasource interface.

I recall work on having such LDF server Datasource interface for LevelGraph and it seems that it requires approximate count without fetching result levelgraph/levelgraph#72

/cc @RubenVerborgh

Provide examples

May provide a working example how to use the library to store triples inside it. It would be even better to include also queries using node-quadstore-sparql.

'This sample should parse a triples from a string variable or download a dataset from dbpedia for example. Then how we can query these data using the api of library or using sparql queries form quadstore-spraql.

Then would help a lot to have an overview on how to use the library.

Thanks in advance.

Regards,

Fix ECONNRESET in Mocha suite

It seems like there's no way to force the ldf-client instance to close its connection to the server before the latter calls socket.destroy(). This triggers a ECONNRESET error - currently unhandled - that breaks the mocha suite.

Related issue on ldf-client's repo: LinkedDataFragments/Client.js#39

Switch to asynciterators

There's a lot of streaming inside quadstore that could benefit from asynciterator's leaner approach.

Support Promise interface (in addition to callback)

hi @jacoscaz - great project! Having been a fan of levelgraph, I'm really excited to try out node-quadstore.

Do you have any plans to support a Promise-returning interface to your various async methods? Either instead of, or in addition to the old-style callback interface that they're using right now. As in, if there is no callback param, return a promise (otherwise, invoke the callback as usual).

Research alternatives to LevelDB supporting range queries

Goal

Add support for range-based queries.

Check out

Notes

  • TaffyDB
    • last commit in 2018
  • sql.js
    • doesn't persist changes unless one dumps to a TypedArray
  • alasql
    • last publish on NPM dated 16 days
    • 6k+ weekly downloads
  • nedb
    • last publish on NPM dated 4 years ago
    • 40k+ weekly downloads
  • PouchDB
    • works both locally and remotely, can sync with remote counterpart
    • JavaScript implementation of CouchDB's API
    • 14k+ weekly downloads
    • can use indexeddb and leveldb as backends

Dynamic indexes

  • In addition to switching to a different backend, a potential alternative would be to bring back dynamic indexes and implement a query engine that integrates index scans and in-memory strategies to compute responses to queries based on which indexes are currently registered on a given instance of quadstore.
  • Worth mentioning that Jena + TBD only uses 3 indexes more than quadstore: GOSP, GPOS, GSPO, OSPG, SPOG, POSG, SPO, POS, OSP.
  • Quadstore could throw errors on range queries that cannot be run with current indexes.

Support for SPARQL UPDATE

We're adding native SPARQL support in version 7!

https://www.w3.org/TR/sparql11-update/#updateLanguage

Quad Management

  • INSERT DATA (v7.0.0, WIP in feature/sparql-and-complex-searches)
  • DELETE DATA (v7.0.0, WIP in feature/sparql-and-complex-searches)
  • INSERT / DELETE (v7.0.0, WIP in feature/sparql-and-complex-searches)
  • LOAD
  • CLEAR

Graph Management

  • CREATE
  • DROP
  • COPY
  • MOVE
  • ADD

Add delExisting and putNew

From ##32

Also it might be useful to have a delExisting method like in LD-PATCH and possibly a putNew (like addNew) method?

API change: refactor delput() and getdelput() into patch()

As per @dmitrizagidulin's suggestion in #18

Proposed API change

.patch(Array|Object delete, Array insert)

If delete is of type Array, treat it as DELETE DATA (explicit list of quads to delete), and if delete is of type Object, treat it as DELETE WHERE (like getdelput currently does).

References

TurtlePatch Proposal, note the two DELETE WHERE and INSERT DATA sections. (This is equivalent semantics to your getdelput() method.)

SPARQL 1.1 Update recommendation, specifically meant for RDF Graph Stores. The main difference from the Turtle Patch proposal is that instead of DELETE WHERE, this spec uses DELETE DATA -- they don't allow querying for deleted quads, they want them to be listed explicitly. (This is equivalent semantics to your delput() method.)

More level-ish interface?

Hi there,

Just giving a try to node-quadstore and I wonder if it would make sense to provide a more level-ish interface to allow getting a store instance with:

var level = require('level');
var quadstore = require('quadstore').Quadstore;
var db = level(DB_PATH);
var store = quadstore(db);

const quads = [
    {subject: 's', predicate: 'p', object: 'o', graph: 'g'}
];

store.put(quads, (putErr) => {});

Allowing this to work with level-sublevel would also follow.

Cheers,

Jun

JSON-LD support

Does node-quadstore support direct loading of JSON-LD (including context handling)? If not, is it a planned feature?
Same would be nice for output (so returning query results as JSON-LD).

Basically, does something like levelgraph-jsonld exist for node-quadstore?

Add browser support

LevelGraph works in node.js as well as in web browsers, does node-quadstore also work in web browsers?

"outer" join

Hi,

It would be nice to have an "outer" join such that this code:

const QuadStore = require('quadstore').QuadStore;
const store = new QuadStore('./db', { db: require('leveldown') });

const initialQuads = [
  { subject: 'g0s', predicate: 'g0p', object: 'g0o', graph: 'g1s' },
  { subject: 'g1s', predicate: 'g1p', object: 'g1o', graph: 'g2s' },
  { subject: 'g2s', predicate: 'g2p', object: 'g2o', graph: 'g3s' }
];

store
  .del({}).then(() => {
    store
      .put(initialQuads)
      .then(() => {
        return store
          .query({})
          .join(store.query({}), ['graph'], ['subject'])
          .get()
          .then((quads) => console.log(format(quads)));
      })
    })

const format = (quads) => quads
  .map(({subject, predicate, object, graph}) => ({s: subject, p: predicate, o: object, g: graph}))
  .reduce((acc, {s,p,o,g}) => acc + `${s}\t${p}\t${o}\t${g}` + "\n", "\n")

Instead of outputting currently:

g0s	g0p	g0o	g1s
g1s	g1p	g1o	g2s

Would output the third triple too (matching from queryB). i.e.

g0s	g0p	g0o	g1s
g1s	g1p	g1o	g2s
g2s	g2p	g2o	g3s

Cheers,

Jun

Advanced queries (searches)

How to implement advanced queries? We need a few key components:

  • query planner
  • query optimizer
  • query executor

The optimizer could go in a later release.

How to implement joins? I've had a preliminary look and it looks like the "merge-sort join" algorithm would be a good pick as we're using lexicographically-ordered index keys.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.