diff --git a/builder/codegen.js b/builder/codegen.js index c5cf7d9..76286a7 100644 --- a/builder/codegen.js +++ b/builder/codegen.js @@ -24,6 +24,7 @@ const type = contract.resolveIndex('@keet/devices-by-name') */ const gen = require('generate-object-property') +const genFunc = require('generate-function') const s = require('generate-string') const IndexTypeMap = new Map([ @@ -122,6 +123,14 @@ module.exports = function generateCode (hyperdb) { return str } +function toArrayFunction (fn) { + const i = fn.indexOf('(') + const isArrow = !fn.slice(0, i).includes('function') + fn = fn.slice(i) + if (isArrow) return fn + return fn.replace('{', '=> {') +} + function generateCommonPrefix (id, type) { let str = '' @@ -129,6 +138,13 @@ function generateCommonPrefix (id, type) { str += `const ${id}_key = ${generateIndexKeyEncoding(type)}\n` str += '\n' + if (type.isMapped) { + const fn = genFunc() + fn(type.map) + str += `const ${id}_map = ${toArrayFunction(fn.toString())}\n` + str += '\n' + } + str += `function ${id}_indexify (record) {\n` str += ' const arr = []\n' str += '\n' @@ -227,23 +243,35 @@ function generateEncodeCollectionKey (id, collection) { } function generateEncodeIndexKeys (id, index) { - const accessors = index.fullKey.map(c => { - return c.split('.').reduce(gen, 'record') - }) let str = '' - str += 'function encodeKeys (record) {\n' - str += ` const key = [${accessors.join(', ')}]\n` - str += ` return [${id + '_key'}.encode(key)]\n` + str += 'function encodeKeys (record, context) {\n' + if (index.isMapped) { + const accessors = index.fullKey.map(c => { + return c.split('.').reduce(gen, 'structKey') + }) + str += ` const mapped = ${id}_map(record, context)\n` + str += ' const keys = new Array(mapped.length)\n' + str += ' for (let i = 0; i < keys.length; i++) {\n' + str += ' const structKey = mapped[i]\n' + str += ` keys[i] = ${id + '_key'}.encode([${accessors.join(', ')}])\n` + str += ' }\n' + str += ' return keys\n' + } else { + const accessors = index.fullKey.map(c => { + return c.split('.').reduce(gen, 'record') + }) + str += ` return [${id + '_key'}.encode([${accessors.join(', ')}])]\n` + } str += ' }' return str } function generateIndexKeyEncoding (type) { let str = 'new IndexEncoder([\n' - for (let i = 0; i < type.fullKey.length; i++) { + for (let i = 0; i < type.keyEncoding.length; i++) { const component = type.keyEncoding[i] str += ' ' + IndexTypeMap.get(component) - if (i !== type.fullKey.length - 1) str += ',\n' + if (i !== type.keyEncoding.length - 1) str += ',\n' else str += '\n' } str += `], { prefix: ${type.id} })` diff --git a/builder/example.js b/builder/example.js index 7e9f7de..726fa62 100644 --- a/builder/example.js +++ b/builder/example.js @@ -41,10 +41,6 @@ example.register({ name: 'field2', type: '@example/struct1', required: true - }, - { - name: 'name', - type: 'string' } ] }) @@ -62,14 +58,38 @@ example.register({ type: 'uint', required: true }, + { + name: 'id3', + type: 'uint', + required: true + }, { name: 'struct1', type: '@example/struct2', required: true }, { - name: 'text', + name: 'name', type: 'string' + }, + { + name: 'age', + type: 'uint' + }, + { + name: 'tags', + type: 'string', + array: true + } + ] +}) + +example.register({ + name: 'collection-info', + fields: [ + { + name: 'count', + type: 'uint' } ] }) @@ -79,17 +99,63 @@ Hyperschema.toDisk(schema) const db = HyperDB.from(SCHEMA_DIR, DB_DIR) const exampleDb = db.namespace('example') +exampleDb.collections.register({ + name: 'collection1-info', + schema: '@example/collection-info', + derived: true +}) + exampleDb.collections.register({ name: 'collection1', schema: '@example/record1', - key: ['id1', 'id2'] + key: ['id1', 'id2'], + trigger: async (db, key, record, context) => { + const info = (await db.get('@example/collection1-info')) || { count: 0 } + const existing = await db.get('@example/collection1', key) + if (existing && record) return + await db.insert('@example/collection1-info', { count: record ? info.count + 1 : info.count - 1 }) + } }) exampleDb.indexes.register({ - name: 'collection1-by-struct', + name: 'collection1-by-struct-mapped', + collection: '@example/collection1', + key: { + type: { + fields: [ + { + name: 'name', + type: 'string' + }, + { + name: 'age', + type: 'uint' + } + ] + }, + map: (record, context) => [ + { name: record.name, age: record.age } + ] + } +}) + +exampleDb.indexes.register({ + name: 'collection1-by-id3', collection: '@example/collection1', - key: ['struct1.field1', 'struct1.field2'], + key: ['id3'], unique: true }) +exampleDb.indexes.register({ + name: 'collection1-by-struct', + collection: '@example/collection1', + key: ['name', 'age'] +}) + +exampleDb.indexes.register({ + name: 'collection1-by-tags', + collection: '@example/collection1', + key: ['name', 'tags'] +}) + HyperDB.toDisk(db) diff --git a/builder/index.js b/builder/index.js index 2e62a2f..8391c10 100644 --- a/builder/index.js +++ b/builder/index.js @@ -20,6 +20,7 @@ class DBType { this.key = description.key this.fullKey = [] + this.isMapped = false this.isIndex = false this.isCollection = false @@ -64,20 +65,23 @@ class Collection extends DBType { constructor (builder, namespace, description) { super(builder, namespace, description) this.isCollection = true + this.derived = !!description.derived this.indexes = [] this.schema = this.builder.schema.resolve(description.schema) if (!this.schema) throw new Error('Schema not found: ' + description.schema) - this.key = description.key + this.key = description.key || [] this.fullKey = this.key this.keyEncoding = [] this.valueEncoding = this.fqn + '/value' - for (const component of this.key) { - const field = this.schema.fieldsByName.get(component) - const resolvedType = this.builder.schema.resolve(field.type.fqn, { aliases: false }) - this.keyEncoding.push(resolvedType.name) + if (this.key.length) { + for (const component of this.key) { + const field = this.schema.fieldsByName.get(component) + const resolvedType = this.builder.schema.resolve(field.type.fqn, { aliases: false }) + this.keyEncoding.push(resolvedType.name) + } } // Register a value encoding type (the portion of the record that will not be in the primary key) @@ -98,6 +102,7 @@ class Collection extends DBType { type: COLLECTION_TYPE, indexes: this.indexes.map(i => i.fqn), schema: this.schema.fqn, + derived: this.derived, key: this.key } } @@ -108,24 +113,44 @@ class Index extends DBType { super(builder, namespace, description) this.isIndex = true this.unique = !!description.unique + this.isMapped = !Array.isArray(description.key) + this.collection = this.builder.typesByName.get(description.collection) this.keyEncoding = [] - this.key = description.key - this.fullKey = [...this.key] - if (!this.collection || !this.collection.isCollection) { throw new Error('Invalid index target: ' + description.collection) } this.collection.indexes.push(this) + this.key = description.key + this.fullKey = null + + this.map = null + if (this.isMapped) { + this.map = (typeof this.key.map === 'function') ? this.key.map.toString() : this.key.map + } + // Key encoding will be an IndexEncoder of the secondary index's key fields - // If the key is not unique, then the primary key should also be included - for (const component of this.key) { - const resolvedType = this._resolveKey(this.collection.schema, component) - this.keyEncoding.push(resolvedType.name) + // If an Array is provided, the keys are intepreted as fields from the source collection + // This can be overridden by providing { type, map } options to the key field + if (Array.isArray(this.key)) { + this.fullKey = [...this.key] + for (const component of this.key) { + const resolvedType = this._resolveKey(this.collection.schema, component) + this.keyEncoding.push(resolvedType.name) + } + } else { + this.fullKey = [] + for (const field of this.key.type.fields) { + const resolvedType = this.builder.schema.resolve(field.type, { aliases: false }) + this.keyEncoding.push(resolvedType.name) + this.fullKey.push(field.name) + } } - if (!this.unique) { + + // If the key is not unique, then the primary key should also be included + if (!this.unique && !this.isMapped) { for (let i = 0; i < this.collection.keyEncoding.length; i++) { this.keyEncoding.push(this.collection.keyEncoding[i]) this.fullKey.push(this.collection.key[i]) @@ -139,7 +164,12 @@ class Index extends DBType { type: INDEX_TYPE, collection: this.description.collection, unique: this.unique, - key: this.key + key: Array.isArray(this.key) + ? this.key + : { + type: this.key.type, + map: (typeof this.key.map === 'function') ? this.key.map.toString() : this.key.map + } } } } @@ -174,7 +204,6 @@ class BuilderNamespace { this.collections = new BuilderCollections(this) this.indexes = new BuilderIndexes(this) - this.schema = this.builder.schema.namespace(this.name) this.descriptions = [] } @@ -188,10 +217,10 @@ class BuilderNamespace { } class Builder { - constructor (schema, dbJson, { dbDir = null, schemaDir = null } = {}) { + constructor (schema, dbJson, { offset, dbDir = null, schemaDir = null } = {}) { this.schema = schema this.version = dbJson ? dbJson.version : 0 - this.offset = dbJson ? dbJson.offset : 0 + this.offset = dbJson ? dbJson.offset : (offset || 0) this.dbDir = dbDir this.schemaDir = schemaDir diff --git a/package.json b/package.json index f96426c..8f6d4d5 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,10 @@ "dependencies": { "b4a": "^1.6.6", "compact-encoding": "^2.15.0", - "hyperschema": "^0.0.14", + "generate-function": "^2.3.1", "generate-object-property": "^2.0.0", "generate-string": "^1.0.1", + "hyperschema": "^0.0.14", "rocksdb-native": "^2.3.1", "streamx": "^2.20.0" },