generated from coulomb/repo-seed
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>
This commit is contained in:
224
docs/ihp-data-and-queries.md
Normal file
224
docs/ihp-data-and-queries.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# 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`).
|
||||
Reference in New Issue
Block a user