# IHP: Data Modeling and Queries > How IHP handles schema definition, type generation, querying, relationships, and migrations. --- ## Schema.sql — Single Source of Truth All models originate in `Application/Schema.sql`. IHP parses this file and **auto-generates Haskell record types** on every save — no manual codegen step. ```sql CREATE TABLE widgets ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, name TEXT NOT NULL, widget_type TEXT NOT NULL, hub_id UUID NOT NULL, capability_ref TEXT, policy_scope TEXT, status TEXT NOT NULL DEFAULT 'active', version INTEGER NOT NULL DEFAULT 1, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); ``` This produces a Haskell type roughly equivalent to: ```haskell data Widget = Widget { id :: !(Id Widget) , name :: !Text , widgetType :: !Text , hubId :: !(Id Hub) , capabilityRef :: !(Maybe Text) , policyScope :: !(Maybe Text) , status :: !Text , version :: !Int , createdAt :: !UTCTime } ``` ### Conventions | SQL | Haskell | |-----|---------| | `snake_case` column | `camelCase` field | | `NOT NULL` | plain type | | nullable | `Maybe` | | UUID primary key | `Id Widget` (newtype wrapping UUID) | | Foreign key `hub_id UUID` | `hubId :: !(Id Hub)` | The `Id Widget` newtype prevents `Id Hub` from being passed where `Id Widget` is expected — foreign key mixups are compile errors. ### Supported PostgreSQL Types UUID, Text, Int, Integer, Double, Bool, TIMESTAMP WITH TIME ZONE, DATE, JSONB (`Value` via Aeson), ARRAY types, custom ENUMs, INET, Point, and 30+ others — all auto-marshaled to Haskell equivalents. --- ## Schema Designer A GUI schema editor at `http://localhost:8001/Tables` during development. All operations modify the AST of `Schema.sql` and write back to the file. Editing `Schema.sql` directly in a code editor works equally well. --- ## Query Builder (v1.5: built on `hasql`) Type-safe fluent API. All queries require an implicit `?modelContext :: ModelContext` — the compiler tracks which code touches the DB. ```haskell -- Fetch all widgets <- query @Widget |> fetch -- Filter + order + limit recentEvents <- query @InteractionEvent |> filterWhere (#widgetId, widgetId) |> orderByDesc #occurredAt |> limit 50 |> fetch -- Fetch single (throws RecordNotFoundException on missing) widget <- fetch widgetId -- Fetch maybe (returns Nothing on missing) mWidget <- fetchMaybe widgetId -- Count n <- query @Annotation |> filterWhere (#widgetId, widgetId) |> fetchCount ``` ### Pipeline Mode (v1.5) Sends multiple queries in a single network round-trip: ```haskell (widgets, events) <- fetchPipelined do widgets <- query @Widget |> fetch events <- query @InteractionEvent |> fetch pure (widgets, events) ``` ### Typed SQL Quasiquoter (v1.5) Connects to the dev DB at compile time to verify table names, column names, and types: ```haskell result <- [typedSql| SELECT w.id, w.name, COUNT(e.id) as event_count FROM widgets w LEFT JOIN interaction_events e ON e.widget_id = w.id WHERE w.hub_id = ${hubId} GROUP BY w.id, w.name |] ``` Type-checks column references, parameter types, and return shape. Compile error on typo or schema mismatch. --- ## Relationships ### Has-Many ```haskell -- User with their widgets user <- fetch userId >>= fetchRelated #widgets -- user.widgets :: [Widget] ``` Uses two queries: one for parent, one `WHERE id IN (...)` for children (no N+1). **With ordering:** ```haskell user <- fetch userId >>= pure . modify #widgets (orderByDesc #createdAt) >>= fetchRelated #widgets ``` ### Belongs-To ```haskell event <- fetch eventId >>= fetchRelated #widgetId -- event.widgetId :: Widget (resolved from Id Widget to Widget) ``` ### Many-to-Many Use `innerJoin`, `innerJoinThirdTable`, `labelResults` from the query builder. ### Cascade Deletes Configure in `Schema.sql`: ```sql ALTER TABLE interaction_events ADD CONSTRAINT interaction_events_widget_id_fkey FOREIGN KEY (widget_id) REFERENCES widgets(id) ON DELETE CASCADE; ``` --- ## Migrations Migration files are plain SQL in `Application/Migration/-description.sql`. ### Generating ``` # Via Code Generator web UI at localhost:8001/Generators # Or CLI: new-migration "add widget envelope fields" ``` ### Running ```bash migrate # run all pending migrations DATABASE_URL=postgres://... migrate # against a specific DB ``` Executed migrations are tracked in `schema_migrations`. ### Development Workflow 1. Use Schema Designer to iterate on `Schema.sql` (visual, live) 2. Once stable, copy the relevant DDL into a new migration file 3. Migration files are what run in production and CI ### Example Migration ```sql -- Application/Migration/1711500000-add-interaction-events.sql CREATE TABLE interaction_events ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, widget_id UUID NOT NULL REFERENCES widgets(id) ON DELETE CASCADE, event_type TEXT NOT NULL, actor_id UUID, actor_type TEXT NOT NULL DEFAULT 'user', view_context JSONB DEFAULT '{}' NOT NULL, occurred_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); CREATE INDEX interaction_events_widget_id_idx ON interaction_events (widget_id); CREATE INDEX interaction_events_occurred_at_idx ON interaction_events (occurred_at DESC); ``` --- ## IHF Schema Notes The IHF Phase 1 data artifacts map naturally to `Schema.sql` tables: | IHF Artifact | Suggested Table | Key Fields | |-------------|----------------|-----------| | Widget | `widgets` | `id`, `name`, `widget_type`, `hub_id`, `capability_ref`, `policy_scope`, `status`, `version` | | WidgetVersion | `widget_versions` | `widget_id`, `version`, `schema_snapshot` (JSONB), `created_at` | | Hub | `hubs` | `id`, `slug`, `name`, `domain` | | CapabilityReference | `capability_references` | `id`, `hub_id`, `capability_key`, `description` | | ViewContext | `view_contexts` | `id`, `widget_id`, `context_path`, `metadata` (JSONB) | | InteractionEvent | `interaction_events` | `widget_id`, `event_type`, `actor_id`, `actor_type`, `view_context_id`, `occurred_at` | | Annotation | `annotations` | `widget_id`, `body`, `category`, `actor_id`, `actor_type`, `widget_state_ref`, `created_at` | All tables should use UUID primary keys (`uuid_generate_v4()`), `NOT NULL` on required fields, and `TIMESTAMP WITH TIME ZONE` for timestamps (never plain `TIMESTAMP`).