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

225 lines
6.4 KiB
Markdown

# 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/<timestamp>-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`).