From c16789f6977eaf60f2144a3ad19aed577c7fd796 Mon Sep 17 00:00:00 2001 From: fullex <0xfullex@gmail.com> Date: Fri, 26 Dec 2025 22:45:13 +0800 Subject: [PATCH] feat(database): update README and column helpers for schema guidelines - Expanded the README with detailed database schema guidelines, including naming conventions for tables, columns, and export names. - Introduced new column helper functions for UUID primary keys (v4 and v7) to streamline table definitions. - Updated existing schemas (group, message, tag, topic) to utilize the new UUID primary key helpers for improved consistency and auto-generation. --- .../sqlite-drizzle/meta/0002_snapshot.json | 102 ++++------------- migrations/sqlite-drizzle/meta/_journal.json | 2 +- src/main/data/db/README.md | 107 +++++++++++++++++- src/main/data/db/schemas/columnHelpers.ts | 21 +++- src/main/data/db/schemas/group.ts | 4 +- src/main/data/db/schemas/message.ts | 4 +- src/main/data/db/schemas/tag.ts | 4 +- src/main/data/db/schemas/topic.ts | 4 +- 8 files changed, 158 insertions(+), 90 deletions(-) diff --git a/migrations/sqlite-drizzle/meta/0002_snapshot.json b/migrations/sqlite-drizzle/meta/0002_snapshot.json index 4d56d6a4f2..2a8330fec1 100644 --- a/migrations/sqlite-drizzle/meta/0002_snapshot.json +++ b/migrations/sqlite-drizzle/meta/0002_snapshot.json @@ -91,9 +91,7 @@ "indexes": { "entity_tag_tag_id_idx": { "name": "entity_tag_tag_id_idx", - "columns": [ - "tag_id" - ], + "columns": ["tag_id"], "isUnique": false } }, @@ -102,23 +100,15 @@ "name": "entity_tag_tag_id_tag_id_fk", "tableFrom": "entity_tag", "tableTo": "tag", - "columnsFrom": [ - "tag_id" - ], - "columnsTo": [ - "id" - ], + "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" - ], + "columns": ["entity_type", "entity_id", "tag_id"], "name": "entity_tag_entity_type_entity_id_tag_id_pk" } }, @@ -175,10 +165,7 @@ "indexes": { "group_entity_sort_idx": { "name": "group_entity_sort_idx", - "columns": [ - "entity_type", - "sort_order" - ], + "columns": ["entity_type", "sort_order"], "isUnique": false } }, @@ -314,24 +301,17 @@ "indexes": { "message_parent_id_idx": { "name": "message_parent_id_idx", - "columns": [ - "parent_id" - ], + "columns": ["parent_id"], "isUnique": false }, "message_topic_created_idx": { "name": "message_topic_created_idx", - "columns": [ - "topic_id", - "created_at" - ], + "columns": ["topic_id", "created_at"], "isUnique": false }, "message_trace_id_idx": { "name": "message_trace_id_idx", - "columns": [ - "trace_id" - ], + "columns": ["trace_id"], "isUnique": false } }, @@ -340,12 +320,8 @@ "name": "message_parent_id_message_id_fk", "tableFrom": "message", "tableTo": "message", - "columnsFrom": [ - "parent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -353,12 +329,8 @@ "name": "message_topic_id_topic_id_fk", "tableFrom": "message", "tableTo": "topic", - "columnsFrom": [ - "topic_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["topic_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -420,10 +392,7 @@ "foreignKeys": {}, "compositePrimaryKeys": { "preference_scope_key_pk": { - "columns": [ - "scope", - "key" - ], + "columns": ["scope", "key"], "name": "preference_scope_key_pk" } }, @@ -472,9 +441,7 @@ "indexes": { "tag_name_unique": { "name": "tag_name_unique", - "columns": [ - "name" - ], + "columns": ["name"], "isUnique": true } }, @@ -592,40 +559,27 @@ "indexes": { "topic_group_updated_idx": { "name": "topic_group_updated_idx", - "columns": [ - "group_id", - "updated_at" - ], + "columns": ["group_id", "updated_at"], "isUnique": false }, "topic_group_sort_idx": { "name": "topic_group_sort_idx", - "columns": [ - "group_id", - "sort_order" - ], + "columns": ["group_id", "sort_order"], "isUnique": false }, "topic_updated_at_idx": { "name": "topic_updated_at_idx", - "columns": [ - "updated_at" - ], + "columns": ["updated_at"], "isUnique": false }, "topic_is_pinned_idx": { "name": "topic_is_pinned_idx", - "columns": [ - "is_pinned", - "pinned_order" - ], + "columns": ["is_pinned", "pinned_order"], "isUnique": false }, "topic_assistant_id_idx": { "name": "topic_assistant_id_idx", - "columns": [ - "assistant_id" - ], + "columns": ["assistant_id"], "isUnique": false } }, @@ -634,12 +588,8 @@ "name": "topic_active_node_id_message_id_fk", "tableFrom": "topic", "tableTo": "message", - "columnsFrom": [ - "active_node_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["active_node_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -647,12 +597,8 @@ "name": "topic_group_id_group_id_fk", "tableFrom": "topic", "tableTo": "group", - "columnsFrom": [ - "group_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["group_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -674,4 +620,4 @@ "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 b83fe4ed13..c3aa7c7c47 100644 --- a/migrations/sqlite-drizzle/meta/_journal.json +++ b/migrations/sqlite-drizzle/meta/_journal.json @@ -24,4 +24,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/src/main/data/db/README.md b/src/main/data/db/README.md index 8bc38b01c4..720348e666 100644 --- a/src/main/data/db/README.md +++ b/src/main/data/db/README.md @@ -1,2 +1,105 @@ -- All the database table names use **singular** form, snake_casing -- Export table names use `xxxxTable` +# Database Schema Guidelines + +## Naming Conventions + +- **Table names**: Use **singular** form with snake_case (e.g., `topic`, `message`, `app_state`) +- **Export names**: Use `xxxTable` pattern (e.g., `topicTable`, `messageTable`) +- **Column names**: Drizzle auto-infers from property names, no need to specify explicitly + +## Column Helpers + +All helpers are exported from `./schemas/columnHelpers.ts`. + +### Primary Keys + +| Helper | UUID Version | Use Case | +|--------|--------------|----------| +| `uuidPrimaryKey()` | v4 (random) | General purpose tables | +| `uuidPrimaryKeyOrdered()` | v7 (time-ordered) | Large tables with time-based queries | + +**Usage:** + +```typescript +import { uuidPrimaryKey, uuidPrimaryKeyOrdered } from './columnHelpers' + +// General purpose table +export const topicTable = sqliteTable('topic', { + id: uuidPrimaryKey(), + name: text(), + ... +}) + +// Large table with time-ordered data +export const messageTable = sqliteTable('message', { + id: uuidPrimaryKeyOrdered(), + content: text(), + ... +}) +``` + +**Behavior:** + +- ID is auto-generated if not provided during insert +- Can be manually specified for migration scenarios +- Use `.returning()` to get the generated ID after insert + +### Timestamps + +| Helper | Fields | Use Case | +|--------|--------|----------| +| `createUpdateTimestamps` | `createdAt`, `updatedAt` | Tables without soft delete | +| `createUpdateDeleteTimestamps` | `createdAt`, `updatedAt`, `deletedAt` | Tables with soft delete | + +**Usage:** + +```typescript +import { createUpdateTimestamps, createUpdateDeleteTimestamps } from './columnHelpers' + +// Without soft delete +export const tagTable = sqliteTable('tag', { + id: uuidPrimaryKey(), + name: text(), + ...createUpdateTimestamps +}) + +// With soft delete +export const topicTable = sqliteTable('topic', { + id: uuidPrimaryKey(), + name: text(), + ...createUpdateDeleteTimestamps +}) +``` + +**Behavior:** + +- `createdAt`: Auto-set to `Date.now()` on insert +- `updatedAt`: Auto-set on insert, auto-updated on update +- `deletedAt`: `null` by default, set to timestamp for soft delete + +## JSON Fields + +For JSON column support, use `{ mode: 'json' }`: + +```typescript +data: text({ mode: 'json' }).$type() +``` + +Drizzle handles JSON serialization/deserialization automatically. + +## Foreign Keys + +```typescript +// SET NULL: preserve record when referenced record is deleted +groupId: text().references(() => groupTable.id, { onDelete: 'set null' }) + +// CASCADE: delete record when referenced record is deleted +topicId: text().references(() => topicTable.id, { onDelete: 'cascade' }) +``` + +## Migrations + +Generate migrations after schema changes: + +```bash +yarn db:migrations:generate +``` diff --git a/src/main/data/db/schemas/columnHelpers.ts b/src/main/data/db/schemas/columnHelpers.ts index 7623afd0ed..c5ea83804c 100644 --- a/src/main/data/db/schemas/columnHelpers.ts +++ b/src/main/data/db/schemas/columnHelpers.ts @@ -1,4 +1,23 @@ -import { integer } from 'drizzle-orm/sqlite-core' +import { integer, text } from 'drizzle-orm/sqlite-core' +import { v4 as uuidv4, v7 as uuidv7 } from 'uuid' + +/** + * UUID v4 primary key with auto-generation + * Use for general purpose tables + */ +export const uuidPrimaryKey = () => + text() + .primaryKey() + .$defaultFn(() => uuidv4()) + +/** + * UUID v7 primary key with auto-generation (time-ordered) + * Use for tables with large datasets that benefit from sequential inserts + */ +export const uuidPrimaryKeyOrdered = () => + text() + .primaryKey() + .$defaultFn(() => uuidv7()) const createTimestamp = () => { return Date.now() diff --git a/src/main/data/db/schemas/group.ts b/src/main/data/db/schemas/group.ts index dc7bd088c2..6ef06c522f 100644 --- a/src/main/data/db/schemas/group.ts +++ b/src/main/data/db/schemas/group.ts @@ -1,6 +1,6 @@ import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' -import { createUpdateTimestamps } from './columnHelpers' +import { createUpdateTimestamps, uuidPrimaryKey } from './columnHelpers' /** * Group table - general-purpose grouping for entities @@ -11,7 +11,7 @@ import { createUpdateTimestamps } from './columnHelpers' export const groupTable = sqliteTable( 'group', { - id: text().primaryKey(), + id: uuidPrimaryKey(), // Entity type this group belongs to: topic, session, assistant entityType: text().notNull(), // Display name of the group diff --git a/src/main/data/db/schemas/message.ts b/src/main/data/db/schemas/message.ts index b5c1081652..08545b9f9b 100644 --- a/src/main/data/db/schemas/message.ts +++ b/src/main/data/db/schemas/message.ts @@ -3,7 +3,7 @@ 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' -import { createUpdateDeleteTimestamps } from './columnHelpers' +import { createUpdateDeleteTimestamps, uuidPrimaryKeyOrdered } from './columnHelpers' import { topicTable } from './topic' /** @@ -16,7 +16,7 @@ import { topicTable } from './topic' export const messageTable = sqliteTable( 'message', { - id: text().primaryKey(), + id: uuidPrimaryKeyOrdered(), // Adjacency list parent reference for tree structure // SET NULL: preserve child messages when parent is deleted parentId: text().references(() => messageTable.id, { onDelete: 'set null' }), diff --git a/src/main/data/db/schemas/tag.ts b/src/main/data/db/schemas/tag.ts index 8a84e6d704..87820fadf9 100644 --- a/src/main/data/db/schemas/tag.ts +++ b/src/main/data/db/schemas/tag.ts @@ -1,6 +1,6 @@ import { sqliteTable, text } from 'drizzle-orm/sqlite-core' -import { createUpdateTimestamps } from './columnHelpers' +import { createUpdateTimestamps, uuidPrimaryKey } from './columnHelpers' /** * Tag table - general-purpose tags for entities @@ -9,7 +9,7 @@ import { createUpdateTimestamps } from './columnHelpers' * via the entity_tag join table. */ export const tagTable = sqliteTable('tag', { - id: text().primaryKey(), + id: uuidPrimaryKey(), // Unique tag name name: text().notNull().unique(), // Display color (hex code) diff --git a/src/main/data/db/schemas/topic.ts b/src/main/data/db/schemas/topic.ts index 74a0587107..b121c5405d 100644 --- a/src/main/data/db/schemas/topic.ts +++ b/src/main/data/db/schemas/topic.ts @@ -1,7 +1,7 @@ import type { AssistantMeta } from '@shared/data/types/meta' import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' -import { createUpdateDeleteTimestamps } from './columnHelpers' +import { createUpdateDeleteTimestamps, uuidPrimaryKey } from './columnHelpers' import { groupTable } from './group' import { messageTable } from './message' @@ -14,7 +14,7 @@ import { messageTable } from './message' export const topicTable = sqliteTable( 'topic', { - id: text().primaryKey(), + id: uuidPrimaryKey(), name: text(), // Whether the name was manually edited by user isNameManuallyEdited: integer({ mode: 'boolean' }).default(false),