Squirrelix maps Postgres column and parameter types to Elixir typespecs used in generated @specs and row @type definitions. This guide describes the supported mappings and intentional differences from Gleam Squirrel.

How types are used

Types appear in two places:

  1. Generated @specs and @type definitions — static documentation and Dialyzer hints for your query functions and row maps.
  2. Runtime encode/decode — generated helper functions convert Elixir values to Postgrex parameters and Postgrex result values to plain Elixir terms.

Row shapes use map/0 with required/1 keys:

@type find_user_row :: %{
  required(:name) => String.t(),
  required(:age) => integer()
}

@spec find_user(Postgrex.conn(), integer()) :: [find_user_row()]

Nullable columns add | nil to the value type:

@type find_user_row :: %{
  required(:bio) => String.t() | nil
}

Supported Postgres types

Postgres type@spec / row typeRuntime encode/decode
boolboolean()true / false
text, char, bpchar, varchar, citext, nameString.t()UTF-8 strings
float4, float8float()floats
numericDecimal.t()Decimal structs
int2, int4, int8integer()integers
json, jsonbterm()maps, lists, or primitives
uuidString.t()UUID strings
byteabinary()binaries
dateDate.t()Date structs
timeTime.t()Time structs
timestampNaiveDateTime.t()NaiveDateTime structs
timestamptzDateTime.t()DateTime structs
<type>[][inner_type]lists
user-defined enumString.t()enum label strings
user-defined domainbase typeresolved to base type

JSON and JSONB

JSON columns use term() in @specs because JSON values are not always objects. Postgrex may return maps, lists, strings, numbers, booleans, or nil depending on the stored payload.

@type payload_row :: %{required(:payload) => term()}

At runtime, JSON values pass through as decoded Elixir terms.

UUID

UUID columns use String.t() rather than a custom opaque type. Values are standard UUID strings such as "550e8400-e29b-41d4-a716-446655440000".

Dialyzer

Generated modules are written so adopter Dialyzer stays useful on call sites:

  • Public query @specs and row @types use precise stdlib types (String.t(), integer(), required/1 maps, term() for JSON, and so on).
  • Encode helpers carry matching @specs plus guards (is_integer/1, is_binary/1, is_struct/2, …) so invalid argument shapes fail early.
  • Soft companions document {:ok, …} | {:error, Exception.t()}; command soft companions use non_neg_integer() for affected-row counts.

  • Shared private decode helpers intentionally omit broad @specs that would be underspecs ([map()] / term()) relative to per-query row types.

Recommended Dialyzer flags for apps that include generated sql.ex files: :error_handling, :underspecs, :unknown, and :unmatched_returns.

Known limitations (document, do not “fix away”):

TopicExpectation
:overspecs / :specdiffsMay warn that public row contracts are more precise than success typing of shared decode_rows / Map.new helpers. That precision is intentional for call sites.
term() JSONJSON/JSONB cannot be narrowed further without lying about arrays/scalars.
Exception.t()Soft-error contracts use Exception.t(); Dialyzer success typing is often a looser exception map.
Postgrex.conn()Narrower than Dialyzer’s expanded conn success typing (pid / via / DBConnection).

Squirrelix does not ship Dialyxir as a dependency. Run Dialyzer in your app (see Phoenix + CI Cookbook).

Timestamptz

Squirrelix maps timestamptz to DateTime.t(). Gleam Squirrel rejects timestamptz with a hint to prefer plain timestamp; mapping to DateTime.t() is an intentional Elixir-native choice. Prefer timestamp columns when you want to avoid time-zone conversion surprises at the Postgres connection layer.

Arrays

Postgres array types map to Elixir lists recursively:

PostgresElixir
integer[][integer()]
text[][String.t()]
uuid[][String.t()]

Nullable elements inside arrays follow Postgrex decoding behaviour.

Postgres enums

User-defined Postgres enums (create type ... as enum) map to String.t() in generated code. Squirrelix does not generate Elixir enum modules.

Given:

create type squirrel_colour as enum ('light_brown', 'grey', 'red');

A column typed squirrel_colour appears as String.t() in @specs and decodes to "light_brown", "grey", or "red".

This differs from Gleam Squirrel, which generates custom enum types. The Elixir approach keeps generated modules simple and avoids naming conflicts when enum labels do not map cleanly to Elixir identifiers.

Enums with no variants are rejected with a structured error.

Domains

User-defined domains (create domain ... as ...) are resolved to their base Postgres type for both inference and code generation.

Unsupported types

Some Postgres types are intentionally unsupported. When inference encounters them, Squirrelix returns UnsupportedPostgresType with the type name and an actionable hint where one is defined.

Composite types (policy)

Decision: reject with actionable hints — do not generate nested row modules, map composites to map()/term(), or treat them as opaque strings.

Postgres composite types (create type ... as (field type, ...), typtype = c) are rejected. This matches Gleam Squirrel and keeps the Elixir-native API limited to stdlib typespecs and flat required/1 row maps.

Workarounds when a query would otherwise return a composite:

  • Select individual fields: (location).x, (location).y
  • Cast in SQL: location::json, location::jsonb, or location::text
  • Reshape the schema into ordinary columns

Ranges and multiranges

All built-in and user-defined range / multirange types are rejected:

Postgres typesPolicy
int4range, int8range, numrange, tsrange, tstzrange, daterangeRejected
int4multirange, int8multirange, nummultirange, tsmultirange, tstzmultirange, datemultirangeRejected
User-defined create type ... as range / multirangeRejected

Workarounds:

  • Select lower/upper bounds as separate supported columns (for example lower(ages), upper(ages)).
  • Cast the range to text or jsonb in SQL if you need the literal form.

Postgrex can decode ranges as Postgrex.Range at runtime, but Squirrelix does not generate typed helpers for them — keeping the supported surface aligned with Gleam Squirrel and Elixir stdlib typespecs.

Other unsupported built-ins

CategoryExamplesSuggested workaround
Geometricpoint, box, circle, line, lseg, path, polygonCast to text, or select coordinates as floats
Networkinet, cidr, macaddr, macaddr8Cast to text
Intervalintervalextract(epoch from ...)::float8 or cast to text
Bit stringsbit, varbitCast to text
Full-text searchtsvector, tsqueryCast to text, or return ranking/boolean scalars
MoneymoneyPrefer numeric
XMLxmlPrefer text or jsonb
OID familyoid, xid, tid, pg_lsnCast to text
hstorehstorePrefer jsonb
Time with time zonetimetzPrefer time

Metadata type atoms

When using a metadata file instead of --infer, use these atoms for :params and column :type values:

AtomMeaning
:booleanbool
:stringtext-like types and enums
:integerint2, int4, int8
:floatfloat4, float8
:decimalnumeric
:binarybytea
:uuiduuid
:datedate
:timetime
:naive_datetimetimestamp
:utc_datetimetimestamptz
:mapjson, jsonb
{:list, inner}Postgres arrays

Example metadata entry:

"lib/my_app/sql/find_user.sql" => [
  params: [:integer, :string],
  returns: [
    %{name: "id", type: :integer, nullable?: false},
    %{name: "tags", type: {:list, :string}, nullable?: false},
    %{name: "metadata", type: :map, nullable?: true}
  ]
]

Differences from Gleam Squirrel

AspectGleam SquirrelSquirrelix
Row shapeCustom record typemap() with required/1
Postgres enumsGenerated enum ADTString.t()
UUIDuuid.Uuid opaque typeString.t()
JSON decodeString (Gleam)term()
timestamptzHint to use timestampDateTime.t()
Database driverpogPostgrex

See NOTICE for upstream attribution.

Next steps