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
125 changes: 64 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Interface with PostgreSQL databases from Roc.

This package implements a PostgreSQL client on pure Roc that depends only on a TCP effect from the platform.
This package implements a PostgreSQL client on pure Roc that depends only on a TCP effect from the platform.

It exposes a simple API that allows you to run SQL commands as strings and a query builder that helps you write composable type-safe queries against your schema.

Expand All @@ -27,24 +27,26 @@ The plan is to support all the other SQL commands, but that's coming later. In t
Connecting and performing a query

```haskell
task : Task (List { name: Str, price: Dec }) _
task =
client <- Pg.Client.withConnect {
host: "localhost",
port: 5432,
user: "postgres",
database: "postgres",
auth: Password "password"
}

Pg.Cmd.new "select name, price from products"
|> Pg.Cmd.expectN (
Pg.Result.succeed {
name: <- Pg.Result.str "name" |> Pg.Result.apply,
price: <- Pg.Result.dec "price" |> Pg.Result.apply
}
)
|> Pg.Client.command client
products! : {} => Result (List { name : Str, price : Dec }) _
products! = |_|
client = Pg.Client.connect!(
{
host: "localhost",
port: 5432,
user: "postgres",
auth: None,
database: "postgres",
},
)?

Pg.Cmd.new("select name, price from products")
|> Pg.Cmd.expect_n(
{ Pg.Result.combine <-
name: Pg.Result.str("name"),
price: Pg.Result.dec("price"),
},
)
|> Pg.Client.command!(client)
```

<details>
Expand All @@ -53,15 +55,15 @@ Parameterized queries
</summary>

```elm
Pg.Cmd.new "select name, price from products where id = $1"
|> Pg.Cmd.bind [ Pg.Cmd.u32 productId ]
|> Pg.Cmd.expect1 (
Pg.Result.succeed {
name: <- Pg.Result.str "name" |> Pg.Result.apply,
price: <- Pg.Result.dec "price" |> Pg.Result.apply
}
)
|> Pg.Client.command client
Pg.Cmd.new("select name, price from products where id = $1")
|> Pg.Cmd.bind([ Pg.Cmd.u32(product_id) ])
|> Pg.Cmd.expect1(
{ Pg.Result.combine <-
name: Pg.Result.str("name"),
price: Pg.Result.dec("price"),
},
)
|> Pg.Client.command!(client)?
```

</details>
Expand All @@ -72,16 +74,14 @@ Prepared statements
</summary>

```elm
selectUser <-
select_user =
"select email from users where id = $1"
|> Pg.Client.prepare { client, name: "selectUser" }
|> await

selectUser
|> Pg.Cmd.bind [ Pg.Cmd.u32 userId ]
|> Pg.Cmd.expect1 (Pg.Result.str "email")
|> Pg.Client.command client
|> Pg.Client.prepare!({ client, name: "select_user" })?

select_user
|> Pg.Cmd.bind([Pg.Cmd.u32(user_id)])
|> Pg.Cmd.expect1(Pg.Result.str("email"))
|> Pg.Client.command!(client)?
```

</details>
Expand All @@ -92,33 +92,36 @@ Batch commands in a single roundtrip (applicative)
</summary>

```elm
Pg.Batch.succeed \email -> \products -> { email, products }
|> Pg.Batch.with
Pg.Batch.succeed(|email| |products| { email, products })
|> Pg.Batch.with(
(
selectUser
|> Pg.Cmd.bind [ Pg.Cmd.u32 userId ]
|> Pg.Cmd.expect1 (Pg.Result.str "email")
)
|> Pg.Batch.with
select_user
|> Pg.Cmd.bind([Pg.Cmd.u32(user_id)])
|> Pg.Cmd.expect1(Pg.Result.str("email"))
),
)
|> Pg.Batch.with(
(
Pg.Cmd.new
Pg.Cmd.new(
"""
select name, price from products
inner join orders on orders.product_id = products.id
where orders.id = $1
"""
|> Pg.Cmd.bind [ Pg.Cmd.u32 orderId ]
|> Pg.Cmd.expectN (
Pg.Result.succeed {
name: <- Pg.Result.str "name" |> Pg.Result.apply,
price: <- Pg.Result.dec "price" |> Pg.Result.apply
}
)
)
|> Pg.Client.batch client
""",
)
|> Pg.Cmd.bind([Pg.Cmd.u32(order_id)])
|> Pg.Cmd.expect_n(
{ Pg.Result.combine <-
name: Pg.Result.str("name"),
price: Pg.Result.dec("price"),
},
)
),
)
|> Pg.Client.batch!(client)?
```

Note: `selectUser` referes to prepared statement in the previous example
Note: `select_user` referes to prepared statement in the previous example

</details>

Expand All @@ -128,14 +131,14 @@ Batch commands in a single roundtrip (list)
</summary>

```elm
updateCmd = \product ->
Pg.Cmd.new "update products set desc = $1 where id = $2"
|> Pg.Cmd.bind [ Pg.Cmd.str product.desc, Pg.Cmd.u32 product.id ]
update_cmd = |product|
Pg.Cmd.new("update products set desc = $1 where id = $2")
|> Pg.Cmd.bind([Pg.Cmd.str(product.desc), Pg.Cmd.u32(product.id)])

productsToUpdate
|> List.map updateCmd
products_to_update
|> List.map(update_cmd)
|> Pg.Batch.sequence
|> Pg.Client.batch client
|> Pg.Client.batch!(client)?
```

Note: `roc-pg` automatically reuses statements in a batch by only parsing (and describing) once per unique SQL string. This also works with applicative batches.
Expand Down
63 changes: 63 additions & 0 deletions examples/basic-webserver.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
app [Model, init!, respond!] {
pf: platform "https://github.com/roc-lang/basic-webserver/releases/download/0.12.0/Q4h_In-sz1BqAvlpmCsBHhEJnn_YvfRRMiNACB_fBbk.tar.br",
pg: "../src/main.roc",
}

import pf.Stdout
import pg.Pg.Client exposing [Client]
import pg.Pg.Cmd
import pg.Pg.Result

Model : {
client : Client,
}

init! = |_|
_ = Stdout.line!("Start!")

client = Pg.Client.connect!(
{
host: "localhost",
port: 5432,
user: "postgres",
auth: None,
database: "postgres",
},
)?

_ = Stdout.line!("Connected!")

Ok({ client })

respond! = |_request, model|
rows =
Pg.Cmd.new(
"""
select $1 as name, $2 as age
union all
select 'Julio' as name, 23 as age
""",
)
|> Pg.Cmd.bind([Pg.Cmd.str("John"), Pg.Cmd.u8(32)])
|> Pg.Cmd.expect_n(
{ Pg.Result.combine <-
name: Pg.Result.str("name"),
age: Pg.Result.u8("age"),
},
)
|> Pg.Client.command!(model.client)?

response_body =
rows
|> List.map(Inspect.to_str)
|> Str.join_with("\n")

Stdout.line!("Responding with: ${response_body}")?

Ok(
{
status: 200,
headers: [],
body: Str.to_utf8(response_body),
},
)
107 changes: 57 additions & 50 deletions examples/batch.roc
Original file line number Diff line number Diff line change
@@ -1,58 +1,65 @@
app [main] {
app [main!] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br",
pg: "../src/main.roc",
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br",
}

import pf.Stdout
import pg.Pg.Cmd
import pg.Pg.BasicCliClient
import pg.Pg.Client
import pg.Pg.Batch
import pg.Pg.Result

main =
client = Pg.BasicCliClient.connect! {
host: "localhost",
port: 5432,
user: "postgres",
auth: None,
database: "postgres",
}

Stdout.line! "Connected!"

resultWith =
Pg.Batch.succeed (\hi -> \eleven -> \thirtyOne -> { hi, fortyTwo: eleven + thirtyOne })
|> Pg.Batch.with
(
Pg.Cmd.new "select 'hi' as value"
|> Pg.Cmd.expect1 (Pg.Result.str "value")
)
|> Pg.Batch.with
(
Pg.Cmd.new "select $1::int as value"
|> Pg.Cmd.bind [Pg.Cmd.u8 11]
|> Pg.Cmd.expect1 (Pg.Result.u8 "value")
)
|> Pg.Batch.with
(
Pg.Cmd.new "select $1::int as value"
|> Pg.Cmd.bind [Pg.Cmd.u8 31]
|> Pg.Cmd.expect1 (Pg.Result.u8 "value")
)
|> Pg.BasicCliClient.batch! client

str42 = Num.toStr resultWith.fortyTwo
Stdout.line! "$(resultWith.hi) $(str42)"

resultSeq =
List.range { start: At 0, end: At 20 }
|> List.map \num ->
Pg.Cmd.new "select $1::int as value"
|> Pg.Cmd.bind [Pg.Cmd.u8 num]
|> Pg.Cmd.expect1 (Pg.Result.u8 "value")
|> Pg.Batch.sequence
|> Pg.BasicCliClient.batch! client

resultSeqStr = resultSeq |> List.map Num.toStr |> Str.joinWith ", "

Stdout.line resultSeqStr
main! = |_|
client = Pg.Client.connect!(
{
host: "localhost",
port: 5432,
user: "postgres",
auth: None,
database: "postgres",
},
)?

_ = Stdout.line!("Connected!")

result_with =
Pg.Batch.succeed(|hi| |eleven| |thirty_one| { hi, forty_two: eleven + thirty_one })
|> Pg.Batch.with(
(
Pg.Cmd.new("select 'hi' as value")
|> Pg.Cmd.expect1(Pg.Result.str("value"))
),
)
|> Pg.Batch.with(
(
Pg.Cmd.new("select $1::int as value")
|> Pg.Cmd.bind([Pg.Cmd.u8(11)])
|> Pg.Cmd.expect1(Pg.Result.u8("value"))
),
)
|> Pg.Batch.with(
(
Pg.Cmd.new("select $1::int as value")
|> Pg.Cmd.bind([Pg.Cmd.u8(31)])
|> Pg.Cmd.expect1(Pg.Result.u8("value"))
),
)
|> Pg.Client.batch!(client)?

str42 = Num.to_str(result_with.forty_two)
_ = Stdout.line!("${result_with.hi} ${str42}")

result_seq =
List.range({ start: At(0), end: At(20) })
|> List.map(
|num|
Pg.Cmd.new("select $1::int as value")
|> Pg.Cmd.bind([Pg.Cmd.u8(num)])
|> Pg.Cmd.expect1(Pg.Result.u8("value")),
)
|> Pg.Batch.sequence
|> Pg.Client.batch!(client)?

result_seq_str = result_seq |> List.map(Num.to_str) |> Str.join_with(", ")

Stdout.line!(result_seq_str)
Loading