db: split legacy schema bundle migrations
This commit is contained in:
@@ -20,6 +20,20 @@ function getAppliedSchemaMigrations(
|
||||
.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', () => {
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
@@ -35,12 +49,9 @@ describe('initializeDatabaseSchema', () => {
|
||||
try {
|
||||
initializeDatabaseSchema(database);
|
||||
|
||||
expect(getAppliedSchemaMigrations(database)).toEqual([
|
||||
{
|
||||
version: 1,
|
||||
name: 'legacy_schema_bundle',
|
||||
},
|
||||
]);
|
||||
expect(getAppliedSchemaMigrations(database)).toEqual(
|
||||
getExpectedSchemaMigrations(),
|
||||
);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
@@ -53,12 +64,9 @@ describe('initializeDatabaseSchema', () => {
|
||||
initializeDatabaseSchema(database);
|
||||
initializeDatabaseSchema(database);
|
||||
|
||||
expect(getAppliedSchemaMigrations(database)).toEqual([
|
||||
{
|
||||
version: 1,
|
||||
name: 'legacy_schema_bundle',
|
||||
},
|
||||
]);
|
||||
expect(getAppliedSchemaMigrations(database)).toEqual(
|
||||
getExpectedSchemaMigrations(),
|
||||
);
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
@@ -86,12 +94,9 @@ describe('initializeDatabaseSchema', () => {
|
||||
try {
|
||||
initializeDatabaseSchema(reopened);
|
||||
|
||||
expect(getAppliedSchemaMigrations(reopened)).toEqual([
|
||||
{
|
||||
version: 1,
|
||||
name: 'legacy_schema_bundle',
|
||||
},
|
||||
]);
|
||||
expect(getAppliedSchemaMigrations(reopened)).toEqual(
|
||||
getExpectedSchemaMigrations(),
|
||||
);
|
||||
} finally {
|
||||
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 { 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 {
|
||||
SchemaMigrationArgs,
|
||||
SchemaMigrationDefinition,
|
||||
@@ -10,6 +15,11 @@ const SCHEMA_MIGRATIONS_TABLE = 'schema_migrations';
|
||||
|
||||
const ORDERED_SCHEMA_MIGRATIONS: readonly SchemaMigrationDefinition[] = [
|
||||
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 {
|
||||
|
||||
224
src/db/schema.ts
224
src/db/schema.ts
@@ -1,10 +1,7 @@
|
||||
import { Database } from 'bun:sqlite';
|
||||
|
||||
import { SERVICE_SESSION_SCOPE, normalizeServiceId } from '../config.js';
|
||||
import {
|
||||
inferAgentTypeFromServiceShadow,
|
||||
resolveRoleServiceShadow,
|
||||
} from '../role-service-shadow.js';
|
||||
import { resolveRoleServiceShadow } from '../role-service-shadow.js';
|
||||
import {
|
||||
fillCanonicalChannelOwnerLeaseMetadata,
|
||||
fillCanonicalPairedTaskMetadata,
|
||||
@@ -15,33 +12,17 @@ import {
|
||||
backfillPairedTurnAttemptsFromTurns,
|
||||
buildPairedTurnAttemptId,
|
||||
} from './paired-turn-attempts.js';
|
||||
import { normalizeStoredAgentType } from './room-registration.js';
|
||||
import {
|
||||
backfillLegacyServiceSessions,
|
||||
dropLegacyServiceSessionsTable,
|
||||
migrateSessionsTableToCompositePk,
|
||||
} from './sessions.js';
|
||||
getTableColumns,
|
||||
tableHasColumn,
|
||||
tryExecMigration,
|
||||
} from './migrations/helpers.js';
|
||||
import { normalizeStoredAgentType } from './room-registration.js';
|
||||
|
||||
// Legacy monolithic migration bundle kept for compatibility with databases
|
||||
// that predate ordered schema migration tracking. New schema changes belong in
|
||||
// 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 {
|
||||
const row = database
|
||||
.prepare(
|
||||
@@ -56,14 +37,6 @@ function getTableSql(database: Database, tableName: string): string | null {
|
||||
return row?.sql ?? null;
|
||||
}
|
||||
|
||||
function tryExecMigration(database: Database, sql: string): void {
|
||||
try {
|
||||
database.exec(sql);
|
||||
} catch {
|
||||
/* column already exists */
|
||||
}
|
||||
}
|
||||
|
||||
function buildPairedTurnAttemptIdSql(
|
||||
turnIdExpr: string,
|
||||
attemptNoExpr: string,
|
||||
@@ -183,43 +156,6 @@ function tableHasForeignKey(
|
||||
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 {
|
||||
rowid: number;
|
||||
role: string;
|
||||
@@ -2574,98 +2510,10 @@ function backfillCanonicalWorkItemServiceIds(database: Database): void {
|
||||
|
||||
export function applyLegacySchemaMigrations(
|
||||
database: Database,
|
||||
args: {
|
||||
_args: {
|
||||
assistantName: string;
|
||||
},
|
||||
): 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(
|
||||
database,
|
||||
`ALTER TABLE service_handoffs ADD COLUMN intended_role TEXT`,
|
||||
@@ -2935,41 +2783,6 @@ export function applyLegacySchemaMigrations(
|
||||
backfillCanonicalServiceHandoffServiceIds(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
|
||||
.prepare(
|
||||
`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/*. */
|
||||
|
||||
Reference in New Issue
Block a user