Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/petite-cats-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rescript-bun": patch
---

SQL API Bindings
33 changes: 33 additions & 0 deletions playground/examples/SQL.E1.Basic.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions playground/examples/SQL.E1.Basic.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// With PostgreSQL
let mysql = SQL.make("mysql://user:pass@localhost:3306/mydb")
let mysqlResults = await mysql->SQL.query`
SELECT * FROM users
WHERE active = ${Boolean(true)}
`

// With SQLite
let sqlite = SQL.make("sqlite://myapp.db")
let sqliteResults = await sqlite->SQL.query`
SELECT * FROM users
WHERE active = ${Number(1.0)}
`
53 changes: 53 additions & 0 deletions playground/examples/SQL.E2.InsertData.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions playground/examples/SQL.E2.InsertData.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
let db = SQL.fromOptions({filename:":memory:", adapter:"sqlite"})

(await db->SQL.query`
CREATE TABLE users (
name TEXT NOT NULL,
email TEXT PRIMARY KEY
);
`)->ignore

let insertUser = async (name, email) => await db->SQL.query`
INSERT INTO users (name, email)
VALUES (${String(name)}, ${String(email)})
RETURNING *
`

let userData = {
"name": "Alice",
"email": "alice@example.com",
}

let newUser = await db->SQL.query`
INSERT INTO users ${Query(db->SQL.object(userData, ["name", "email"]))}
RETURNING *
`

Console.log(newUser)

(await insertUser("Bob", "bob@email.com"))->ignore

switch await db->SQL.query`SELECT * FROM users` {
| Array(users) => Console.log(users)
| _ => ()
}
2 changes: 1 addition & 1 deletion playground/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions src/SQL.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

159 changes: 159 additions & 0 deletions src/SQL.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
open Globals
module SQLQuery = {
type t<'a> = promise<'a>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this primarily for autocomplete?


@val external active: t<'a> => bool = "active"
@val external cancelled: t<'a> => bool = "cancelled"
@send external cancel: t<'a> => t<'a> = "cancel"
@send external simple: t<'a> => t<'a> = "simple"
@send external execute: t<'a> => t<'a> = "execute"
@send external raw: t<'a> => t<'a> = "raw"
@send external values: t<'a> => t<'a> = "values"
}

@unboxed
type rec param =
| Boolean(bool)
| @as(null) Null
| String(string)
| Number(float)
| Dict(Core__Dict.t<param>)
| Array(array<param>)
| Query(SQLQuery.t<param>)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can another query be a param? Interesting!


type t

@module("bun") @new
external make: string => t = "SQL"

module TemplateStringsArray = {
let fromArray: array<'a> => array<'a> = %raw(`
function(array) {
array.raw = [...array]
return array
}
`)
}

let query: (t, array<string>, array<param>) => SQLQuery.t<param> = %raw(`
function(t, strings, params) {
let templateArray = TemplateStringsArray.fromArray(strings)

return t(templateArray, ...params)
}
`)

let object: (t, {..}, array<string>) => SQLQuery.t<param> = %raw(`
function(t, obj, cols) {
return t(obj, ...cols)
}
`)

let values: (t, array<'a>) => SQLQuery.t<param> = %raw(`
function(t, values) {
return t(arr)
}
`)

@send
external commitDistributed: (t, string) => promise<unit> = "commitDistributed"

@send
external rollbackDistributed: (t, string) => promise<unit> = "rollbackDistributed"

@send
external connect: (t, unit) => promise<unit> = "connect"

type options = {
timeout?: float
}

@send
external close: (t, ~options: options=?) => promise<unit> = "close"

@send
external end: (t, ~options: options=?) => promise<unit> = "end"

@send
external flush: t => unit = "flush"

@send
external reserve: t => t = "reserve"

type sqlTransactionContextCallback<'a> = t => promise<'a>

@send
external begin: (t, sqlTransactionContextCallback<'b>) => promise<'a> = "begin"

@send
external transaction: (t, sqlTransactionContextCallback<'b>) => promise<'a> = "transaction"

@send
external beginDistributed: (t, string, sqlTransactionContextCallback<'b>) => promise<'a> = "beginDistributed"

@send
external distributed: (t, string, sqlTransactionContextCallback<'b>) => promise<'a> = "distributed"

@send
external unsafe: (t, string, ~values: array<JSON.t>=?) => SQLQuery.t<param> = "unsafe"

@send
external file: (t, string, ~values: array<JSON.t>=?) => SQLQuery.t<param> = "file"

type rec sqlOptions = {
filename?: string,
// Connection URL (can be string or URL object)
url?: URL.t,
// Database server hostname
host?: string,
// Database server hostname (alias for host)
hostname?: string,
// Database server port number
port?: int,
// Database user for authentication
username?: string,
// Database user for authentication (alias for username)
user?: string,
// Database password for authentication
password?: string,
// Database password for authentication (alias for password)
pass?: string,
// Name of the database to connect to
database?: string,
// Name of the database to connect to (alias for database)
db?: string,
// Database adapter/driver to use
adapter?: string,
// Maximum time in seconds to wait for connection to become available
idleTimeout?: float,
// Maximum time in seconds to wait for connection to become available (alias for idleTimeout)
idle_timeout?: float,
// Maximum time in seconds to wait when establishing a connection
connectionTimeout?: float,
// Maximum time in seconds to wait when establishing a connection (alias for connectionTimeout)
connection_timeout?: float,
// Maximum lifetime in seconds of a connection
maxLifetime?: float,
// Maximum lifetime in seconds of a connection (alias for maxLifetime)
max_lifetime?: float,
// Whether to use TLS/SSL for the connection
tls?: bool,
// Whether to use TLS/SSL for the connection (alias for tls)
ssl?: bool,
// Callback function executed when a connection is established
onconnect?: t => unit,
// Callback function executed when a connection is closed
onclose?: t => unit,
// Maximum number of connections in the pool
max?: int,
// By default values outside i32 range are returned as strings. If this is true, values outside i32 range are returned as BigInts.
bigint?: bool,
// Automatic creation of prepared statements, defaults to true
prepare?: bool,
}

@get
external options: t => sqlOptions = "options"

@module("bun") @new
external fromOptions: sqlOptions => t = "SQL"