From 7dc63e625d8d808476b683499cc45b5a7a472d95 Mon Sep 17 00:00:00 2001 From: Thomas Richter Date: Sat, 31 Jan 2026 13:03:17 +0100 Subject: [PATCH] feat(04-01): add tags schema with case-insensitive unique index - Add tags table with nanoid PK and case-insensitive unique index on name - Add entry_tags junction table with composite PK and cascade deletes - Export lower() helper function for case-insensitive queries - Export Tag, NewTag, EntryTag types for TypeScript inference --- src/lib/server/db/schema.ts | 41 ++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 0104d1f..5db6fdf 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -1,4 +1,11 @@ -import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; +import { sqliteTable, text, integer, uniqueIndex, primaryKey } from 'drizzle-orm/sqlite-core'; +import { sql, type SQL } from 'drizzle-orm'; +import type { AnySQLiteColumn } from 'drizzle-orm/sqlite-core'; + +// Helper function for case-insensitive indexing +export function lower(column: AnySQLiteColumn): SQL { + return sql`lower(${column})`; +} // Entry types: 'task' (actionable) or 'thought' (reference) export const entries = sqliteTable('entries', { @@ -36,3 +43,35 @@ export const images = sqliteTable('images', { export type Image = typeof images.$inferSelect; export type NewImage = typeof images.$inferInsert; + +// Tags table: reusable tags for organizing entries +export const tags = sqliteTable( + 'tags', + { + id: text('id').primaryKey(), // nanoid + name: text('name').notNull(), + createdAt: text('created_at') + .notNull() + .$defaultFn(() => new Date().toISOString()) + }, + (table) => [uniqueIndex('tagNameUniqueIndex').on(lower(table.name))] +); + +export type Tag = typeof tags.$inferSelect; +export type NewTag = typeof tags.$inferInsert; + +// Junction table for many-to-many entry-tag relationship +export const entryTags = sqliteTable( + 'entry_tags', + { + entryId: text('entry_id') + .notNull() + .references(() => entries.id, { onDelete: 'cascade' }), + tagId: text('tag_id') + .notNull() + .references(() => tags.id, { onDelete: 'cascade' }) + }, + (t) => [primaryKey({ columns: [t.entryId, t.tagId] })] +); + +export type EntryTag = typeof entryTags.$inferSelect;