Skip to main content

CREATE TYPE

Turso Extension: CREATE TYPE is a Turso-specific statement not available in standard SQLite. Custom types require STRICT tables. This feature is experimental and must be enabled before use.
The CREATE TYPE statement defines a user-defined type that controls how values are encoded before storage, decoded when read, validated on input, compared for ordering, and what default value to use. Custom types extend the STRICT table type system beyond the five built-in SQLite storage classes.

Syntax

Description

A custom type wraps one of the four base storage types with user-defined logic. When a value is written to a column of a custom type, the ENCODE expression transforms it before storage. When a value is read, the DECODE expression transforms it back. This lets you store data in an efficient on-disk representation while presenting a different form to queries. Custom types work only with STRICT tables. Using a custom type name in a non-STRICT table has no effect.

Clauses

IF NOT EXISTS

Suppresses the error that would occur if a type with the same name already exists. The existing type is left unchanged.

BASE

Specifies the underlying SQLite storage class used to store values on disk. Every custom type must have a BASE clause.

ENCODE / DECODE

The ENCODE expression is evaluated whenever a value is written to a column of this type. The DECODE expression is evaluated whenever a value is read. Both expressions use the identifier value to refer to the input.
  • ENCODE: Transforms the input value into the base storage form. Runs on INSERT, UPDATE, and CAST.
  • DECODE: Transforms the stored value back into the presentation form. Runs on SELECT.
  • NULL handling: NULL values bypass both ENCODE and DECODE. A NULL input produces a NULL output without evaluating the expression.
The ENCODE expression is the place to put validation logic. If the ENCODE expression raises an error, the INSERT or UPDATE is aborted.

Parameters

Custom types can accept parameters that are available in the ENCODE and DECODE expressions. Parameters are declared as a parenthesized list after the type name. Each parameter has a name and an optional type annotation.
When a column uses a parametric type, the arguments are supplied in parentheses after the type name:
The first parameter is always the input value. Additional parameters are the arguments provided in the column type declaration, in order.

OPERATOR

The OPERATOR clause maps SQL operators to functions. This allows columns of a custom type to participate in comparisons, arithmetic, and ordering.
When an operator is defined, expressions like column + 1 are rewritten into function_name(column, 1).

Ordering with OPERATOR ’<’

The < operator is special: it controls how values of this type are sorted in ORDER BY, MIN, MAX, and CREATE INDEX.
  • OPERATOR '<' (no function name): Uses the base type’s native comparison for ordering. This is sufficient for types where the encoded form sorts correctly (e.g., text dates in ISO 8601 format).
  • OPERATOR '<' my_compare: Uses my_compare(a, b) as a custom comparator. The function must return a negative integer if a < b, zero if a = b, and a positive integer if a > b.
  • No < operator: The type cannot be used in ORDER BY or CREATE INDEX.
The = operator, when defined, is also used to derive !=. The < operator is used to derive >, >=, and <= automatically by swapping arguments or negating the result.

DEFAULT

The DEFAULT clause sets a type-level default value. When a column of this type does not specify its own DEFAULT, this value is used.
A column-level DEFAULT overrides the type-level DEFAULT:

Validation with RAISE

Use CASE/WHEN with RAISE in the ENCODE expression to validate input at write time. If the condition fails, the statement is aborted with the specified error message.

CAST Support

The CAST expression applies a custom type’s ENCODE logic:
This is useful for converting values to the encoded form outside of INSERT/UPDATE, such as in WHERE clauses and CHECK constraints.

CHECK Constraints with Custom Types

In STRICT tables with custom types, CHECK constraints operate on the decoded (presentation) values. When comparing a column of a custom type against a literal, use CAST to encode the literal:

Inspecting Types

PRAGMA list_types

Lists all available types, including built-in and user-defined:

sqlite_turso_types Virtual Table

The sqlite_turso_types virtual table provides the name and SQL definition of each type:

Using with ALTER TABLE

Custom types can be used when adding columns with ALTER TABLE ADD COLUMN:
The added column follows the same STRICT type-checking rules as columns defined in the original CREATE TABLE.

Built-in Types

Turso provides the following built-in custom types in STRICT tables:
The uuid, json, and jsonb types require their respective extensions to be compiled in. They are available by default in standard Turso builds.

Composite Types: STRUCT and UNION

In addition to encode/decode custom types, CREATE TYPE supports two composite type forms: STRUCT (named product type) and UNION (discriminated union / tagged variant). Both store data as blobs on disk and require STRICT tables.

STRUCT

A STRUCT groups multiple named fields into a single column. Use dot notation to read and filter on individual fields:
Dot notation works everywhere a column expression does — SELECT, WHERE, ORDER BY, GROUP BY, HAVING, and aggregate functions:
When a table name and column name collide, table references always win. Use an alias to reach the struct field:

struct_pack()

Creates a struct value for INSERT and UPDATE. Arguments are positional, matching the field order in the type definition.

struct_extract()

Function form of dot notation — struct_extract(col, 'field') is equivalent to col.field. Primarily useful in expression indexes, where dot notation cannot be used:

UNION

A UNION is a discriminated union (tagged variant). Each value carries exactly one of the declared variants, identified by a tag. Use dot notation to extract a variant’s value — it returns NULL when the active variant doesn’t match:
For unions whose variants are struct types, chain dot access to reach nested fields:

union_value()

Creates a union value for INSERT and UPDATE. The first argument is the variant tag (a string literal), the second is the value. The tag is resolved against the target column’s union type.

union_tag()

Returns the tag name of the active variant as text.

union_extract()

Function form of dot notation — union_extract(col, 'variant') is equivalent to col.variant. Primarily useful in expression indexes:

NULL handling

NULL values propagate through all struct and union operations:

Dot notation precedence

When a dot expression a.b is ambiguous (e.g., a could be a table name or a column name), table references always take priority over struct field access. This follows DuckDB’s resolution rules.
For a.b.c, the resolution order is: database.table.column, then table.column.field.

Inline types not supported

Inline STRUCT/UNION declarations in column definitions are not supported. Always use CREATE TYPE first:

Array Types

Array columns store ordered collections of values. Unlike STRUCT and UNION, arrays don’t require an explicit CREATE TYPE — declare them by appending [] to any base type name in a STRICT table column definition:
Multi-dimensional arrays use multiple bracket pairs:
Element types are validated on insert — an INTEGER[] column rejects values that cannot be stored as integers. Arrays are stored internally as compact record-format BLOBs and displayed as JSON arrays on output. For the full set of array functions (array_agg, array_append, array_contains, array_slice, etc.), operators (@>, &&, ||), and subscript syntax, see Array Functions.

Restrictions

  • STRICT tables only: Custom type names are ignored in non-STRICT tables. The column uses standard type affinity rules instead.
  • Cannot drop while in use: A type cannot be dropped with DROP TYPE while any table has a column of that type.
If a non-STRICT table was created with a type name that did not exist at the time (e.g., CREATE TABLE t(x mytype)), and a custom type with that name is created afterwards (CREATE TYPE mytype ...), the type’s ENCODE/DECODE logic will not be retroactively applied to the existing table. This is not a bug: non-STRICT tables treat all type names as plain affinity hints regardless of whether a matching custom type exists. STRICT tables prevent this scenario because they reject unknown type names at CREATE TABLE time.
  • No subqueries in expressions: The ENCODE, DECODE, and DEFAULT expressions cannot contain subqueries, aggregate functions, or window functions.
  • No indexes on STRUCT/UNION columns: CREATE INDEX on a STRUCT or UNION column is not supported.

Examples

Identity Type (Passthrough)

A minimal type that stores and retrieves values without transformation:

Monetary Values as Cents

Store dollar amounts as integer cents for exact arithmetic, present them as the original value:

JSON Validation

Validate that inserted values are valid JSON:

Unsigned Integer with Operators

A non-negative integer type with arithmetic and comparison operators:

Fixed-Point Decimal

Use the built-in numeric type for precise decimal arithmetic:

See Also

  • DROP TYPE for removing custom types
  • CREATE DOMAIN for defining constrained type aliases without ENCODE/DECODE logic
  • Data Types for an overview of the type system and type affinity
  • Array Types for native array columns (INTEGER[], TEXT[], etc.)
  • Array Functions for array construction, manipulation, and operators
  • CREATE TABLE for STRICT table definitions