A sqlite-backed database for tasl instances.
import * as tasl from "tasl"
export declare class DB {
// Open an existing database at the given path
static open(path: string, options?: { readOnly?: boolean }): DB
// Create a new database at the given path with the given schema.
// Use path: null for an in-memory sqlite instance
static create(path: string | null, schema: tasl.Schema): DB
// Import a tasl instance from an AsyncIterable
static import(
path: string | null,
schema: tasl.Schema,
stream: AsyncIterable<Buffer>
): Promise<DB>
// Close the database
close(): void
// Get the database schema
get schema(): tasl.Schema
// Export the database to an AsyncIterable
export(options?: { chunkSize?: number }): AsyncIterable<Buffer>
// Get an element value
get(key: string, id: number): tasl.values.Value
// Iterate over elements ids, values, or [id, value] entries
keys(key: string): Iterable<number>
values(key: string): Iterable<tasl.values.Value>
entries(key: string): Iterable<[number, tasl.values.Value]>
// Check for the presence of an element with a given id
has(key: string, id: number): boolean
// Count the elements of a given class
count(key: string): number
// Push a new value of a class using an autoincrementing id, returning the id
push(key: string, value: tasl.values.Value): number
set(key: string, id: number, value: tasl.values.Value): void
merge(elements: Record<string, tasl.values.Element[]>): void
migrate(mapping: tasl.Mapping, targetPath: string | null): Promise<DB>
}
taslite uses better-sqlite for sqlite3 bindings, which has a synchronous API that blocks on the main thread. All the database methods here, except for DB.import
/ DB.export
and DB.migrate
, are also synchronous.
Elements in a tasl instance each have an unsigned integer id
, unique within each class. The simplest way to use taslite is with the .get
and .set
methods.
import { DB } from "taslite"
import * as tasl from "tasl"
const schema = tasl.parseSchema(`
namespace s http://schema.org/
class s:Person {
s:name -> string
s:email -> uri
}
`)
// the path here can also be null to open an in-memory database
const db = DB.create("my-database.sqlite", schema)
// element IDs don't have to be created in order
db.set(
"http://schema.org/Person",
19,
tasl.values.product({
"http://schema.org/name": tasl.values.string("John Doe"),
"http://schema.org/email": tasl.values.uri("mailto:johndoe@example.com"),
})
)
db.set(
"http://schema.org/Person",
19103,
tasl.values.product({
"http://schema.org/name": tasl.values.string("Jane Doe"),
"http://schema.org/email": tasl.values.uri("mailto:me@janedoe.com"),
})
)
db.get("http://schema.org/Person", 19)
// {
// kind: 'product',
// components: {
// 'http://schema.org/email': { kind: 'uri', value: 'mailto:johndoe@example.com' },
// 'http://schema.org/name': { kind: 'literal', value: 'John Doe' }
// }
// }
db.get("http://schema.org/Person", 19103)
// {
// kind: 'product',
// components: {
// 'http://schema.org/email': { kind: 'uri', value: 'mailto:me@janedoe.com' },
// 'http://schema.org/name': { kind: 'literal', value: 'Jane Doe' }
// }
// }
db.get("http://schema.org/Person", 0)
// Uncaught Error: no element in http://schema.org/Person with id 0
db.get("http://schema.org/Book", 0)
// Uncaught Error: key not found
Just like tasl.Instance
, the DB
class has three methods for iterating (synchronously!) over elements in the database. Iteration always happens in ascending ID order.
for (const id of db.keys("http://schema.org/Person")) {
console.log(id)
}
// 19
// 19103
for (const value of db.values("http://schema.org/Person")) {
console.log(value)
}
// {
// kind: 'product',
// components: {
// 'http://schema.org/email': { kind: 'uri', value: 'mailto:johndoe@example.com' },
// 'http://schema.org/name': { kind: 'literal', value: 'John Doe' }
// }
// }
// {
// kind: 'product',
// components: {
// 'http://schema.org/email': { kind: 'uri', value: 'mailto:me@janedoe.com' },
// 'http://schema.org/name': { kind: 'literal', value: 'Jane Doe' }
// }
// }
for (const [id, value] of db.entries("http://schema.org/Person")) {
console.log(id, value)
}
// 19 {
// kind: 'product',
// components: {
// 'http://schema.org/email': { kind: 'uri', value: 'mailto:johndoe@example.com' },
// 'http://schema.org/name': { kind: 'literal', value: 'John Doe' }
// }
// }
// 19103 {
// kind: 'product',
// components: {
// 'http://schema.org/email': { kind: 'uri', value: 'mailto:me@janedoe.com' },
// 'http://schema.org/name': { kind: 'literal', value: 'Jane Doe' }
// }
// }
Classes in a tasl schema can reference each other, which makes for awkward DX when trying to update them. For example, here's a valid tasl schema:
import * as tasl from "tasl"
const schema = tasl.parseSchema(`
namespace ex http://example.com/
class ex:Person {
ex:name -> string
ex:favoriteBook -> * ex:Book
}
class ex:Book {
ex:name -> string
ex:author -> * ex:Person
}
`)
Here we have two classes, each of whose types reference the other. Let's try creating a database with this schema inserting some elements:
const db = DB.create("my-database.sqlite", schema)
db.set(
"http://example.com/Person",
100,
tasl.values.product({
"http://example.com/name": tasl.values.string("John Doe"),
"http://example.com/favoriteBook": tasl.values.reference(0),
})
)
// SqliteError: FOREIGN KEY constraint failed
That's because every element value of ex:Person
needs to reference an element value of ex:Book
- but there are no ex:Book
elements yet! The only way around this is to add elements to both classes at the same time, which we can do using the DB.merge
method.
db.merge({
"http://example.com/Person": [
{
id: 100,
value: tasl.values.product({
"http://example.com/name": tasl.values.string("John Doe"),
"http://example.com/favoriteBook": tasl.values.reference(0),
}),
},
],
"http://example.com/Book": [
{
id: 0,
value: tasl.values.product({
"http://example.com/name": tasl.values.string(
"John Doe: My Autobiography"
),
"http://example.com/author": tasl.values.reference(100),
}),
},
],
})
Create a new database with a given schema
import fs from "node:fs"
import { DB } from "taslite"
import * as tasl from "tasl"
const schemaPath = "./test/instances/micro.schema"
const schema = tasl.parseSchema(fs.readFileSync(schemaPath, "utf-8"))
const db = DB.create("micro.sqlite", schema)
Open an existing .sqlite
database
import fs from "node:fs"
import { DB } from "taslite"
import * as tasl from "tasl"
const databasePath = "example.sqlite"
const db = DB.open("example.sqlite")
Import an existing instance from a .instance
file
import fs from "node:fs"
import { DB } from "taslite"
import * as tasl from "tasl"
const schemaPath = "./example.schema"
const instancePath = "./example.instance"
const schema = tasl.parseSchema(fs.readFileSync(schemaPath, "utf-8"))
const db = await DB.import(
"example.sqlite",
schema,
fs.createReadStream(instancePath)
)
Apply a migration to an existing database
import * as tasl from "tas"
import assert from "node:assert"
const sourceSchema = tasl.parseSchema(`
namespace s http://schema.org/
class s:Person {
s:name -> string
s:gender -> [
s:Male
s:Female
s:value <- string
]
}`)
const targetSchema = new tasl.Schema({
"http://example.com/person": tasl.types.product({
"http://example.com/name": tasl.types.string,
"http://example.com/gender": tasl.types.string,
}),
})
const targetSchema = tasl.parseSchema(`
namespace ex http://example.com/
class ex:person {
ex:name -> string
ex:gender -> string
}`)
const mapping = tasl.parseMapping(
sourceSchema,
targetSchema,
`
namespace s http://schema.org/
namepsace ex http://example.com/
map ex:person <= s:Person (p) => {
ex:name <= p / s:name
ex:gender <= p / s:gender [
s:Male (u) => "Male"
s:Female (u) => "Female"
s:value (v) => v
]
}
`
)
const sourceDB = DB.open("./path/to/source.sqlite")
assert(sourceDB.schema.isEqualTo(sourceSchema))
const targetDB = await sourceDB.migrate(mapping, "./path/to/target.sqlite")