feat(database): enhance message and topic schemas with new fields

- Added `siblingsGroupId` to `message` schema for better message organization.
- Introduced `activeNodeId` in `topic` schema to manage message tree structure.
- Updated `assistantMeta` and `modelMeta` fields to use specific types for improved type safety.
- Included `isNameManuallyEdited` and `sortOrder` in `topic` schema for enhanced topic management.
- Added a new entry in the migration journal for version tracking.
This commit is contained in:
fullex 2025-12-26 19:21:42 +08:00
parent f84a2588fd
commit 61e80f2e7f
6 changed files with 754 additions and 19 deletions

View File

@ -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);

View File

@ -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": {}
}
}

View File

@ -15,6 +15,13 @@
"when": 1766670360754,
"tag": "0001_faulty_ogun",
"breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1766748070409,
"tag": "0002_noisy_zzzax",
"breakpoints": true
}
]
}
}

View File

@ -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
}

View File

@ -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<MessageData>().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<AssistantMeta>(),
// Model identifier
modelId: text(),
// Preserved model info (provider, name)
modelMeta: text({ mode: 'json' }),
// Main content - contains blocks[], mentions, etc.
data: text({ mode: 'json' }).$type<MessageData>().notNull(),
modelMeta: text({ mode: 'json' }).$type<ModelMeta>(),
// Trace ID for tracking
traceId: text(),
// Statistics: token usage, performance metrics, etc.
stats: text({ mode: 'json' }).$type<MessageStats>(),
// Trace ID for tracking
traceId: text(),
// Searchable text extracted from data.blocks (populated by trigger, used for FTS5)
searchableText: text(),
...createUpdateDeleteTimestamps
},
(t) => [

View File

@ -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<AssistantMeta>(),
// 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) => [