db: split legacy schema bundle migrations
This commit is contained in:
@@ -20,6 +20,20 @@ function getAppliedSchemaMigrations(
|
|||||||
.all() as Array<{ version: number; name: string }>;
|
.all() as Array<{ version: number; name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getExpectedSchemaMigrations(): Array<{
|
||||||
|
version: number;
|
||||||
|
name: string;
|
||||||
|
}> {
|
||||||
|
return [
|
||||||
|
{ version: 1, name: 'legacy_schema_bundle' },
|
||||||
|
{ version: 2, name: 'scheduled_task_columns' },
|
||||||
|
{ version: 3, name: 'room_registration_canonical_columns' },
|
||||||
|
{ version: 4, name: 'message_seq_and_work_item_indexes' },
|
||||||
|
{ version: 5, name: 'sessions_composite_key' },
|
||||||
|
{ version: 6, name: 'chat_channel_metadata' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
describe('initializeDatabaseSchema', () => {
|
describe('initializeDatabaseSchema', () => {
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
@@ -35,12 +49,9 @@ describe('initializeDatabaseSchema', () => {
|
|||||||
try {
|
try {
|
||||||
initializeDatabaseSchema(database);
|
initializeDatabaseSchema(database);
|
||||||
|
|
||||||
expect(getAppliedSchemaMigrations(database)).toEqual([
|
expect(getAppliedSchemaMigrations(database)).toEqual(
|
||||||
{
|
getExpectedSchemaMigrations(),
|
||||||
version: 1,
|
);
|
||||||
name: 'legacy_schema_bundle',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
} finally {
|
} finally {
|
||||||
database.close();
|
database.close();
|
||||||
}
|
}
|
||||||
@@ -53,12 +64,9 @@ describe('initializeDatabaseSchema', () => {
|
|||||||
initializeDatabaseSchema(database);
|
initializeDatabaseSchema(database);
|
||||||
initializeDatabaseSchema(database);
|
initializeDatabaseSchema(database);
|
||||||
|
|
||||||
expect(getAppliedSchemaMigrations(database)).toEqual([
|
expect(getAppliedSchemaMigrations(database)).toEqual(
|
||||||
{
|
getExpectedSchemaMigrations(),
|
||||||
version: 1,
|
);
|
||||||
name: 'legacy_schema_bundle',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
} finally {
|
} finally {
|
||||||
database.close();
|
database.close();
|
||||||
}
|
}
|
||||||
@@ -86,12 +94,9 @@ describe('initializeDatabaseSchema', () => {
|
|||||||
try {
|
try {
|
||||||
initializeDatabaseSchema(reopened);
|
initializeDatabaseSchema(reopened);
|
||||||
|
|
||||||
expect(getAppliedSchemaMigrations(reopened)).toEqual([
|
expect(getAppliedSchemaMigrations(reopened)).toEqual(
|
||||||
{
|
getExpectedSchemaMigrations(),
|
||||||
version: 1,
|
);
|
||||||
name: 'legacy_schema_bundle',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
} finally {
|
} finally {
|
||||||
reopened.close();
|
reopened.close();
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/db/migrations/002_scheduled-task-columns.ts
Normal file
41
src/db/migrations/002_scheduled-task-columns.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { SchemaMigrationDefinition } from './types.js';
|
||||||
|
import { tryExecMigration } from './helpers.js';
|
||||||
|
|
||||||
|
export const SCHEDULED_TASK_COLUMNS_MIGRATION = {
|
||||||
|
version: 2,
|
||||||
|
name: 'scheduled_task_columns',
|
||||||
|
apply(database) {
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE scheduled_tasks ADD COLUMN context_mode TEXT DEFAULT 'isolated'`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE scheduled_tasks ADD COLUMN agent_type TEXT`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE scheduled_tasks ADD COLUMN ci_provider TEXT`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE scheduled_tasks ADD COLUMN ci_metadata TEXT`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE scheduled_tasks ADD COLUMN max_duration_ms INTEGER`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE scheduled_tasks ADD COLUMN status_message_id TEXT`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE scheduled_tasks ADD COLUMN status_started_at TEXT`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE scheduled_tasks ADD COLUMN suspended_until TEXT`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} satisfies SchemaMigrationDefinition;
|
||||||
72
src/db/migrations/003_room-registration-canonical-columns.ts
Normal file
72
src/db/migrations/003_room-registration-canonical-columns.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import type { SchemaMigrationDefinition } from './types.js';
|
||||||
|
import { tableHasColumn, tryExecMigration } from './helpers.js';
|
||||||
|
|
||||||
|
export const ROOM_REGISTRATION_CANONICAL_COLUMNS_MIGRATION = {
|
||||||
|
version: 3,
|
||||||
|
name: 'room_registration_canonical_columns',
|
||||||
|
apply(database) {
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE room_settings ADD COLUMN mode_source TEXT NOT NULL DEFAULT 'explicit'`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE room_settings ADD COLUMN name TEXT`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE room_settings ADD COLUMN folder TEXT`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE room_settings ADD COLUMN trigger_pattern TEXT`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE room_settings ADD COLUMN requires_trigger INTEGER DEFAULT 1`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE room_settings ADD COLUMN is_main INTEGER DEFAULT 0`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE room_settings ADD COLUMN owner_agent_type TEXT`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE room_settings ADD COLUMN work_dir TEXT`,
|
||||||
|
);
|
||||||
|
tryExecMigration(
|
||||||
|
database,
|
||||||
|
`ALTER TABLE room_settings ADD COLUMN created_at TEXT`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tableHasColumn(database, 'room_settings', 'created_at')) {
|
||||||
|
database.exec(`
|
||||||
|
UPDATE room_settings
|
||||||
|
SET created_at = COALESCE(created_at, updated_at, CURRENT_TIMESTAMP)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
database.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS room_role_overrides (
|
||||||
|
chat_jid TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
agent_type TEXT NOT NULL,
|
||||||
|
agent_config_json TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (chat_jid, role),
|
||||||
|
CHECK (role IN ('owner', 'reviewer', 'arbiter')),
|
||||||
|
CHECK (agent_type IN ('claude-code', 'codex'))
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
database.exec(
|
||||||
|
`UPDATE room_settings
|
||||||
|
SET mode_source = 'explicit'
|
||||||
|
WHERE COALESCE(mode_source, '') NOT IN ('explicit', 'inferred')`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} satisfies SchemaMigrationDefinition;
|
||||||
36
src/db/migrations/004_message-seq-and-work-item-indexes.ts
Normal file
36
src/db/migrations/004_message-seq-and-work-item-indexes.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type { SchemaMigrationDefinition } from './types.js';
|
||||||
|
import { backfillMessageSeq, tryExecMigration } from './helpers.js';
|
||||||
|
|
||||||
|
export const MESSAGE_SEQ_AND_WORK_ITEM_INDEXES_MIGRATION = {
|
||||||
|
version: 4,
|
||||||
|
name: 'message_seq_and_work_item_indexes',
|
||||||
|
apply(database, args) {
|
||||||
|
try {
|
||||||
|
database.exec(
|
||||||
|
`ALTER TABLE messages ADD COLUMN is_bot_message INTEGER DEFAULT 0`,
|
||||||
|
);
|
||||||
|
database
|
||||||
|
.prepare(`UPDATE messages SET is_bot_message = 1 WHERE content LIKE ?`)
|
||||||
|
.run(`${args.assistantName}:%`);
|
||||||
|
} catch {
|
||||||
|
/* column already exists */
|
||||||
|
}
|
||||||
|
|
||||||
|
tryExecMigration(database, `ALTER TABLE messages ADD COLUMN seq INTEGER`);
|
||||||
|
backfillMessageSeq(database);
|
||||||
|
|
||||||
|
database.exec(`
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_seq ON messages(seq);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_chat_jid_seq ON messages(chat_jid, seq);
|
||||||
|
`);
|
||||||
|
database.exec(`DROP INDEX IF EXISTS idx_work_items_group_agent;`);
|
||||||
|
database.exec(`DROP INDEX IF EXISTS idx_work_items_open;`);
|
||||||
|
database.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_work_items_group_agent
|
||||||
|
ON work_items(chat_jid, agent_type, service_id, delivery_role, status);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_work_items_open
|
||||||
|
ON work_items(chat_jid, agent_type, IFNULL(service_id, ''), IFNULL(delivery_role, ''))
|
||||||
|
WHERE status IN ('produced', 'delivery_retry');
|
||||||
|
`);
|
||||||
|
},
|
||||||
|
} satisfies SchemaMigrationDefinition;
|
||||||
17
src/db/migrations/005_sessions-composite-key.ts
Normal file
17
src/db/migrations/005_sessions-composite-key.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { inferAgentTypeFromServiceShadow } from '../../role-service-shadow.js';
|
||||||
|
import {
|
||||||
|
backfillLegacyServiceSessions,
|
||||||
|
dropLegacyServiceSessionsTable,
|
||||||
|
migrateSessionsTableToCompositePk,
|
||||||
|
} from '../sessions.js';
|
||||||
|
import type { SchemaMigrationDefinition } from './types.js';
|
||||||
|
|
||||||
|
export const SESSIONS_COMPOSITE_KEY_MIGRATION = {
|
||||||
|
version: 5,
|
||||||
|
name: 'sessions_composite_key',
|
||||||
|
apply(database) {
|
||||||
|
migrateSessionsTableToCompositePk(database, 'claude-code');
|
||||||
|
backfillLegacyServiceSessions(database, inferAgentTypeFromServiceShadow);
|
||||||
|
dropLegacyServiceSessionsTable(database);
|
||||||
|
},
|
||||||
|
} satisfies SchemaMigrationDefinition;
|
||||||
26
src/db/migrations/006_chat-channel-metadata.ts
Normal file
26
src/db/migrations/006_chat-channel-metadata.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { SchemaMigrationDefinition } from './types.js';
|
||||||
|
|
||||||
|
export const CHAT_CHANNEL_METADATA_MIGRATION = {
|
||||||
|
version: 6,
|
||||||
|
name: 'chat_channel_metadata',
|
||||||
|
apply(database) {
|
||||||
|
try {
|
||||||
|
database.exec(`ALTER TABLE chats ADD COLUMN channel TEXT`);
|
||||||
|
database.exec(`ALTER TABLE chats ADD COLUMN is_group INTEGER DEFAULT 0`);
|
||||||
|
database.exec(
|
||||||
|
`UPDATE chats SET channel = 'whatsapp', is_group = 1 WHERE jid LIKE '%@g.us'`,
|
||||||
|
);
|
||||||
|
database.exec(
|
||||||
|
`UPDATE chats SET channel = 'whatsapp', is_group = 0 WHERE jid LIKE '%@s.whatsapp.net'`,
|
||||||
|
);
|
||||||
|
database.exec(
|
||||||
|
`UPDATE chats SET channel = 'discord', is_group = 1 WHERE jid LIKE 'dc:%'`,
|
||||||
|
);
|
||||||
|
database.exec(
|
||||||
|
`UPDATE chats SET channel = 'telegram', is_group = 1 WHERE jid LIKE 'tg:%'`,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
/* columns already exist */
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} satisfies SchemaMigrationDefinition;
|
||||||
64
src/db/migrations/helpers.ts
Normal file
64
src/db/migrations/helpers.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Database } from 'bun:sqlite';
|
||||||
|
|
||||||
|
function getTableColumns(database: Database, tableName: string): string[] {
|
||||||
|
return (
|
||||||
|
database.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{
|
||||||
|
name: string;
|
||||||
|
}>
|
||||||
|
).map((column) => column.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tableHasColumn(
|
||||||
|
database: Database,
|
||||||
|
tableName: string,
|
||||||
|
columnName: string,
|
||||||
|
): boolean {
|
||||||
|
return getTableColumns(database, tableName).includes(columnName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tryExecMigration(database: Database, sql: string): void {
|
||||||
|
try {
|
||||||
|
database.exec(sql);
|
||||||
|
} catch {
|
||||||
|
/* column already exists */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function backfillMessageSeq(database: Database): void {
|
||||||
|
const rows = database
|
||||||
|
.prepare(
|
||||||
|
`SELECT rowid, seq
|
||||||
|
FROM messages
|
||||||
|
ORDER BY CASE WHEN seq IS NULL THEN 1 ELSE 0 END, seq, timestamp, rowid`,
|
||||||
|
)
|
||||||
|
.all() as Array<{ rowid: number; seq: number | null }>;
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextSeq = 1;
|
||||||
|
const assignSeq = database.prepare(
|
||||||
|
'UPDATE messages SET seq = ? WHERE rowid = ? AND seq IS NULL',
|
||||||
|
);
|
||||||
|
const tx = database.transaction(() => {
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row.seq === null) {
|
||||||
|
assignSeq.run(nextSeq, row.rowid);
|
||||||
|
}
|
||||||
|
nextSeq = Math.max(nextSeq, (row.seq ?? nextSeq) + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tx();
|
||||||
|
|
||||||
|
const maxSeqRow = database
|
||||||
|
.prepare('SELECT MAX(seq) AS maxSeq FROM messages')
|
||||||
|
.get() as { maxSeq: number | null };
|
||||||
|
const maxSeq = maxSeqRow.maxSeq ?? 0;
|
||||||
|
if (maxSeq > 0) {
|
||||||
|
database
|
||||||
|
.prepare('INSERT OR IGNORE INTO message_sequence (id) VALUES (?)')
|
||||||
|
.run(maxSeq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getTableColumns };
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import type { Database } from 'bun:sqlite';
|
import type { Database } from 'bun:sqlite';
|
||||||
|
|
||||||
import { LEGACY_SCHEMA_BUNDLE_MIGRATION } from './001_legacy-schema-bundle.js';
|
import { LEGACY_SCHEMA_BUNDLE_MIGRATION } from './001_legacy-schema-bundle.js';
|
||||||
|
import { SCHEDULED_TASK_COLUMNS_MIGRATION } from './002_scheduled-task-columns.js';
|
||||||
|
import { ROOM_REGISTRATION_CANONICAL_COLUMNS_MIGRATION } from './003_room-registration-canonical-columns.js';
|
||||||
|
import { MESSAGE_SEQ_AND_WORK_ITEM_INDEXES_MIGRATION } from './004_message-seq-and-work-item-indexes.js';
|
||||||
|
import { SESSIONS_COMPOSITE_KEY_MIGRATION } from './005_sessions-composite-key.js';
|
||||||
|
import { CHAT_CHANNEL_METADATA_MIGRATION } from './006_chat-channel-metadata.js';
|
||||||
import type {
|
import type {
|
||||||
SchemaMigrationArgs,
|
SchemaMigrationArgs,
|
||||||
SchemaMigrationDefinition,
|
SchemaMigrationDefinition,
|
||||||
@@ -10,6 +15,11 @@ const SCHEMA_MIGRATIONS_TABLE = 'schema_migrations';
|
|||||||
|
|
||||||
const ORDERED_SCHEMA_MIGRATIONS: readonly SchemaMigrationDefinition[] = [
|
const ORDERED_SCHEMA_MIGRATIONS: readonly SchemaMigrationDefinition[] = [
|
||||||
LEGACY_SCHEMA_BUNDLE_MIGRATION,
|
LEGACY_SCHEMA_BUNDLE_MIGRATION,
|
||||||
|
SCHEDULED_TASK_COLUMNS_MIGRATION,
|
||||||
|
ROOM_REGISTRATION_CANONICAL_COLUMNS_MIGRATION,
|
||||||
|
MESSAGE_SEQ_AND_WORK_ITEM_INDEXES_MIGRATION,
|
||||||
|
SESSIONS_COMPOSITE_KEY_MIGRATION,
|
||||||
|
CHAT_CHANNEL_METADATA_MIGRATION,
|
||||||
];
|
];
|
||||||
|
|
||||||
function ensureSchemaMigrationsTable(database: Database): void {
|
function ensureSchemaMigrationsTable(database: Database): void {
|
||||||
|
|||||||
224
src/db/schema.ts
224
src/db/schema.ts
@@ -1,10 +1,7 @@
|
|||||||
import { Database } from 'bun:sqlite';
|
import { Database } from 'bun:sqlite';
|
||||||
|
|
||||||
import { SERVICE_SESSION_SCOPE, normalizeServiceId } from '../config.js';
|
import { SERVICE_SESSION_SCOPE, normalizeServiceId } from '../config.js';
|
||||||
import {
|
import { resolveRoleServiceShadow } from '../role-service-shadow.js';
|
||||||
inferAgentTypeFromServiceShadow,
|
|
||||||
resolveRoleServiceShadow,
|
|
||||||
} from '../role-service-shadow.js';
|
|
||||||
import {
|
import {
|
||||||
fillCanonicalChannelOwnerLeaseMetadata,
|
fillCanonicalChannelOwnerLeaseMetadata,
|
||||||
fillCanonicalPairedTaskMetadata,
|
fillCanonicalPairedTaskMetadata,
|
||||||
@@ -15,33 +12,17 @@ import {
|
|||||||
backfillPairedTurnAttemptsFromTurns,
|
backfillPairedTurnAttemptsFromTurns,
|
||||||
buildPairedTurnAttemptId,
|
buildPairedTurnAttemptId,
|
||||||
} from './paired-turn-attempts.js';
|
} from './paired-turn-attempts.js';
|
||||||
import { normalizeStoredAgentType } from './room-registration.js';
|
|
||||||
import {
|
import {
|
||||||
backfillLegacyServiceSessions,
|
getTableColumns,
|
||||||
dropLegacyServiceSessionsTable,
|
tableHasColumn,
|
||||||
migrateSessionsTableToCompositePk,
|
tryExecMigration,
|
||||||
} from './sessions.js';
|
} from './migrations/helpers.js';
|
||||||
|
import { normalizeStoredAgentType } from './room-registration.js';
|
||||||
|
|
||||||
// Legacy monolithic migration bundle kept for compatibility with databases
|
// Legacy monolithic migration bundle kept for compatibility with databases
|
||||||
// that predate ordered schema migration tracking. New schema changes belong in
|
// that predate ordered schema migration tracking. New schema changes belong in
|
||||||
// src/db/migrations/*.
|
// src/db/migrations/*.
|
||||||
|
|
||||||
function getTableColumns(database: Database, tableName: string): string[] {
|
|
||||||
return (
|
|
||||||
database.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{
|
|
||||||
name: string;
|
|
||||||
}>
|
|
||||||
).map((column) => column.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tableHasColumn(
|
|
||||||
database: Database,
|
|
||||||
tableName: string,
|
|
||||||
columnName: string,
|
|
||||||
): boolean {
|
|
||||||
return getTableColumns(database, tableName).includes(columnName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTableSql(database: Database, tableName: string): string | null {
|
function getTableSql(database: Database, tableName: string): string | null {
|
||||||
const row = database
|
const row = database
|
||||||
.prepare(
|
.prepare(
|
||||||
@@ -56,14 +37,6 @@ function getTableSql(database: Database, tableName: string): string | null {
|
|||||||
return row?.sql ?? null;
|
return row?.sql ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryExecMigration(database: Database, sql: string): void {
|
|
||||||
try {
|
|
||||||
database.exec(sql);
|
|
||||||
} catch {
|
|
||||||
/* column already exists */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPairedTurnAttemptIdSql(
|
function buildPairedTurnAttemptIdSql(
|
||||||
turnIdExpr: string,
|
turnIdExpr: string,
|
||||||
attemptNoExpr: string,
|
attemptNoExpr: string,
|
||||||
@@ -183,43 +156,6 @@ function tableHasForeignKey(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function backfillMessageSeq(database: Database): void {
|
|
||||||
const rows = database
|
|
||||||
.prepare(
|
|
||||||
`SELECT rowid, seq
|
|
||||||
FROM messages
|
|
||||||
ORDER BY CASE WHEN seq IS NULL THEN 1 ELSE 0 END, seq, timestamp, rowid`,
|
|
||||||
)
|
|
||||||
.all() as Array<{ rowid: number; seq: number | null }>;
|
|
||||||
if (rows.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let nextSeq = 1;
|
|
||||||
const assignSeq = database.prepare(
|
|
||||||
'UPDATE messages SET seq = ? WHERE rowid = ? AND seq IS NULL',
|
|
||||||
);
|
|
||||||
const tx = database.transaction(() => {
|
|
||||||
for (const row of rows) {
|
|
||||||
if (row.seq === null) {
|
|
||||||
assignSeq.run(nextSeq, row.rowid);
|
|
||||||
}
|
|
||||||
nextSeq = Math.max(nextSeq, (row.seq ?? nextSeq) + 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
tx();
|
|
||||||
|
|
||||||
const maxSeqRow = database
|
|
||||||
.prepare('SELECT MAX(seq) AS maxSeq FROM messages')
|
|
||||||
.get() as { maxSeq: number | null };
|
|
||||||
const maxSeq = maxSeqRow.maxSeq ?? 0;
|
|
||||||
if (maxSeq > 0) {
|
|
||||||
database
|
|
||||||
.prepare('INSERT OR IGNORE INTO message_sequence (id) VALUES (?)')
|
|
||||||
.run(maxSeq);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LegacyExecutionLeaseServiceRow {
|
interface LegacyExecutionLeaseServiceRow {
|
||||||
rowid: number;
|
rowid: number;
|
||||||
role: string;
|
role: string;
|
||||||
@@ -2574,98 +2510,10 @@ function backfillCanonicalWorkItemServiceIds(database: Database): void {
|
|||||||
|
|
||||||
export function applyLegacySchemaMigrations(
|
export function applyLegacySchemaMigrations(
|
||||||
database: Database,
|
database: Database,
|
||||||
args: {
|
_args: {
|
||||||
assistantName: string;
|
assistantName: string;
|
||||||
},
|
},
|
||||||
): void {
|
): void {
|
||||||
const { assistantName } = args;
|
|
||||||
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE scheduled_tasks ADD COLUMN context_mode TEXT DEFAULT 'isolated'`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE scheduled_tasks ADD COLUMN agent_type TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE scheduled_tasks ADD COLUMN ci_provider TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE scheduled_tasks ADD COLUMN ci_metadata TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE scheduled_tasks ADD COLUMN max_duration_ms INTEGER`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE scheduled_tasks ADD COLUMN status_message_id TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE scheduled_tasks ADD COLUMN status_started_at TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE scheduled_tasks ADD COLUMN suspended_until TEXT`,
|
|
||||||
);
|
|
||||||
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE room_settings ADD COLUMN mode_source TEXT NOT NULL DEFAULT 'explicit'`,
|
|
||||||
);
|
|
||||||
tryExecMigration(database, `ALTER TABLE room_settings ADD COLUMN name TEXT`);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE room_settings ADD COLUMN folder TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE room_settings ADD COLUMN trigger_pattern TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE room_settings ADD COLUMN requires_trigger INTEGER DEFAULT 1`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE room_settings ADD COLUMN is_main INTEGER DEFAULT 0`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE room_settings ADD COLUMN owner_agent_type TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE room_settings ADD COLUMN work_dir TEXT`,
|
|
||||||
);
|
|
||||||
tryExecMigration(
|
|
||||||
database,
|
|
||||||
`ALTER TABLE room_settings ADD COLUMN created_at TEXT`,
|
|
||||||
);
|
|
||||||
if (tableHasColumn(database, 'room_settings', 'created_at')) {
|
|
||||||
database.exec(`
|
|
||||||
UPDATE room_settings
|
|
||||||
SET created_at = COALESCE(created_at, updated_at, CURRENT_TIMESTAMP)
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
database.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS room_role_overrides (
|
|
||||||
chat_jid TEXT NOT NULL,
|
|
||||||
role TEXT NOT NULL,
|
|
||||||
agent_type TEXT NOT NULL,
|
|
||||||
agent_config_json TEXT,
|
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
PRIMARY KEY (chat_jid, role),
|
|
||||||
CHECK (role IN ('owner', 'reviewer', 'arbiter')),
|
|
||||||
CHECK (agent_type IN ('claude-code', 'codex'))
|
|
||||||
)
|
|
||||||
`);
|
|
||||||
|
|
||||||
tryExecMigration(
|
tryExecMigration(
|
||||||
database,
|
database,
|
||||||
`ALTER TABLE service_handoffs ADD COLUMN intended_role TEXT`,
|
`ALTER TABLE service_handoffs ADD COLUMN intended_role TEXT`,
|
||||||
@@ -2935,41 +2783,6 @@ export function applyLegacySchemaMigrations(
|
|||||||
backfillCanonicalServiceHandoffServiceIds(database);
|
backfillCanonicalServiceHandoffServiceIds(database);
|
||||||
backfillCanonicalWorkItemServiceIds(database);
|
backfillCanonicalWorkItemServiceIds(database);
|
||||||
|
|
||||||
database.exec(
|
|
||||||
`UPDATE room_settings
|
|
||||||
SET mode_source = 'explicit'
|
|
||||||
WHERE COALESCE(mode_source, '') NOT IN ('explicit', 'inferred')`,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
database.exec(
|
|
||||||
`ALTER TABLE messages ADD COLUMN is_bot_message INTEGER DEFAULT 0`,
|
|
||||||
);
|
|
||||||
database
|
|
||||||
.prepare(`UPDATE messages SET is_bot_message = 1 WHERE content LIKE ?`)
|
|
||||||
.run(`${assistantName}:%`);
|
|
||||||
} catch {
|
|
||||||
/* column already exists */
|
|
||||||
}
|
|
||||||
|
|
||||||
tryExecMigration(database, `ALTER TABLE messages ADD COLUMN seq INTEGER`);
|
|
||||||
|
|
||||||
backfillMessageSeq(database);
|
|
||||||
|
|
||||||
database.exec(`
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_seq ON messages(seq);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_chat_jid_seq ON messages(chat_jid, seq);
|
|
||||||
`);
|
|
||||||
database.exec(`DROP INDEX IF EXISTS idx_work_items_group_agent;`);
|
|
||||||
database.exec(`DROP INDEX IF EXISTS idx_work_items_open;`);
|
|
||||||
database.exec(`
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_work_items_group_agent
|
|
||||||
ON work_items(chat_jid, agent_type, service_id, delivery_role, status);
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_work_items_open
|
|
||||||
ON work_items(chat_jid, agent_type, IFNULL(service_id, ''), IFNULL(delivery_role, ''))
|
|
||||||
WHERE status IN ('produced', 'delivery_retry');
|
|
||||||
`);
|
|
||||||
|
|
||||||
const pairedTasksSqlRow = database
|
const pairedTasksSqlRow = database
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'paired_tasks'`,
|
`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'paired_tasks'`,
|
||||||
@@ -3134,29 +2947,6 @@ export function applyLegacySchemaMigrations(
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
migrateSessionsTableToCompositePk(database, 'claude-code');
|
|
||||||
backfillLegacyServiceSessions(database, inferAgentTypeFromServiceShadow);
|
|
||||||
dropLegacyServiceSessionsTable(database);
|
|
||||||
|
|
||||||
try {
|
|
||||||
database.exec(`ALTER TABLE chats ADD COLUMN channel TEXT`);
|
|
||||||
database.exec(`ALTER TABLE chats ADD COLUMN is_group INTEGER DEFAULT 0`);
|
|
||||||
database.exec(
|
|
||||||
`UPDATE chats SET channel = 'whatsapp', is_group = 1 WHERE jid LIKE '%@g.us'`,
|
|
||||||
);
|
|
||||||
database.exec(
|
|
||||||
`UPDATE chats SET channel = 'whatsapp', is_group = 0 WHERE jid LIKE '%@s.whatsapp.net'`,
|
|
||||||
);
|
|
||||||
database.exec(
|
|
||||||
`UPDATE chats SET channel = 'discord', is_group = 1 WHERE jid LIKE 'dc:%'`,
|
|
||||||
);
|
|
||||||
database.exec(
|
|
||||||
`UPDATE chats SET channel = 'telegram', is_group = 1 WHERE jid LIKE 'tg:%'`,
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
/* columns already exist */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated New schema changes should be added under src/db/migrations/*. */
|
/** @deprecated New schema changes should be added under src/db/migrations/*. */
|
||||||
|
|||||||
Reference in New Issue
Block a user