migration consolidation patch

This commit is contained in:
Storme-bit
2026-08-17 04:53:12 -07:00
parent e570bbecf2
commit 0353ce96e0
6 changed files with 644 additions and 87 deletions
+65
View File
@@ -0,0 +1,65 @@
// Migration runner — tests the REAL migrate() version-stepping logic against a
// minimal fake db (emulating the better-sqlite3 pragma getter/setter + transaction
// API), so it runs with zero dependencies. Guards that migrations apply in order,
// advance user_version correctly, and no-op when the DB is already current.
const { test } = require('node:test');
const assert = require('node:assert');
const { migrate, LATEST_VERSION } = require('../packages/memory-service/src/db/migrations');
// Emulates just the slice of the better-sqlite3 API that migrate() uses.
function fakeDb(startVersion = 0) {
let version = startVersion;
return {
ran: [],
setCalls: [],
pragma(str, opts) {
const set = str.match(/^user_version\s*=\s*(\d+)$/);
if (set) { version = Number(set[1]); this.setCalls.push(version); return; }
if (str === 'user_version') return opts && opts.simple ? version : [{ user_version: version }];
throw new Error('unexpected pragma: ' + str);
},
transaction(fn) { return (...args) => fn(...args); }, // execute immediately
get version() { return version; },
};
}
test('a fresh DB (user_version 0) is stamped to LATEST_VERSION', () => {
const db = fakeDb(0);
const result = migrate(db);
assert.strictEqual(result, LATEST_VERSION);
assert.strictEqual(db.version, LATEST_VERSION);
});
test('an already-current DB is a no-op (no version writes)', () => {
const db = fakeDb(LATEST_VERSION);
const result = migrate(db);
assert.strictEqual(result, LATEST_VERSION);
assert.deepStrictEqual(db.setCalls, [], 'should not touch user_version when current');
});
test('injected migrations run in order, each bumping the version by one', () => {
const db = fakeDb(0);
const order = [];
const migs = [
() => order.push('v1'),
() => order.push('v2'),
() => order.push('v3'),
];
const result = migrate(db, migs);
assert.strictEqual(result, 3);
assert.deepStrictEqual(order, ['v1', 'v2', 'v3'], 'migrations apply in sequence');
assert.deepStrictEqual(db.setCalls, [1, 2, 3], 'user_version advances one step at a time');
});
test('only migrations newer than the current version run', () => {
const db = fakeDb(1); // already at v1
const order = [];
const migs = [
() => order.push('v1'), // should be skipped
() => order.push('v2'),
() => order.push('v3'),
];
migrate(db, migs);
assert.deepStrictEqual(order, ['v2', 'v3'], 'v1 is not re-run');
assert.deepStrictEqual(db.setCalls, [2, 3]);
});
+58
View File
@@ -0,0 +1,58 @@
// Schema completeness — execs the REAL schema.js into a throwaway SQLite DB (via
// built-in node:sqlite) and asserts a fresh database gets every column the old
// per-boot ALTER statements used to add. This is the guard for the migration
// consolidation: if someone drops a column from schema.js, a fresh install would
// silently lose it, and this test goes red.
const { test, before } = require('node:test');
const assert = require('node:assert');
const schema = require('../packages/memory-service/src/db/schema');
let DatabaseSync;
try { ({ DatabaseSync } = require('node:sqlite')); } catch { DatabaseSync = null; }
// The complete intended shape = original base tables + every historical ALTER.
const EXPECTED = {
sessions: ['id','external_id','created_at','updated_at','metadata','name','project_id'],
episodes: ['id','session_id','user_message','ai_response','created_at','token_count','metadata'],
entities: ['id','name','type','notes','created_at','updated_at','metadata','mention_count','confidence','source','last_seen_at'],
relationships: ['id','from_id','to_id','label','created_at','metadata','mention_count','notes'],
entity_episodes: ['entity_id','episode_id'],
projects: ['id','name','description','colour','icon','created_at','isolated','notes','system_prompt'],
summaries: ['id','session_id','project_id','content','token_count','episode_range','created_at','metadata'],
};
const EXPECTED_OBJECTS = [
'idx_relationships_from','idx_relationships_to','idx_entity_episodes_entity','idx_entity_episodes_episode',
'idx_episodes_session','idx_episodes_created','idx_entities_type','idx_sessions_project',
'idx_summaries_project','idx_summaries_session',
'episodes_fts','episodes_fts_insert','episodes_fts_delete','episodes_fts_update',
];
let db;
before(() => {
if (!DatabaseSync) return;
db = new DatabaseSync(':memory:');
db.exec('PRAGMA foreign_keys = ON');
db.exec(schema); // throws here if schema.js has a syntax/ordering error
});
for (const [table, cols] of Object.entries(EXPECTED)) {
test(`${table} has exactly its full column set`, { skip: !DatabaseSync }, () => {
const got = db.prepare(`PRAGMA table_info(${table})`).all().map(r => r.name);
assert.deepStrictEqual([...got].sort(), [...cols].sort(), `${table} columns differ from the intended shape`);
});
}
test('all indexes, FTS table, and triggers are present', { skip: !DatabaseSync }, () => {
const names = db.prepare(`SELECT name FROM sqlite_master WHERE name LIKE 'idx_%' OR name LIKE 'episodes_fts%'`)
.all().map(r => r.name);
const missing = EXPECTED_OBJECTS.filter(o => !names.includes(o));
assert.deepStrictEqual(missing, [], `missing schema objects: ${missing}`);
});
test('FTS trigger populates the index on episode insert', { skip: !DatabaseSync }, () => {
db.prepare(`INSERT INTO sessions(external_id) VALUES ('s-test')`).run();
db.prepare(`INSERT INTO episodes(session_id, user_message, ai_response) VALUES (1,'find me qdrant','ok')`).run();
const hit = db.prepare(`SELECT rowid FROM episodes_fts WHERE episodes_fts MATCH 'qdrant'`).all();
assert.strictEqual(hit.length, 1);
});