diff --git a/migrations/sqlite-drizzle/0002_noisy_zzzax.sql b/migrations/sqlite-drizzle/0002_noisy_zzzax.sql new file mode 100644 index 0000000000..b9c2b04d57 --- /dev/null +++ b/migrations/sqlite-drizzle/0002_noisy_zzzax.sql @@ -0,0 +1,2 @@ +ALTER TABLE `message` RENAME COLUMN "response_group_id" TO "siblings_group_id";--> statement-breakpoint +ALTER TABLE `topic` ADD `active_node_id` text REFERENCES message(id); \ No newline at end of file diff --git a/migrations/sqlite-drizzle/meta/0002_snapshot.json b/migrations/sqlite-drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000000..4d56d6a4f2 --- /dev/null +++ b/migrations/sqlite-drizzle/meta/0002_snapshot.json @@ -0,0 +1,677 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b4613090-1bbb-4986-a27b-f58b638f540b", + "prevId": "ae53858a-1786-4059-9ff7-9e87267911b6", + "tables": { + "app_state": { + "name": "app_state", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "entity_tag": { + "name": "entity_tag", + "columns": { + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "entity_tag_tag_id_idx": { + "name": "entity_tag_tag_id_idx", + "columns": [ + "tag_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "entity_tag_tag_id_tag_id_fk": { + "name": "entity_tag_tag_id_tag_id_fk", + "tableFrom": "entity_tag", + "tableTo": "tag", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "entity_tag_entity_type_entity_id_tag_id_pk": { + "columns": [ + "entity_type", + "entity_id", + "tag_id" + ], + "name": "entity_tag_entity_type_entity_id_tag_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "group_entity_sort_idx": { + "name": "group_entity_sort_idx", + "columns": [ + "entity_type", + "sort_order" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "message": { + "name": "message", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "searchable_text": { + "name": "searchable_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "siblings_group_id": { + "name": "siblings_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "assistant_id": { + "name": "assistant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "assistant_meta": { + "name": "assistant_meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model_meta": { + "name": "model_meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "trace_id": { + "name": "trace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stats": { + "name": "stats", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "message_parent_id_idx": { + "name": "message_parent_id_idx", + "columns": [ + "parent_id" + ], + "isUnique": false + }, + "message_topic_created_idx": { + "name": "message_topic_created_idx", + "columns": [ + "topic_id", + "created_at" + ], + "isUnique": false + }, + "message_trace_id_idx": { + "name": "message_trace_id_idx", + "columns": [ + "trace_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "message_parent_id_message_id_fk": { + "name": "message_parent_id_message_id_fk", + "tableFrom": "message", + "tableTo": "message", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "message_topic_id_topic_id_fk": { + "name": "message_topic_id_topic_id_fk", + "tableFrom": "message", + "tableTo": "topic", + "columnsFrom": [ + "topic_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": { + "message_role_check": { + "name": "message_role_check", + "value": "\"message\".\"role\" IN ('user', 'assistant', 'system')" + }, + "message_status_check": { + "name": "message_status_check", + "value": "\"message\".\"status\" IN ('success', 'error', 'paused')" + } + } + }, + "preference": { + "name": "preference", + "columns": { + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "preference_scope_key_pk": { + "columns": [ + "scope", + "key" + ], + "name": "preference_scope_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tag": { + "name": "tag", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "tag_name_unique": { + "name": "tag_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "topic": { + "name": "topic", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_name_manually_edited": { + "name": "is_name_manually_edited", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "assistant_id": { + "name": "assistant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "assistant_meta": { + "name": "assistant_meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_node_id": { + "name": "active_node_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "is_pinned": { + "name": "is_pinned", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "pinned_order": { + "name": "pinned_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "topic_group_updated_idx": { + "name": "topic_group_updated_idx", + "columns": [ + "group_id", + "updated_at" + ], + "isUnique": false + }, + "topic_group_sort_idx": { + "name": "topic_group_sort_idx", + "columns": [ + "group_id", + "sort_order" + ], + "isUnique": false + }, + "topic_updated_at_idx": { + "name": "topic_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "topic_is_pinned_idx": { + "name": "topic_is_pinned_idx", + "columns": [ + "is_pinned", + "pinned_order" + ], + "isUnique": false + }, + "topic_assistant_id_idx": { + "name": "topic_assistant_id_idx", + "columns": [ + "assistant_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "topic_active_node_id_message_id_fk": { + "name": "topic_active_node_id_message_id_fk", + "tableFrom": "topic", + "tableTo": "message", + "columnsFrom": [ + "active_node_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "topic_group_id_group_id_fk": { + "name": "topic_group_id_group_id_fk", + "tableFrom": "topic", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": { + "\"message\".\"response_group_id\"": "\"message\".\"siblings_group_id\"" + } + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/migrations/sqlite-drizzle/meta/_journal.json b/migrations/sqlite-drizzle/meta/_journal.json index 960ba8a1e0..b83fe4ed13 100644 --- a/migrations/sqlite-drizzle/meta/_journal.json +++ b/migrations/sqlite-drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1766670360754, "tag": "0001_faulty_ogun", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1766748070409, + "tag": "0002_noisy_zzzax", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/shared/data/types/meta.ts b/packages/shared/data/types/meta.ts new file mode 100644 index 0000000000..2bba74d700 --- /dev/null +++ b/packages/shared/data/types/meta.ts @@ -0,0 +1,36 @@ +/** + * Soft reference metadata types + * + * These types store snapshots of referenced entities at creation time, + * preserving display information even if the original entity is deleted. + */ + +/** + * Preserved assistant info for display when assistant is deleted + * Used in: message.assistantMeta, topic.assistantMeta + */ +export interface AssistantMeta { + /** Original assistant ID, used to attempt reference recovery */ + id: string + /** Assistant display name shown in UI */ + name: string + /** Assistant icon emoji for visual identification */ + emoji?: string + /** Assistant type, e.g., 'default', 'custom', 'agent' */ + type?: string +} + +/** + * Preserved model info for display when model is unavailable + * Used in: message.modelMeta + */ +export interface ModelMeta { + /** Original model ID, used to attempt reference recovery */ + id: string + /** Model display name, e.g., "GPT-4o", "Claude 3.5 Sonnet" */ + name: string + /** Provider identifier, e.g., "openai", "anthropic", "google" */ + provider: string + /** Model family/group, e.g., "gpt-4", "claude-3", useful for grouping in UI */ + group?: string +} diff --git a/src/main/data/db/schemas/message.ts b/src/main/data/db/schemas/message.ts index 91692ebd3f..b5c1081652 100644 --- a/src/main/data/db/schemas/message.ts +++ b/src/main/data/db/schemas/message.ts @@ -1,4 +1,5 @@ import type { MessageData, MessageStats } from '@shared/data/types/message' +import type { AssistantMeta, ModelMeta } from '@shared/data/types/meta' import { sql } from 'drizzle-orm' import { check, index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' @@ -16,35 +17,39 @@ export const messageTable = sqliteTable( 'message', { id: text().primaryKey(), + // Adjacency list parent reference for tree structure + // SET NULL: preserve child messages when parent is deleted + parentId: text().references(() => messageTable.id, { onDelete: 'set null' }), // FK to topic - CASCADE: delete messages when topic is deleted topicId: text() .notNull() .references(() => topicTable.id, { onDelete: 'cascade' }), - // Adjacency list parent reference for tree structure - // SET NULL: preserve child messages when parent is deleted - parentId: text().references(() => messageTable.id, { onDelete: 'set null' }), - // Group ID for multi-model responses (0 = normal branch) - responseGroupId: integer().default(0), // Message role: user, assistant, system role: text().notNull(), + // Main content - contains blocks[], mentions, etc. + data: text({ mode: 'json' }).$type().notNull(), + // Searchable text extracted from data.blocks (populated by trigger, used for FTS5) + searchableText: text(), + // Final status: SUCCESS, ERROR, PAUSED status: text().notNull(), + + // Group ID for siblings (0 = normal branch) + siblingsGroupId: integer().default(0), // FK to assistant assistantId: text(), // Preserved assistant info for display - assistantMeta: text({ mode: 'json' }), + assistantMeta: text({ mode: 'json' }).$type(), // Model identifier modelId: text(), // Preserved model info (provider, name) - modelMeta: text({ mode: 'json' }), - // Main content - contains blocks[], mentions, etc. - data: text({ mode: 'json' }).$type().notNull(), + modelMeta: text({ mode: 'json' }).$type(), + // Trace ID for tracking + + traceId: text(), // Statistics: token usage, performance metrics, etc. stats: text({ mode: 'json' }).$type(), - // Trace ID for tracking - traceId: text(), - // Searchable text extracted from data.blocks (populated by trigger, used for FTS5) - searchableText: text(), + ...createUpdateDeleteTimestamps }, (t) => [ diff --git a/src/main/data/db/schemas/topic.ts b/src/main/data/db/schemas/topic.ts index 2f08d58fd2..74a0587107 100644 --- a/src/main/data/db/schemas/topic.ts +++ b/src/main/data/db/schemas/topic.ts @@ -1,7 +1,9 @@ +import type { AssistantMeta } from '@shared/data/types/meta' import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' import { createUpdateDeleteTimestamps } from './columnHelpers' import { groupTable } from './group' +import { messageTable } from './message' /** * Topic table - stores conversation topics/threads @@ -14,21 +16,27 @@ export const topicTable = sqliteTable( { id: text().primaryKey(), name: text(), + // Whether the name was manually edited by user + isNameManuallyEdited: integer({ mode: 'boolean' }).default(false), + // FK to assistant table assistantId: text(), // Preserved assistant info for display when assistant is deleted - assistantMeta: text({ mode: 'json' }), + assistantMeta: text({ mode: 'json' }).$type(), // Topic-specific prompt override prompt: text(), + // Active node ID in the message tree + // SET NULL: reset to null when the referenced message is deleted + activeNodeId: text().references(() => messageTable.id, { onDelete: 'set null' }), + // FK to group table for organization // SET NULL: preserve topic when group is deleted groupId: text().references(() => groupTable.id, { onDelete: 'set null' }), + // Sort order within group + sortOrder: integer().default(0), // Pinning state and order isPinned: integer({ mode: 'boolean' }).default(false), pinnedOrder: integer().default(0), - // Sort order within group - sortOrder: integer().default(0), - // Whether the name was manually edited by user - isNameManuallyEdited: integer({ mode: 'boolean' }).default(false), + ...createUpdateDeleteTimestamps }, (t) => [