Skip to content

Commit 91bd893

Browse files
committed
fix: migrate scoped memory columns before index creation
1 parent 505403a commit 91bd893

2 files changed

Lines changed: 135 additions & 0 deletions

File tree

src/main/sqliteStore.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,16 @@ export class SqliteStore {
118118
this.db.run(`
119119
CREATE TABLE IF NOT EXISTS user_memories (
120120
id TEXT PRIMARY KEY,
121+
metabot_id INTEGER REFERENCES metabots(id),
121122
text TEXT NOT NULL,
122123
fingerprint TEXT NOT NULL,
123124
confidence REAL NOT NULL DEFAULT 0.75,
124125
is_explicit INTEGER NOT NULL DEFAULT 0,
125126
status TEXT NOT NULL DEFAULT 'created',
127+
scope_kind TEXT NOT NULL DEFAULT 'owner',
128+
scope_key TEXT NOT NULL DEFAULT 'owner:self',
129+
usage_class TEXT NOT NULL DEFAULT 'profile_fact',
130+
visibility TEXT NOT NULL DEFAULT 'local_only',
126131
created_at INTEGER NOT NULL,
127132
updated_at INTEGER NOT NULL,
128133
last_used_at INTEGER
@@ -750,6 +755,30 @@ export class SqliteStore {
750755
}
751756
this.save();
752757
}
758+
if (!umColumns.includes('scope_kind')) {
759+
this.db.run("ALTER TABLE user_memories ADD COLUMN scope_kind TEXT NOT NULL DEFAULT 'owner';");
760+
}
761+
if (!umColumns.includes('scope_key')) {
762+
this.db.run("ALTER TABLE user_memories ADD COLUMN scope_key TEXT NOT NULL DEFAULT 'owner:self';");
763+
}
764+
if (!umColumns.includes('usage_class')) {
765+
this.db.run("ALTER TABLE user_memories ADD COLUMN usage_class TEXT NOT NULL DEFAULT 'profile_fact';");
766+
}
767+
if (!umColumns.includes('visibility')) {
768+
this.db.run("ALTER TABLE user_memories ADD COLUMN visibility TEXT NOT NULL DEFAULT 'local_only';");
769+
}
770+
this.db.run(`
771+
CREATE INDEX IF NOT EXISTS idx_user_memories_scope_status_updated
772+
ON user_memories(metabot_id, scope_kind, scope_key, status, updated_at DESC)
773+
`);
774+
this.db.run(`
775+
CREATE INDEX IF NOT EXISTS idx_user_memories_scope_fingerprint
776+
ON user_memories(metabot_id, scope_kind, scope_key, fingerprint)
777+
`);
778+
this.db.run(`
779+
CREATE INDEX IF NOT EXISTS idx_user_memories_usage_visibility
780+
ON user_memories(metabot_id, usage_class, visibility, status, updated_at DESC)
781+
`);
753782
} catch (error) {
754783
console.warn('Failed to migrate user_memories metabot_id:', error);
755784
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import path from 'node:path';
4+
import fs from 'node:fs';
5+
import os from 'node:os';
6+
import { createRequire } from 'node:module';
7+
8+
const require = createRequire(import.meta.url);
9+
const initSqlJs = require('sql.js');
10+
const Module = require('node:module');
11+
const { DB_FILENAME } = require('../dist-electron/appConstants.js');
12+
13+
const worktreeRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
14+
const repoRoot = path.resolve(worktreeRoot, '..', '..');
15+
const sqlWasmPath = path.join(repoRoot, 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm');
16+
17+
async function createSqlDatabase() {
18+
const SQL = await initSqlJs({
19+
locateFile: () => sqlWasmPath,
20+
});
21+
return new SQL.Database();
22+
}
23+
24+
function getColumns(db, tableName) {
25+
const result = db.exec(`PRAGMA table_info(${tableName})`);
26+
return (result[0]?.values || []).map((row) => String(row[1]));
27+
}
28+
29+
function getIndexNames(db, tableName) {
30+
const result = db.exec(`PRAGMA index_list(${tableName})`);
31+
return (result[0]?.values || []).map((row) => String(row[1]));
32+
}
33+
34+
test('SqliteStore.create() upgrades legacy user_memories scope columns before creating scoped indexes', async () => {
35+
const legacyDb = await createSqlDatabase();
36+
legacyDb.run(`
37+
CREATE TABLE kv (
38+
key TEXT PRIMARY KEY,
39+
value TEXT NOT NULL,
40+
updated_at INTEGER NOT NULL
41+
);
42+
`);
43+
legacyDb.run(`
44+
CREATE TABLE metabots (
45+
id INTEGER PRIMARY KEY,
46+
name TEXT,
47+
avatar TEXT,
48+
metabot_type TEXT
49+
);
50+
`);
51+
legacyDb.run(`
52+
INSERT INTO metabots (id, name, avatar, metabot_type)
53+
VALUES (1, 'Twin', NULL, 'twin');
54+
`);
55+
legacyDb.run(`
56+
CREATE TABLE user_memories (
57+
id TEXT PRIMARY KEY,
58+
text TEXT NOT NULL,
59+
fingerprint TEXT NOT NULL,
60+
confidence REAL NOT NULL DEFAULT 0.75,
61+
is_explicit INTEGER NOT NULL DEFAULT 0,
62+
status TEXT NOT NULL DEFAULT 'created',
63+
created_at INTEGER NOT NULL,
64+
updated_at INTEGER NOT NULL,
65+
last_used_at INTEGER
66+
);
67+
`);
68+
69+
const userDataPath = fs.mkdtempSync(path.join(os.tmpdir(), 'idbots-sqlitestore-memory-scope-'));
70+
const dbPath = path.join(userDataPath, DB_FILENAME);
71+
fs.writeFileSync(dbPath, Buffer.from(legacyDb.export()));
72+
73+
const originalLoad = Module._load;
74+
Module._load = function patchedModuleLoad(request, parent, isMain) {
75+
if (request === 'electron') {
76+
return {
77+
app: {
78+
isPackaged: false,
79+
getAppPath: () => repoRoot,
80+
getPath: () => userDataPath,
81+
},
82+
};
83+
}
84+
return originalLoad(request, parent, isMain);
85+
};
86+
87+
try {
88+
const { SqliteStore } = require('../dist-electron/sqliteStore.js');
89+
const sqliteStore = await SqliteStore.create(userDataPath);
90+
const db = sqliteStore.getDatabase();
91+
92+
const columns = getColumns(db, 'user_memories');
93+
assert(columns.includes('scope_kind'));
94+
assert(columns.includes('scope_key'));
95+
assert(columns.includes('usage_class'));
96+
assert(columns.includes('visibility'));
97+
98+
const indexNames = getIndexNames(db, 'user_memories');
99+
assert(indexNames.includes('idx_user_memories_scope_status_updated'));
100+
assert(indexNames.includes('idx_user_memories_scope_fingerprint'));
101+
assert(indexNames.includes('idx_user_memories_usage_visibility'));
102+
} finally {
103+
Module._load = originalLoad;
104+
fs.rmSync(userDataPath, { recursive: true, force: true });
105+
}
106+
});

0 commit comments

Comments
 (0)