@dialog-db/query
Version:
Datalog query engine inspired by Datomic
238 lines (222 loc) • 5.31 kB
JavaScript
import * as API from './api.js'
import * as Link from './data/link.js'
import * as Bytes from './data/bytes.js'
import * as Entity from './data/entity.js'
export { Link, Bytes }
/**
* @param {unknown} value
* @returns {value is API.Scalar}
*/
export const is = (value) => {
switch (typeof value) {
case 'string':
case 'number':
case 'bigint':
case 'boolean':
return true
case 'object':
return value === null || value instanceof Uint8Array || Entity.is(value)
default:
return false
}
}
/**
* @param {API.Scalar} self
* @param {API.Scalar} other
*/
export const equal = (self, other) => {
if (self === other) {
return true
} else if (self instanceof Uint8Array) {
return other instanceof Uint8Array && Bytes.equal(self, other)
} else if (Entity.is(self)) {
return Entity.is(other) && Entity.equal(self, other)
} else {
return false
}
}
/**
*
* @param {API.Scalar} self
*/
export const toJSON = (self) => {
if (self instanceof Uint8Array) {
return Bytes.toJSON(self)
} else if (Entity.is(self)) {
return Entity.toJSON(self)
} else {
return self
}
}
/**
* @param {API.Scalar} value
*/
export const entropy = (value) => {
switch (typeof value) {
case 'boolean':
return 1 // 2 possible values = 1 bit
case 'number':
return 64 // IEEE 754 double = 64 bits
case 'bigint': {
// Get actual bytes needed to represent this bigint
const size = Math.ceil(
(value < 0 ? -value : value).toString(16).length / 2
)
return size * 8
}
case 'string': {
// Assume UTF-8 byte length
return value.length * 8
}
default: {
if (Entity.is(value)) {
// Assume 32 byte hashes
return 256 // 32 bytes = 256 bits
} else if (Bytes.is(value)) {
return value.byteLength * 8
} else {
return 32 // Conservative default
}
}
}
}
/**
* @param {API.Scalar} self
*/
export const toString = (self) => {
if (self === null) {
return 'null'
} else if (self instanceof Uint8Array) {
return Bytes.toString(self)
} else if (Link.is(self)) {
return Link.toString(self)
} else {
return String(self)
}
}
/**
* @param {API.Scalar} self
*/
export const toDebugString = (self) => {
if (self === null) {
return 'null'
} else if (self instanceof Uint8Array) {
return Bytes.toString(self)
} else if (Link.is(self)) {
return `${self}`
} else {
return JSON.stringify(self)
}
}
/**
* @param {API.Scalar} value
*/
const toTypeOrder = (value) => {
switch (typeof value) {
case 'boolean':
return BOOLEAN
case 'number':
return (
Number.isInteger(value) ? INT32
: Number.isFinite(value) ? FLOAT32
: INT64
)
case 'bigint':
return INT64
case 'string':
return STRING
default:
if (value === null) {
return NULL
} else if (value instanceof Uint8Array) {
return BYTES
} else if (Entity.is(value)) {
return REFERENCE
} else {
return RECORD
}
}
}
const NULL = 0
const BOOLEAN = 1
const INT32 = 2
const INT64 = 3
const FLOAT32 = 4
const STRING = 5
const BYTES = 6
const RECORD = 7
const REFERENCE = 9
/**
* @param {API.Scalar} self
* @param {API.Scalar} to
* @returns {0|-1|1}
*/
export const compare = (self, to) => {
// If we have a same value there is no point in trying to compare
if (self === to) {
return 0
}
const selfType = toTypeOrder(self)
const toType = toTypeOrder(to)
if (selfType < toType) {
return -1
} else if (selfType > toType) {
return 1
}
// Doing this so that TS will not complain about them possibly being null
// if the both were first if would have returned already. If one of them
// was than type comparison would have returned already.
self = /** @type {API.Scalar&{}} */ (self)
to = /** @type {API.Scalar&{}} */ (to)
// If we got this far we have constants of the same type that are not
// equal.
switch (selfType) {
// If boolean `true` is greater than `false`
case BOOLEAN:
return self === true ? -1 : 1
case INT32:
case INT64:
case FLOAT32:
return to ? -1 : 1
case STRING:
return /** @type {-1|0|1} */ (
/** @type {string} */ (self).localeCompare(/** @type {string} */ (to))
)
case BYTES:
return Bytes.compare(
/** @type {Uint8Array} */ (self),
/** @type {Uint8Array} */ (to)
)
case RECORD:
return Link.compare(Link.of(self), Link.of(to))
case REFERENCE:
return Link.compare(
/** @type {API.Entity} */ (self),
/** @type {API.Entity} */ (to)
)
default:
return 0
}
}
/**
* @param {API.Scalar} actual
* @param {API.Scalar} expected
* @param {API.Bindings} scope
*/
export const unify = (actual, expected, scope) =>
actual === expected || equal(actual, expected) ?
{ ok: scope }
: { error: new RangeError(`Expected ${expected} got ${actual}`) }
/**
* @template {API.Scalar} T
* @param {T|undefined} value
* @param {string} message
* @returns {T}
*/
export const expect = (value, message) => {
if (value === undefined) {
throw new RangeError(message)
} else {
return value
}
}