From a9bedfb42d8b6d026668a1f7aa08df51a1d6f75c Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Thu, 23 May 2024 22:00:19 +0000 Subject: [PATCH] feat: rebuild SQLite when migrations occur See [#436]. [#436]: https://github.com/digidem/comapeo-core/issues/436 --- multi-core-indexer-1.0.0-alpha.10.tgz | Bin 0 -> 13373 bytes package-lock.json | 7 +- package.json | 2 +- src/constants.js | 3 + src/datastore/README.md | 1 + src/datastore/index.js | 4 +- src/lib/drizzle-helpers.js | 48 ++++++++++++ src/mapeo-project.js | 57 ++++++++++----- test-e2e/migration.js | 101 +++++++++++++++++++++++++- test/data-type.js | 4 + test/datastore.js | 51 ++++++++++++- test/icon-api.js | 1 + test/lib/drizzle-helpers.js | 30 ++++++++ test/translation-api.js | 1 + 14 files changed, 283 insertions(+), 27 deletions(-) create mode 100644 multi-core-indexer-1.0.0-alpha.10.tgz create mode 100644 src/lib/drizzle-helpers.js create mode 100644 test/lib/drizzle-helpers.js diff --git a/multi-core-indexer-1.0.0-alpha.10.tgz b/multi-core-indexer-1.0.0-alpha.10.tgz new file mode 100644 index 0000000000000000000000000000000000000000..053064aba2aa81a1778b02929111c1dd7854ea06 GIT binary patch literal 13373 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 literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json index 79587be4a..21e7c0baa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "magic-bytes.js": "^1.10.0", "map-obj": "^5.0.2", "mime": "^4.0.3", - "multi-core-indexer": "^1.0.0-alpha.10", + "multi-core-indexer": "file:multi-core-indexer-1.0.0-alpha.10.tgz", "p-defer": "^4.0.0", "p-event": "^6.0.1", "p-timeout": "^6.1.2", @@ -6373,8 +6373,9 @@ }, "node_modules/multi-core-indexer": { "version": "1.0.0-alpha.10", - "resolved": "https://registry.npmjs.org/multi-core-indexer/-/multi-core-indexer-1.0.0-alpha.10.tgz", - "integrity": "sha512-H9QdpJ/MaelrBZw6jCcsrInE+hwUQmfz/2swtIdQNNh1IHUDGEdPkakjcZAyahpM5iIVz7EqyWO74aC03A3qSA==", + "resolved": "file:multi-core-indexer-1.0.0-alpha.10.tgz", + "integrity": "sha512-Gt3UqUK6mbVPWM8/D9bZIzkQb8kTW0S91qrJFfqiL/66eOkiHnB3tjlxVebggj26dCtrY69C2x0Y/w9DEuMWaQ==", + "license": "MIT", "dependencies": { "@types/node": "^18.16.19", "@types/streamx": "^2.9.1", diff --git a/package.json b/package.json index d254d6688..ecb196d1e 100644 --- a/package.json +++ b/package.json @@ -182,7 +182,7 @@ "magic-bytes.js": "^1.10.0", "map-obj": "^5.0.2", "mime": "^4.0.3", - "multi-core-indexer": "^1.0.0-alpha.10", + "multi-core-indexer": "file:multi-core-indexer-1.0.0-alpha.10.tgz", "p-defer": "^4.0.0", "p-event": "^6.0.1", "p-timeout": "^6.1.2", diff --git a/src/constants.js b/src/constants.js index a95af9df3..d93929c79 100644 --- a/src/constants.js +++ b/src/constants.js @@ -32,3 +32,6 @@ export const NAMESPACE_SCHEMAS = /** @type {const} */ ({ }) export const SUPPORTED_CONFIG_VERSION = 1 + +// WARNING: This value is persisted. Be careful when changing it. +export const DRIZZLE_MIGRATIONS_TABLE = '__drizzle_migrations' diff --git a/src/datastore/README.md b/src/datastore/README.md index cc1f2a841..e685c7fbb 100644 --- a/src/datastore/README.md +++ b/src/datastore/README.md @@ -19,6 +19,7 @@ const datastore = new DataStore({ // Process entries here using an indexer... }, namespace: 'data', + reindex: false, }) /** @type {MapeoDoc} */ diff --git a/src/datastore/index.js b/src/datastore/index.js index 990146122..6a29e78a6 100644 --- a/src/datastore/index.js +++ b/src/datastore/index.js @@ -51,8 +51,9 @@ export class DataStore extends TypedEmitter { * @param {TNamespace} opts.namespace * @param {(entries: MultiCoreIndexer.Entry<'binary'>[]) => Promise} opts.batch * @param {MultiCoreIndexer.StorageParam} opts.storage + * @param {boolean} opts.reindex */ - constructor({ coreManager, namespace, batch, storage }) { + constructor({ coreManager, namespace, batch, storage, reindex }) { super() this.#coreManager = coreManager this.#namespace = namespace @@ -66,6 +67,7 @@ export class DataStore extends TypedEmitter { this.#coreIndexer = new MultiCoreIndexer(cores, { storage, batch: (entries) => this.#handleEntries(entries), + reindex, }) coreManager.on('add-core', (coreRecord) => { if (coreRecord.namespace !== namespace) return diff --git a/src/lib/drizzle-helpers.js b/src/lib/drizzle-helpers.js new file mode 100644 index 000000000..781bec238 --- /dev/null +++ b/src/lib/drizzle-helpers.js @@ -0,0 +1,48 @@ +import { sql } from 'drizzle-orm' +import { assert } from '../utils.js' +/** @import { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3' */ + +/** + * @param {unknown} queryResult + * @returns {number} + */ +const getNumberResult = (queryResult) => { + assert( + queryResult && + typeof queryResult === 'object' && + 'result' in queryResult && + typeof queryResult.result === 'number', + 'expected query to return proper result' + ) + return queryResult.result +} + +/** + * Get the number of rows in a table using `SELECT COUNT(*)`. + * Returns 0 if the table doesn't exist. + * + * @param {BetterSQLite3Database} db + * @param {string} tableName + * @returns {number} + */ +export const tableCountIfExists = (db, tableName) => + db.transaction((tx) => { + const existsQuery = sql` + SELECT EXISTS ( + SELECT 1 + FROM sqlite_master + WHERE type IS 'table' + AND name IS ${tableName} + ) AS result + ` + const existsResult = tx.get(existsQuery) + const exists = getNumberResult(existsResult) + if (!exists) return 0 + + const countQuery = sql` + SELECT COUNT(*) AS result + FROM ${sql.identifier(tableName)} + ` + const countResult = tx.get(countQuery) + return getNumberResult(countResult) + }) diff --git a/src/mapeo-project.js b/src/mapeo-project.js index 404586d71..13ac5d3fd 100644 --- a/src/mapeo-project.js +++ b/src/mapeo-project.js @@ -6,7 +6,11 @@ import { migrate } from 'drizzle-orm/better-sqlite3/migrator' import { discoveryKey } from 'hypercore-crypto' import { TypedEmitter } from 'tiny-typed-emitter' -import { NAMESPACES, NAMESPACE_SCHEMAS } from './constants.js' +import { + NAMESPACES, + NAMESPACE_SCHEMAS, + DRIZZLE_MIGRATIONS_TABLE, +} from './constants.js' import { CoreManager } from './core-manager/index.js' import { DataStore } from './datastore/index.js' import { DataType, kCreateWithDocId } from './datatype/index.js' @@ -44,6 +48,7 @@ import { projectKeyToPublicId, valueOf, } from './utils.js' +import { tableCountIfExists } from './lib/drizzle-helpers.js' import { omit } from './lib/omit.js' import { MemberApi } from './member-api.js' import { SyncApi, kHandleDiscoveryKey } from './sync/sync-api.js' @@ -139,11 +144,37 @@ export class MapeoProject extends TypedEmitter { this.#isArchiveDevice = isArchiveDevice ///////// 1. Setup database + this.#sqlite = new Database(dbPath) const db = drizzle(this.#sqlite) - migrate(db, { migrationsFolder: projectMigrationsFolder }) + const migrationsBefore = tableCountIfExists(db, DRIZZLE_MIGRATIONS_TABLE) + migrate(db, { + migrationsFolder: projectMigrationsFolder, + migrationsTable: DRIZZLE_MIGRATIONS_TABLE, + }) + const migrationsAfter = tableCountIfExists(db, DRIZZLE_MIGRATIONS_TABLE) + const reindex = migrationsBefore > 0 && migrationsAfter !== migrationsBefore + + const indexedTables = [ + observationTable, + trackTable, + presetTable, + fieldTable, + coreOwnershipTable, + roleTable, + deviceInfoTable, + iconTable, + translationTable, + remoteDetectionAlertTable, + ] + + ///////// 2. Wipe data if we need to re-index + + if (reindex) { + for (const table of indexedTables) db.delete(table).run() + } - ///////// 2. Setup random-access-storage functions + ///////// 3. Setup random-access-storage functions /** @type {ConstructorParameters[0]['storage']} */ const coreManagerStorage = (name) => @@ -153,7 +184,7 @@ export class MapeoProject extends TypedEmitter { const indexerStorage = (name) => coreStorage(path.join(INDEXER_STORAGE_FOLDER_NAME, name)) - ///////// 3. Create instances + ///////// 4. Create instances this.#coreManager = new CoreManager({ projectSecretKey, @@ -166,18 +197,7 @@ export class MapeoProject extends TypedEmitter { }) this.#indexWriter = new IndexWriter({ - tables: [ - observationTable, - trackTable, - presetTable, - fieldTable, - coreOwnershipTable, - roleTable, - deviceInfoTable, - iconTable, - translationTable, - remoteDetectionAlertTable, - ], + tables: indexedTables, sqlite: this.#sqlite, getWinner, mapDoc: (doc, version) => { @@ -199,6 +219,7 @@ export class MapeoProject extends TypedEmitter { namespace: 'auth', batch: (entries) => this.#indexWriter.batch(entries), storage: indexerStorage, + reindex, }), config: new DataStore({ coreManager: this.#coreManager, @@ -209,12 +230,14 @@ export class MapeoProject extends TypedEmitter { sharedIndexWriter, }), storage: indexerStorage, + reindex, }), data: new DataStore({ coreManager: this.#coreManager, namespace: 'data', batch: (entries) => this.#indexWriter.batch(entries), storage: indexerStorage, + reindex, }), } @@ -363,7 +386,7 @@ export class MapeoProject extends TypedEmitter { dataType: this.#dataTypes.translation, }) - ///////// 4. Replicate local peers automatically + ///////// 5. Replicate local peers automatically // Replicate already connected local peers for (const peer of localPeers.peers) { diff --git a/test-e2e/migration.js b/test-e2e/migration.js index 319e9baa5..aa16b9468 100644 --- a/test-e2e/migration.js +++ b/test-e2e/migration.js @@ -1,18 +1,111 @@ -import test from 'node:test' import { KeyManager } from '@mapeo/crypto' -import RAM from 'random-access-memory' -import { MapeoManager } from '../src/mapeo-manager.js' import Fastify from 'fastify' import assert from 'node:assert/strict' import fsPromises from 'node:fs/promises' +import test from 'node:test' +import RAM from 'random-access-memory' import { temporaryDirectory } from 'tempy' -import { createOldManagerOnVersion2_0_1 } from './utils.js' +import { MapeoManager } from '../src/mapeo-manager.js' +import { + connectPeers, + createManager, + createOldManagerOnVersion2_0_1, + invite, +} from './utils.js' const projectMigrationsFolder = new URL('../drizzle/project', import.meta.url) .pathname const clientMigrationsFolder = new URL('../drizzle/client', import.meta.url) .pathname +test('migrations pick up values that were not previously understood', async (t) => { + // Create Manager 1, which has new data. + + const manager1 = createManager('a', t) + await manager1.setDeviceInfo({ + name: 'a', + deviceType: 'selfHostedServer', + // Old versions shouldn't be able to recognize this. + selfHostedServerDetails: { baseUrl: 'https://comapeo-test.example/' }, + }) + + const projectId = await manager1.createProject({ name: 'test project' }) + const manager1Project = await manager1.getProject(projectId) + + { + const manager1Members = await manager1Project.$member.getMany() + assert( + manager1Members.some( + (member) => + member.selfHostedServerDetails?.baseUrl === + 'https://comapeo-test.example/' + ), + 'test setup: new manager has new data' + ) + } + + // Create Manager 2, which is not yet up to date. + + const manager2DbFolder = temporaryDirectory() + const manager2CoreStorage = temporaryDirectory() + t.after(() => fsPromises.rm(manager2DbFolder, { recursive: true })) + t.after(() => fsPromises.rm(manager2CoreStorage, { recursive: true })) + + const manager2BeforeMigration = await createOldManagerOnVersion2_0_1('b', { + dbFolder: manager2DbFolder, + coreStorage: manager2CoreStorage, + }) + await manager2BeforeMigration.setDeviceInfo({ + name: 'b', + deviceType: 'mobile', + }) + + // Connect them and ensure that Manager 2 doesn't yet know about the new data. + + const disconnect = connectPeers([manager1, manager2BeforeMigration]) + + await invite({ + projectId, + invitor: manager1, + invitees: [manager2BeforeMigration], + }) + + { + const manager2Project = await manager2BeforeMigration.getProject(projectId) + await manager2Project.$sync.waitForSync('initial') + const manager2Members = await manager2Project.$member.getMany() + assert( + !manager2Members.some((member) => 'selfHostedServerDetails' in member), + "test setup: old manager doesn't understand new data (yet)" + ) + + await manager2Project.close() + } + + await disconnect() + + // Migrate Manager 2 and see that it now knows about the data. + + const manager2AfterMigration = createManager('b', t, { + dbFolder: manager2DbFolder, + coreStorage: manager2CoreStorage, + }) + + { + const manager2Project = await manager2AfterMigration.getProject(projectId) + const manager2Members = await manager2Project.$member.getMany() + const serverMember = manager2Members.find( + (member) => member.deviceType === 'selfHostedServer' + ) + assert(serverMember, 'we still have the server member') + assert.equal( + serverMember.selfHostedServerDetails?.baseUrl, + 'https://comapeo-test.example/', + 'migrated manager has new data' + ) + } +}) + test('migration of localDeviceInfo table', async (t) => { const dbFolder = temporaryDirectory() const rootKey = KeyManager.generateRootKey() diff --git a/test/data-type.js b/test/data-type.js index fd78752b9..297e3c4c0 100644 --- a/test/data-type.js +++ b/test/data-type.js @@ -60,6 +60,7 @@ test('private createWithDocId() method', async () => { return indexWriter.batch(entries) }, storage: () => new RAM(), + reindex: false, }) const dataType = new DataType({ dataStore, @@ -95,6 +96,7 @@ test('private createWithDocId() method throws when doc exists', async () => { return indexWriter.batch(entries) }, storage: () => new RAM(), + reindex: false, }) const dataType = new DataType({ dataStore, @@ -316,6 +318,7 @@ async function testenv(opts = {}) { namespace: 'data', batch: async (entries) => indexWriter.batch(entries), storage: () => new RAM(), + reindex: false, }) const configDataStore = new DataStore({ @@ -347,6 +350,7 @@ async function testenv(opts = {}) { return indexed }, storage: () => new RAM(), + reindex: false, }) const translationDataType = new DataType({ diff --git a/test/datastore.js b/test/datastore.js index fc8299cb8..633b12dee 100644 --- a/test/datastore.js +++ b/test/datastore.js @@ -1,4 +1,4 @@ -import test from 'node:test' +import test, { mock } from 'node:test' import assert from 'node:assert/strict' import { randomBytes } from 'node:crypto' import { DataStore } from '../src/datastore/index.js' @@ -46,6 +46,7 @@ test('read and write', async () => { return {} }, storage: () => new RAM(), + reindex: false, }) const written = await dataStore.write(obs) const coreDiscoveryKey = discoveryKey(writerCore.key) @@ -80,6 +81,7 @@ test('writeRaw and read', async () => { return {} }, storage: () => new RAM(), + reindex: false, }) const buf = Buffer.from('myblob') const versionId = await dataStore.writeRaw(buf) @@ -101,6 +103,7 @@ test('index events', async () => { return {} }, storage: () => new RAM(), + reindex: false, }) dataStore.indexer.on('index-state', (state) => { indexStates.push(omit(state, ['entriesPerSecond'])) @@ -120,3 +123,49 @@ test('index events', async () => { ] assert.deepEqual(indexStates, expectedStates, 'expected index states emitted') }) + +test('re-indexing', async (t) => { + const cm = createCoreManager() + const writerCore = cm.getWriterCore('data').core + await writerCore.ready() + + /** @satisfies {ConstructorParameters[0]} */ + const commonOptions = { + coreManager: cm, + namespace: 'data', + batch: async () => ({}), + storage: RAM.reusable(), + reindex: false, + } + + const dataStore1 = new DataStore({ ...commonOptions }) + const written = await dataStore1.write(obs) + await dataStore1.close() + + const shouldNotBeCalled = mock.fn(() => Promise.resolve({})) + const dataStore2 = new DataStore({ + ...commonOptions, + batch: shouldNotBeCalled, + }) + await once(dataStore2.indexer, 'idle') + assert.equal( + shouldNotBeCalled.mock.callCount(), + 0, + 'test setup: this data store should not re-index' + ) + await dataStore2.close() + + const shouldBeCalled = mock.fn(() => Promise.resolve({})) + const dataStore3 = new DataStore({ + ...commonOptions, + batch: shouldBeCalled, + reindex: true, + }) + t.after(() => dataStore3.close()) + await once(dataStore3.indexer, 'idle') + const indexedVersionIds = shouldBeCalled.mock.calls[0]?.arguments[0]?.map( + ({ index, key }) => + getVersionId({ coreDiscoveryKey: discoveryKey(key), index }) + ) + assert.deepEqual(indexedVersionIds, [written.versionId], 're-indexing occurs') +}) diff --git a/test/icon-api.js b/test/icon-api.js index 6f8402f28..cbce80d2f 100644 --- a/test/icon-api.js +++ b/test/icon-api.js @@ -684,6 +684,7 @@ function setup({ coreManager: cm, storage: () => new RAM(), batch: async (entries) => indexWriter.batch(entries), + reindex: false, }) const iconDataType = new DataType({ diff --git a/test/lib/drizzle-helpers.js b/test/lib/drizzle-helpers.js new file mode 100644 index 000000000..1244292b8 --- /dev/null +++ b/test/lib/drizzle-helpers.js @@ -0,0 +1,30 @@ +import Database from 'better-sqlite3' +import { drizzle } from 'drizzle-orm/better-sqlite3' +import assert from 'node:assert/strict' +import test, { describe } from 'node:test' +import { tableCountIfExists } from '../../src/lib/drizzle-helpers.js' + +describe('table count if exists', () => { + const db = new Database(':memory:') + + db.exec('CREATE TABLE empty (ignored)') + + db.exec('CREATE TABLE filled (n INT)') + db.exec('INSERT INTO filled (n) VALUES (9)') + db.exec('INSERT INTO filled (n) VALUES (8)') + db.exec('INSERT INTO filled (n) VALUES (7)') + + const driz = drizzle(db) + + test("when table doesn't exist", () => { + assert.equal(tableCountIfExists(driz, 'doesnt_exist'), 0) + }) + + test('when table is empty', () => { + assert.equal(tableCountIfExists(driz, 'empty'), 0) + }) + + test('when table has rows', () => { + assert.equal(tableCountIfExists(driz, 'filled'), 3) + }) +}) diff --git a/test/translation-api.js b/test/translation-api.js index 26c54fa2b..a9b2c4950 100644 --- a/test/translation-api.js +++ b/test/translation-api.js @@ -137,6 +137,7 @@ function setup() { coreManager: cm, storage: () => new RAM(), batch: async (entries) => indexWriter.batch(entries), + reindex: false, }) const dataType = new DataType({