A Haskell code generator plugin for sqlc, allowing you to generate idiomatic Haskell types and functions directly from your SQL queries.
It leverages postgresql-simple, hasql, mysql-simple, and sqlite-simple, generating a thin layer on top of these well-known libraries.
sqlc-hs can be used as a WASM plugin or as a process plugin. Refer to the sqlc documentation for more information.
Every release ships a
sqlc-hs-<version>.wasm module. sqlc downloads and runs it for you — no
local installation required. Point your sqlc.yaml at the release asset and
its sha256:
version: '2'
plugins:
- name: haskell
wasm:
url: https://github.com/alexbiehl/sqlc-hs/releases/download/v0.2.0.1/sqlc-hs-v0.2.0.1.wasm
sha256: <sha256 of the .wasm file>The release notes of each release contain this snippet with the correct sha256 filled in — copy it from there.
Alternatively, install the sqlc-hs executable (e.g. via
cabal install sqlc-hs), ensure it is on your PATH when invoking sqlc, and
configure it as a process plugin:
version: '2'
plugins:
- name: haskell
process:
cmd: sqlc-hsUse the plugin for your schemas like so
sql:
- engine: postgresql
queries: query.sql
schema: schema.sql
codegen:
- out: gen
plugin: haskell
options:
cabal_package_name: your-package
cabal_package_version: 0.1.0.0
haskell_module_prefix: Database.Queries
overrides:
- db_type: bytea
haskell_type:
package: bytestring
module: Data.ByteString
type: Data.ByteString.ByteStringdriver selects the Haskell library the generated code is written against.
Every engine has a default, so configurations that don't set it keep generating
exactly the code they did before.
| Engine | driver |
Default |
|---|---|---|
postgresql |
postgresql-simple, hasql |
postgresql-simple |
mysql |
mysql-simple |
mysql-simple |
sqlite |
sqlite-simple |
sqlite-simple |
sql:
- engine: postgresql
queries: query.sql
schema: schema.sql
codegen:
- out: gen
plugin: haskell
options:
driver: hasql
cabal_package_name: your-packageRequires hasql >= 1.10, 2.x included. The generated cabal file depends on
hasql without a version bound, like every other dependency it emits, so an
older hasql pinned elsewhere in your project shows up as a compile error
rather than a solver one.
The generated Queries.Internal module gives every query the same helpers as
the other drivers do — exec, execRows, execResult, queryOne,
queryMany and execMany — taking a Hasql.Connection.Connection and
returning IO (Either RunnerError a):
users <- queryMany connection query_ListUsers Params_ListUsers {age = 42}Each of those runs its query in a session of its own. Every one also comes as a
…Session variant returning a Hasql.Session.Session, for when several
queries have to share one session:
result <-
Hasql.Connection.use connection $ do
_ <- execSession query_InsertAuthor (Params_InsertAuthor {name = "Kafka"})
queryManySession query_ListAuthors Params_ListAuthors {}Two things work differently from the postgresql-simple driver:
- There is no
fold. A query's result shape is fixed by its command annotation, so a query you want to fold, or to decode into something other than the generated row, is a query to declare with that shape. sqlc.sliceparameters are bound as one array parameter, which PostgreSQL only accepts with the array operators, soIN ($1)is generated as= ANY ($1)andNOT IN ($1)as<> ALL ($1). A slice used anywhere else is an error; write= ANY(sqlc.arg(...)::type[])in your SQL instead of usingsqlc.slice.
Each query module carries a hasql-mapping IsStatement
instance for its parameters, which is where the SQL, the parameter encoder and
the result decoder come together:
instance IsStatement (Params "ListUsers") where
type Result (Params "ListUsers") = Data.Vector.Vector (Queries.Internal.Result "ListUsers")
statement = Hasql.Statement.preparable sql paramsEncoder (Hasql.Decoders.rowVector rowDecoder)
where
Query sql = query_ListUsersThe associated Result is what sqlc's command annotation means, spelled as a
type: Maybe a row for :one, a Vector of them for :many, () for
:exec, Int64 for :execrows. The runners above return it, which is why they
need no constraint beyond IsStatement.
The parameter encoder and row decoder are local to statement: the instance is
the interface, and a query module exports nothing but its Query, its Params
and its Result.
Anything that takes an IsStatement — Hasql.Mapping.IsStatement.toSession,
toTransaction — therefore works on a generated query without adapting it.
hasql's encoders and decoders are values, chosen per SQL type, and from 1.10 on
it checks that a column's type matches the decoder reading it — so text,
varchar and bpchar, all Text on the Haskell side, each need their own
decoder. sqlc-hs picks the codec from the SQL type sqlc reported, for every type
it knows.
Columns typed by an overrides entry are a different matter: sqlc-hs cannot
know what codec your type wants. Those go through
hasql-mapping's IsScalar:
class IsScalar a where
encoder :: Hasql.Encoders.Value a
decoder :: Hasql.Decoders.Value aInstances ship for Bool, the sized Ints, Float, Double, Scientific,
Text, ByteString, UUID, Day, LocalTime, UTCTime, TimeOfDay,
(TimeOfDay, TimeZone), DiffTime, IPRange and Data.Aeson.Value. The usual
overrides therefore need nothing extra — a uuid column mapped to
Data.UUID.UUID, a timestamptz to UTCTime, a date to Day all work as
they stand. For a type of your own, write the instance:
instance IsScalar UserId where
encoder = Data.Functor.Contravariant.contramap unUserId encoder
decoder = fmap UserId decoderIsScalar is scalar-only by contract, so do not instantiate it for an array
type: an array column is wrapped for you from the column's own arrayness, and
an override whose Haskell type is itself an array should name its codecs with
hasql_encoder and hasql_decoder instead.
Because the class comes from a library rather than from the generated code, the instance can live wherever the type does — including a package the generated one depends on, which an instance of a generated class could not.
When an instance is the wrong place for it, because the same Haskell type wants
different codecs in different columns, an override can name the codecs itself
with hasql_encoder and hasql_decoder. UTCTime's instance is
timestamptz, so a timestamp column read as a UTCTime has to spell out the
conversion:
overrides:
- db_type: pg_catalog.timestamp
haskell_type:
package: time
module: Data.Time
type: Data.Time.UTCTime
hasql_encoder: Data.Functor.Contravariant.contramap (Data.Time.utcToLocalTime Data.Time.utc) Hasql.Encoders.timestamp
hasql_decoder: fmap (Data.Time.localTimeToUTC Data.Time.utc) Hasql.Decoders.timestampBoth are Haskell expressions spliced into the generated code, and both have to be given together.
Enum codecs are generated into Queries.Types from the catalog, so enums need
nothing either.
moneyandnamecolumns cannot be typed: hasql ships no codec for either, so sqlc-hs reports them as unresolved rather than generating a decoder that fails against the server. To use one anyway, give it an override whosehasql_encoder/hasql_decoderare built withHasql.Encoders.customandHasql.Decoders.custom.- A value cannot be decoded from a SQL type it is not stored as.
postgresql-simple parses the text form, so
CAST(created_at AS TEXT)can be read as aUTCTime(see the override examples below); hasql decodes the binary form and checks the column's type, so the SQL type and the codec have to agree.
overrides is a list of mappings — each entry tells sqlc-hs to map a
database type (or a specific column) to a Haskell type of your choosing. They
take precedence over the built-in type mappings for the relevant engine.
Each override accepts the following keys:
| Key | Required | Description |
|---|---|---|
db_type |
no* | The fully qualified database type to match, e.g. bytea, pg_catalog.int4, text. Matched against the column type sqlc reports. |
haskell_type |
yes | The Haskell type to use. Either a single mapping (see below) or a list of mappings — additional entries declare extra package/module dependencies to import. |
column |
no* | Match a specific column: column, table.column or schema.table.column. The table part matches the table name or its query alias; a bare column also matches aliased expression outputs (e.g. CAST(... AS TEXT) AS created_at), which carry no table. |
engine |
no | Restrict the override to a specific engine (postgresql, mysql, or sqlite). Useful when one configuration targets multiple engines. |
nullable |
no | If true, only match columns that are nullable. If false or omitted, only match columns that are NOT NULL. |
hasql_encoder |
no | hasql only: the Hasql.Encoders.Value expression to encode matching columns with. Must be given together with hasql_decoder. See The hasql driver. |
hasql_decoder |
no | hasql only: the Hasql.Decoders.Value expression to decode matching columns with. Must be given together with hasql_encoder. |
* At least one of db_type or column must be given. When both are given,
both must match.
A haskell_type mapping has three fields, all of which must be fully
qualified:
| Key | Description |
|---|---|
package |
The cabal package providing the type, e.g. bytestring. Added as a dependency in the generated cabal file. |
module |
The module to import, e.g. Data.ByteString. |
type |
The fully qualified type name, e.g. Data.ByteString.ByteString. May be a composed type like Data.Vector.Vector Data.Text.Text. |
Map every bytea column to a strict ByteString:
overrides:
- db_type: bytea
haskell_type:
package: bytestring
module: Data.ByteString
type: Data.ByteString.ByteStringGive haskell_type as a list when the type is composed from more than
one module: the first entry's type is what gets rendered, and every
entry's package/module is added as a cabal dependency and import. Here a
custom x509_certificate domain maps to Binary ByteString, which needs
both postgresql-simple and bytestring:
overrides:
- db_type: x509_certificate
haskell_type:
- type: Database.PostgreSQL.Simple.Binary Data.ByteString.ByteString
- package: postgresql-simple
module: Database.PostgreSQL.Simple
- package: bytestring
module: Data.ByteStringMap a single column (accounts.id) to a custom UserId newtype:
overrides:
- column: accounts.id
haskell_type:
package: your-package
module: Your.Types
type: Your.Types.UserIdType a computed/aliased query output that the engine can only describe as
TEXT — e.g. a normalized timestamp — as a proper UTCTime (the runtime
FromField/FromRow instances of your driver do the decoding):
overrides:
- column: last_error_at
haskell_type:
package: time
module: Data.Time
type: Data.Time.UTCTimeApply different mappings depending on the engine:
overrides:
- db_type: jsonb
engine: postgresql
haskell_type:
package: aeson
module: Data.Aeson
type: Data.Aeson.Value
- db_type: json
engine: mysql
haskell_type:
package: aeson
module: Data.Aeson
type: Data.Aeson.ValueOverride only nullable columns of a given type:
overrides:
- db_type: text
nullable: true
haskell_type:
package: text
module: Data.Text
type: GHC.Base.Maybe Data.Text.Textnaming customizes how the names of generated declarations are rendered,
using mustache-style {{variable}} templates. Every key is optional — an
omitted template falls back to the default, which reproduces sqlc-hs's
historical naming, so existing configurations keep generating byte-identical
code.
codegen:
- plugin: haskell
out: queries
options:
naming:
query: "run{{query}}"
params_constructor: "Mk{{query}}Args"
result_constructor: "{{query}}Row"
enum_constructor: "Enum_{{enum}}_{{value}}"
field: "{{column}}_of_{{table}}"| Key | Default | Context variables |
|---|---|---|
query |
query_{{query}} |
query — the query's name |
params_constructor |
Params_{{query}} |
query |
result_constructor |
Result_{{query}} |
query |
enum_constructor |
Enum_{{enum}}_{{value}} |
enum — the database type name; value — the enum value |
field |
{{prefix}}{{column}} |
column, table, table_alias, schema, prefix (see below) |
With that configuration, a ListUsers query over a users table renders as
runListUsers :: Query "ListUsers" "SELECT"
data instance Params "ListUsers" = MkListUsersArgs
{ age_of_ :: Data.Int.Int32
}
data instance Result "ListUsers" = ListUsersRow
{ id_of_users :: !Data.Int.Int32,
name_of_users :: !Data.Text.Text
}instead of the default query_ListUsers / Params_ListUsers /
Result_ListUsers with users_id / users_name fields. (Note age_of_:
the parameter has no table, so {{table}} rendered empty — see the
naming-templates golden test for the full output.)
prefix is the historical field namespacing, precomputed so the default
template needs no conditionals: the query's table alias (or, failing that,
the table name) followed by _ — and empty for table-less outputs such as
computed/aliased expressions.
Templates support plain {{variable}} interpolation only (whitespace inside
the braces is ignored); unknown variables render as the empty string, like in
mustache. Rendered names are always fixed up to be valid Haskell identifiers:
characters that cannot appear in an identifier become _, functions and
fields get a lower-case first letter (or a leading _ for digits),
constructors an upper-case one, and Haskell keywords used as field names are
suffixed with '.
$ protoc --plugin=protoc-gen-haskell=`cabal exec which proto-lens-protoc` --haskell_out=sqlc-hs-protos/ protos/codegen.proto