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.
This commit is contained in:
fullex 2025-12-26 22:45:13 +08:00
parent fe7358a33c
commit c16789f697
8 changed files with 158 additions and 90 deletions

View File

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

View File

@ -24,4 +24,4 @@
"breakpoints": true
}
]
}
}

View File

@ -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<MyDataType>()
```
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
```

View File

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

View File

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

View File

@ -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' }),

View File

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

View File

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