Files
inter-hub/docs/ihp-data-and-queries.md
tegwick 8b6ce5bbc8 docs: add specification, reference docs, workplan, and agent guidance
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>
2026-03-27 02:07:13 +01:00

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

  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

-- 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).