···11+# Contributing
22+33+If you're reading this, thank you so much for trying to contribute to
44+`squirrel`!
55+I tried to do my best and comment the code as much as possible and make it easy
66+to understand. Each module also starts with a small comment to explain what it
77+does, so it should be easier to dive in the codebase.
88+99+> 💡 If you feel like some pieces of code are not commented enough or are too
1010+> obscure, than that's a bug! Please do reach out, I'd love to hear your
1111+> feedback and make `squirrel` easier to contribute to!
1212+1313+## Running the tests
1414+1515+Most of the tests are snapshot tests that directly call the `postgres.main`
1616+function to let it type the queries. In order to do that `squirrel` will have to
1717+connect to a postgres server that must be running during the tests.
1818+1919+- In CI this is taken care of automatically
2020+- Locally you'll need a little bit of setup:
2121+ - There must be a user called `squirrel_test`
2222+ - It must be able to read and write to a database called `squirrel_test`
2323+ - It will use the empty password to connect at localhost's port 5432
2424+2525+## Writing tests
2626+2727+`squirrel` uses a lot of snapshot tests, to add new tests for the code
2828+generation bits you can have a look and copy the existing ones.
2929+There's no hard requirements but I have some suggestion to write good snapshot
3030+tests:
3131+3232+- Have at most one snapshot per test function
3333+- Try to keep the snapshots as small as possible.
3434+ Ideally one snapshot should assert a single property of the generated code so
3535+ that it is easier to focus on a specific aspect of the code when reviewing it
3636+- Use a long descriptive title for the snapshots: a title should describe what
3737+ one expects to see in the produced snapshot to guide the review process
···11+ Apache License
22+ Version 2.0, January 2004
33+ http://www.apache.org/licenses/
44+55+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
66+77+ 1. Definitions.
88+99+ "License" shall mean the terms and conditions for use, reproduction,
1010+ and distribution as defined by Sections 1 through 9 of this document.
1111+1212+ "Licensor" shall mean the copyright owner or entity authorized by
1313+ the copyright owner that is granting the License.
1414+1515+ "Legal Entity" shall mean the union of the acting entity and all
1616+ other entities that control, are controlled by, or are under common
1717+ control with that entity. For the purposes of this definition,
1818+ "control" means (i) the power, direct or indirect, to cause the
1919+ direction or management of such entity, whether by contract or
2020+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
2121+ outstanding shares, or (iii) beneficial ownership of such entity.
2222+2323+ "You" (or "Your") shall mean an individual or Legal Entity
2424+ exercising permissions granted by this License.
2525+2626+ "Source" form shall mean the preferred form for making modifications,
2727+ including but not limited to software source code, documentation
2828+ source, and configuration files.
2929+3030+ "Object" form shall mean any form resulting from mechanical
3131+ transformation or translation of a Source form, including but
3232+ not limited to compiled object code, generated documentation,
3333+ and conversions to other media types.
3434+3535+ "Work" shall mean the work of authorship, whether in Source or
3636+ Object form, made available under the License, as indicated by a
3737+ copyright notice that is included in or attached to the work
3838+ (an example is provided in the Appendix below).
3939+4040+ "Derivative Works" shall mean any work, whether in Source or Object
4141+ form, that is based on (or derived from) the Work and for which the
4242+ editorial revisions, annotations, elaborations, or other modifications
4343+ represent, as a whole, an original work of authorship. For the purposes
4444+ of this License, Derivative Works shall not include works that remain
4545+ separable from, or merely link (or bind by name) to the interfaces of,
4646+ the Work and Derivative Works thereof.
4747+4848+ "Contribution" shall mean any work of authorship, including
4949+ the original version of the Work and any modifications or additions
5050+ to that Work or Derivative Works thereof, that is intentionally
5151+ submitted to Licensor for inclusion in the Work by the copyright owner
5252+ or by an individual or Legal Entity authorized to submit on behalf of
5353+ the copyright owner. For the purposes of this definition, "submitted"
5454+ means any form of electronic, verbal, or written communication sent
5555+ to the Licensor or its representatives, including but not limited to
5656+ communication on electronic mailing lists, source code control systems,
5757+ and issue tracking systems that are managed by, or on behalf of, the
5858+ Licensor for the purpose of discussing and improving the Work, but
5959+ excluding communication that is conspicuously marked or otherwise
6060+ designated in writing by the copyright owner as "Not a Contribution."
6161+6262+ "Contributor" shall mean Licensor and any individual or Legal Entity
6363+ on behalf of whom a Contribution has been received by Licensor and
6464+ subsequently incorporated within the Work.
6565+6666+ 2. Grant of Copyright License. Subject to the terms and conditions of
6767+ this License, each Contributor hereby grants to You a perpetual,
6868+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
6969+ copyright license to reproduce, prepare Derivative Works of,
7070+ publicly display, publicly perform, sublicense, and distribute the
7171+ Work and such Derivative Works in Source or Object form.
7272+7373+ 3. Grant of Patent License. Subject to the terms and conditions of
7474+ this License, each Contributor hereby grants to You a perpetual,
7575+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
7676+ (except as stated in this section) patent license to make, have made,
7777+ use, offer to sell, sell, import, and otherwise transfer the Work,
7878+ where such license applies only to those patent claims licensable
7979+ by such Contributor that are necessarily infringed by their
8080+ Contribution(s) alone or by combination of their Contribution(s)
8181+ with the Work to which such Contribution(s) was submitted. If You
8282+ institute patent litigation against any entity (including a
8383+ cross-claim or counterclaim in a lawsuit) alleging that the Work
8484+ or a Contribution incorporated within the Work constitutes direct
8585+ or contributory patent infringement, then any patent licenses
8686+ granted to You under this License for that Work shall terminate
8787+ as of the date such litigation is filed.
8888+8989+ 4. Redistribution. You may reproduce and distribute copies of the
9090+ Work or Derivative Works thereof in any medium, with or without
9191+ modifications, and in Source or Object form, provided that You
9292+ meet the following conditions:
9393+9494+ (a) You must give any other recipients of the Work or
9595+ Derivative Works a copy of this License; and
9696+9797+ (b) You must cause any modified files to carry prominent notices
9898+ stating that You changed the files; and
9999+100100+ (c) You must retain, in the Source form of any Derivative Works
101101+ that You distribute, all copyright, patent, trademark, and
102102+ attribution notices from the Source form of the Work,
103103+ excluding those notices that do not pertain to any part of
104104+ the Derivative Works; and
105105+106106+ (d) If the Work includes a "NOTICE" text file as part of its
107107+ distribution, then any Derivative Works that You distribute must
108108+ include a readable copy of the attribution notices contained
109109+ within such NOTICE file, excluding those notices that do not
110110+ pertain to any part of the Derivative Works, in at least one
111111+ of the following places: within a NOTICE text file distributed
112112+ as part of the Derivative Works; within the Source form or
113113+ documentation, if provided along with the Derivative Works; or,
114114+ within a display generated by the Derivative Works, if and
115115+ wherever such third-party notices normally appear. The contents
116116+ of the NOTICE file are for informational purposes only and
117117+ do not modify the License. You may add Your own attribution
118118+ notices within Derivative Works that You distribute, alongside
119119+ or as an addendum to the NOTICE text from the Work, provided
120120+ that such additional attribution notices cannot be construed
121121+ as modifying the License.
122122+123123+ You may add Your own copyright statement to Your modifications and
124124+ may provide additional or different license terms and conditions
125125+ for use, reproduction, or distribution of Your modifications, or
126126+ for any such Derivative Works as a whole, provided Your use,
127127+ reproduction, and distribution of the Work otherwise complies with
128128+ the conditions stated in this License.
129129+130130+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131131+ any Contribution intentionally submitted for inclusion in the Work
132132+ by You to the Licensor shall be under the terms and conditions of
133133+ this License, without any additional terms or conditions.
134134+ Notwithstanding the above, nothing herein shall supersede or modify
135135+ the terms of any separate license agreement you may have executed
136136+ with Licensor regarding such Contributions.
137137+138138+ 6. Trademarks. This License does not grant permission to use the trade
139139+ names, trademarks, service marks, or product names of the Licensor,
140140+ except as required for reasonable and customary use in describing the
141141+ origin of the Work and reproducing the content of the NOTICE file.
142142+143143+ 7. Disclaimer of Warranty. Unless required by applicable law or
144144+ agreed to in writing, Licensor provides the Work (and each
145145+ Contributor provides its Contributions) on an "AS IS" BASIS,
146146+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147147+ implied, including, without limitation, any warranties or conditions
148148+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149149+ PARTICULAR PURPOSE. You are solely responsible for determining the
150150+ appropriateness of using or redistributing the Work and assume any
151151+ risks associated with Your exercise of permissions under this License.
152152+153153+ 8. Limitation of Liability. In no event and under no legal theory,
154154+ whether in tort (including negligence), contract, or otherwise,
155155+ unless required by applicable law (such as deliberate and grossly
156156+ negligent acts) or agreed to in writing, shall any Contributor be
157157+ liable to You for damages, including any direct, indirect, special,
158158+ incidental, or consequential damages of any character arising as a
159159+ result of this License or out of the use or inability to use the
160160+ Work (including but not limited to damages for loss of goodwill,
161161+ work stoppage, computer failure or malfunction, or any and all
162162+ other commercial damages or losses), even if such Contributor
163163+ has been advised of the possibility of such damages.
164164+165165+ 9. Accepting Warranty or Additional Liability. While redistributing
166166+ the Work or Derivative Works thereof, You may choose to offer,
167167+ and charge a fee for, acceptance of support, warranty, indemnity,
168168+ or other liability obligations and/or rights consistent with this
169169+ License. However, in accepting such obligations, You may act only
170170+ on Your own behalf and on Your sole responsibility, not on behalf
171171+ of any other Contributor, and only if You agree to indemnify,
172172+ defend, and hold each Contributor harmless for any liability
173173+ incurred by, or claims asserted against, such Contributor by reason
174174+ of your accepting any such warranty or additional liability.
175175+176176+ END OF TERMS AND CONDITIONS
177177+178178+ APPENDIX: How to apply the Apache License to your work.
179179+180180+ To apply the Apache License to your work, attach the following
181181+ boilerplate notice, with the fields enclosed by brackets "[]"
182182+ replaced with your own identifying information. (Don't include
183183+ the brackets!) The text should be enclosed in the appropriate
184184+ comment syntax for the file format. We also recommend that a
185185+ file or class name and description of purpose be included on the
186186+ same "printed page" as the copyright notice for easier
187187+ identification within third-party archives.
188188+189189+ Copyright 2024 Giacomo Cavalieri
190190+191191+ Licensed under the Apache License, Version 2.0 (the "License");
192192+ you may not use this file except in compliance with the License.
193193+ You may obtain a copy of the License at
194194+195195+ http://www.apache.org/licenses/LICENSE-2.0
196196+197197+ Unless required by applicable law or agreed to in writing, software
198198+ distributed under the License is distributed on an "AS IS" BASIS,
199199+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200200+ See the License for the specific language governing permissions and
201201+ limitations under the License.
···11+# 🐿️ squirrel - type safe SQL in Gleam
22+33+[](https://hex.pm/packages/squirrel)
44+[](https://hexdocs.pm/squirrel/)
55+66+## What's Squirrel?
77+88+If you need to talk with a database in Gleam you'll have to write something like
99+this:
1010+1111+```gleam
1212+import gleam/pgo
1313+import decode
1414+1515+pub type FindSquirrelRow {
1616+ FindSquirrelRow(name: String, owned_acorns: Int)
1717+}
1818+1919+/// Find a squirrel and its owned acorns given its name.
2020+///
2121+pub fn find_squirrel(db: pgo.Connection, name: String) {
2222+ let squirrel_row_decoder =
2323+ decode.into({
2424+ use name <- decode.parameter
2525+ use owned_acorns <- decode.parameter
2626+ FindSquirrelRow(name: name, owned_acorns: owned_acorns)
2727+ })
2828+ |> decode.field(0, decode.string)
2929+ |> decode.field(1, decode.int)
3030+3131+ "select name, owned_acorns
3232+ from squirrel
3333+ where name = $1"
3434+ |> pgo.execute(db, [pgo.text(name)], squirrel_row_decoder)
3535+}
3636+```
3737+3838+This is probably fine if you have a few small queries but it can become quite
3939+the burden when you have a lot of queries:
4040+4141+- The SQL query you write is just a plain string, you do not get syntax
4242+ highlighting, auto formatting, suggestions... all the little niceties you
4343+ would otherwise get if you where writing a plain `*.sql` file.
4444+- This also means you loose the ability to run these queries on their own with
4545+ other external tools, inspect them and so on.
4646+- You have to manually keep in sync the decoder with the query's output.
4747+4848+One might be tempted to hide all of this by reaching for something like an ORM.
4949+Squirrel proposes a different approach: instead of trying to hide the SQL it
5050+_embraces it and leaves you in control._
5151+You write the SQL queries in plain old `*.sql` files and Squirrel will take care
5252+of generating all the corresponding functions.
5353+5454+A code snippet is worth a thousand words, so let's have a look at an example.
5555+Instead of the hand written example shown earlier you can instead just write the
5656+following query:
5757+5858+```sql
5959+-- we're in file `src/squirrels/sql/find_squirrel.sql`
6060+-- Find a squirrel and its owned acorns given its name.
6161+select
6262+ name,
6363+ owned_acorns
6464+from
6565+ squirrel
6666+where
6767+ name = $1
6868+```
6969+7070+And run `gleam run -m squirrel`. Just like magic you'll now have a type-safe
7171+function `find_squirrel` you can use just as you'd expect:
7272+7373+```gleam
7474+import squirrels/sql
7575+7676+pub fn main() {
7777+ let db = todo as "the pgo connection"
7878+ // And it just works as you'd expect:
7979+ let assert Ok(#(_rows_count, rows)) = sql.find_squirrel("sandy")
8080+ let assert [FindSquirrelRow(name: "sandy", owned_acorns: 11_111)] = rows
8181+}
8282+```
8383+8484+Behind the scenes Squirrel generates the decoders and functions you need; and
8585+it's pretty-printed, standard Gleam code (actually it's exactly like the hand
8686+written example I showed you earlier)!
8787+So now you get the best of both worlds:
8888+8989+- You don't have to take care of keeping encoders and decoders in sync, Squirrel
9090+ does that for you.
9191+- And you're not compromising on type safety either: Squirrel is able to
9292+ understand the types of your query and produce a correct decoder.
9393+- You can stick to writing plain SQL in `*.sql` files. You'll have better
9494+ editor support, syntax highlighting and completions.
9595+- You can run each query on its own: need to `explain` a query?
9696+ No big deal, it's just a plain old `*.sql` file.
9797+9898+## Usage
9999+100100+First you'll need to add Squirrel to your project as a dev dependency:
101101+102102+```sh
103103+gleam add squirrel --dev
104104+105105+# Remember to add these packages if you haven't yet, they are needed by the
106106+# generated code to run and decode the read rows!
107107+gleam add gleam_pgo
108108+gleam add decode
109109+```
110110+111111+Then you can ask it to generate code running the `squirrel` module:
112112+113113+```sh
114114+gleam run -m squirrel
115115+```
116116+117117+And that's it! As long as you follow a couple of conventions Squirrel will just
118118+work:
119119+120120+- Squirrel will look for all `*.sql` files in any `sql` directory under your
121121+ project's `src` directory.
122122+- Each `sql` directory will be turned into a single Gleam module containing a
123123+ function for each `*.sql` file inside it. The generated Gleam module is going
124124+ to be located in the same directory as the corresponding `sql` directory and
125125+ it's name is `sql.gleam`.
126126+- Each `*.sql` file _must contain a single SQL query._ And the name of the file
127127+ is going to be the name of the corresponding Gleam function to run that query.
128128+129129+> Let's make an example. Imagine you have a Gleam project that looks like this
130130+>
131131+> ```txt
132132+> ├── src
133133+> │ ├── squirrels
134134+> │ │ └── sql
135135+> │ │ ├── find_squirrel.sql
136136+> │ │ └── list_squirrels.sql
137137+> │ └── squirrels.gleam
138138+> └── test
139139+> └── squirrels_test.gleam
140140+> ```
141141+>
142142+> Running `gleam run -m squirrel` will create a `src/squirrels/sql.gleam` file
143143+> defining two functions `find_squirrel` and `list_squirrels` you can then
144144+> import and use in your code.
145145+146146+### Talking to the database
147147+148148+In order to understand the type of your queries, Squirrel needs to connect to
149149+the Postgres server where the database is defined. To connect, it will read the
150150+[Postgres env variables](https://www.postgresql.org/docs/current/libpq-envars.html)
151151+and use the following defaults if one is not set:
152152+153153+- `PGHOST`: `"localhost"`
154154+- `PGPORT`: `5432`
155155+- `PGUSER`: `"root"`
156156+- `PGDATABASE`: `"database"`
157157+- `PGPASSWORD`: `""`
158158+159159+## FAQ
160160+161161+### What flavour of SQL does squirrel support?
162162+163163+Squirrel only has support for Postgres.
164164+165165+### Why isn't squirrel configurable in any way?
166166+167167+By going the "convention over configuration" route, Squirrel enforces that all
168168+projects adopting it will always have the same structure.
169169+If you need to contribute to a project using Squirrel you'll immediately know
170170+which directories and modules to look for.
171171+172172+This makes it easier to get started with a new project and cuts down on all the
173173+bike shedding: _"Where should I put my queries?",_
174174+_"How many queries should go in on file?",_ ...
175175+176176+## References
177177+178178+This package draws a lot of inspiration from the amazing
179179+[yesql](https://github.com/krisajenkins/yesql) and
180180+[sqlx](https://github.com/launchbadge/sqlx).
181181+182182+## Contributing
183183+184184+If you think there’s any way to improve this package, or if you spot a bug don’t
185185+be afraid to open PRs, issues or requests of any kind! Any contribution is
186186+welcome 💜
···11+---
22+version: 1.1.8
33+title: array decoding
44+file: ./test/squirrel_test.gleam
55+test_name: array_decoding_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(res: List(Int))
1515+}
1616+1717+/// Runs the `query` query
1818+/// defined in `query.sql`.
1919+///
2020+/// > 🐿️ This function was generated automatically using v-test of
2121+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2222+///
2323+pub fn query(db) {
2424+ let decoder =
2525+ decode.into({
2626+ use res <- decode.parameter
2727+ QueryRow(res: res)
2828+ })
2929+ |> decode.field(0, decode.list(decode.int))
3030+3131+ "select array[1, 2, 3] as res"
3232+ |> pgo.execute(db, [], decode.from(decoder, _))
3333+}
···11+---
22+version: 1.1.8
33+title: array encoding
44+file: ./test/squirrel_test.gleam
55+test_name: array_encoding_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(res: Bool)
1515+}
1616+1717+/// Runs the `query` query
1818+/// defined in `query.sql`.
1919+///
2020+/// > 🐿️ This function was generated automatically using v-test of
2121+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2222+///
2323+pub fn query(db, arg_1) {
2424+ let decoder =
2525+ decode.into({
2626+ use res <- decode.parameter
2727+ QueryRow(res: res)
2828+ })
2929+ |> decode.field(0, decode.bool)
3030+3131+ "select true as res where $1 = array[1, 2, 3]"
3232+ |> pgo.execute(
3333+ db,
3434+ [pgo.array(list.map(arg_1, fn(a) {pgo.int(a)}))],
3535+ decode.from(decoder, _),
3636+ )
3737+}
···11+---
22+version: 1.1.8
33+title: bool decoding
44+file: ./test/squirrel_test.gleam
55+test_name: bool_decoding_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(res: Bool)
1515+}
1616+1717+/// Runs the `query` query
1818+/// defined in `query.sql`.
1919+///
2020+/// > 🐿️ This function was generated automatically using v-test of
2121+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2222+///
2323+pub fn query(db) {
2424+ let decoder =
2525+ decode.into({
2626+ use res <- decode.parameter
2727+ QueryRow(res: res)
2828+ })
2929+ |> decode.field(0, decode.bool)
3030+3131+ "select true as res"
3232+ |> pgo.execute(db, [], decode.from(decoder, _))
3333+}
···11+---
22+version: 1.1.8
33+title: bool encoding
44+file: ./test/squirrel_test.gleam
55+test_name: bool_encoding_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(res: Bool)
1515+}
1616+1717+/// Runs the `query` query
1818+/// defined in `query.sql`.
1919+///
2020+/// > 🐿️ This function was generated automatically using v-test of
2121+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2222+///
2323+pub fn query(db, arg_1) {
2424+ let decoder =
2525+ decode.into({
2626+ use res <- decode.parameter
2727+ QueryRow(res: res)
2828+ })
2929+ |> decode.field(0, decode.bool)
3030+3131+ "select true as res where $1 = true"
3232+ |> pgo.execute(db, [pgo.bool(arg_1)], decode.from(decoder, _))
3333+}
···11+---
22+version: 1.1.8
33+title: float decoding
44+file: ./test/squirrel_test.gleam
55+test_name: float_decoding_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(res: Float)
1515+}
1616+1717+/// Runs the `query` query
1818+/// defined in `query.sql`.
1919+///
2020+/// > 🐿️ This function was generated automatically using v-test of
2121+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2222+///
2323+pub fn query(db) {
2424+ let decoder =
2525+ decode.into({
2626+ use res <- decode.parameter
2727+ QueryRow(res: res)
2828+ })
2929+ |> decode.field(0, decode.float)
3030+3131+ "select 1.1 as res"
3232+ |> pgo.execute(db, [], decode.from(decoder, _))
3333+}
···11+---
22+version: 1.1.8
33+title: float encoding
44+file: ./test/squirrel_test.gleam
55+test_name: float_encoding_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(res: Bool)
1515+}
1616+1717+/// Runs the `query` query
1818+/// defined in `query.sql`.
1919+///
2020+/// > 🐿️ This function was generated automatically using v-test of
2121+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2222+///
2323+pub fn query(db, arg_1) {
2424+ let decoder =
2525+ decode.into({
2626+ use res <- decode.parameter
2727+ QueryRow(res: res)
2828+ })
2929+ |> decode.field(0, decode.bool)
3030+3131+ "select true as res where $1 = 1.1"
3232+ |> pgo.execute(db, [pgo.float(arg_1)], decode.from(decoder, _))
3333+}
···11+---
22+version: 1.1.8
33+title: generated type fields are labelled with their name in the select list
44+file: ./test/squirrel_test.gleam
55+test_name: generated_type_fields_are_labelled_with_their_name_in_the_select_list_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(acorns: Option(Int), squirrel_name: String)
1515+}
1616+1717+/// Runs the `query` query
1818+/// defined in `query.sql`.
1919+///
2020+/// > 🐿️ This function was generated automatically using v-test of
2121+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2222+///
2323+pub fn query(db) {
2424+ let decoder =
2525+ decode.into({
2626+ use acorns <- decode.parameter
2727+ use squirrel_name <- decode.parameter
2828+ QueryRow(acorns: acorns, squirrel_name: squirrel_name)
2929+ })
3030+ |> decode.field(0, decode.optional(decode.int))
3131+ |> decode.field(1, decode.string)
3232+3333+ "
3434+select
3535+ acorns,
3636+ name as squirrel_name
3737+from
3838+ squirrel
3939+"
4040+ |> pgo.execute(db, [], decode.from(decoder, _))
4141+}
···11+---
22+version: 1.1.8
33+title: generated type has the same name as the function but in pascal case
44+file: ./test/squirrel_test.gleam
55+test_name: generated_type_has_the_same_name_as_the_function_but_in_pascal_case_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(res: Bool)
1515+}
1616+1717+/// Runs the `query` query
1818+/// defined in `query.sql`.
1919+///
2020+/// > 🐿️ This function was generated automatically using v-test of
2121+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2222+///
2323+pub fn query(db) {
2424+ let decoder =
2525+ decode.into({
2626+ use res <- decode.parameter
2727+ QueryRow(res: res)
2828+ })
2929+ |> decode.field(0, decode.bool)
3030+3131+ "
3232+select true as res
3333+"
3434+ |> pgo.execute(db, [], decode.from(decoder, _))
3535+}
···11+---
22+version: 1.1.8
33+title: int decoding
44+file: ./test/squirrel_test.gleam
55+test_name: int_decoding_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(res: Int)
1515+}
1616+1717+/// Runs the `query` query
1818+/// defined in `query.sql`.
1919+///
2020+/// > 🐿️ This function was generated automatically using v-test of
2121+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2222+///
2323+pub fn query(db) {
2424+ let decoder =
2525+ decode.into({
2626+ use res <- decode.parameter
2727+ QueryRow(res: res)
2828+ })
2929+ |> decode.field(0, decode.int)
3030+3131+ "select 11 as res"
3232+ |> pgo.execute(db, [], decode.from(decoder, _))
3333+}
···11+---
22+version: 1.1.8
33+title: int encoding
44+file: ./test/squirrel_test.gleam
55+test_name: int_encoding_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(res: Bool)
1515+}
1616+1717+/// Runs the `query` query
1818+/// defined in `query.sql`.
1919+///
2020+/// > 🐿️ This function was generated automatically using v-test of
2121+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2222+///
2323+pub fn query(db, arg_1) {
2424+ let decoder =
2525+ decode.into({
2626+ use res <- decode.parameter
2727+ QueryRow(res: res)
2828+ })
2929+ |> decode.field(0, decode.bool)
3030+3131+ "select true as res where $1 = 11"
3232+ |> pgo.execute(db, [pgo.int(arg_1)], decode.from(decoder, _))
3333+}
···11+---
22+version: 1.1.8
33+title: optional decoding
44+file: ./test/squirrel_test.gleam
55+test_name: optional_decoding_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(acorns: Option(Int))
1515+}
1616+1717+/// Runs the `query` query
1818+/// defined in `query.sql`.
1919+///
2020+/// > 🐿️ This function was generated automatically using v-test of
2121+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2222+///
2323+pub fn query(db) {
2424+ let decoder =
2525+ decode.into({
2626+ use acorns <- decode.parameter
2727+ QueryRow(acorns: acorns)
2828+ })
2929+ |> decode.field(0, decode.optional(decode.int))
3030+3131+ "select acorns from squirrel"
3232+ |> pgo.execute(db, [], decode.from(decoder, _))
3333+}
···11+---
22+version: 1.1.8
33+title: query with comment
44+file: ./test/squirrel_test.gleam
55+test_name: query_with_comment_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(res: Bool)
1515+}
1616+1717+/// This is a comment
1818+///
1919+/// > 🐿️ This function was generated automatically using v-test of
2020+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2121+///
2222+pub fn query(db) {
2323+ let decoder =
2424+ decode.into({
2525+ use res <- decode.parameter
2626+ QueryRow(res: res)
2727+ })
2828+ |> decode.field(0, decode.bool)
2929+3030+ "
3131+-- This is a comment
3232+select true as res
3333+"
3434+ |> pgo.execute(db, [], decode.from(decoder, _))
3535+}
···11+---
22+version: 1.1.8
33+title: query with multiline comment
44+file: ./test/squirrel_test.gleam
55+test_name: query_with_multiline_comment_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(res: Bool)
1515+}
1616+1717+/// This is a comment
1818+/// that goes over multiple lines!
1919+///
2020+/// > 🐿️ This function was generated automatically using v-test of
2121+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2222+///
2323+pub fn query(db) {
2424+ let decoder =
2525+ decode.into({
2626+ use res <- decode.parameter
2727+ QueryRow(res: res)
2828+ })
2929+ |> decode.field(0, decode.bool)
3030+3131+ "
3232+-- This is a comment
3333+-- that goes over multiple lines!
3434+select true as res
3535+"
3636+ |> pgo.execute(db, [], decode.from(decoder, _))
3737+}
···11+---
22+version: 1.1.8
33+title: string decoding
44+file: ./test/squirrel_test.gleam
55+test_name: string_decoding_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(res: String)
1515+}
1616+1717+/// Runs the `query` query
1818+/// defined in `query.sql`.
1919+///
2020+/// > 🐿️ This function was generated automatically using v-test of
2121+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2222+///
2323+pub fn query(db) {
2424+ let decoder =
2525+ decode.into({
2626+ use res <- decode.parameter
2727+ QueryRow(res: res)
2828+ })
2929+ |> decode.field(0, decode.string)
3030+3131+ "select 'wibble' as res"
3232+ |> pgo.execute(db, [], decode.from(decoder, _))
3333+}
···11+---
22+version: 1.1.8
33+title: string encoding
44+file: ./test/squirrel_test.gleam
55+test_name: string_encoding_test
66+---
77+/// A row you get from running the `query` query
88+/// defined in `query.sql`.
99+///
1010+/// > 🐿️ This type definition was generated automatically using v-test of the
1111+/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
1212+///
1313+pub type QueryRow {
1414+ QueryRow(res: Bool)
1515+}
1616+1717+/// Runs the `query` query
1818+/// defined in `query.sql`.
1919+///
2020+/// > 🐿️ This function was generated automatically using v-test of
2121+/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
2222+///
2323+pub fn query(db, arg_1) {
2424+ let decoder =
2525+ decode.into({
2626+ use res <- decode.parameter
2727+ QueryRow(res: res)
2828+ })
2929+ |> decode.field(0, decode.bool)
3030+3131+ "select true as res where $1 = 'wibble'"
3232+ |> pgo.execute(db, [pgo.text(arg_1)], decode.from(decoder, _))
3333+}
···11+//// In this module lies the core of `squirrel`.
22+//// It exposes a single public function called `main` that is used to turn a
33+//// list of untyped queries into typed ones.
44+////
55+//// To do so, `squirrel` will try to connect to a database, have it parse the
66+//// queries and reply with the types it could infer.
77+//// Then it's as simple as (not that simple in practice 😆) converting the
88+//// Postgres types into Gleam types.
99+////
1010+//// > 💡 I tried to do my best to comment everything as much as possible and
1111+//// > make things easy to read.
1212+//// > If you feel something is poorly commented or hard to understand, then
1313+//// > that is a bug! Please do reach out, I'd love to hear your feedback.
1414+////
1515+1616+import eval
1717+import gleam/bit_array
1818+import gleam/bool
1919+import gleam/dict.{type Dict}
2020+import gleam/dynamic.{type DecodeErrors, type Dynamic} as d
2121+import gleam/int
2222+import gleam/json
2323+import gleam/list
2424+import gleam/option.{type Option, None, Some}
2525+import gleam/result
2626+import gleam/set.{type Set}
2727+import gleam/string
2828+import squirrel/internal/database/postgres_protocol as pg
2929+import squirrel/internal/error.{
3030+ type Error, type Pointer, type ValueIdentifierError, ByteIndex,
3131+ CannotParseQuery, PgCannotAuthenticate, PgCannotDecodeReceivedMessage,
3232+ PgCannotDescribeQuery, PgCannotReceiveMessage, PgCannotSendMessage, Pointer,
3333+ QueryHasInvalidColumn, QueryHasUnsupportedType,
3434+}
3535+import squirrel/internal/eval_extra
3636+import squirrel/internal/gleam
3737+import squirrel/internal/query.{
3838+ type TypedQuery, type UntypedQuery, TypedQuery, UntypedQuery,
3939+}
4040+4141+const find_postgres_type_query = "
4242+select
4343+ -- The name of the type or, if the type is an array, the name of its
4444+ -- elements' type.
4545+ case
4646+ when elem.typname is null then type.typname
4747+ else elem.typname
4848+ end as type,
4949+ -- Tells us how to interpret the firs column: if this is true then the first
5050+ -- column is the type of the elements of the array type.
5151+ -- Otherwise it means we've found a base type.
5252+ case
5353+ when elem.typname is null then false
5454+ else true
5555+ end as is_array
5656+from
5757+ pg_type as type
5858+ left join pg_type as elem on type.typelem = elem.oid
5959+where
6060+ type.oid = $1
6161+"
6262+6363+const find_column_nullability_query = "
6464+select
6565+ -- Whether the column has a not-null constraint.
6666+ attnotnull
6767+from
6868+ pg_attribute
6969+where
7070+ -- The oid of the table the column comes from.
7171+ attrelid = $1
7272+ -- The index of the column we're looking for.
7373+ and attnum = $2
7474+"
7575+7676+// --- TYPES -------------------------------------------------------------------
7777+7878+/// A Postgres type.
7979+///
8080+/// > ⚠️Postgres has loads of types and this might not cover the more exotic
8181+/// > ones but for now it feels more than enough.
8282+///
8383+type PgType {
8484+ /// A base type, like `integer`, `text`, `char`, ...
8585+ ///
8686+ PBase(name: String)
8787+8888+ /// An array type like `int[]`, `text[]`, ...
8989+ ///
9090+ PArray(inner: PgType)
9191+9292+ /// A type that could also be `NULL`, this is particularly common for columns
9393+ /// that do not have a `not null` constraint; or for those coming from partial
9494+ /// joins.
9595+ ///
9696+ POption(inner: PgType)
9797+}
9898+9999+/// The context in which all database-related actions will take place.
100100+///
101101+type Context {
102102+ Context(
103103+ /// A connection to the database. Squirrel does nothing fancy and just uses
104104+ /// a single connection to run all the queries.
105105+ ///
106106+ db: pg.Connection,
107107+ /// A cache from `oid` to corresponding Gleam type.
108108+ /// We use this to avoid having to reach to the database every time we need
109109+ /// to infer a type.
110110+ ///
111111+ /// > 💡 An oid is an integer identifier that is used by Postgres to
112112+ /// > uniquely identify types (and a lot of other various objects, see
113113+ /// > [this documentation page](https://www.postgresql.org/docs/current/datatype-oid.html)).
114114+ ///
115115+ gleam_types: Dict(Int, gleam.Type),
116116+ /// A cache from table `oid` and column index to its nullability.
117117+ /// We use this to avoid having to reach to the database every time we need
118118+ /// to type a column.
119119+ ///
120120+ column_nullability: Dict(#(Int, Int), Nullability),
121121+ )
122122+}
123123+124124+/// Information about a column's nullability.
125125+/// If a column has a `not null` constraint then it will be `NotNullable`, in
126126+/// all other cases it will be `Nullable`.
127127+///
128128+/// > ⚠️ A column with a `not null` constraint might still be considered
129129+/// > nullable if it comes from a left/right join!
130130+///
131131+type Nullability {
132132+ Nullable
133133+ NotNullable
134134+}
135135+136136+/// A query plan produced by Postgres when we ask it to `explain` a query.
137137+///
138138+type Plan {
139139+ Plan(
140140+ join_type: Option(JoinType),
141141+ parent_relation: Option(ParentRelation),
142142+ output: Option(List(String)),
143143+ plans: Option(List(Plan)),
144144+ )
145145+}
146146+147147+type JoinType {
148148+ Full
149149+ Left
150150+ Right
151151+ Other
152152+}
153153+154154+type ParentRelation {
155155+ Inner
156156+ NotInner
157157+}
158158+159159+/// This is the type of a database-related action.
160160+/// In order to be carried out it needs to have access to the database `Context`
161161+/// and could fail with an `Error`.
162162+///
163163+type Db(a) =
164164+ eval.Eval(a, Error, Context)
165165+166166+/// The options used to establish a connection to the Postgres database.
167167+///
168168+pub type ConnectionOptions {
169169+ ConnectionOptions(
170170+ host: String,
171171+ port: Int,
172172+ user: String,
173173+ password: String,
174174+ database: String,
175175+ timeout: Int,
176176+ )
177177+}
178178+179179+// --- POSTGRES TO GLEAM TYPES CONVERSIONS -------------------------------------
180180+181181+/// This function turns a Postgres type into a Gleam one, returning an error
182182+/// with the type name if it is not currently supported.
183183+///
184184+fn pg_to_gleam_type(type_: PgType) -> Result(gleam.Type, String) {
185185+ case type_ {
186186+ PArray(inner: inner) ->
187187+ pg_to_gleam_type(inner)
188188+ |> result.map(gleam.List)
189189+ |> result.map_error(fn(inner) { inner <> "[]" })
190190+191191+ POption(inner: inner) ->
192192+ pg_to_gleam_type(inner)
193193+ |> result.map(gleam.Option)
194194+ |> result.map_error(fn(inner) { inner <> "?" })
195195+196196+ PBase(name: name) ->
197197+ case name {
198198+ "bool" -> Ok(gleam.Bool)
199199+ "text" | "char" -> Ok(gleam.String)
200200+ "float4" | "float8" | "numeric" -> Ok(gleam.Float)
201201+ "int2" | "int4" | "int8" -> Ok(gleam.Int)
202202+ _ -> Error(name)
203203+ }
204204+ }
205205+}
206206+207207+// --- CLI ENTRY POINT ---------------------------------------------------------
208208+209209+/// Connects to a Postgres database (using the given options) and types a list
210210+/// of queries.
211211+///
212212+/// This might fail with an `Error` if a database connection cannot be
213213+/// established, making it impossible to type any of the queries.
214214+/// Otherwise, it will try typing all the queries, retuning a list of typed ones
215215+/// and a list of possible errors for the ones it couldn't type.
216216+///
217217+pub fn main(
218218+ queries: List(UntypedQuery),
219219+ connection: ConnectionOptions,
220220+) -> Result(#(List(TypedQuery), List(Error)), Error) {
221221+ let context =
222222+ Context(
223223+ db: pg.connect(connection.host, connection.port, connection.timeout),
224224+ gleam_types: dict.new(),
225225+ column_nullability: dict.new(),
226226+ )
227227+228228+ // Once the server has confirmed that it is ready to accept query requests we
229229+ // can start gathering information about all the different queries.
230230+ // After each one we need to make sure the server is ready to go on with the
231231+ // next one.
232232+ //
233233+ // https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY
234234+ //
235235+ let #(context, connection) = eval.step(authenticate(connection), context)
236236+ case connection {
237237+ Error(error) -> Error(error)
238238+ // After successfully authenticating we can try and type all the queries.
239239+ Ok(_) ->
240240+ list.map(queries, infer_types)
241241+ |> eval_extra.run_all(context)
242242+ |> result.partition
243243+ |> Ok
244244+ }
245245+}
246246+247247+fn authenticate(connection: ConnectionOptions) -> Db(Nil) {
248248+ let params = [#("user", connection.user), #("database", connection.database)]
249249+ use _ <- eval.try(send(pg.FeStartupMessage(params)))
250250+251251+ use msg <- eval.try(receive())
252252+ use _ <- eval.try(case msg {
253253+ pg.BeAuthenticationOk -> eval.return(Nil)
254254+ _ -> unexpected_message(PgCannotAuthenticate, "AuthenticationOk", msg)
255255+ })
256256+257257+ use _ <- eval.try(wait_until_ready())
258258+ eval.return(Nil)
259259+}
260260+261261+/// Returns type information about a query.
262262+///
263263+fn infer_types(query: UntypedQuery) -> Db(TypedQuery) {
264264+ // Postgres doesn't give us 100% accurate data regardin a query's type.
265265+ // We'll need to perform a couple of database interrogations and do some
266266+ // guessing.
267267+ //
268268+ // The big picture idea is the following:
269269+ // - We ask the server to prepare the query.
270270+ // - Postgres will reply with type information about the returned rows and the
271271+ // query's parameters.
272272+ let action = parameters_and_returns(query)
273273+ use #(parameters, returns) <- eval.try(action)
274274+ // - The parameters' types are just OIDs so we need to interrogate the
275275+ // database to learn the actual corresponding Gleam type.
276276+ use parameters <- eval.try(resolve_parameters(query, parameters))
277277+ // - The returns' types are just OIDs so we have to do the same for those.
278278+ // - Here comes the tricky part: we can't know if a column is nullable just
279279+ // from the server's previous answer.
280280+ // - For columns coming from a database table we'll look it up and see if
281281+ // the column is nullable or not
282282+ // - But this is not enough! If a returned column comes from a left/right
283283+ // join it will be nullable even if it is not in the original table.
284284+ // To work around this we'll have to inspect the query plan.
285285+ use plan <- eval.try(query_plan(query, list.length(parameters)))
286286+ let nullables = nullables_from_plan(plan)
287287+ use returns <- eval.try(resolve_returns(query, returns, nullables))
288288+289289+ query
290290+ |> query.add_types(parameters, returns)
291291+ |> eval.return
292292+}
293293+294294+fn parameters_and_returns(query: UntypedQuery) -> Db(_) {
295295+ // We need to send three messages:
296296+ // - `Parse` with the query to parse
297297+ // - `Describe` to ask the server to reply with a description of the query's
298298+ // return types and parameter types. This is what we need to understand the
299299+ // SQL inferred types and generate the corresponding Gleam types.
300300+ // - `Sync` to ask the server to immediately reply with the results of parsing
301301+ //
302302+ use _ <- eval.try(
303303+ send_all([
304304+ pg.FeParse("", query.content, []),
305305+ pg.FeDescribe(pg.PreparedStatement, ""),
306306+ pg.FeSync,
307307+ ]),
308308+ )
309309+310310+ // Error builder used in the following steps in case the message sequence
311311+ // doesn't go as planned.
312312+ let cannot_describe = fn(expected, got) {
313313+ PgCannotDescribeQuery(
314314+ file: query.file,
315315+ query_name: gleam.identifier_to_string(query.name),
316316+ expected: expected,
317317+ got: got,
318318+ )
319319+ }
320320+321321+ use msg <- eval.try(receive())
322322+ case msg {
323323+ pg.BeErrorResponse(errors) ->
324324+ eval.throw(error_fields_to_parse_error(query, errors))
325325+ pg.BeParseComplete -> {
326326+ use msg <- eval.try(receive())
327327+ use parameters <- eval.try(case msg {
328328+ pg.BeParameterDescription(parameters) -> eval.return(parameters)
329329+ _ -> unexpected_message(cannot_describe, "ParameterDescription", msg)
330330+ })
331331+332332+ use msg <- eval.try(receive())
333333+ use rows <- eval.try(case msg {
334334+ pg.BeRowDescriptions(rows) -> eval.return(rows)
335335+ _ -> unexpected_message(cannot_describe, "RowDescriptions", msg)
336336+ })
337337+338338+ use msg <- eval.try(receive())
339339+ use _ <- eval.try(case msg {
340340+ pg.BeReadyForQuery(_) -> eval.return(Nil)
341341+ _ -> unexpected_message(cannot_describe, "ReadyForQuery", msg)
342342+ })
343343+344344+ eval.return(#(parameters, rows))
345345+ }
346346+ _ ->
347347+ unexpected_message(cannot_describe, "ParseComplete or ErrorResponse", msg)
348348+ }
349349+}
350350+351351+/// Given an untyped query and the error fields we got back from the database in
352352+/// case it couldn't be parsed, produces an appropriate `Error`.
353353+///
354354+fn error_fields_to_parse_error(
355355+ query: UntypedQuery,
356356+ errors: Set(pg.ErrorOrNoticeField),
357357+) -> Error {
358358+ // We first look for the relevant errors in the set of errors the database
359359+ // returned. This way we can attach additional information explaining why the
360360+ // query failed.
361361+ let #(error_code, message, position, hint) = {
362362+ use acc, error_field <- set.fold(errors, from: #(None, None, None, None))
363363+ let #(code, message, position, hint) = acc
364364+ case error_field {
365365+ pg.Code(code) -> #(Some(code), message, position, hint)
366366+ pg.Message(message) -> #(code, Some(message), position, hint)
367367+ pg.Hint(hint) -> #(code, message, position, Some(hint))
368368+ pg.Position(position) ->
369369+ case int.parse(position) {
370370+ Ok(position) -> #(code, message, Some(position), hint)
371371+ Error(_) -> acc
372372+ }
373373+ _ -> acc
374374+ }
375375+ }
376376+377377+ // If we found both a `Message` and `Position` error messages then we can turn
378378+ // those into a pointer that will be shown in the error message.
379379+ let pointer = case message, position {
380380+ Some(message), Some(position) ->
381381+ Some(Pointer(point_to: ByteIndex(position), message: message))
382382+ _, _ -> None
383383+ }
384384+385385+ cannot_parse_error(query, error_code, hint, pointer)
386386+}
387387+388388+fn resolve_parameters(
389389+ query: UntypedQuery,
390390+ parameters: List(Int),
391391+) -> Db(List(gleam.Type)) {
392392+ use oid <- eval_extra.try_map(parameters)
393393+ find_gleam_type(query, oid)
394394+}
395395+396396+/// Looks up a type with the given id in the Postgres registry.
397397+///
398398+/// > ⚠️ This function assumes that the oid is present in the database and
399399+/// > will crash otherwise. This should only be called with oids coming from
400400+/// > a database interrogation.
401401+///
402402+fn find_gleam_type(query: UntypedQuery, oid: Int) -> Db(gleam.Type) {
403403+ // We first look for the Gleam type corresponding to this id in the cache to
404404+ // avoid hammering the db with needless queries.
405405+ use <- with_cached_gleam_type(oid)
406406+407407+ // The only parameter to this query is the oid of the type to lookup:
408408+ // that's a 32bit integer (its oid needed to prepare the query is 23).
409409+ let params = [pg.Parameter(<<oid:32>>)]
410410+ let run_query = find_postgres_type_query |> run_query(params, [23])
411411+ use res <- eval.try(run_query)
412412+413413+ // We know the output must only contain two values: the name and a boolean to
414414+ // check wether it is an array or not.
415415+ // It's safe to assert because this query is hard coded in our code and the
416416+ // output shape cannot change without us changing that query.
417417+ let assert [name, is_array] = res
418418+419419+ // We then decode the bitarrays we got as a result:
420420+ // - `name` is just a string
421421+ // - `is_array` is a pg boolean
422422+ let assert Ok(name) = bit_array.to_string(name)
423423+ let type_ = case bit_array_to_bool(is_array) {
424424+ True -> PArray(PBase(name))
425425+ False -> PBase(name)
426426+ }
427427+428428+ pg_to_gleam_type(type_)
429429+ |> result.map_error(unsupported_type_error(query, _))
430430+ |> eval.from_result
431431+}
432432+433433+/// Returns the query plan for a given query.
434434+/// `parameters` is the number of parameter placeholders in the query.
435435+///
436436+fn query_plan(query: UntypedQuery, parameters: Int) -> Db(Plan) {
437437+ // We ask postgres to give us the query plan. To do that we need to fill in
438438+ // all the possible holes in the user supplied query with null values;
439439+ // otherwise, the server would complain that it has arguments that are not
440440+ // bound.
441441+ let query = "explain (format json, verbose) " <> query.content
442442+ let params = list.repeat(pg.Null, parameters)
443443+ let run_query = run_query(query, params, [])
444444+ use res <- eval.try(run_query)
445445+446446+ // We know the output will only contain a single row that is the json string
447447+ // containing the query plan.
448448+ let assert [plan] = res
449449+ let assert Ok([plan, ..]) = json.decode_bits(plan, json_plans_decoder)
450450+ eval.return(plan)
451451+}
452452+453453+/// Given a query plan, returns a set with the indices of the output columns
454454+/// that can contain null values.
455455+///
456456+fn nullables_from_plan(plan: Plan) -> Set(Int) {
457457+ let outputs = case plan.output {
458458+ Some(outputs) -> list.index_fold(outputs, dict.new(), dict.insert)
459459+ None -> dict.new()
460460+ }
461461+462462+ do_nullables_from_plan(plan, outputs, set.new())
463463+}
464464+465465+fn do_nullables_from_plan(
466466+ plan: Plan,
467467+ // A dict from "column name" to its position in the query output.
468468+ query_outputs: Dict(String, Int),
469469+ nullables: Set(Int),
470470+) -> Set(Int) {
471471+ let nullables = case plan.output, plan.join_type, plan.parent_relation {
472472+ // - All the outputs of a full join must be marked as nullable
473473+ // - All the outputs of an inner half join must be marked as nullable
474474+ Some(outputs), Some(Full), _ | Some(outputs), _, Some(Inner) -> {
475475+ use nullables, output <- list.fold(outputs, from: nullables)
476476+ case dict.get(query_outputs, output) {
477477+ Ok(i) -> set.insert(nullables, i)
478478+ Error(_) -> nullables
479479+ }
480480+ }
481481+ _, _, _ -> nullables
482482+ }
483483+484484+ case plan.plans, plan.join_type {
485485+ // If this is an inner half join we keep inspecting the children to mark
486486+ // their outputs as nullable.
487487+ Some(plans), Some(Left) | Some(plans), Some(Right) -> {
488488+ use nullables, plan <- list.fold(plans, from: nullables)
489489+ do_nullables_from_plan(plan, query_outputs, nullables)
490490+ }
491491+ _, _ -> nullables
492492+ }
493493+}
494494+495495+/// Given a list of `RowDescriptionFields` it turns those into Gleam fields with
496496+/// a name and a type.
497497+///
498498+/// This also uses nullability info coming from the query plan to figure out if
499499+/// a column can be nullable or not:
500500+/// - If the column name ends with `!` it will be forced to be not nullable
501501+/// - If the column name ends with `?` it will be forced to be nullable
502502+/// - If the column appears in the `nullables` set then it will be nullable
503503+/// - Othwerwise we look for its metadata in the database and if it has a
504504+/// not-null constraint it will be not nullable; otherwise it will be nullable
505505+///
506506+fn resolve_returns(
507507+ query: UntypedQuery,
508508+ returns: List(pg.RowDescriptionField),
509509+ nullables: Set(Int),
510510+) -> Db(List(gleam.Field)) {
511511+ use column, i <- eval_extra.try_index_map(returns)
512512+ let pg.RowDescriptionField(
513513+ data_type_oid: type_oid,
514514+ attr_number: column,
515515+ table_oid: table,
516516+ name: name,
517517+ ..,
518518+ ) = column
519519+520520+ use type_ <- eval.try(find_gleam_type(query, type_oid))
521521+522522+ let ends_with_exclamation_mark = string.ends_with(name, "!")
523523+ let ends_with_question_mark = string.ends_with(name, "?")
524524+ use nullability <- eval.try(case ends_with_exclamation_mark {
525525+ True -> eval.return(NotNullable)
526526+ False ->
527527+ case ends_with_question_mark {
528528+ True -> eval.return(Nullable)
529529+ False ->
530530+ case set.contains(nullables, i) {
531531+ True -> eval.return(Nullable)
532532+ False -> column_nullability(table: table, column: column)
533533+ }
534534+ }
535535+ })
536536+537537+ let type_ = case nullability {
538538+ Nullable -> gleam.Option(type_)
539539+ NotNullable -> type_
540540+ }
541541+542542+ let try_convert_name =
543543+ // If the name ends with a `?` or `!` we don't want that to be included in
544544+ // the gleam name or it would be invalid!
545545+ case ends_with_exclamation_mark || ends_with_question_mark {
546546+ True -> string.drop_right(name, 1)
547547+ False -> name
548548+ }
549549+ |> gleam.identifier
550550+ |> result.map_error(invalid_column_error(query, name, _))
551551+552552+ use name <- eval.try(eval.from_result(try_convert_name))
553553+554554+ let field = gleam.Field(label: name, type_: type_)
555555+ eval.return(field)
556556+}
557557+558558+fn column_nullability(table table: Int, column column: Int) -> Db(Nullability) {
559559+ // We first check if the table+column is cached to avoid making redundant
560560+ // queries to the database.
561561+ use <- with_cached_column(table: table, column: column)
562562+563563+ // If the table oid is 0 that means the column doesn't come from any table so
564564+ // we just assume it's not nullable.
565565+ use <- bool.guard(when: table == 0, return: eval.return(NotNullable))
566566+567567+ // This query has 2 parameters:
568568+ // - the oid of the table (a 32bit integer, oid is 23)
569569+ // - the index of the column (a 32 bit integer, oid is 23)
570570+ let params = [pg.Parameter(<<table:32>>), pg.Parameter(<<column:32>>)]
571571+ let run_query = find_column_nullability_query |> run_query(params, [23, 23])
572572+ use res <- eval.try(run_query)
573573+574574+ // We know the output will only have only one column, that is the boolean
575575+ // telling us if the column has a not-null constraint.
576576+ let assert [has_non_null_constraint] = res
577577+ case bit_array_to_bool(has_non_null_constraint) {
578578+ True -> eval.return(NotNullable)
579579+ False -> eval.return(Nullable)
580580+ }
581581+}
582582+583583+// --- DB ACTION HELPERS -------------------------------------------------------
584584+585585+/// Runs a query against the database.
586586+/// - `parameters` are the parameter values that need to be supplied in place of
587587+/// the query placeholders
588588+/// - `parameters_object_ids` are the oids describing the type of each
589589+/// parameter.
590590+///
591591+/// > ⚠️ The `parameters_objects_ids` should have the same length of
592592+/// > `parameters` and correctly describe each parameter's type. This function
593593+/// > makes no attempt whatsoever to verify this assumption is correct so be
594594+/// > careful!
595595+///
596596+/// > ⚠️ This function makes the assumption that the query will only return one
597597+/// > single row. This is totally fine here because we only use this to run
598598+/// > specific hard coded queries that are guaranteed to return a single row.
599599+///
600600+fn run_query(
601601+ query: String,
602602+ parameters: List(pg.ParameterValue),
603603+ parameters_object_ids: List(Int),
604604+) -> Db(List(BitArray)) {
605605+ // The message exchange to run a query works as follow:
606606+ // - `Parse` we ask the server to parse the query, we do not give it a name
607607+ // - `Bind` we bind the query to the unnamed portal so that it is ready to
608608+ // be executed.
609609+ // - `Execute` we ask the server to run the unnamed portal and return all
610610+ // the rows (0 means return all rows).
611611+ // - `Close` we ask to close the unnamed query and the unnamed portal to free
612612+ // their resources.
613613+ // - `Sync` this acts as a synchronization point that needs to go at the end
614614+ // of the sequence before the next one.
615615+ //
616616+ // As you can see in the receiving part, each message we send corresponds to a
617617+ // specific answer from the server:
618618+ // - `ParseComplete` the query was parsed correctly
619619+ // - `BindComplete` the query was bound to a portal
620620+ // - `MessageDataRow` the result(s) coming from the query execution
621621+ // - `CommandComplete` when the result coming from the query is over
622622+ // - `CloseComplete` the portal/statement was closed
623623+ // - `ReadyForQuery` final reply to the sync message signalling we can go on
624624+ // making new requests
625625+ use _ <- eval.try(
626626+ send_all([
627627+ pg.FeParse("", query, parameters_object_ids),
628628+ pg.FeBind(
629629+ portal: "",
630630+ statement_name: "",
631631+ parameter_format: pg.FormatAll(pg.Binary),
632632+ parameters:,
633633+ result_format: pg.FormatAll(pg.Binary),
634634+ ),
635635+ pg.FeExecute("", 0),
636636+ pg.FeClose(pg.PreparedStatement, ""),
637637+ pg.FeClose(pg.Portal, ""),
638638+ pg.FeSync,
639639+ ]),
640640+ )
641641+642642+ use msg <- eval.try(receive())
643643+ let assert pg.BeParseComplete = msg
644644+ use msg <- eval.try(receive())
645645+ let assert pg.BeBindComplete = msg
646646+ use msg <- eval.try(receive())
647647+ let assert pg.BeMessageDataRow(res) = msg
648648+ use msg <- eval.try(receive())
649649+ let assert pg.BeCommandComplete(_, _) = msg
650650+ use msg <- eval.try(receive())
651651+ let assert pg.BeCloseComplete = msg
652652+ use msg <- eval.try(receive())
653653+ let assert pg.BeCloseComplete = msg
654654+ use msg <- eval.try(receive())
655655+ let assert pg.BeReadyForQuery(_) = msg
656656+ eval.return(res)
657657+}
658658+659659+/// Receive a single message from the database.
660660+///
661661+fn receive() -> Db(pg.BackendMessage) {
662662+ use Context(db: db, ..) as context <- eval.from
663663+ case pg.receive(db) {
664664+ Ok(#(db, msg)) -> #(Context(..context, db: db), Ok(msg))
665665+ Error(pg.ReadDecodeError(error)) -> #(
666666+ context,
667667+ Error(PgCannotDecodeReceivedMessage(string.inspect(error))),
668668+ )
669669+ Error(pg.SocketError(error)) -> #(
670670+ context,
671671+ Error(PgCannotReceiveMessage(string.inspect(error))),
672672+ )
673673+ }
674674+}
675675+676676+/// Send a single message to the database.
677677+///
678678+fn send(message message: pg.FrontendMessage) -> Db(Nil) {
679679+ use Context(db: db, ..) as context <- eval.from
680680+681681+ let result =
682682+ message
683683+ |> pg.encode_frontend_message
684684+ |> pg.send(db, _)
685685+686686+ let #(db, result) = case result {
687687+ Ok(db) -> #(db, Ok(Nil))
688688+ Error(error) -> #(db, Error(PgCannotSendMessage(string.inspect(error))))
689689+ }
690690+691691+ #(Context(..context, db: db), result)
692692+}
693693+694694+/// Send many messages, one after the other.
695695+///
696696+fn send_all(messages messages: List(pg.FrontendMessage)) -> Db(Nil) {
697697+ use acc, msg <- eval_extra.try_fold(messages, from: Nil)
698698+ use _ <- eval.try(send(msg))
699699+ eval.return(acc)
700700+}
701701+702702+/// Start receiving and discarding messages until a `ReadyForQuery` message is
703703+/// received.
704704+///
705705+fn wait_until_ready() -> Db(Nil) {
706706+ use _ <- eval.try(send(pg.FeFlush))
707707+ do_wait_until_ready()
708708+}
709709+710710+fn do_wait_until_ready() -> Db(Nil) {
711711+ use msg <- eval.try(receive())
712712+ case msg {
713713+ pg.BeReadyForQuery(_) -> eval.return(Nil)
714714+ _ -> do_wait_until_ready()
715715+ }
716716+}
717717+718718+/// Throws an error built from an expected message and an unexpected message
719719+/// that is turned into a string.
720720+///
721721+fn unexpected_message(
722722+ builder: fn(String, String) -> Error,
723723+ expected expected: String,
724724+ got got: pg.BackendMessage,
725725+) {
726726+ builder(expected, string.inspect(got)) |> eval.throw
727727+}
728728+729729+/// Looks up for a type with the given id in a global cache.
730730+/// If the type is present it immediately returns it.
731731+/// Otherwise it runs the database action to fetch it and then caches it to be
732732+/// reused later.
733733+///
734734+fn with_cached_gleam_type(
735735+ lookup oid: Int,
736736+ otherwise do: fn() -> Db(gleam.Type),
737737+) -> Db(gleam.Type) {
738738+ use context: Context <- eval.from
739739+ case dict.get(context.gleam_types, oid) {
740740+ Ok(type_) -> #(context, Ok(type_))
741741+ Error(_) ->
742742+ case eval.step(do(), context) {
743743+ #(_, Error(_)) as result -> result
744744+ #(Context(gleam_types: gleam_types, ..) as context, Ok(type_)) -> {
745745+ let gleam_types = dict.insert(gleam_types, oid, type_)
746746+ let new_context = Context(..context, gleam_types: gleam_types)
747747+ #(new_context, Ok(type_))
748748+ }
749749+ }
750750+ }
751751+}
752752+753753+/// Looks up for the nullability of table's column.
754754+/// If the nullability is cached it is immediately returns it.
755755+/// Otherwise it runs the database action to fetch it and then caches it to be
756756+/// reused later.
757757+///
758758+fn with_cached_column(
759759+ table table_oid: Int,
760760+ column column: Int,
761761+ otherwise do: fn() -> Db(Nullability),
762762+) -> Db(Nullability) {
763763+ use context: Context <- eval.from
764764+ let key = #(table_oid, column)
765765+ case dict.get(context.column_nullability, key) {
766766+ Ok(type_) -> #(context, Ok(type_))
767767+ Error(_) ->
768768+ case eval.step(do(), context) {
769769+ #(_, Error(_)) as result -> result
770770+ #(
771771+ Context(
772772+ column_nullability: column_nullability,
773773+ ..,
774774+ ) as context,
775775+ Ok(type_),
776776+ ) -> {
777777+ let column_nullability = dict.insert(column_nullability, key, type_)
778778+ let new_context =
779779+ Context(..context, column_nullability: column_nullability)
780780+ #(new_context, Ok(type_))
781781+ }
782782+ }
783783+ }
784784+}
785785+786786+// --- HELPERS TO BUILD ERRORS -------------------------------------------------
787787+788788+fn unsupported_type_error(query: UntypedQuery, type_: String) -> Error {
789789+ let UntypedQuery(
790790+ content: content,
791791+ file: file,
792792+ name: name,
793793+ starting_line: starting_line,
794794+ comment: _,
795795+ ) = query
796796+ QueryHasUnsupportedType(
797797+ file: file,
798798+ name: gleam.identifier_to_string(name),
799799+ content: content,
800800+ type_: type_,
801801+ starting_line: starting_line,
802802+ )
803803+}
804804+805805+fn cannot_parse_error(
806806+ query: UntypedQuery,
807807+ error_code: Option(String),
808808+ hint: Option(String),
809809+ pointer: Option(Pointer),
810810+) -> Error {
811811+ let UntypedQuery(
812812+ content: content,
813813+ file: file,
814814+ name: name,
815815+ starting_line: starting_line,
816816+ comment: _,
817817+ ) = query
818818+ CannotParseQuery(
819819+ content: content,
820820+ file: file,
821821+ name: gleam.identifier_to_string(name),
822822+ error_code: error_code,
823823+ hint: hint,
824824+ pointer: pointer,
825825+ starting_line: starting_line,
826826+ )
827827+}
828828+829829+fn invalid_column_error(
830830+ query: UntypedQuery,
831831+ column_name: String,
832832+ reason: ValueIdentifierError,
833833+) -> Error {
834834+ let UntypedQuery(
835835+ name: _,
836836+ file: file,
837837+ content: content,
838838+ starting_line: starting_line,
839839+ comment: _,
840840+ ) = query
841841+ QueryHasInvalidColumn(
842842+ file: file,
843843+ column_name: column_name,
844844+ suggested_name: gleam.similar_identifier_string(column_name)
845845+ |> option.from_result,
846846+ content: content,
847847+ reason: reason,
848848+ starting_line: starting_line,
849849+ )
850850+}
851851+852852+// --- DECODERS ----------------------------------------------------------------
853853+854854+fn json_plans_decoder(data: Dynamic) -> Result(List(Plan), DecodeErrors) {
855855+ d.list(d.field("Plan", plan_decoder))(data)
856856+}
857857+858858+fn plan_decoder(data: Dynamic) -> Result(Plan, DecodeErrors) {
859859+ d.decode4(
860860+ Plan,
861861+ d.optional_field("Join Type", join_type_decoder),
862862+ d.optional_field("Parent Relationship", parent_relation_decoder),
863863+ d.optional_field("Output", d.list(d.string)),
864864+ d.optional_field("Plans", d.list(plan_decoder)),
865865+ )(data)
866866+}
867867+868868+fn join_type_decoder(data: Dynamic) -> Result(JoinType, DecodeErrors) {
869869+ use data <- result.map(d.string(data))
870870+ case data {
871871+ "Full" -> Full
872872+ "Left" -> Left
873873+ "Right" -> Right
874874+ _ -> Other
875875+ }
876876+}
877877+878878+fn parent_relation_decoder(
879879+ data: Dynamic,
880880+) -> Result(ParentRelation, DecodeErrors) {
881881+ use data <- result.map(d.string(data))
882882+ case data {
883883+ "Inner" -> Inner
884884+ _ -> NotInner
885885+ }
886886+}
887887+888888+// --- UTILS -------------------------------------------------------------------
889889+890890+/// Turns a bit array into a boolean value.
891891+/// Returns `False` if the bit array is all `0`s or empty, `True` otherwise.
892892+///
893893+fn bit_array_to_bool(bit_array: BitArray) -> Bool {
894894+ case bit_array {
895895+ <<0, rest:bits>> -> bit_array_to_bool(rest)
896896+ <<>> -> False
897897+ _ -> True
898898+ }
899899+}
···11+import birdie
22+import filepath
33+import gleam/dynamic
44+import gleam/list
55+import gleam/pgo
66+import gleam/string
77+import gleeunit
88+import simplifile
99+import squirrel/internal/database/postgres
1010+import squirrel/internal/error.{type Error}
1111+import squirrel/internal/query.{type TypedQuery}
1212+import temporary
1313+1414+pub fn main() {
1515+ setup_database()
1616+ gleeunit.main()
1717+}
1818+1919+// --- TEST SETUP --------------------------------------------------------------
2020+2121+const host = "localhost"
2222+2323+const user = "squirrel_test"
2424+2525+const database = "squirrel_test"
2626+2727+const port = 5432
2828+2929+fn setup_database() {
3030+ let config =
3131+ pgo.Config(
3232+ ..pgo.default_config(),
3333+ port: port,
3434+ user: user,
3535+ host: host,
3636+ database: database,
3737+ )
3838+ let db = pgo.connect(config)
3939+4040+ let assert Ok(_) =
4141+ "
4242+create table if not exists squirrel(
4343+ name text primary key,
4444+ acorns int
4545+)
4646+"
4747+ |> pgo.execute(db, [], dynamic.dynamic)
4848+4949+ pgo.disconnect(db)
5050+}
5151+5252+// --- ASSERTION HELPERS -------------------------------------------------------
5353+5454+fn should_codegen(query: String) -> String {
5555+ // We assert everything went smoothly and we have no errors in the query.
5656+ let assert Ok(#(queries, [])) = codegen_queries([#("query", query)])
5757+ list.map(queries, query.generate_code("v-test", _))
5858+ |> string.join(with: "\n\n")
5959+}
6060+6161+fn codegen_queries(
6262+ queries: List(#(String, String)),
6363+) -> Result(#(List(TypedQuery), List(Error)), Error) {
6464+ // If there's any error with the temporary package we just fail the test,
6565+ // there's no reason to try and keep going.
6666+ let assert Ok(result) = {
6767+ use temp_dir <- temporary.create(temporary.directory())
6868+6969+ // We parse all the queries.
7070+ let queries = {
7171+ use #(file, query) <- list.map(queries)
7272+ let out_file = filepath.join(temp_dir, file <> ".sql")
7373+ let assert Ok(_) = simplifile.write(to: out_file, contents: query)
7474+ let assert Ok(query) = query.from_file(out_file)
7575+ // We manually change the file name here: we do not want to use the full
7676+ // out_file name in tests because that will change across different runs,
7777+ // causing the snapshot tests to fail.
7878+ let query = query.UntypedQuery(..query, file: file <> ".sql")
7979+ query
8080+ }
8181+8282+ // We can then ask squirrel to type check all the queries.
8383+ postgres.main(
8484+ queries,
8585+ postgres.ConnectionOptions(
8686+ host: host,
8787+ port: port,
8888+ user: user,
8989+ database: database,
9090+ password: "",
9191+ timeout: 1000,
9292+ ),
9393+ )
9494+ }
9595+9696+ result
9797+}
9898+9999+// --- ENCODING/DECODING CODEGEN TESTS -----------------------------------------
100100+// This is a group of tests to ensure the generated encoders/decoders are what
101101+// we expect for all the supported data types.
102102+//
103103+104104+pub fn int_decoding_test() {
105105+ "select 11 as res"
106106+ |> should_codegen
107107+ |> birdie.snap(title: "int decoding")
108108+}
109109+110110+pub fn int_encoding_test() {
111111+ "select true as res where $1 = 11"
112112+ |> should_codegen
113113+ |> birdie.snap(title: "int encoding")
114114+}
115115+116116+pub fn float_decoding_test() {
117117+ "select 1.1 as res"
118118+ |> should_codegen
119119+ |> birdie.snap(title: "float decoding")
120120+}
121121+122122+pub fn float_encoding_test() {
123123+ "select true as res where $1 = 1.1"
124124+ |> should_codegen
125125+ |> birdie.snap(title: "float encoding")
126126+}
127127+128128+pub fn string_decoding_test() {
129129+ "select 'wibble' as res"
130130+ |> should_codegen
131131+ |> birdie.snap(title: "string decoding")
132132+}
133133+134134+pub fn string_encoding_test() {
135135+ "select true as res where $1 = 'wibble'"
136136+ |> should_codegen
137137+ |> birdie.snap(title: "string encoding")
138138+}
139139+140140+pub fn bool_decoding_test() {
141141+ "select true as res"
142142+ |> should_codegen
143143+ |> birdie.snap(title: "bool decoding")
144144+}
145145+146146+pub fn bool_encoding_test() {
147147+ "select true as res where $1 = true"
148148+ |> should_codegen
149149+ |> birdie.snap(title: "bool encoding")
150150+}
151151+152152+pub fn array_decoding_test() {
153153+ "select array[1, 2, 3] as res"
154154+ |> should_codegen
155155+ |> birdie.snap(title: "array decoding")
156156+}
157157+158158+pub fn array_encoding_test() {
159159+ "select true as res where $1 = array[1, 2, 3]"
160160+ |> should_codegen
161161+ |> birdie.snap(title: "array encoding")
162162+}
163163+164164+pub fn optional_decoding_test() {
165165+ "select acorns from squirrel"
166166+ |> should_codegen
167167+ |> birdie.snap(title: "optional decoding")
168168+}
169169+170170+// --- CODEGEN STRUCTURE TESTS -------------------------------------------------
171171+// This is a group of tests to ensure the generated code has some specific
172172+// structure (e.g. the names and comments are what we expect...)
173173+//
174174+175175+pub fn query_with_comment_test() {
176176+ "
177177+-- This is a comment
178178+select true as res
179179+"
180180+ |> should_codegen
181181+ |> birdie.snap(title: "query with comment")
182182+}
183183+184184+pub fn query_with_multiline_comment_test() {
185185+ "
186186+-- This is a comment
187187+-- that goes over multiple lines!
188188+select true as res
189189+"
190190+ |> should_codegen
191191+ |> birdie.snap(title: "query with multiline comment")
192192+}
193193+194194+pub fn generated_type_has_the_same_name_as_the_function_but_in_pascal_case_test() {
195195+ "
196196+select true as res
197197+"
198198+ |> should_codegen
199199+ |> birdie.snap(
200200+ title: "generated type has the same name as the function but in pascal case",
201201+ )
202202+}
203203+204204+pub fn generated_type_fields_are_labelled_with_their_name_in_the_select_list_test() {
205205+ "
206206+select
207207+ acorns,
208208+ name as squirrel_name
209209+from
210210+ squirrel
211211+"
212212+ |> should_codegen
213213+ |> birdie.snap(
214214+ title: "generated type fields are labelled with their name in the select list",
215215+ )
216216+}