Skip to content

Latest commit

 

History

History
184 lines (128 loc) · 5.95 KB

File metadata and controls

184 lines (128 loc) · 5.95 KB

pgtyped-rescript

This small readme focuses on the differences between regular pgtyped and this fork that is compatible with ReScript.

Differences to regular pgtyped

  • It outputs ReScript instead of TypeScript.

Everything else should work pretty much the same as stock pgtyped.

Getting started

Make sure you have ReScript v11.1, and ReScript Core (plus RescriptCore opened globally).

  1. npm install -D pgtyped-rescript rescript-embed-lang (install rescript-embed-lang if you want to use the SQL-in-ReScript mode)
  2. npm install @pgtyped/runtime pg rescript @rescript/core (@pgtyped/runtime and pg are the only required runtime dependencies of pgtyped)
  3. Create a PgTyped pgtyped.config.json file.
  4. Run npx pgtyped-rescript -w -c pgtyped.config.json to start PgTyped in watch mode.

Example of setting up and running pgtyped-rescript

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.

Separate SQL files mode

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

SQL-in-ReScript

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.

Check Constraint Support

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.

Supported Constraint Patterns

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))

Generated Types

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]>,
}

Limitations

  • 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

Example

/* @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.

API

PgTyped

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