diff --git a/builder/codegen.js b/builder/codegen.js index 71a1815..c98e456 100644 --- a/builder/codegen.js +++ b/builder/codegen.js @@ -437,7 +437,7 @@ function generateIndexKeyEncoding(type) { const component = type.keyEncoding[i] const keyType = type.builder.schema.types.get(`@${type.namespace}/${component}`) - if (keyType?.isEnum) str += ' IndexEncoder.UINT' + if (keyType?.isEnum) str += keyType.strings ? ' IndexEncoder.STRING' : ' IndexEncoder.UINT' else str += ' ' + IndexTypeMap.get(component) if (i !== type.keyEncoding.length - 1) str += ',\n' diff --git a/test/basic.js b/test/basic.js index 2fa9373..006b3b3 100644 --- a/test/basic.js +++ b/test/basic.js @@ -607,3 +607,24 @@ test('enum as key type', async function ({ create, bee }, t) { await db.close() }) + +test('enum with strings: true as key type', async function ({ create }, t) { + const db = await create({ fixture: 8 }) + + await db.insert('@db/tasks', { project: 'alpha', status: 'pending', name: 'task1' }) + await db.insert('@db/tasks', { project: 'alpha', status: 'active', name: 'task2' }) + await db.insert('@db/tasks', { project: 'beta', status: 'completed', name: 'task3' }) + await db.flush() + + { + const task = await db.get('@db/tasks', { project: 'alpha', status: 'pending', name: 'task1' }) + t.alike(task, { project: 'alpha', status: 'pending', name: 'task1' }) + } + + { + const all = await db.find('@db/tasks').toArray() + t.is(all.length, 3) + } + + await db.close() +}) diff --git a/test/fixtures/builders/8.js b/test/fixtures/builders/8.js new file mode 100644 index 0000000..1f34056 --- /dev/null +++ b/test/fixtures/builders/8.js @@ -0,0 +1,50 @@ +const HyperDB = require('../../../builder') +const Hyperschema = require('hyperschema') +const path = require('path') + +const SCHEMA_DIR = path.join(__dirname, '../generated/8/hyperschema') +const DB_DIR = path.join(__dirname, '../generated/8/hyperdb') + +const schema = Hyperschema.from(SCHEMA_DIR) + +const dbSchema = schema.namespace('db') + +dbSchema.register({ + name: 'status', + enum: ['pending', 'active', 'completed'], + strings: true +}) + +dbSchema.register({ + name: 'task', + fields: [ + { + name: 'project', + type: 'string', + required: true + }, + { + name: 'status', + type: '@db/status', + required: true + }, + { + name: 'name', + type: 'string', + required: true + } + ] +}) + +Hyperschema.toDisk(schema) + +const db = HyperDB.from(SCHEMA_DIR, DB_DIR) +const testDb = db.namespace('db') + +testDb.collections.register({ + name: 'tasks', + schema: '@db/task', + key: ['project', 'status', 'name'] +}) + +HyperDB.toDisk(db) diff --git a/test/fixtures/generate.js b/test/fixtures/generate.js index fece4cd..50006a6 100644 --- a/test/fixtures/generate.js +++ b/test/fixtures/generate.js @@ -5,3 +5,4 @@ require('./builders/4.js') require('./builders/5.js') require('./builders/6.js') require('./builders/7.js') +require('./builders/8.js') diff --git a/test/fixtures/generated/8/hyperdb/db.json b/test/fixtures/generated/8/hyperdb/db.json new file mode 100644 index 0000000..7a9e047 --- /dev/null +++ b/test/fixtures/generated/8/hyperdb/db.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "offset": 0, + "schema": [ + { + "name": "tasks", + "namespace": "db", + "id": 0, + "type": 1, + "version": 1, + "versionField": null, + "indexes": [], + "schema": "@db/task", + "derived": false, + "key": [ + "project", + "status", + "name" + ], + "trigger": null + } + ] +} \ No newline at end of file diff --git a/test/fixtures/generated/8/hyperdb/index.js b/test/fixtures/generated/8/hyperdb/index.js new file mode 100644 index 0000000..014dc4e --- /dev/null +++ b/test/fixtures/generated/8/hyperdb/index.js @@ -0,0 +1,115 @@ +// This file is autogenerated by the hyperdb compiler +/* eslint-disable camelcase */ + +const { IndexEncoder, c, b4a } = require('hyperdb/runtime') +const { version, getEncoding, setVersion } = require('./messages.js') + +const versions = { schema: version, db: 1 } + +// '@db/tasks' collection key +const collection0_key = new IndexEncoder([ + IndexEncoder.STRING, + IndexEncoder.STRING, + IndexEncoder.STRING +], { prefix: 0 }) + +function collection0_indexify (record) { + const arr = [] + + const a0 = record.project + if (a0 === undefined) return arr + arr.push(a0) + + const a1 = record.status + if (a1 === undefined) return arr + arr.push(a1) + + const a2 = record.name + if (a2 === undefined) return arr + arr.push(a2) + + return arr +} + +// '@db/tasks' value encoding +const collection0_enc = getEncoding('@db/task/hyperdb#0') + +// '@db/tasks' reconstruction function +function collection0_reconstruct (schemaVersion, keyBuf, valueBuf) { + const key = collection0_key.decode(keyBuf) + setVersion(schemaVersion) + const state = { start: 0, end: valueBuf.byteLength, buffer: valueBuf } + const type = c.uint.decode(state) + if (type !== 0) throw new Error('Unknown collection type: ' + type) + collection0.decodedVersion = c.uint.decode(state) + const record = collection0_enc.decode(state) + record.project = key[0] + record.status = key[1] + record.name = key[2] + return record +} +// '@db/tasks' key reconstruction function +function collection0_reconstruct_key (keyBuf) { + const key = collection0_key.decode(keyBuf) + return { + project: key[0], + status: key[1], + name: key[2] + } +} + +// '@db/tasks' +const collection0 = { + name: '@db/tasks', + id: 0, + version: 1, + encodeKey (record) { + const key = [record.project, record.status, record.name] + return collection0_key.encode(key) + }, + encodeKeyRange ({ gt, lt, gte, lte } = {}) { + return collection0_key.encodeRange({ + gt: gt ? collection0_indexify(gt) : null, + lt: lt ? collection0_indexify(lt) : null, + gte: gte ? collection0_indexify(gte) : null, + lte: lte ? collection0_indexify(lte) : null + }) + }, + encodeValue (schemaVersion, collectionVersion, record) { + setVersion(schemaVersion) + const state = { start: 0, end: 2, buffer: null } + collection0_enc.preencode(state, record) + state.buffer = b4a.allocUnsafe(state.end) + state.buffer[state.start++] = 0 + state.buffer[state.start++] = collectionVersion + collection0_enc.encode(state, record) + return state.buffer + }, + trigger: null, + reconstruct: collection0_reconstruct, + reconstructKey: collection0_reconstruct_key, + indexes: [], + decodedVersion: 0 +} + +const collections = [ + collection0 +] + +const indexes = [ +] + +module.exports = { versions, collections, indexes, resolveCollection, resolveIndex } + +function resolveCollection (name) { + switch (name) { + case '@db/tasks': return collection0 + default: return null + } +} + +function resolveIndex (name) { + switch (name) { + default: return null + } +} diff --git a/test/fixtures/generated/8/hyperdb/messages.js b/test/fixtures/generated/8/hyperdb/messages.js new file mode 100644 index 0000000..f70d7a0 --- /dev/null +++ b/test/fixtures/generated/8/hyperdb/messages.js @@ -0,0 +1,160 @@ +// This file is autogenerated by the hyperschema compiler +// Schema Version: 1 +/* eslint-disable camelcase */ +/* eslint-disable quotes */ +/* eslint-disable space-before-function-paren */ + +const { c } = require('hyperschema/runtime') + +const VERSION = 1 + +// eslint-disable-next-line no-unused-vars +let version = VERSION + +const encoding0_enum = { + pending: 'pending', + active: 'active', + completed: 'completed' +} + +// @db/status enum +const encoding0 = { + preencode (state, m) { + state.end++ // max enum is 3 so always one byte + }, + encode (state, m) { + switch (m) { + case 'pending': + c.uint.encode(state, 1) + break + case 'active': + c.uint.encode(state, 2) + break + case 'completed': + c.uint.encode(state, 3) + break + default: + throw new Error('Unknown enum') + } + }, + decode (state) { + switch (c.uint.decode(state)) { + case 1: + return 'pending' + case 2: + return 'active' + case 3: + return 'completed' + default: return null + } + } +} + +// @db/task +const encoding1 = { + preencode(state, m) { + c.string.preencode(state, m.project) + encoding0.preencode(state, m.status) + c.string.preencode(state, m.name) + }, + encode(state, m) { + c.string.encode(state, m.project) + encoding0.encode(state, m.status) + c.string.encode(state, m.name) + }, + decode(state) { + const r0 = c.string.decode(state) + const r1 = encoding0.decode(state) + const r2 = c.string.decode(state) + + return { + project: r0, + status: r1, + name: r2 + } + } +} + +// @db/task/hyperdb#0 +const encoding2 = { + preencode(state, m) { + + }, + encode(state, m) { + + }, + decode(state) { + return { + project: null, + status: null, + name: null + } + } +} + +function setVersion(v) { + version = v +} + +function encode(name, value, v = VERSION) { + version = v + return c.encode(getEncoding(name), value) +} + +function decode(name, buffer, v = VERSION) { + version = v + return c.decode(getEncoding(name), buffer) +} + +function getEnum(name) { + switch (name) { + case '@db/status': + return encoding0_enum + default: + throw new Error('Enum not found ' + name) + } +} + +function getEncoding(name) { + switch (name) { + case '@db/status': + return encoding0 + case '@db/task': + return encoding1 + case '@db/task/hyperdb#0': + return encoding2 + default: + throw new Error('Encoder not found ' + name) + } +} + +function getStruct(name, v = VERSION) { + const enc = getEncoding(name) + return { + preencode(state, m) { + version = v + enc.preencode(state, m) + }, + encode(state, m) { + version = v + enc.encode(state, m) + }, + decode(state) { + version = v + return enc.decode(state) + } + } +} + +const resolveStruct = getStruct // compat + +module.exports = { + resolveStruct, + getStruct, + getEnum, + getEncoding, + encode, + decode, + setVersion, + version +} diff --git a/test/fixtures/generated/8/hyperschema/index.js b/test/fixtures/generated/8/hyperschema/index.js new file mode 100644 index 0000000..6d93e05 --- /dev/null +++ b/test/fixtures/generated/8/hyperschema/index.js @@ -0,0 +1,141 @@ +// This file is autogenerated by the hyperschema compiler +// Schema Version: 1 +/* eslint-disable camelcase */ +/* eslint-disable quotes */ +/* eslint-disable space-before-function-paren */ + +const { c } = require('hyperschema/runtime') + +const VERSION = 1 + +// eslint-disable-next-line no-unused-vars +let version = VERSION + +const encoding0_enum = { + pending: 'pending', + active: 'active', + completed: 'completed' +} + +// @db/status enum +const encoding0 = { + preencode (state, m) { + state.end++ // max enum is 3 so always one byte + }, + encode (state, m) { + switch (m) { + case 'pending': + c.uint.encode(state, 1) + break + case 'active': + c.uint.encode(state, 2) + break + case 'completed': + c.uint.encode(state, 3) + break + default: + throw new Error('Unknown enum') + } + }, + decode (state) { + switch (c.uint.decode(state)) { + case 1: + return 'pending' + case 2: + return 'active' + case 3: + return 'completed' + default: return null + } + } +} + +// @db/task +const encoding1 = { + preencode(state, m) { + c.string.preencode(state, m.project) + encoding0.preencode(state, m.status) + c.string.preencode(state, m.name) + }, + encode(state, m) { + c.string.encode(state, m.project) + encoding0.encode(state, m.status) + c.string.encode(state, m.name) + }, + decode(state) { + const r0 = c.string.decode(state) + const r1 = encoding0.decode(state) + const r2 = c.string.decode(state) + + return { + project: r0, + status: r1, + name: r2 + } + } +} + +function setVersion(v) { + version = v +} + +function encode(name, value, v = VERSION) { + version = v + return c.encode(getEncoding(name), value) +} + +function decode(name, buffer, v = VERSION) { + version = v + return c.decode(getEncoding(name), buffer) +} + +function getEnum(name) { + switch (name) { + case '@db/status': + return encoding0_enum + default: + throw new Error('Enum not found ' + name) + } +} + +function getEncoding(name) { + switch (name) { + case '@db/status': + return encoding0 + case '@db/task': + return encoding1 + default: + throw new Error('Encoder not found ' + name) + } +} + +function getStruct(name, v = VERSION) { + const enc = getEncoding(name) + return { + preencode(state, m) { + version = v + enc.preencode(state, m) + }, + encode(state, m) { + version = v + enc.encode(state, m) + }, + decode(state) { + version = v + return enc.decode(state) + } + } +} + +const resolveStruct = getStruct // compat + +module.exports = { + resolveStruct, + getStruct, + getEnum, + getEncoding, + encode, + decode, + setVersion, + version +} diff --git a/test/fixtures/generated/8/hyperschema/schema.json b/test/fixtures/generated/8/hyperschema/schema.json new file mode 100644 index 0000000..28a0ad5 --- /dev/null +++ b/test/fixtures/generated/8/hyperschema/schema.json @@ -0,0 +1,51 @@ +{ + "version": 1, + "schema": [ + { + "name": "status", + "namespace": "db", + "offset": 1, + "enum": [ + { + "key": "pending", + "version": 1 + }, + { + "key": "active", + "version": 1 + }, + { + "key": "completed", + "version": 1 + } + ], + "strings": true + }, + { + "name": "task", + "namespace": "db", + "compact": false, + "flagsPosition": -1, + "fields": [ + { + "name": "project", + "required": true, + "type": "string", + "version": 1 + }, + { + "name": "status", + "required": true, + "type": "@db/status", + "version": 1 + }, + { + "name": "name", + "required": true, + "type": "string", + "version": 1 + } + ] + } + ] +}