generated from coulomb/repo-seed
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>
225 lines
6.4 KiB
Markdown
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`).
|