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
timestampInstant, UTCTIMESTAMPTZDateTime64(3, 'UTC')TIMESTAMPDATETIME
dateCalendar dateDATEDateDATEDATE
timeTime of dayTIMEStringTIMETEXT
uuidUUIDUUIDUUIDUUIDTEXT
ulid26-char Crockford ULIDCHAR(26)FixedString(26)VARCHAR(26)TEXT
jsonbJSON documentJSONBStringJSONTEXT
enumA key of a named enum, whose table the column referencesTEXTStringVARCHAR(255)TEXT
stateEnum bound to a stateflowTEXTStringVARCHAR(255)TEXT
fileReference into the content storeTEXTStringTEXTTEXT
arrayList of valuesJSONBStringJSONTEXT
bytesBinary blobBYTEAStringBLOBBLOB
vectorEmbedding: a fixed-dimension list of numbers with a distance metric. See Search and vectorsvector(N), halfvec(N), bit(N) or sparsevec(N) by precision, via pgvectornot supportednot supportednot supported

A timestamp is a UTC instant, everywhere. It is stored as one (TIMESTAMPTZ, a DateTime64 pinned to 'UTC', a UTC wall clock on DuckDB and SQLite), the audit columns are written as one, and it is rendered as one: an API response carries 2026-09-20T10:44:06.493805Z, never the service host’s offset. A value sent with an offset is accepted and converted; the offset is not kept.

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. An enum is a table of its own (enum_<name>, with a key and a value column) and the field’s column is a plain string holding the key, on every backend alike. Membership is enforced by the reference from the field to that table, not by a database type or a CHECK; see Enums.
  • 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 int: int8 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, compliancesRules every field of the type carries, ahead of the field’s own—
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.

The type declares the floor and the field adds to it: validations:, transforms: and compliances: on the type run on every field declared with it, before the field’s own lists. A currency_code type carrying regex ^[A-Z]{3}$ and uppercase therefore rejects us1 on every field that uses it. There is one front-end for this block; the bundled repository examples are written in it, and a types: key is refused as unknown.

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.