Skip to content
Talk to our solutions team

Types

Every field carries one logical type from a closed set of 19. The logical type drives runtime coercion, default generation and validation; the backend dialect turns it into a concrete column type at CREATE TABLE time. A schema-declared custom type overrides that mapping.

The canonical token is what type: resolves to internally. The four backend columns are what the dialect emits for a bare declaration of that token, with no custom type in play. Introspecting a live column reports a wider, dialect-oriented vocabulary (integer, float, json, smallint, ip_address) rather than these canonical tokens, so the mapping does not round-trip by name.

TypeHoldsPostgreSQLClickHouseDuckDBSQLite
stringBounded textVARCHAR(255)StringVARCHAR(255)TEXT
textUnbounded textTEXTStringTEXTTEXT
int32-bit signed integerINTEGERInt32INTEGERINTEGER
bigint64-bit signed integerBIGINTInt64BIGINTINTEGER
doubleDouble-precision floatDOUBLE PRECISIONFloat64DOUBLEREAL
decimalExact fixed-point numberDECIMAL(20,6)Decimal64(6)DECIMAL(20,6)TEXT
moneyMonetary amountDECIMAL(20,6)Decimal64(6)DECIMAL(20,6)TEXT
booleanTrue / falseBOOLEANBoolBOOLEANINTEGER
timestampInstantTIMESTAMPTZDateTime64(3)TIMESTAMPDATETIME
dateCalendar dateDATEDateDATEDATE
timeTime of dayTIMEStringTIMETEXT
uuidUUIDUUIDUUIDUUIDTEXT
ulid26-char Crockford ULIDCHAR(26)FixedString(26)VARCHAR(26)TEXT
jsonbJSON documentJSONBStringJSONTEXT
enumOne value from a named enumTEXTStringVARCHAR(255)TEXT
stateEnum bound to a stateflowTEXTStringVARCHAR(255)TEXT
fileReference into the content storeTEXTStringTEXTTEXT
arrayList of valuesJSONBStringJSONTEXT
bytesBinary blobBYTEAStringBLOBBLOB

Notes on individual rows:

  • decimal and money are always (20,6). There is no field-level precision: or scale:. The only way to change the precision of a numeric column is a custom type — see below.
  • money narrows to decimal in the loader, so its ClickHouse column is Decimal64(6), not the wider Decimal128(6) the ClickHouse dialect reserves for the money branch.
  • text narrows to string at the logical layer but keeps its spelling, which is what makes PostgreSQL and DuckDB emit an unbounded TEXT instead of VARCHAR(255).
  • No backend gets a database enum type. The PostgreSQL dialect can generate CREATE TYPE ... AS ENUM, but nothing in the shipping service calls it — not the migration planner, not the direct create path — so the column is a plain string on every backend and the database constrains nothing. To reject bad values, declare an explicit validations: [{type: enum, values: [...]}] on the field, or create the type yourself in a datastore init script and bind it through a kind: database custom type.
  • SQLite type names are affinity hints. DATE and DATETIME are stored as ISO-8601 text; decimal is stored as text to avoid float rounding.

type: is lower-cased and trimmed before resolution. These spellings resolve into the set above.

You writeResolves toColumn effect
varchar, charstringSame as string
integer, int32, int16, int8intSame as intint8 means 8-bit here, not PostgreSQL’s 64-bit int8
long, int64bigintSame as bigint
float, float32, float64, realdoubleSame as double
numericdecimalSame as decimal
boolbooleanSame as boolean
timestampz, datetimetimestampSame as timestamp
object, jsonjsonbSame as jsonb
bytea, binarybytesSame as bytes
klidulidSpelling preserved: CHAR(27) on PostgreSQL (one-letter prefix + ULID); the other backends fall back to the ulid column
uint8, uint16, uint32intSpelling preserved: UInt8 / UInt16 / UInt32 on ClickHouse; INTEGER elsewhere
uint64bigintSpelling preserved: UInt64 on ClickHouse; BIGINT on PostgreSQL and DuckDB; INTEGER on SQLite
<type>[]string, spelling preservedPostgreSQL and DuckDB <mapped>[], ClickHouse Array(<mapped>), SQLite TEXT. The suffix is a dialect-side rule only: the loader does not recognise it, so the field’s logical type is string, not array
array(<type>)arraySame as bare array. The element type is parsed and then discarded — nothing downstream reads it, and no per-element column type or validation follows from it
vector(<n>)customPostgreSQL vector(<n>) (needs the pgvector extension), DuckDB FLOAT[<n>], ClickHouse String, SQLite TEXT

An empty type: resolves to string.

customtypes: is a top-level block. Each entry names a reusable type that fields reference by name from type:.

KeyMeaningDefault
nameType name; what a field writes in type:
descriptionHuman-readable note carried to schema exports
kindcomposite, application or database; anything else silently becomes compositecomposite
basetypeSQL type the value renders as; also the last fallback for database kind
dbtypeConcrete SQL type, used when no per-backend entry matches
databaseA single SQL type string, or a per-backend map
nativetypeSuperseded by basetype; carried, not read
encode / decodeEncode and decode expressions
fieldsField list for a composite type
precision, scaleNumeric precision and scale0
lengthString length0
validations, transforms, compliancesType-level rule lists
kindPostgreSQLClickHouseSQLiteOther backends
databaseThe database: entry for the active backend, else dbtype:, else basetype:same rulesame rulesame rule
compositeJSONBStringTEXTJSON
applicationbasetype: verbatimbasetype: verbatimbasetype: verbatimbasetype: verbatim

Keys accepted in the per-backend database: map:

BackendAccepted keys
PostgreSQLpostgresql, postgres, pg
ClickHouseclickhouse, ch
DuckDBduckdb
SQLitesqlite, sqlite3

After the kind resolves a SQL type, a size suffix is appended — but only when the resolved type carries no parenthesised group already, and only for these shapes:

ConditionSuffix
precision > 0 and scale >= 0 and the resolved type is DECIMAL or NUMERIC(precision,scale)
length > 0 and the resolved type is VARCHAR, CHAR or STRING(length)

This is the only route by which an author-chosen width reaches the DDL. Every other width in the tables above — VARCHAR(255), DECIMAL(20,6), CHAR(26) — is hard-coded in the dialect.

A numeric type with its own precision, a bounded string, and two backend-specific types:

customtypes:
- name: amount
kind: application
basetype: decimal
precision: 20
scale: 6
description: "Monetary amount with 6 decimal places"
- name: currency_code
kind: application
basetype: varchar
length: 3
- name: pg_tsvector
kind: database
basetype: string
database:
postgresql: tsvector
- name: ch_low_cardinality_string
kind: database
basetype: string
database:
clickhouse: "LowCardinality(String)"

Referencing them from a field:

entities:
- name: invoice
fields:
- name: total
type: amount
- name: currency
type: currency_code

total becomes decimal(20,6) and currency becomes varchar(3). The emitted spelling is the basetype: you wrote plus the size suffix — the registry does not canonicalise it.

Every column type — in CREATE TABLE and in the compiled DML — comes out of one function, given the spelling you wrote in type: and the field itself. It resolves in this order:

  1. Extension overlay for the datastore’s declared extensions, matched on the spelling you wrote, then on the canonical logical type.
  2. customtypes:, matched the same way.
  3. Array suffix<type>[] strips the suffix, maps the element, and re-applies the backend’s array form.
  4. vector(<n>) parametric form.
  5. The backend’s static switch over the canonical tokens.
  6. Fallback: retry with the field’s narrowed logical type, then TEXT (String on ClickHouse) if that also misses.

Extensions win over customtypes: deliberately: an extension is an explicit per-datastore declaration, so its type names win a name collision.

Available once the datastore declares the extension.

BackendExtensionTypes added
PostgreSQLpgvectorvector, halfvec, sparsevec
PostgreSQLpostgisgeometry, geography, box2d, box3d
PostgreSQLcitextcitext
PostgreSQLpg_trgmnone (operators and functions only)
PostgreSQLuuid-osspnone (functions only)
DuckDBspatialgeometry, geography
DuckDBicunone (collations and time zones only)

A type: value that matches none of the spellings above is not rejected at load. It is kept on the field as a custom-type reference and the field’s logical type is narrowed to string.

This is deliberate. Schemas are accumulated from many files, and a field may reference a custom type declared in a file loaded later, so resolution is deferred to the compile pass rather than enforced at parse time.

There is a second consequence on the production path: a field whose type: names a custom type keeps string as its logical type regardless of the type’s basetype:. The column is correct — the registry resolves it — but defaults, coercion and validation treat the value as a string. A field typed amount gets a decimal(20,6) column and string-shaped runtime handling.

  • Fields — the rest of the field surface: modifiers, defaults, validations, compliance.
  • Enums — declaring the named enums that enum: references.
  • Embeddables — reusable field groups stored inline as a JSON column.
  • Datastores — backend selection and extension declaration.