# squirrel: type safe SQL in Gleam without an ORM

> squirrel is a Gleam code generator that turns plain .sql files into typed functions, so you keep raw SQL, your editor's syntax highlighting and the ability to run a query on its own, while the generated Gleam code carries the encoders and decoders. It needs a live Postgres connection to infer query types, and it ships a check command for CI.

**giacomocavalieri/squirrel** — 🐿️ Type safe SQL in Gleam

- Repository: https://github.com/giacomocavalieri/squirrel
- Website: https://hexdocs.pm/squirrel/
- Stars: 692 · Forks: 37
- Language: Gleam
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/giacomocavalieri-squirrel

## The problem: a query as a Gleam string with a hand-written decoder

The README opens with the code you would write if you talked to a database from Gleam directly, using pog as the driver. It is a row type, a decoder built with decode.field calls, and a SQL query written as a string literal that gets piped through pog.query, pog.parameter, pog.returning and pog.execute.

The author calls this probably fine if you have a few small queries, and then names what it costs as the number grows. The SQL is a plain string, so you get no syntax highlighting, no auto formatting and no suggestions, the things you would have if you were writing a .sql file. You also lose the ability to run those queries on their own with external tools, to inspect them, because they are not files. And you have to manually keep the decoder in sync with the query output, which is the kind of task that silently rots.

An ORM is the obvious answer, and the project's position on that is stated with some heat in the README: instead of trying to hide the SQL it embraces it and leaves you in control. You write the SQL in plain old .sql files and Squirrel generates the corresponding functions.

The video linked in the README is the author's FOSDEM 2026 talk, titled You Don't Need an ORM, so this is a considered position rather than a passing remark. The argument is about where the work goes. A query becomes a file, a file is a unit you can lint, diff, run and explain, and the parts of it that are tedious to maintain by hand are the parts a generator can own.

## A .sql file in, a typed Gleam function out

The transformation is shown with a single file. Suppose you have a query file at src/my_app/sql/find_squirrel.sql containing a comment and the statement:

```sql
-- we're in file `src/my_app/sql/find_squirrel.sql`
-- Find a squirrel and its owned acorns given its name.
select
  name,
  owned_acorns
from
  squirrel
where
  name = $1
```

Run gleam run -m squirrel and you get a type-safe function find_squirrel you use as you would expect:

```gleam
import pog
import my_app/sql

pub fn main() {
  let db = todo as "the pog connection"
  // And it just works as you'd expect:
  let assert Ok(pog.Returned(_rows_count, rows)) = sql.find_squirrel(db, "sandy")
  let assert [sql.FindSquirrelRow(name: "sandy", owned_acorns: 11_111)] = rows
}
```

Two things are visible in that call site. The row type is named sql.FindSquirrelRow, so the generated module is a namespace for the row types as well as the functions, and the result is matched with a let assert against a concrete list, so a wrong number of columns or a wrong type is a compile error rather than a runtime surprise.

The README is specific that the generated output is pretty-printed standard Gleam, and that it is exactly the same as the hand-written example at the top of the page. That is a strong claim about a code generator, and it is a claim you can check: the decoder the generator writes for the two selected columns is the same decode.field sequence a person would have written.

Because the query file is a real .sql file, the operational benefits follow directly. You can explain a query on its own, you get editor support, and the file is reviewable in a diff without decoding a Gleam string literal.

## The conventions are narrow, and that is what makes it work

Squirrel's behaviour comes from a small set of rules, and the README states them plainly.

It looks for all .sql files in any sql directory under your project's src directory. Each sql directory becomes a single Gleam module containing a function for each .sql file inside it, and the generated module sits in the same directory as the corresponding sql directory, named sql.gleam. Each .sql file must contain a single SQL query, and the file name becomes the name of the generated function.

The worked example in the README shows the resulting layout, with a src directory holding my_app with a sql subdirectory containing find_squirrel.sql and list_squirrels.sql, and a my_app.gleam file next to it. Running gleam run -m squirrel creates src/my_app/sql.gleam defining find_squirrel and list_squirrels.

The single-query rule is the one constraint with teeth, and it is worth understanding why it exists rather than resenting it. A generator that has to infer a result type from a file containing several statements needs to know which statement to analyse, and one file per query removes the ambiguity entirely. It also matches how you would want to use the files anyway, since a file per query is what makes running one query on its own possible.

The naming convention carries over from the file name, which means the function name follows the snake_case of the file. Squirrel does not attempt to convert find_squirrel.sql into findSquirrel. The generated Gleam code is idiomatic in the sense of being valid and formatted, not in the sense of being renamed to Erlang naming rules.

The module-per-directory rule is what makes the import list short. You import my_app/sql and get every query in that directory, and the row types come along with the same import.

## A check command is the missing half of a code generator

The CLI section lists two commands, and the second one is the one that matters for a team.

The first is gleam run -m squirrel, which generates type-safe Gleam code for all SQL queries found under the sql directories in src. The second is gleam run -m squirrel check, which validates that the generated Gleam code is up to date with the SQL queries. The README calls this particularly useful to run in a CI pipeline so that you do not forget to run the generator.

This is the standard problem with generated code, and it is worth being clear about the failure mode it prevents. Without a check, someone edits a .sql file, forgets to regenerate, and the committed Gleam code still describes the old query. The mismatch is invisible in the editor, the code compiles, and the bug appears in production as a wrong column or a parameter that no longer matches. With the check in CI, the build fails and the message points at the missing regeneration step.

The dependency is added as a dev dependency, which follows from the fact that the generator is a build-time tool rather than a runtime one:

```sh
gleam add squirrel --dev
```

and the generation step is:

```sh
gleam run -m squirrel
```

Both are ordinary Gleam project commands, so the generation step fits into whatever task runner you already use. Nothing about the workflow requires a separate build system, and there is no configuration file to write: the conventions are the configuration.

## Type inference needs a live database, and that has consequences

This is the constraint that most affects adoption, so it is worth stating plainly. In order to understand the type of your queries, Squirrel needs to connect to the Postgres server where the database is defined.

The connection comes from the DATABASE_URL environment variable, which has to be a valid connection string of this form:

```txt
postgres://user:password@host:port/database?connect_timeout=seconds
```

If DATABASE_URL is not set, Squirrel reads the standard Postgres environment variables and applies defaults for the ones that are missing: PGHOST defaults to localhost, PGPORT to 5432, PGUSER to postgres, PGDATABASE to the name of your Gleam project, PGPASSWORD to an empty string, and PGCONNECT_TIMEOUT to 5 seconds.

The design makes sense once you see it. The generator is not parsing SQL with a hand-written parser, it is asking the database what a query returns. Postgres can resolve a select list, a where clause and a join against real tables and real column types, and it can do that with the same planner the application will later use. That is why the generator can produce a correct decoder for a query whose result shape is not obvious from the text alone.

The cost is that generation is not a pure function of your source tree. It needs a reachable server with the schema in place, and a developer whose local database is behind the schema gets different results from one whose is current. For a team that runs a database container in development, the repository ships a docker-compose.yml that starts a postgres:16 service with a squirrel_test database and a squirrel_test user on port 5432, so the dependency is easy to satisfy locally.

It also means the generated code is tied to a schema snapshot in time. A migration that changes a column type changes the generated decoder, which is correct behaviour, but it is a coupling between two things that a type checker cannot see across.

## Type mapping is the layer where the real work sits

Squirrel takes care of the mapping between Postgres types and Gleam types, and the README explains why that is needed in two places. Gleam values need to be encoded into Postgres values when you fill in the holes of a prepared statement, the $1 and $2 placeholders, and Postgres values need to be decoded into Gleam ones when you read the rows back.

That two-sided requirement is a useful way to think about the whole library. An encoder that handles text parameters and a decoder that handles the rows are separate problems that usually live in separate layers of an application, and a mismatch between them is a common source of runtime errors. Generating both from the same query means the parameter type and the column type come from the same source of truth.

The README then presents a table of supported Postgres types with what each is encoded as and decoded as. The table is where the interesting limits are, and it is truncated in what you can see here, so the specific mappings are worth checking in the published documentation before you commit to a schema that leans on exotic types.

The two directions also have different failure characteristics. An encoding problem shows up as a type error at the call site, because the generated function's parameter type is fixed by the placeholder's inferred type. A decoding problem shows up where a column type is broader than the Gleam type you would want, for example a nullable column, and how that is handled is exactly the kind of decision the type table documents.

The project is Apache-2.0 licensed with a LICENSE file at the root, it is published on Hex as squirrel with docs on hexdocs.pm, and the releases run v4.7.0 from 2026-06-03, v4.6.0 from 2025-11-15 and v4.5.0 from 2025-10-24. The last push was on 2026-09-10 and the project is not archived. The tree carries a CHANGELOG.md, a CONTRIBUTING.md, an integration_test directory, a test directory and a docker-compose.yml, so the generated code is checked against a real database in the project's own tests.

## Conclusion

Adopt squirrel if you write Gleam against Postgres and want typed query functions without giving up plain .sql files, editor tooling and the ability to run a single query directly against the database. Do not adopt it if you cannot give the generator a reachable Postgres instance during development, because inferring the shape of a query requires connecting to the server where the database is defined. Before you commit, set DATABASE_URL or the PG* variables as the README describes, run gleam run -m squirrel once, then add gleam run -m squirrel check to CI so that a changed .sql file and stale generated code cannot diverge.

## FAQ

### What is the squirrel Gleam package?

It is a code generator that turns plain .sql files into type-safe Gleam functions with generated encoders and decoders. The README positions it against ORMs: instead of hiding the SQL it embraces it and leaves you in control.

### How do I add squirrel to a Gleam project and generate the code?

Add it as a dev dependency with gleam add squirrel --dev, then run gleam run -m squirrel. Squirrel looks for .sql files in any sql directory under src, turns each directory into a sql.gleam module next to it, and names each function after the file.

### Does squirrel need a database connection to generate code?

Yes. To understand the type of your queries it connects to the Postgres server where the database is defined, reading DATABASE_URL if set, otherwise the standard PGHOST, PGPORT, PGUSER, PGDATABASE, PGPASSWORD and PGCONNECT_TIMEOUT variables with documented defaults.

### How do I stop the generated Gleam code from going stale?

Run gleam run -m squirrel check, which validates that the generated code is up to date with the SQL queries. The README recommends running it in a CI pipeline so that a changed .sql file with stale generated code fails the build.

### What are the naming and file layout rules for squirrel queries?

Each .sql file must contain a single SQL query, and the file name becomes the name of the generated Gleam function. Every sql directory under src becomes one sql.gleam module placed in the same directory, and each function is called with the connection and its parameters.

## Sources

- [giacomocavalieri/squirrel on GitHub](https://github.com/giacomocavalieri/squirrel)
- [License: Apache-2.0](https://github.com/giacomocavalieri/squirrel/blob/main/LICENSE)
- [Project website](https://hexdocs.pm/squirrel/)
- [README](https://github.com/giacomocavalieri/squirrel/blob/main/README.md)
- [Releases](https://github.com/giacomocavalieri/squirrel/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/giacomocavalieri-squirrel
