diff --git a/src/main/data/db/README.md b/src/main/data/db/README.md index ab772615dc..10d3a44593 100644 --- a/src/main/data/db/README.md +++ b/src/main/data/db/README.md @@ -88,6 +88,8 @@ Drizzle handles JSON serialization/deserialization automatically. ## Foreign Keys +### Basic Usage + ```typescript // SET NULL: preserve record when referenced record is deleted groupId: text().references(() => groupTable.id, { onDelete: 'set null' }) @@ -96,6 +98,69 @@ groupId: text().references(() => groupTable.id, { onDelete: 'set null' }) topicId: text().references(() => topicTable.id, { onDelete: 'cascade' }) ``` +### Self-Referencing Foreign Keys + +For self-referencing foreign keys (e.g., tree structures with parentId), **always use the `foreignKey` operator** in the table's third parameter: + +```typescript +import { foreignKey, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +export const messageTable = sqliteTable( + 'message', + { + id: uuidPrimaryKeyOrdered(), + parentId: text(), // Do NOT use .references() here + // ...other fields + }, + (t) => [ + // Use foreignKey operator for self-referencing + foreignKey({ columns: [t.parentId], foreignColumns: [t.id] }).onDelete('set null') + ] +) +``` + +**Why this approach:** +- Avoids TypeScript circular reference issues (no need for `AnySQLiteColumn` type annotation) +- More explicit and readable +- Allows chaining `.onDelete()` / `.onUpdate()` actions + +### Circular Foreign Key References + +**Avoid circular foreign key references between tables.** For example: + +```typescript +// ❌ BAD: Circular FK between tables +// tableA.currentItemId -> tableB.id +// tableB.ownerId -> tableA.id +``` + +If you encounter a scenario that seems to require circular references: + +1. **Identify which relationship is "weaker"** - typically the one that can be null or is less critical for data integrity +2. **Remove the FK constraint from the weaker side** - let the application layer handle validation and consistency (this is known as "soft references" pattern) +3. **Document the application-layer constraint** in code comments + +```typescript +// ✅ GOOD: Break the cycle by handling one side at application layer +export const topicTable = sqliteTable('topic', { + id: uuidPrimaryKey(), + // Application-managed reference (no FK constraint) + // Validated by TopicService.setCurrentMessage() + currentMessageId: text(), +}) + +export const messageTable = sqliteTable('message', { + id: uuidPrimaryKeyOrdered(), + // Database-enforced FK + topicId: text().references(() => topicTable.id, { onDelete: 'cascade' }), +}) +``` + +**Why soft references for SQLite:** +- SQLite does not support `DEFERRABLE` constraints (unlike PostgreSQL/Oracle) +- Application-layer validation provides equivalent data integrity +- Simplifies insert/update operations without transaction ordering concerns + ## Migrations Generate migrations after schema changes: