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:
citextrequires thecitextPostgres extension. The migration emitted bytide applydoes not install it; the operator mustCREATE EXTENSION citextonce per database.numericis mapped tostringon the wire to preserve exact precision. The wire format is the canonical PostgreSQL numeric string (e.g."123.4500"). Unparameterizednumeric(no(p, s)) is not supported.jsonbpayloads 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.uuiduses the canonical RFC 4122 hyphenated lowercase form on the wire. Non-canonical input is rejected by Postgres at parse time.bigintover the wire: protoint64. If you serialize a response to JSON yourself, proto-JSON encodesint64as a string by default to preserve precision.varchar(N)length is enforced by Postgres (value too long for typeonINSERT); Atlantis does not pre-validate.char(N)blank-pads on write: a three-character value stored in achar(10)column reads back with seven trailing spaces.charwith no length isCHAR(1), matching Postgres, rather than the unbounded formvarchartakes.moneyrenders throughlc_monetary, so the same row reads back as$1.23under theClocale and differently under another. The value written is a plain decimal string; the value read carries the locale’s symbol and separators.jsonandxmlhave no equality operator in PostgreSQL, so neither can be a primary key, a filter, or anORDER BYcolumn.jsonbhas both and is the type to declare for a new column;jsonexists so an existing column can be described exactly.jsonis stored and returned as bytes and not parsed. Unlikejsonbit preserves key order and whitespace exactly as written.nameis PostgreSQL’s internal 63-byte identifier type. It appears in catalog-derived tables;textis the type for a new column.tsquerynormalises on write —a & breads back as'a' & 'b'.bitandvarbittravel as a string of0and1characters. BarebitisBIT(1); barevarbitis unbounded.tsvectornormalises on write —a breads back as'a' 'b'.varcharwithout a length accepts strings of any size. It exists so a legacy column declared that way can be described exactly;textis the better choice in a new schema. The two are not interchangeable to Atlantis — they are different Postgres types, so declaringtextagainst avarcharcolumn 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 |
timestampis PostgreSQL’stimestamp without time zone.google.protobuf.Timestampis anchored to UTC, so a value crossing the wire is read as UTC. Declaretimestamptzfor a new column;timestampexists so an existing column can be described exactly.timeandtimetztravel as strings, in PostgreSQL’s own text form:03:04:05.000000and03:04:05+00. proto has no wall-clock-time well-known type to carry them.datecarries00:00:00 UTCas the time-of-day. A caller sending a non-zero time-of-day has the time fraction truncated to midnight on insert.intervalround-trips exactly, including months and years. PostgreSQL stores months, days and microseconds as three separate components, andatlantis.common.v1.Intervalcarries the same three unchanged. No conversion happens at the wire boundary, because none is possible there:1 monthis 28 to 31 days depending on the month it is added to, and1 dayis 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.
inetnormalises on write:10.0.0.1reads back as10.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
pgvectorextension. The operator mustCREATE EXTENSION vectoronce per database. Nis the dimension. pgvector capsvectorat 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
Nerrors at request time. halfvec,sparsevec, and pgvector’sbittype 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
.atlis 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 plansays 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 valueuntil 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
atlantisschema whatevertableoverride 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
tsrangewritten as[2024-01-01,2024-02-01)reads back as["2024-01-01 00:00:00","2024-02-01 00:00:00"). - Multirange types (
int4multirangeand 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 orORDER BYon one could not be executed. boxnormalises its corners on write:((0,0),(1,1))reads back as(1,1),(0,0).- These are storage only. PostGIS
geometryandgeographyare different types and are not supported.
Arrays
.atl type |
PostgreSQL | Proto | Go |
|---|---|---|---|
[]T |
T[] |
repeated <T-proto> |
[]<T-Go> |
- Only one-dimensional arrays.
[][]Tis rejected at parse time. - Element type
Tis any scalar above exceptvectorand the types atlantis carries as text:inet,cidr,bit,varbit,tsvector, the range types and the geometric types.tide planrefuses an array of one of those. pgx decodes an array with its element’s codec, soinet[]would read back as the binary array body reinterpreted as characters — a wrong answer rather than an error. Declaretext[]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.
Related
- DSL grammar — the full type grammar and where types appear inside entity, query, and procedure declarations.