Skip to content

Type mapping

How each supported .atl field type maps to PostgreSQL, protobuf, and Go.

How each supported .atl field type maps to PostgreSQL, protobuf, and Go.

Scope

This page covers the types atlantis currently supports. The following PostgreSQL types are not supported yet: oid, hstore, ltree, domains and composites. File an issue if you need one.

Scalars

.atl type PostgreSQL Proto Go
bigint BIGINT int64 int64
int INTEGER int32 int32
smallint SMALLINT int32 int32
real REAL float float32
double DOUBLE PRECISION double float64
boolean BOOLEAN bool bool
varchar(N) VARCHAR(N) string string
varchar VARCHAR string string
text TEXT string string
citext CITEXT string string
jsonb JSONB bytes []byte
bytea BYTEA bytes []byte
uuid UUID string string
numeric(p, s) NUMERIC(p,s) string string
char(N) CHAR(N) string string
char CHAR string string
name NAME string string
money MONEY string string
xml XML string string
tsquery TSQUERY string string
json JSON bytes []byte
bit(N) BIT(N) string string
bit BIT string string
varbit(N) BIT VARYING(N) string string
varbit BIT VARYING string string
tsvector TSVECTOR string string

Notes:

  • citext requires the citext Postgres extension. The migration emitted by tide apply does not install it; the operator must CREATE EXTENSION citext once per database.
  • numeric is mapped to string on the wire to preserve exact precision. The wire format is the canonical PostgreSQL numeric string (e.g. "123.4500"). Unparameterized numeric (no (p, s)) is not supported.
  • jsonb payloads are stored as bytes and not parsed by Atlantis. Validity is checked by Postgres at insert time; an invalid JSON body errors at the storage layer, not at the gRPC boundary.
  • uuid uses the canonical RFC 4122 hyphenated lowercase form on the wire. Non-canonical input is rejected by Postgres at parse time.
  • bigint over the wire: proto int64. If you serialize a response to JSON yourself, proto-JSON encodes int64 as a string by default to preserve precision.
  • varchar(N) length is enforced by Postgres (value too long for type on INSERT); Atlantis does not pre-validate.
  • char(N) blank-pads on write: a three-character value stored in a char(10) column reads back with seven trailing spaces. char with no length is CHAR(1), matching Postgres, rather than the unbounded form varchar takes.
  • money renders through lc_monetary, so the same row reads back as $1.23 under the C locale and differently under another. The value written is a plain decimal string; the value read carries the locale’s symbol and separators.
  • json and xml have no equality operator in PostgreSQL, so neither can be a primary key, a filter, or an ORDER BY column. jsonb has both and is the type to declare for a new column; json exists so an existing column can be described exactly.
  • json is stored and returned as bytes and not parsed. Unlike jsonb it preserves key order and whitespace exactly as written.
  • name is PostgreSQL’s internal 63-byte identifier type. It appears in catalog-derived tables; text is the type for a new column.
  • tsquery normalises on write — a & b reads back as 'a' & 'b'.
  • bit and varbit travel as a string of 0 and 1 characters. Bare bit is BIT(1); bare varbit is unbounded.
  • tsvector normalises on write — a b reads back as 'a' 'b'.
  • varchar without a length accepts strings of any size. It exists so a legacy column declared that way can be described exactly; text is the better choice in a new schema. The two are not interchangeable to Atlantis — they are different Postgres types, so declaring text against a varchar column reports drift rather than agreement.

Time

.atl type PostgreSQL Proto Go
timestamptz TIMESTAMPTZ google.protobuf.Timestamp *timestamppb.Timestamp
date DATE google.protobuf.Timestamp *timestamppb.Timestamp
timestamp TIMESTAMP google.protobuf.Timestamp *timestamppb.Timestamp
time TIME string string
timetz TIMETZ string string
interval INTERVAL atlantis.common.v1.Interval *commonpb.Interval
  • timestamp is PostgreSQL’s timestamp without time zone. google.protobuf.Timestamp is anchored to UTC, so a value crossing the wire is read as UTC. Declare timestamptz for a new column; timestamp exists so an existing column can be described exactly.
  • time and timetz travel as strings, in PostgreSQL’s own text form: 03:04:05.000000 and 03:04:05+00. proto has no wall-clock-time well-known type to carry them.
  • date carries 00:00:00 UTC as the time-of-day. A caller sending a non-zero time-of-day has the time fraction truncated to midnight on insert.
  • interval round-trips exactly, including months and years. PostgreSQL stores months, days and microseconds as three separate components, and atlantis.common.v1.Interval carries the same three unchanged. No conversion happens at the wire boundary, because none is possible there: 1 month is 28 to 31 days depending on the month it is added to, and 1 day is 23, 24 or 25 hours across a daylight-saving boundary. A caller that wants a single duration has the date to compute it against; this layer does not.

Network

.atl type PostgreSQL Proto Go
inet INET string string
cidr CIDR string string
macaddr MACADDR string string
macaddr8 MACADDR8 string string
  • All four travel as PostgreSQL’s own text form.
  • inet normalises on write: 10.0.0.1 reads back as 10.0.0.1/32, because PostgreSQL stores the netmask alongside the address.

Vectors

.atl type PostgreSQL Proto Go
vector(N) vector(N) (pgvector) repeated float []float32
  • Requires the pgvector extension. The operator must CREATE EXTENSION vector once per database.
  • N is the dimension. pgvector caps vector at 16,000 dimensions; the lexer does not enforce this but Postgres rejects creation past the cap.
  • A wire payload whose length doesn’t match the declared N errors at request time.
  • halfvec, sparsevec, and pgvector’s bit type are not supported yet.

Enums

An enum is declared at the top level and used as a field type:

enum Mood in app { happy, sad, "in progress" }

entity Person in app {
  id   bigint primary
  mood Mood not null
}
.atl type PostgreSQL Proto Go
a declared enum <namespace>_<name> in the atlantis schema string string
  • The column carries the label, as a string. PostgreSQL enforces the set: a label the type does not list is refused at write time with invalid input value for enum.
  • A label is an identifier, or a quoted string when it is not — "in progress" is a legal PostgreSQL label and has no bare spelling.
  • Labels are ordered as declared, which is the order PostgreSQL sorts them in. Reordering an existing type is not something PostgreSQL can do, so a reordering in the .atl is not reported as a change.
  • Enum columns are filterable and orderable. Ordering follows label order, not alphabetical order.
  • Adding a label is additive. Removing one is refused. PostgreSQL has no ALTER TYPE ... DROP VALUE; removing a label needs a new type, a rewrite of every column using it, and a drop of the old one. tide plan says so rather than emitting DDL that cannot run.
  • A label added by a migration cannot be used by that same migration — PostgreSQL refuses it as unsafe use of new value until the transaction commits. Adding a label and defaulting a column to it in one change is refused with that reason. A type created and defaulted to in the same migration is fine; PostgreSQL exempts that case.
  • Arrays of enums are not supported, for the same reason as the text-carried types above.
  • The type lives in the atlantis schema whatever table override the entities using it carry, because it is atlantis’s to create and drop.

Ranges

.atl type PostgreSQL Proto Go
int4range INT4RANGE string string
int8range INT8RANGE string string
numrange NUMRANGE string string
tsrange TSRANGE string string
tstzrange TSTZRANGE string string
daterange DATERANGE string string
  • Each travels as PostgreSQL’s own text form, bounds included: [1,5) is inclusive of 1 and exclusive of 5.
  • PostgreSQL normalises on write. A tsrange written as [2024-01-01,2024-02-01) reads back as ["2024-01-01 00:00:00","2024-02-01 00:00:00").
  • Multirange types (int4multirange and the rest) are not supported yet.

Geometric

.atl type PostgreSQL Proto Go
point POINT string string
line LINE string string
lseg LSEG string string
box BOX string string
path PATH string string
polygon POLYGON string string
circle CIRCLE string string
  • Each travels as PostgreSQL’s own text form: (1,2) for a point, <(0,0),1> for a circle.
  • None is filterable or orderable. PostgreSQL sorts none of the seven and defines = on only five, so a filter, primary key or ORDER BY on one could not be executed.
  • box normalises its corners on write: ((0,0),(1,1)) reads back as (1,1),(0,0).
  • These are storage only. PostGIS geometry and geography are different types and are not supported.

Arrays

.atl type PostgreSQL Proto Go
[]T T[] repeated <T-proto> []<T-Go>
  • Only one-dimensional arrays. [][]T is rejected at parse time.
  • Element type T is any scalar above except vector and the types atlantis carries as text: inet, cidr, bit, varbit, tsvector, the range types and the geometric types. tide plan refuses an array of one of those. pgx decodes an array with its element’s codec, so inet[] would read back as the binary array body reinterpreted as characters — a wrong answer rather than an error. Declare text[] and cast in a custom query.
  • A null array (nil slice in Go, missing field on the wire) is distinguishable from an empty array ([]T{} in Go, present-but-empty on the wire); both round-trip.

Filtering

Every type above is filterable through Query<Entity> except json, xml, the seven geometric types, vector, interval, and arrays ([]T). A column of one of those types can still be selected and written; it just has no predicate field on the generated <Entity>Filter message.

They are absent for two different reasons. json, xml and the geometric types have no equality or ordering operators in PostgreSQL, so a filter on one could not be executed. vector, interval and arrays have the operators and no predicate message.

Ordering is separate: every scalar type is orderable except json, xml, the geometric types, vector and arrays.

Two notes on floats. Comparisons run at the column’s own width, so a real column is compared as float4 rather than being widened — which is what makes eq on a real column match the literal you wrote. And eq on a float is still float equality: a value that was computed rather than stored from the same literal may not compare equal at either width.

Nullability

Fields are non-nullable by default; declaring not null is redundant for primary (which implies it). A field declared without not null is nullable. The proto field gets the optional keyword and the Go field becomes a pointer:

.atl type Go (not null) Go (nullable)
bigint, int, smallint int64 / int32 *int64 / *int32
real, double float32 / float64 *float32 / *float64
boolean bool *bool
varchar(N), text, citext, uuid, numeric(p,s) string *string
char(N), name, money, xml, tsquery, time, timetz string *string
inet, cidr, macaddr, bit(N), varbit, tsvector string *string
the range types, the geometric types string *string
a declared enum string *string
timestamptz, date, timestamp *timestamppb.Timestamp *timestamppb.Timestamp (nil = null)
interval *commonpb.Interval *commonpb.Interval (nil = null)
jsonb, bytea, json []byte []byte (nil = null)
vector(N) []float32 []float32 (nil = null)
[]T []T-Go []T-Go (nil = null)

For bytes, Timestamp, Duration, vector, and arrays, the Go type is already nilable; the same value represents both null and (for bytes/vector/[]T) empty. Round-trip preserves the distinction over gRPC because proto3 transmits the field-presence bit separately from the length.

A nullable field with default <expr> is treated as nullable by the proto request (proto sees optional), but a NULL in the request leaves the column null — the default fires only when the request omits the field entirely.

Default-bearing fields

Fields declared with default <expr> become optional in the proto request even when not null:

entity Note in app {
  ...
  created_at timestamptz not null default now()
}

The proto request has optional Timestamp created_at. If the caller leaves it unset, Postgres applies now() on insert. If the caller sends a value, that value is used.

serial columns

A field declared bigint primary serial (or int primary serial) becomes optional in the proto request: the server lets Postgres assign the value from the sequence when the caller omits it. The on-the-wire type is still int64 / int32; only the field-presence semantics differ.

  • DSL grammar — the full type grammar and where types appear inside entity, query, and procedure declarations.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close