fix(memory): migrate source primary key shape

This commit is contained in:
Josh Lehman 2026-06-17 07:08:02 -07:00 committed by Vincent Koc
parent 9cfd1cd287
commit 28fb5b019a
2 changed files with 107 additions and 0 deletions

View file

@ -111,6 +111,41 @@ describe("memory index schema", () => {
}
});
it("upgrades canonical source tables keyed only by path", () => {
const db = new DatabaseSync(":memory:");
try {
db.exec(`
CREATE TABLE memory_index_sources (
path TEXT PRIMARY KEY,
source TEXT NOT NULL DEFAULT 'memory',
hash TEXT NOT NULL,
mtime INTEGER NOT NULL,
size INTEGER NOT NULL
);
INSERT INTO memory_index_sources VALUES ('shared.md', 'memory', 'memory-hash', 10, 20);
`);
ensureMemoryIndexSchema({
db,
cacheEnabled: false,
ftsEnabled: false,
});
db.prepare(
"INSERT INTO memory_index_sources (path, source, hash, mtime, size) VALUES (?, ?, ?, ?, ?)",
).run("shared.md", "sessions", "session-hash", 30, 40);
expect(
db.prepare("SELECT path, source, hash FROM memory_index_sources ORDER BY source").all(),
).toEqual([
{ path: "shared.md", source: "memory", hash: "memory-hash" },
{ path: "shared.md", source: "sessions", hash: "session-hash" },
]);
} finally {
db.close();
}
});
it("leaves unrelated generic tables untouched", () => {
const db = new DatabaseSync(":memory:");
try {

View file

@ -21,6 +21,8 @@ const LEGACY_MEMORY_INDEX_TRIGGERS = [
"memory_chunks_revision_after_delete",
] as const;
const MEMORY_INDEX_SOURCE_COLUMNS = ["path", "source", "hash", "mtime", "size"] as const;
function tableHasExactColumns(
db: DatabaseSync,
tableName: string,
@ -31,6 +33,33 @@ function tableHasExactColumns(
return columns.size === expected.length && expected.every((column) => columns.has(column));
}
function tablePrimaryKeyColumns(db: DatabaseSync, tableName: string): string[] {
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{
name?: unknown;
pk?: unknown;
}>;
return rows
.flatMap((row) =>
typeof row.name === "string" && typeof row.pk === "number" && row.pk > 0
? [{ name: row.name, pk: row.pk }]
: [],
)
.toSorted((left, right) => left.pk - right.pk)
.map((row) => row.name);
}
function tableHasPrimaryKey(
db: DatabaseSync,
tableName: string,
expectedColumns: readonly string[],
): boolean {
const columns = tablePrimaryKeyColumns(db, tableName);
return (
columns.length === expectedColumns.length &&
columns.every((column, index) => column === expectedColumns[index])
);
}
function assertLegacyRowsCopied(db: DatabaseSync, query: string, tableName: string): void {
const row = db.prepare(query).get() as { missing?: unknown } | undefined;
if (Number(row?.missing ?? 0) > 0) {
@ -38,6 +67,46 @@ function assertLegacyRowsCopied(db: DatabaseSync, query: string, tableName: stri
}
}
function migrateCanonicalMemoryIndexSourcesPrimaryKey(db: DatabaseSync): void {
if (
!tableHasExactColumns(db, MEMORY_INDEX_SOURCES_TABLE, MEMORY_INDEX_SOURCE_COLUMNS) ||
tableHasPrimaryKey(db, MEMORY_INDEX_SOURCES_TABLE, ["path", "source"])
) {
return;
}
if (!tableHasPrimaryKey(db, MEMORY_INDEX_SOURCES_TABLE, ["path"])) {
return;
}
db.exec("SAVEPOINT migrate_memory_index_sources_primary_key");
try {
db.exec(`
DROP TRIGGER IF EXISTS memory_index_sources_revision_after_insert;
DROP TRIGGER IF EXISTS memory_index_sources_revision_after_update;
DROP TRIGGER IF EXISTS memory_index_sources_revision_after_delete;
ALTER TABLE ${MEMORY_INDEX_SOURCES_TABLE}
RENAME TO memory_index_sources_path_pk_migration;
CREATE TABLE ${MEMORY_INDEX_SOURCES_TABLE} (
path TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'memory',
hash TEXT NOT NULL,
mtime INTEGER NOT NULL,
size INTEGER NOT NULL,
PRIMARY KEY (path, source)
);
INSERT INTO ${MEMORY_INDEX_SOURCES_TABLE} (path, source, hash, mtime, size)
SELECT path, source, hash, mtime, size FROM memory_index_sources_path_pk_migration;
DROP TABLE memory_index_sources_path_pk_migration;
RELEASE migrate_memory_index_sources_primary_key;
`);
} catch (err) {
db.exec("ROLLBACK TO migrate_memory_index_sources_primary_key");
db.exec("RELEASE migrate_memory_index_sources_primary_key");
throw err;
}
}
function migrateLegacyMemoryIndexTables(db: DatabaseSync): void {
const hasLegacyCoreTables =
tableHasExactColumns(db, "meta", ["key", "value"]) &&
@ -218,6 +287,9 @@ export function ensureMemoryIndexSchema(params: {
revision INTEGER NOT NULL
);
INSERT OR IGNORE INTO ${MEMORY_INDEX_STATE_TABLE} (id, revision) VALUES (1, 0);
`);
migrateCanonicalMemoryIndexSourcesPrimaryKey(params.db);
params.db.exec(`
CREATE TRIGGER IF NOT EXISTS memory_index_sources_revision_after_insert
AFTER INSERT ON ${MEMORY_INDEX_SOURCES_TABLE}