diff --git a/.agents/skills/migrate-to-sqlite/SKILL.md b/.agents/skills/migrate-to-sqlite/SKILL.md index f138f9d0e1..5eb63d3215 100644 --- a/.agents/skills/migrate-to-sqlite/SKILL.md +++ b/.agents/skills/migrate-to-sqlite/SKILL.md @@ -7,11 +7,22 @@ description: Migrate a TinyBase table to SQLite. Use when asked to move a data d - **Schema source of truth:** Rust migration in `crates/db-app/migrations/` - **Drizzle mirror:** `packages/db/src/schema.ts` (typed TS query interface, not schema management) -- **Reads (reactive):** `useDrizzleLiveQuery` — calls `.toSQL()` on a Drizzle query, feeds `{sql, params}` to `useLiveQuery` which uses `subscribe()` from `@hypr/plugin-db` +- **Reads (reactive):** `useDrizzleLiveQuery` — calls `.toSQL()` on a Drizzle query, feeds `{sql, params}` to the underlying `useLiveQuery` which uses `subscribe()` from `@hypr/plugin-db` - **Reads (imperative):** `db.select()...` through the Drizzle sqlite-proxy driver - **Writes:** `db.insert()`, `db.update()`, `db.delete()` through the Drizzle sqlite-proxy driver, wrapped in `useMutation` from tanstack-query - **Reactivity loop:** write via `execute` → SQLite change → Rust `db-live-query` notifies subscribers → `useLiveQuery` fires `onData` → React re-renders. No manual invalidation needed. +### Package layers + +The DB stack uses a factory/DI pattern across four packages: + +1. `@hypr/db-runtime` (`packages/db-runtime/`) — type contracts only: `LiveQueryClient`, `DrizzleProxyClient`, shared row/query types. +2. `@hypr/db` (`packages/db/`) — Drizzle schema (`schema.ts`) + `createDb(client)` factory using `drizzle-orm/sqlite-proxy`. Re-exports Drizzle operators (`eq`, `and`, `sql`, etc.). +3. `@hypr/db-tauri` (`packages/db-tauri/`) — Tauri-specific client that binds `execute`/`executeProxy`/`subscribe` from `@hypr/plugin-db` to the `db-runtime` types. +4. `@hypr/db-react` (`packages/db-react/`) — `createUseLiveQuery(client)` and `createUseDrizzleLiveQuery(client)` factories. + +These are wired together in `apps/desktop/src/db/index.ts`, which exports `db`, `useLiveQuery`, and `useDrizzleLiveQuery`. **Consumer code imports from `~/db`, not directly from the packages.** + ## Steps ### 1. Rust migration @@ -39,7 +50,11 @@ Replace raw TinyBase reads/writes with: - `db.select()...` for imperative reads (returns parsed objects via proxy driver) - `db.insert()`, `db.update()`, `db.delete()` for writes, wrapped in `useMutation` -Live query results come from Rust `subscribe` as raw objects (not through Drizzle driver), so `mapRows` must still handle JSON parsing for JSON columns. +Import `db` and `useDrizzleLiveQuery` from `~/db` (the app-level wiring module), and schema tables/operators from `@hypr/db`. + +Live query results come from Rust `subscribe` as raw objects (not through the Drizzle driver), so `mapRows` must handle two things: +- **JSON parsing** for JSON text columns (e.g. `sections_json`, `targets_json`). +- **snake_case → camelCase mapping.** Live rows use the raw SQLite column names (`pin_order`, `targets_json`), while Drizzle's `$inferSelect` uses camelCase (`pinOrder`, `targetsJson`). Define a separate `LiveRow` type with snake_case keys for `mapRows`, distinct from the Drizzle inferred type. See `TemplateLiveRow` in `apps/desktop/src/templates/queries.ts` for the pattern. ### 6. Remove TinyBase artifacts diff --git a/apps/desktop/src/calendar/components/apple/calendar-selection.tsx b/apps/desktop/src/calendar/components/apple/calendar-selection.tsx index b5e48181af..78da9247ca 100644 --- a/apps/desktop/src/calendar/components/apple/calendar-selection.tsx +++ b/apps/desktop/src/calendar/components/apple/calendar-selection.tsx @@ -7,8 +7,8 @@ import { type CalendarItem, CalendarSelection, } from "~/calendar/components/calendar-selection"; +import { useCalendars, useToggleCalendarEnabled } from "~/calendar/queries"; import { useMountEffect } from "~/shared/hooks/useMountEffect"; -import * as main from "~/store/tinybase/store/main"; const SUBSCRIBED_SOURCE_NAME = "Subscribed Calendars"; @@ -43,20 +43,20 @@ export function useAppleCalendarSelection() { const { cancelDebouncedSync, status, scheduleDebouncedSync, scheduleSync } = useSync(); - const store = main.UI.useStore(main.STORE_ID); - const calendars = main.UI.useTable("calendars", main.STORE_ID); + const allCalendars = useCalendars(); + const toggleEnabled = useToggleCalendarEnabled(); const groups = useMemo((): CalendarGroup[] => { - const appleCalendars = Object.entries(calendars).filter( - ([_, cal]) => cal.provider === "apple", + const appleCalendars = allCalendars.filter( + (cal) => cal.provider === "apple", ); const grouped = new Map(); - for (const [id, cal] of appleCalendars) { + for (const cal of appleCalendars) { const source = cal.source || "Apple Calendar"; if (!grouped.has(source)) grouped.set(source, []); grouped.get(source)!.push({ - id, + id: cal.id, title: cal.name || "Untitled", color: cal.color ?? "#888", enabled: cal.enabled ?? false, @@ -73,14 +73,14 @@ export function useAppleCalendarSelection() { if (b.sourceName === SUBSCRIBED_SOURCE_NAME) return -1; return 0; }); - }, [calendars]); + }, [allCalendars]); const handleToggle = useCallback( (calendar: CalendarItem, enabled: boolean) => { - store?.setPartialRow("calendars", calendar.id, { enabled }); + void toggleEnabled(calendar.id, enabled); scheduleDebouncedSync(); }, - [store, scheduleDebouncedSync], + [toggleEnabled, scheduleDebouncedSync], ); const handleRefresh = useCallback(() => { diff --git a/apps/desktop/src/calendar/components/event-chip.tsx b/apps/desktop/src/calendar/components/event-chip.tsx index 18e84e0453..7adfadbff0 100644 --- a/apps/desktop/src/calendar/components/event-chip.tsx +++ b/apps/desktop/src/calendar/components/event-chip.tsx @@ -11,38 +11,32 @@ import { import { cn } from "@hypr/utils"; import { toTz, useTimezone } from "~/calendar/hooks"; +import { useCalendarById, useEventById } from "~/calendar/queries"; import { EventDisplay } from "~/session/components/outer-header/metadata"; -import { useEvent } from "~/store/tinybase/hooks"; import * as main from "~/store/tinybase/store/main"; import { getOrCreateSessionForEventId } from "~/store/tinybase/store/sessions"; import { useTabs } from "~/store/zustand/tabs"; function useCalendarColor(calendarId: string | null): string | null { - const calendar = main.UI.useRow("calendars", calendarId ?? "", main.STORE_ID); + const calendar = useCalendarById(calendarId); if (!calendarId) return null; - return calendar?.color ? String(calendar.color) : null; + return calendar?.color ?? null; } export function EventChip({ eventId }: { eventId: string }) { const tz = useTimezone(); - const event = main.UI.useResultRow( - main.QUERIES.timelineEvents, - eventId, - main.STORE_ID, - ); - const calendarColor = useCalendarColor( - (event?.calendar_id as string) ?? null, - ); + const event = useEventById(eventId); + const calendarColor = useCalendarColor(event?.calendarId ?? null); if (!event || !event.title) { return null; } - const isAllDay = !!event.is_all_day; + const isAllDay = !!event.isAllDay; const color = calendarColor ?? "#888"; - const startedAt = event.started_at - ? format(toTz(event.started_at as string, tz), "h:mm a") + const startedAt = event.startedAt + ? format(toTz(event.startedAt, tz), "h:mm a") : null; return ( @@ -56,7 +50,7 @@ export function EventChip({ eventId }: { eventId: string }) { ])} style={{ backgroundColor: color }} > - {event.title as string} + {event.title} ) : (