-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 5094dfe
Showing
32 changed files
with
5,049 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
name: GH and Hex.pm Release | ||
|
||
on: | ||
push: | ||
tags: | ||
- v*.*.* | ||
|
||
jobs: | ||
release: | ||
uses: TanklesXL/gleam_actions/.github/workflows/release.yaml@main | ||
secrets: | ||
HEXPM_USER: ${{ secrets.HEXPM_USER }} | ||
HEXPM_PASS: ${{ secrets.HEXPM_PASS }} | ||
with: | ||
gleam_version: 1.4.1 | ||
erlang_version: 27 | ||
test_erlang: true | ||
test_node: false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
name: Test | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
workflow_call: | ||
|
||
jobs: | ||
format: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: TanklesXL/gleam_actions/.github/actions/install_gleam@main | ||
with: | ||
gleam_version: 1.4.1 | ||
erlang_version: 27 | ||
- uses: TanklesXL/gleam_actions/.github/actions/format@main | ||
|
||
deps: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: TanklesXL/gleam_actions/.github/actions/install_gleam@main | ||
with: | ||
gleam_version: 1.4.1 | ||
erlang_version: 27 | ||
- uses: TanklesXL/gleam_actions/.github/actions/deps_cache@main | ||
with: | ||
gleam_version: 1.4.1 | ||
|
||
test: | ||
runs-on: ubuntu-latest | ||
needs: deps | ||
strategy: | ||
fail-fast: true | ||
matrix: | ||
erlang: ["26", "27"] | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: TanklesXL/gleam_actions/.github/actions/deps_restore@main | ||
- uses: TanklesXL/gleam_actions/.github/actions/install_gleam@main | ||
with: | ||
gleam_version: 1.4.1 | ||
erlang_version: ${{ matrix.erlang }} | ||
- uses: TanklesXL/gleam_actions/.github/actions/test@main | ||
with: | ||
target: "erlang" | ||
|
||
services: | ||
postgres: | ||
image: postgres | ||
options: >- | ||
--health-cmd pg_isready | ||
--health-interval 10s | ||
--health-timeout 5s | ||
--health-retries 5 | ||
ports: | ||
- 5432:5432 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
*.beam | ||
*.ez | ||
/build | ||
erl_crash.dump |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# Contributing | ||
|
||
If you're reading this, thank you so much for trying to contribute to | ||
`squirrel`! | ||
I tried to do my best and comment the code as much as possible and make it easy | ||
to understand. Each module also starts with a small comment to explain what it | ||
does, so it should be easier to dive in the codebase. | ||
|
||
> 💡 If you feel like some pieces of code are not commented enough or are too | ||
> obscure, than that's a bug! Please do reach out, I'd love to hear your | ||
> feedback and make `squirrel` easier to contribute to! | ||
## Running the tests | ||
|
||
Most of the tests are snapshot tests that directly call the `postgres.main` | ||
function to let it type the queries. In order to do that `squirrel` will have to | ||
connect to a postgres server that must be running during the tests. | ||
|
||
- In CI this is taken care of automatically | ||
- Locally you'll need a little bit of setup: | ||
- There must be a user called `squirrel_test` | ||
- It must be able to read and write to a database called `squirrel_test` | ||
- It will use the empty password to connect at localhost's port 5432 | ||
|
||
## Writing tests | ||
|
||
`squirrel` uses a lot of snapshot tests, to add new tests for the code | ||
generation bits you can have a look and copy the existing ones. | ||
There's no hard requirements but I have some suggestion to write good snapshot | ||
tests: | ||
|
||
- Have at most one snapshot per test function | ||
- Try to keep the snapshots as small as possible. | ||
Ideally one snapshot should assert a single property of the generated code so | ||
that it is easier to focus on a specific aspect of the code when reviewing it | ||
- Use a long descriptive title for the snapshots: a title should describe what | ||
one expects to see in the produced snapshot to guide the review process |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
# 🐿️ squirrel - type safe SQL in Gleam | ||
|
||
[![Package Version](https://img.shields.io/hexpm/v/squirrel)](https://hex.pm/packages/squirrel) | ||
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/squirrel/) | ||
|
||
## What's Squirrel? | ||
|
||
If you need to talk with a database in Gleam you'll have to write something like | ||
this: | ||
|
||
```gleam | ||
import gleam/pgo | ||
import decode | ||
pub type FindSquirrelRow { | ||
FindSquirrelRow(name: String, owned_acorns: Int) | ||
} | ||
/// Find a squirrel and its owned acorns given its name. | ||
/// | ||
pub fn find_squirrel(db: pgo.Connection, name: String) { | ||
let squirrel_row_decoder = | ||
decode.into({ | ||
use name <- decode.parameter | ||
use owned_acorns <- decode.parameter | ||
FindSquirrelRow(name: name, owned_acorns: owned_acorns) | ||
}) | ||
|> decode.field(0, decode.string) | ||
|> decode.field(1, decode.int) | ||
"select name, owned_acorns | ||
from squirrel | ||
where name = $1" | ||
|> pgo.execute(db, [pgo.text(name)], squirrel_row_decoder) | ||
} | ||
``` | ||
|
||
This is probably fine if you have a few small queries but it can become quite | ||
the burden when you have a lot of queries: | ||
|
||
- The SQL query you write is just a plain string, you do not get syntax | ||
highlighting, auto formatting, suggestions... all the little niceties you | ||
would otherwise get if you where writing a plain `*.sql` file. | ||
- This also means you loose the ability to run these queries on their own with | ||
other external tools, inspect them and so on. | ||
- You have to manually keep in sync the decoder with the query's output. | ||
|
||
One might be tempted to hide all of this by reaching for something like an ORM. | ||
Squirrel proposes a different approach: instead of trying to hide the SQL it | ||
_embraces it and leaves you in control._ | ||
You write the SQL queries in plain old `*.sql` files and Squirrel will take care | ||
of generating all the corresponding functions. | ||
|
||
A code snippet is worth a thousand words, so let's have a look at an example. | ||
Instead of the hand written example shown earlier you can instead just write the | ||
following query: | ||
|
||
```sql | ||
-- we're in file `src/squirrels/sql/find_squirrel.sql` | ||
-- Find a squirrel and its owned acorns given its name. | ||
select | ||
name, | ||
owned_acorns | ||
from | ||
squirrel | ||
where | ||
name = $1 | ||
``` | ||
|
||
And run `gleam run -m squirrel`. Just like magic you'll now have a type-safe | ||
function `find_squirrel` you can use just as you'd expect: | ||
|
||
```gleam | ||
import squirrels/sql | ||
pub fn main() { | ||
let db = todo as "the pgo connection" | ||
// And it just works as you'd expect: | ||
let assert Ok(#(_rows_count, rows)) = sql.find_squirrel("sandy") | ||
let assert [FindSquirrelRow(name: "sandy", owned_acorns: 11_111)] = rows | ||
} | ||
``` | ||
|
||
Behind the scenes Squirrel generates the decoders and functions you need; and | ||
it's pretty-printed, standard Gleam code (actually it's exactly like the hand | ||
written example I showed you earlier)! | ||
So now you get the best of both worlds: | ||
|
||
- You don't have to take care of keeping encoders and decoders in sync, Squirrel | ||
does that for you. | ||
- And you're not compromising on type safety either: Squirrel is able to | ||
understand the types of your query and produce a correct decoder. | ||
- You can stick to writing plain SQL in `*.sql` files. You'll have better | ||
editor support, syntax highlighting and completions. | ||
- You can run each query on its own: need to `explain` a query? | ||
No big deal, it's just a plain old `*.sql` file. | ||
|
||
## Usage | ||
|
||
First you'll need to add Squirrel to your project as a dev dependency: | ||
|
||
```sh | ||
gleam add squirrel --dev | ||
|
||
# Remember to add these packages if you haven't yet, they are needed by the | ||
# generated code to run and decode the read rows! | ||
gleam add gleam_pgo | ||
gleam add decode | ||
``` | ||
|
||
Then you can ask it to generate code running the `squirrel` module: | ||
|
||
```sh | ||
gleam run -m squirrel | ||
``` | ||
|
||
And that's it! As long as you follow a couple of conventions Squirrel will just | ||
work: | ||
|
||
- Squirrel will look for all `*.sql` files in any `sql` directory under your | ||
project's `src` directory. | ||
- Each `sql` directory will be turned into a single Gleam module containing a | ||
function for each `*.sql` file inside it. The generated Gleam module is going | ||
to be located in the same directory as the corresponding `sql` directory and | ||
it's name is `sql.gleam`. | ||
- Each `*.sql` file _must contain a single SQL query._ And the name of the file | ||
is going to be the name of the corresponding Gleam function to run that query. | ||
|
||
> Let's make an example. Imagine you have a Gleam project that looks like this | ||
> | ||
> ```txt | ||
> ├── src | ||
> │ ├── squirrels | ||
> │ │ └── sql | ||
> │ │ ├── find_squirrel.sql | ||
> │ │ └── list_squirrels.sql | ||
> │ └── squirrels.gleam | ||
> └── test | ||
> └── squirrels_test.gleam | ||
> ``` | ||
> | ||
> Running `gleam run -m squirrel` will create a `src/squirrels/sql.gleam` file | ||
> defining two functions `find_squirrel` and `list_squirrels` you can then | ||
> import and use in your code. | ||
### Talking to the database | ||
In order to understand the type of your queries, Squirrel needs to connect to | ||
the Postgres server where the database is defined. To connect, it will read the | ||
[Postgres env variables](https://www.postgresql.org/docs/current/libpq-envars.html) | ||
and use the following defaults if one is not set: | ||
- `PGHOST`: `"localhost"` | ||
- `PGPORT`: `5432` | ||
- `PGUSER`: `"root"` | ||
- `PGDATABASE`: `"database"` | ||
- `PGPASSWORD`: `""` | ||
## FAQ | ||
### What flavour of SQL does squirrel support? | ||
Squirrel only has support for Postgres. | ||
### Why isn't squirrel configurable in any way? | ||
By going the "convention over configuration" route, Squirrel enforces that all | ||
projects adopting it will always have the same structure. | ||
If you need to contribute to a project using Squirrel you'll immediately know | ||
which directories and modules to look for. | ||
This makes it easier to get started with a new project and cuts down on all the | ||
bike shedding: _"Where should I put my queries?",_ | ||
_"How many queries should go in on file?",_ ... | ||
## References | ||
This package draws a lot of inspiration from the amazing | ||
[yesql](https://github.com/krisajenkins/yesql) and | ||
[sqlx](https://github.com/launchbadge/sqlx). | ||
## Contributing | ||
If you think there’s any way to improve this package, or if you spot a bug don’t | ||
be afraid to open PRs, issues or requests of any kind! Any contribution is | ||
welcome 💜 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
--- | ||
version: 1.1.8 | ||
title: array decoding | ||
file: ./test/squirrel_test.gleam | ||
test_name: array_decoding_test | ||
--- | ||
/// A row you get from running the `query` query | ||
/// defined in `query.sql`. | ||
/// | ||
/// > 🐿️ This type definition was generated automatically using v-test of the | ||
/// > [squirrel package](https://github.com/giacomocavalieri/squirrel). | ||
/// | ||
pub type QueryRow { | ||
QueryRow(res: List(Int)) | ||
} | ||
|
||
/// Runs the `query` query | ||
/// defined in `query.sql`. | ||
/// | ||
/// > 🐿️ This function was generated automatically using v-test of | ||
/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). | ||
/// | ||
pub fn query(db) { | ||
let decoder = | ||
decode.into({ | ||
use res <- decode.parameter | ||
QueryRow(res: res) | ||
}) | ||
|> decode.field(0, decode.list(decode.int)) | ||
|
||
"select array[1, 2, 3] as res" | ||
|> pgo.execute(db, [], decode.from(decoder, _)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
--- | ||
version: 1.1.8 | ||
title: array encoding | ||
file: ./test/squirrel_test.gleam | ||
test_name: array_encoding_test | ||
--- | ||
/// A row you get from running the `query` query | ||
/// defined in `query.sql`. | ||
/// | ||
/// > 🐿️ This type definition was generated automatically using v-test of the | ||
/// > [squirrel package](https://github.com/giacomocavalieri/squirrel). | ||
/// | ||
pub type QueryRow { | ||
QueryRow(res: Bool) | ||
} | ||
|
||
/// Runs the `query` query | ||
/// defined in `query.sql`. | ||
/// | ||
/// > 🐿️ This function was generated automatically using v-test of | ||
/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel). | ||
/// | ||
pub fn query(db, arg_1) { | ||
let decoder = | ||
decode.into({ | ||
use res <- decode.parameter | ||
QueryRow(res: res) | ||
}) | ||
|> decode.field(0, decode.bool) | ||
|
||
"select true as res where $1 = array[1, 2, 3]" | ||
|> pgo.execute( | ||
db, | ||
[pgo.array(list.map(arg_1, fn(a) {pgo.int(a)}))], | ||
decode.from(decoder, _), | ||
) | ||
} |
Oops, something went wrong.