Squirrelix is intentionally minimal: convention over configuration. This guide covers the two query sources (Postgres inference and metadata files), connection settings, Mix task options, and the programmatic API.
Query sources
Every generation or check pass needs type information for each query. Squirrelix accepts one of two sources:
| Mode | When to use | How to enable |
|---|---|---|
| Postgres inferrer | Schema is available locally or in CI | --infer |
| Metadata file | No database at generation time | Default (requires squirr_elix.exs) |
Both mix squirrelix.gen and mix squirrelix.check use the same source for a
given invocation.
Postgres inference
Pass --infer to connect to a live database and read types from Postgrex prepare
metadata:
mix squirrelix.gen --infer --database my_app_dev
The database must exist, be reachable, and have the schema your queries reference (migrations applied).
Multi-schema and search_path
--infer uses the session Postgres connection: it does not set search_path
itself, and there is no CLI flag for it. Unqualified table names resolve the same
way they would in psql for that role — via
search_path
/current_schemas(true). Schema-qualified names (app.users) resolve that
schema regardless of search_path.
Practical guidance:
- Prefer schema-qualified table names in
.sqlfiles when objects live outsidepublic, so codegen and runtime see the same relation even if role defaults differ. - If you rely on unqualified names, ensure the role used for
mix squirrelix.gen --infer/mix squirrelix.check --inferhas the samesearch_pathas production (database/roleALTER ... SET search_path, or a connection that already applies it). - Temporary tables (
pg_temp) are resolved like ordinary Postgres sessions.
Intentional gaps (out of scope):
- Squirrelix does not migrate objects across schemas or rewrite SQL to add qualifiers.
- Cross-database federation is unsupported.
- Connection options do not expose a
search_pathoverride; configure it in Postgres (or qualify names in SQL) instead.
Nullability for schema-qualified and search_path-resolved columns is covered in
Writing Queries.
If connection fails, Squirrelix reports a structured error (refused host/port,
timeout, invalid credentials, or missing database) with hints for PG* variables
and Mix flags. Timeouts are reported separately from other connection failures.
When you cannot reach Postgres at generation time, use a metadata file instead of
--infer (see below).
Connection URL
Prefer putting secrets in the environment (PGPASSWORD / DATABASE_URL) rather
than in a URL or --password flag — both appear in process listings and shell
history.
Phoenix-style apps can rely on DATABASE_URL with no extra flags:
export DATABASE_URL=postgres://user:pass@host:5432/database?sslmode=require
mix squirrelix.gen --infer
Or pass an explicit URL:
mix squirrelix.gen --infer \
--url postgres://user@host:5432/database?connect_timeout=5&sslmode=require
Supported URL schemes: postgres:// and postgresql://.
The connect_timeout query parameter sets the connection timeout in seconds.
SSL query parameters:
| Parameter | Postgrex :ssl |
|---|---|
sslmode=disable | false |
sslmode=allow / prefer | false (Postgrex cannot negotiate SSL fallback) |
sslmode=require / ssl=true | [verify: :verify_none] (encrypt; no CA check) |
sslmode=verify-ca / verify-full | true (Postgrex secure defaults) |
ssl=false | false |
Unix sockets are not supported.
--infer runs your .sql files against a real database (prepare + EXPLAIN). Only
point it at trusted SQL and a database you intend to use for codegen.
Environment variables
Squirrelix reads DATABASE_URL (when set) and standard
libpq environment variables:
| Variable | Default |
|---|---|
DATABASE_URL | (unset — fall back to PG*) |
PGHOST | localhost |
PGPORT | 5432 |
PGUSER | postgres |
PGDATABASE | postgres |
PGPASSWORD | "" |
PGCONNECT_TIMEOUT | 5 (seconds) |
PGSSLMODE | (no SSL) |
PGSSLMODE uses the same mapping as URL sslmode above. A URL sslmode /
ssl parameter overrides PGSSLMODE.
Example with direnv:
# .envrc
export DATABASE_URL=postgres://postgres@localhost:5432/my_app_dev
# or:
export PGDATABASE=my_app_dev
export PGUSER=postgres
export PGPASSWORD=secret
direnv allow
mix squirrelix.gen --infer
Squirrelix does not read .env files directly. Use your shell, direnv, or
similar tools to load environment variables.
CLI flags
mix squirrelix.gen --infer \
--hostname db.example.com \
--port 5433 \
--username app \
--database my_app_dev
Connection precedence (highest first): flags → --url → DATABASE_URL → PG*
environment variables → defaults. Prefer PGPASSWORD / DATABASE_URL over
--password.
Metadata files
When --infer is not passed, Squirrelix loads a metadata file that maps query file
paths to parameter and return type descriptors.
Default path: squirr_elix.exs in the project root.
Custom path:
mix squirrelix.gen --metadata config/squirr_elix.exs
mix squirrelix.check --metadata config/squirr_elix.exs
Exporting inferred metadata
Some CI jobs cannot reach Postgres. Capture types once with --infer, then run
offline check from the written metadata file:
# Locally or in a migrate job with Postgres available:
mix squirrelix.gen --infer --database my_app_dev --write-metadata squirr_elix.exs
# Later, offline (no database):
mix squirrelix.check
# or:
mix squirrelix.check --metadata squirr_elix.exs
--write-metadata PATH requires --infer. It writes after a successful inference
pass (all queries typed with no errors) and overwrites PATH. Commit the file if
offline CI should use it.
The export is ordinary evaluated Elixir in the same shape as a hand-written
metadata file — treat it like mix.exs and never load untrusted paths.
Format
The file is evaluated as Elixir (like mix.exs) and must return a map. Only
load trusted, project-local metadata — never evaluate untrusted paths.
%{
"lib/my_app/accounts/sql/find_user.sql" => [
params: [:integer],
returns: [
%{name: "id", type: :integer, nullable?: false},
%{name: "name", type: :string, nullable?: false},
%{name: "email", type: :string, nullable?: true}
]
],
"lib/my_app/accounts/sql/delete_user.sql" => [
params: [:integer],
returns: []
]
}Keys are project-relative or absolute paths to .sql files. Values are keyword lists
with:
:params— list of type atoms for$1,$2, ... in order.:returns— list of column maps with:name,:type, and:nullable?keys. Usereturns: []for command queries with no returned rows.
See Types for the supported type atoms.
Every discovered .sql file must have a metadata entry, or generation fails with
MissingQueryMetadata.
Mix task options
Both mix squirrelix.gen and mix squirrelix.check accept:
| Option | Description |
|---|---|
--metadata PATH | Metadata file path (default: squirr_elix.exs) |
--infer | Infer types from Postgres instead of metadata |
--write-metadata PATH | Write inferred types to a metadata file (requires --infer) |
--url URL | Postgres connection URL |
--database NAME | Database name |
--hostname HOST | Database host |
--username USER | Database user |
--password PASS | Database password (prefer PGPASSWORD) |
--port PORT | Database port |
mix squirrelix.gen also accepts:
| Option | Description |
|---|---|
--watch | After the initial generate, watch {lib,test,dev}/**/sql/*.sql and regenerate on change |
Watch mode
While editing queries, keep generation in sync without re-running the Mix task by hand:
mix squirrelix.gen --watch
mix squirrelix.gen --infer --watch
mix squirrelix.gen --metadata config/squirr_elix.exs --watch
Watch mode:
- Runs a normal generate once (same query source and connection options as without
--watch). - Watches existing
lib/,test/, anddev/trees for changes to.sqlfiles that live directly in asql/directory. - Debounces rapid editor events, then regenerates.
- Logs success or failure for each regenerate; failures do not stop the watcher.
- Stops cleanly on Ctrl-C.
--watch is not supported by mix squirrelix.check (use check in CI for drift detection).
On Linux, watch mode needs inotify-tools
(e.g. apt install inotify-tools). macOS and Windows use built-in file-system backends.
Programmatic API
Supported public API
Pre-1.0, only the following are part of the supported programmatic surface.
Anything else under Squirrelix.* is internal (@moduledoc false) and may
change without a SemVer bump before 1.0.
| Surface | Role |
|---|---|
Squirrelix.generate/3 | Generate sql.ex modules for a project root |
Squirrelix.check/3 | Drift-check without writing files |
Squirrelix.CodegenSummary / CodegenCheckSummary | Return values from generate / check |
mix squirrelix.gen / mix squirrelix.check | Supported Mix task entry points |
Squirrelix.Error.* structs | Pattern-matchable diagnostics in summaries |
Squirrelix.Error.format/1 / format_all/1 | Render the same text Mix prints |
Squirrelix.Postgres.inferrer/1 | Live-Postgres query source for generate / check |
Squirrelix.Inference.Inferrer | Behaviour for custom query sources |
Squirrelix.Query | Struct passed to Inferrer callbacks (fields only) |
Do not expand the public surface silently: new modules or functions intended
for adopters need docs (module/@doc, README / this guide) in the same change.
Usage
Use Squirrelix.generate/3 and Squirrelix.check/3 from Elixir code:
# Metadata map
metadata = %{
"/path/to/lib/my_app/sql/find_user.sql" => [
params: [:integer],
returns: [%{name: "name", type: :string, nullable?: false}]
]
}
Squirrelix.generate("/path/to/project", metadata, version: "v0.5.1")
# Postgres inferrer (function or module implementing Squirrelix.Inference.Inferrer)
{:ok, conn} = Postgrex.start_link(database: "my_app_dev")
try do
Squirrelix.generate("/path/to/project", Squirrelix.Postgres.inferrer(conn),
version: "v0.5.1"
)
after
GenServer.stop(conn)
endOptions:
:version(required) — version string written into generated file headers.:postgrex— module passed to generated code (defaults toPostgrex).
Returns:
Squirrelix.generate/3→%Squirrelix.CodegenSummary{}Squirrelix.check/3→%Squirrelix.CodegenCheckSummary{}
CI setup
A typical CI step runs the check task after migrations:
- name: Apply migrations
run: mix ecto.migrate
- name: Check Squirrelix output
env:
PGDATABASE: my_app_test
PGHOST: localhost
PGUSER: postgres
run: mix squirrelix.check --inferWhen some jobs cannot reach Postgres, export metadata where the database is available and check offline elsewhere:
- name: Export Squirrelix metadata
env:
PGDATABASE: my_app_test
PGHOST: localhost
PGUSER: postgres
run: mix squirrelix.gen --infer --write-metadata squirr_elix.exs
# In a later job without Postgres:
- name: Check Squirrelix output (offline)
run: mix squirrelix.checkCommit generated sql.ex files (and the exported metadata file, when using offline
check) alongside your .sql sources so CI verifies they stay in sync.
Prefer live --infer whenever CI can run Postgres (it catches catalog drift). Use
exported metadata only when the job has no database — and refresh
squirr_elix.exs when queries or the schema change.
Copy-pasteable GitHub Actions workflows for adopters:
examples/github-actions/squirrelix-check.yml— Postgres service +DATABASE_URL+mix squirrelix.check --inferexamples/github-actions/squirrelix-check-offline.yml— offline check from committed metadata
For Phoenix apps — Mix aliases, DATABASE_URL, and cookbook context — see the
Phoenix + CI Cookbook.
Safe overwrite rules
Squirrelix only overwrites files whose header contains the Squirrelix generation
marker (written into @moduledoc). If a sql.ex file exists without that header
marker, generation fails with CannotOverwriteFile.
During check, an outdated generated file produces OutdatedFile.
Atomic generate and check
Code generation is project-wide atomic (Gleam squirrel 4.5+ parity):
- Query-error atomicity — If any
sql/directory has query errors (invalid file names, missing metadata, inference failures, unsupported types, and so on),mix squirrelix.genwrites nothing. Directories that would otherwise succeed are left untouched until every error is fixed. - Write-pass atomicity — When every directory is query-valid, Squirrelix
prepares all
sql.exwrites first. If any prepare fails (for exampleCannotOverwriteFile), nothing is written. Successful commits use temp files- rename, and roll back earlier renames if a later one fails mid-pass.
mix squirrelix.check fails globally when any directory has query errors or
generated-file drift. Errors from every failing directory are reported together;
a single bad directory is enough for a non-zero exit.
Next steps
- Getting Started — first query walkthrough
- Phoenix + CI Cookbook — Phoenix Mix aliases and CI
- Adopter CI workflows — copy-pasteable GitHub Actions
- Types — type mapping reference