Adds all Phase 0 content that was created but never committed: - CLAUDE.md and SCOPE.md — agent and developer orientation - specs/TailwindForInteractionHubs_v0.2.md — IHF Tailwind coding guide - docs/ — five IHP v1.5 reference guides (overview, data, controllers, realtime, ihf-mapping) - workplans/IHUB-WP-0001 — Phase 1 implementation plan (12 tasks, state-hub synced) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6.4 KiB
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.
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:
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.
-- 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:
(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:
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
-- 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:
user <- fetch userId
>>= pure . modify #widgets (orderByDesc #createdAt)
>>= fetchRelated #widgets
Belongs-To
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:
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/<timestamp>-description.sql.
Generating
# Via Code Generator web UI at localhost:8001/Generators
# Or CLI:
new-migration "add widget envelope fields"
Running
migrate # run all pending migrations
DATABASE_URL=postgres://... migrate # against a specific DB
Executed migrations are tracked in schema_migrations.
Development Workflow
- Use Schema Designer to iterate on
Schema.sql(visual, live) - Once stable, copy the relevant DDL into a new migration file
- Migration files are what run in production and CI
Example Migration
-- 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).