Iwf Guide
Database and Migrations
Iwf is PostgreSQL-first. You write the schema in SQL, Iwf generates Idris metadata and model types, and runtime queries go through the PostgreSQL runtime adapter.
1. Schema.sql
The schema source of truth is:
Application/Schema.sql
Start with normal PostgreSQL DDL:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE articles (
id BIGSERIAL PRIMARY KEY,
author_id BIGINT NOT NULL REFERENCES users(id),
slug TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now()
);
Application/Fixtures.sql contains development and test data.
2. Generated Idris Types
The app build runs typegen before Idris compilation. In the canonical app:
cd examples/canonical
iwf build
Typegen emits Generated.Schema under build/generated/.
Generated modules include:
- model records such as
Article. - typed primary and foreign key IDs such as
Id "articles". - enum types.
- JSON, schema, and form decoding support for full model rows.
- table and column metadata.
- nullability and constraint metadata.
- model decoders used by TypedSQL.
schemaMetadatafor PostgreSQL-to-Idris type mapping.databaseMetadatafor runtime/schema features.
SQL names are rendered as Idris names:
audit_logs -> AuditLog
user_id -> userId
Nullable columns become Maybe. Primary keys become Id "table". Foreign keys become IDs of the referenced table.
The parser supports the canonical app style plus a growing PostgreSQL subset: tables, enums, extensions, schemas, indexes, RLS policies, comments, domains, arrays, generated columns, column comments, composite primary keys, richer defaults/checks, and common ALTER TABLE ... ADD ... constraints.
It is not a full PostgreSQL parser. Cover unusual DDL with typegen tests before depending on generated metadata from it.
3. Migrations
Migration files live in:
Application/Migrations/
Use migrations to evolve existing databases. Keep Application/Schema.sql as the desired final state.
The check workflow can load Application/Schema.sql, replay migrations into a clean database, and compare schema-only dumps when migration files are present.
4. Handler Database Access
In controllers, handlers already receive a ControllerContext. Iwf uses that context as an auto implicit database source, so ordinary controller queries do not need an explicit db context wrapper:
listTags : ControllerContext -> ControllerApp (List String)
listTags context =
sqlQuery "select name from tags order by name"
Runtime execution uses the canonical PostgreSQL runtime library under:
libs/idris-postgres-runtime
The current backend is a long-lived Python/libpq helper hidden behind that library boundary. App code should call Iwf database APIs. It should not build psql -c commands for runtime queries.
For scripts or framework maintenance code outside a controller, create a pool and borrow a connection explicitly:
runMaintenance : AppConfig -> IO (Either String ())
runMaintenance config = do
pool <- poolFromConfig config
result <-
withConnection pool $ \connection =>
withTransaction connection $
transactionExec
"update iwf_jobs set locked_at = null where locked_at is not null;"
shutdownDatabasePool pool
pure result
5. Prepared Queries
Use sqlQuery and sqlExec in controllers. They combine compile-time TypedSQL checking with the controller error boundary:
findUser : String -> ControllerContext -> ControllerApp (Maybe User)
findUser email context =
sqlQuery "select users.* from users where email = ${email}"
saveEmail : User -> String -> ControllerContext -> ControllerApp ()
saveEmail user email context =
sqlExec "update users set email = ${email} where id = ${user.id}"
Prepared query parameters are captured from local names or field projections in ${...} placeholders. Parameter values are sent separately from SQL text. Query-failure logs include operation, SQL text, parameter count, and adapter error, but not parameter values.
For checked query definitions, use TypedSQL.
6. Transactions
Use transactionOrFail when several prepared statements must commit or roll back together. The transaction provides the database source for nested queries:
renameArticle :
Id "articles" ->
String ->
ControllerContext ->
ControllerApp ()
renameArticle articleId title context =
transactionOrFail $
sqlExec "update articles set title = ${title} where id = ${articleId}"
Adapter errors roll the transaction back and become controller failures.
Use withTransactionWithAutoRefresh when writes should invalidate live fragments:
withTransactionWithAutoRefresh runtime connection $
transactionExecInvalidating
"update articles set title = 'Updated' where id = 1;"
["articles"]
7. Typed Exception Cleanup
The helpers above are the normal app interface. Plain withConnection is an IO convenience helper for scripts. Use withConnectionApp when framework or advanced code can throw typed Control.App exceptions and the pool slot must still be released:
import Control.App
data MaintenanceAbort = MkMaintenanceAbort
runCheckedMaintenance :
DatabasePool ->
Control.App.App Control.App.Init (Either String ())
runCheckedMaintenance pool =
Control.App.handle {err = MaintenanceAbort}
(withConnectionApp {err = MaintenanceAbort} pool $ \connection => do
result <- Control.App.primIO (executeSql connection "select 1")
case result of
Left _ => Control.App.throw MkMaintenanceAbort
Right _ => pure ())
(\_ => pure (Right ()))
(\_ => pure (Left "maintenance aborted"))
Use withTransactionApp when advanced typed user code can throw inside a runtime transaction. It rolls the transaction back, releases runtime state, and rethrows the same typed exception.
8. Row-Level Security Metadata
Define RLS in SQL:
ALTER TABLE articles ENABLE ROW LEVEL SECURITY;
CREATE POLICY articles_author_policy ON articles
USING (author_id = current_setting('app.current_user_id')::bigint);
Generated schema metadata records protected tables and policies. Use:
authenticatedRlsContextauthUserRlsContextanonymousRlsContextrlsTransactionwithRlsTransaction
These helpers prepend request-local role/user settings before application SQL runs.
9. Useful Runtime Helpers
sqlQuerysqlExectransactionOrFail
Advanced and script-facing helpers:
transactionqueryqueryMaybeexecqueryOrFailqueryMaybeOrFailexecOrFailpoolFromConfigdatabasePoolwithConnectionwithConnectionAppshutdownDatabasePoolcheckDatabasePooldatabasePoolMetricsexecuteSqlexecuteSqlParamsexecuteSqlDetailedexecuteSqlParamsDetailedwithTransactionwithTransactionApptransactionExectransactionExecParamstransactionExecTrackedtransactionExecInvalidatingexecutePreparedqueryPreparedRowsqueryPrepared
Deliberate Non-Features
Iwf does not add a relationship DSL or generated model validation metadata. Keep relationship loading as explicit SQL and explicit Idris functions.
Next
Read TypedSQL.