This small readme focuses on the differences between regular pgtyped
and this fork that is compatible with ReScript.
- It outputs ReScript instead of TypeScript.
Everything else should work pretty much the same as stock pgtyped
.
Make sure you have ReScript v11.1
, and ReScript Core (plus RescriptCore
opened globally).
npm install -D pgtyped-rescript rescript-embed-lang
(installrescript-embed-lang
if you want to use the SQL-in-ReScript mode)npm install @pgtyped/runtime pg rescript @rescript/core
(@pgtyped/runtime
andpg
are the only required runtime dependencies of pgtyped)- Create a PgTyped
pgtyped.config.json
file. - Run
npx pgtyped-rescript -w -c pgtyped.config.json
to start PgTyped in watch mode.
Here's a sample pgtyped.config.json
file:
{
"transforms": [
{
"mode": "sql",
"include": "**/*.sql",
"emitTemplate": "{{dir}}/{{name}}__sql.res"
},
{
"mode": "res",
"include": "**/*.res",
"emitTemplate": "{{dir}}/{{name}}__sql.res"
}
],
"srcDir": "./src",
"dbUrl": "postgres://pgtyped:pgtyped@localhost/pgtyped"
}
Notice how we're configuring what we want the generated ReScript files to be named under
emitTemplate
. For SQL-in-ReScript mode, you need to configure the generated file names exactly as above.
Please refer to the pgtyped
docs for all configuration options.
pgtyped-rescript
supports writing queries in separate SQL files, as well as embedded directly in ReScript source code. Below details the separate SQL files approach:
Create a SQL file anywhere in src
. We call this one books.sql
. Add your queries, together with @name
comments naming them uniquely within the current file:
/* @name findBookById */
SELECT * FROM books WHERE id = :id!;
After running npx pgtyped-rescript -c pgtyped.config.json
we should get a books__sql.res
file, with a module FindBookById
with various functions for executing the query. Here's a full example of how we can connect to a database, and use that generated function to query it:
open PgTyped
external env: {..} = "process.env"
let dbConfig = {
Pg.Client.host: env["PGHOST"]->Option.getWithDefault("127.0.0.1"),
user: env["PGUSER"]->Option.getWithDefault("pgtyped"),
password: env["PGPASSWORD"]->Option.getWithDefault("pgtyped"),
database: env["PGDATABASE"]->Option.getWithDefault("pgtyped"),
port: env["PGPORT"]->Option.flatMap(port => Int.fromString(port))->Option.getWithDefault(5432),
}
let client = Pg.Client.make(dbConfig)
let main = async () => {
await client->Pg.Client.connect
let res = await client->Books__sql.FindBookById.one({id: 1})
Console.log(res)
await client->Pg.Client.end
}
main()->Promise.done
Optionally, you can write SQL directly in your ReScript code and have a seamless, fully typed experience. The above example but with SQL-in-ReScript:
let query = %sql.one(`
SELECT * FROM books WHERE id = :id!
`)
let res = await client->query({id: 1})
Console.log(res)
Notice that with the %sql
tags, there's no requirement to name your queries. You can still name them if you want, but you don't have to.
In order for this mode to work, you need one more thing - configure the rescript-embed-lang
PPX in rescript.json
:
"ppx-flags": ["rescript-embed-lang/ppx"],
With that, you should be able to write queries directly in your ReScript source, and with the watch
mode enabled have a seamless experience with types autogenerated and wired up for you.
pgtyped-rescript
automatically analyzes PostgreSQL check constraints and generates ReScript polyvariant types for enumeration-style constraints. This provides compile-time safety for constrained database fields.
The following constraint patterns are automatically detected and converted to polyvariant types:
-- Pattern 1: IN clause
status TEXT CHECK (status IN ('published', 'draft', 'archived')),
-- Pattern 2: ANY with ARRAY
format TEXT CHECK (format = ANY (ARRAY['hardcover'::text, 'paperback'::text, 'ebook'::text])),
-- Both string and integer values are supported
priority INTEGER CHECK (priority IN (1, 2, 3, 4, 5))
For a table with these constraints:
CREATE TABLE books (
id SERIAL PRIMARY KEY,
status TEXT CHECK (status IN ('published', 'draft', 'archived')),
priority INTEGER CHECK (priority IN (1, 2, 3, 4, 5))
);
The generated ReScript types will include:
type result = {
id: int,
status: option<[#"published" | #"draft" | #"archived"]>,
priority: option<[#1 | #2 | #3 | #4 | #5]>,
}
- Float constraints: Not supported since ReScript polyvariants cannot represent float literals
- Complex constraints: Only simple enumeration patterns are supported (no
BETWEEN
,OR
logic, function calls, etc.) - Mixed types: Constraints mixing different data types in the same clause are not supported
/* @name getBooksByStatus */
SELECT * FROM books WHERE status = :status;
// The generated function will accept a polyvariant for status
let books = await client->GetBooksByStatus.many({
status: #published // Compile-time checked against the database constraint!
})
This feature works seamlessly with both separate SQL files and SQL-in-ReScript modes.
pgtyped-rescript
provides support for PostgreSQL's JSON population functions, enabling type-safe conversion between JSON data and database records.
json_populate_record
- Converts a JSON object to a PostgreSQL recordjson_populate_recordset
- Converts a JSON array to a set of PostgreSQL recordsjsonb_populate_record
- Binary JSON variant ofjson_populate_record
jsonb_populate_recordset
- Binary JSON variant ofjson_populate_recordset
json_to_record
- Converts JSON to a record with explicit column definitionsjsonb_to_recordset
- Converts JSONB array to records with explicit column definitions
Bulk insert with json_populate_recordset
:
/* @name bulkInsertBooks */
INSERT INTO books (name, author_id, categories, rank)
SELECT
event.name,
event.author_id,
event.categories,
event.rank
FROM json_populate_recordset(null::books, :books!) as event
RETURNING *;
Update single record with json_populate_record
:
/* @name updateBookFromJson */
UPDATE books
SET (name, author_id, categories, rank) = (
SELECT
r.name,
r.author_id,
r.categories,
r.rank
FROM json_populate_record(null::books, :bookData!) as r
)
WHERE id = :bookId!
RETURNING *;
Example:
let bulkInsert = %sql.many(`
INSERT INTO books (name, author_id, categories, rank)
SELECT
event.name,
event.author_id,
event.categories,
event.rank
FROM json_populate_recordset(null::books, :books!) as event
RETURNING *
`)
// Usage with full type safety
let newBooks = await client->bulkInsert({
books: [
{name: "Book 1", author_id: 1, categories: ["fiction"], rank: 1},
{name: "Book 2", author_id: 2, categories: ["sci-fi"], rank: 2}
]
})
pgtyped-rescript
automatically infers specific polyvariant types for literal values in your SQL queries, providing enhanced type safety and better development experience.
When your SQL queries return literal values with aliases, pgtyped-rescript
generates specific polyvariant types instead of generic string
, int
, etc. This works for both simple SELECT queries and UNION queries where literals are consistent across all branches.
Simple literals:
/* @name getStatus */
SELECT
'success' as status,
200 as code,
'active' as state
Generated ReScript type:
type getStatusResult = {
status: [#"success"],
code: [#200],
state: [#"active"],
}
Union queries with consistent literals:
/* @name getDocumentStatus */
SELECT 'draft' as status, 1 as version
UNION ALL
SELECT 'published' as status, 2 as version
Generated ReScript type:
type getDocumentStatusResult = {
status: [#"draft" | #"published"],
version: [#1 | #2],
}
SQL-in-ReScript example:
let getOrderStatus = %sql.many(`
SELECT
'pending' as status,
0 as priority
UNION ALL
SELECT
'shipped' as status,
1 as priority
`)
// Returns: array<{status: [#"pending" | #"shipped"], priority: [#0 | #1]}>
let statuses = await client->getOrderStatus()
- Consistent literals: Only infers polyvariants when all literal values for the same alias are actual literals (not expressions)
- Context-aware: Handles nested queries correctly, only inferring from the top-level SELECT
- Duplicate handling: Automatically deduplicates identical literals in UNION queries
- Mixed expressions: Falls back to generic types when mixing literals with expressions (e.g.,
'draft' || 'suffix'
)
The package comes with minimal bindings to be able to set up a pg
client. Please feel free to open issues for anything that's missing. It's also easy to add your own bindings locally by using @send
and binding them to PgTyped.Pg.Client.t
, like:
// Imagine `end` didn't have bindings
@send external end: PgTyped.Pg.Client.t => promise<unit> = "end"
await client->end