Supported Column Types
The following PostgreSQL column types are supported:
bigint.integer.smallint.real.double precision.boolean.timestamp with time zone.timestamp without time zone.date.time without time zone.uuid.text.varchar(N).char(N).bytea.numeric(P,S)(P ≤ 38).jsonb/json.interval.vector(N)/halfvec(N)(pgvector; see Working with Embeddings).- one-dimensional arrays of most of these types (see Supported Array Types).
Other data types (unbounded numeric, xml, tsvector, range/multirange types,
custom enums, composite types, pgvector's sparsevec, inet, cidr, oid,
and the arrays that Supported Array Types lists
as refused at registration) are
rejected when a tiered table is registered, and when a decoupled table is
created. ColdFront refuses silent fallback to varchar - losing
precision/identity is worse than no support. inet/cidr/oid are
rejected because pg_duckdb cannot process them in any Iceberg-backed query,
so no cast makes them readable; store IP data as text and oid values as
bigint instead (you can still index/compare them; cast on the hot side
only if needed).
A few supported types need a closer look:
- Iceberg cannot hold a
numericNaN, or aninfinityor-infinityvalue in adate,timestamp with time zoneortimestamp without time zonecolumn, as array elements too. ColdFront refuses those values with a check constraint namedcoldfront_guard_<column>; a write that goes to the cold tier of a tiered or decoupled table is refused by DuckDB, unless the write reads anumericNaNfrom a PostgreSQL table and stores it as0. char(N)is stored and read asvarchar. Values, comparisons, andlength()round-trip losslessly, but a cold read comes back unpadded withpg_typeof varchar, because pg_duckdb has no fixed-lengthchartype. Usetextorvarcharif blank-padded display matters.jsonandjsonbare stored asvarcharin Iceberg (no native primitive); reads come back as DuckDB'sjson, not the rich PGjsonbtype, because Iceberg-backed reads run entirely in DuckDB.intervalis stored asvarcharin Iceberg, as<months> months <days> days <seconds> seconds, the text PostgreSQL and DuckDB both read as the same value; it is view-cast back to the rich PG type on read.
The archiver adds the NaN/infinity constraints at the first archive pass
that tiers a partition of the table, and validates them once that pass's
bootstrap transaction has committed. A value written before then is accepted,
and each archive pass stops with an error that names the constraint until the
row is fixed; the next pass then validates the constraint and tiers the
table. A column that an ALTER TABLE adds through the DDL hook gets its
constraint in the same statement, which PostgreSQL validates against every
row of the hot table, so an ADD COLUMN whose default the constraint
refuses, such as DEFAULT 'NaN', fails. A renamed column's constraint takes
the new column name, and a column whose name is longer than 47 bytes gets a
constraint named after a hash of the name.
A cold UPDATE ... FROM or DELETE ... USING, a MERGE whose source is a
PostgreSQL table, and an INSERT ... SELECT into a decoupled table read that
PostgreSQL table through DuckDB's postgres extension, which reads a numeric
NaN as 0, also inside an array, and drops an array's lower bound. ColdFront
cannot refuse those values on that path: the write stores the changed values
with no error, and its conditions compare the changed values. A condition that
names 'NaN' fails in such a write, because DuckDB does not accept 'NaN' as
a numeric value. To keep those values out of the cold tier, check the source
table in PostgreSQL before the write, and fix or remove the rows that hold
them.
The following query counts those rows in a source table staging with a
numeric column amount, a numeric array amounts and an array tags:
SELECT count(*)
FROM staging
WHERE amount = 'NaN'
OR 'NaN' = ANY (amounts)
OR array_lower(tags, 1) <> 1;
List every numeric column, numeric array and array column of your source
table the same way; a count of 0 means that the write stores those columns
unchanged.
Queries like data->>'key' and data->'key' work against jsonb, and
ColdFront translates the ::jsonb cast, jsonb_array_length,
jsonb_build_object / jsonb_agg (and their json_ twins, which DuckDB
also lacks) and date_bin on read: the builders become the concat /
to_json / array_agg form both engines evaluate identically (the result
is JSON-equal to jsonb's rendering, in compact form and in argument order),
and date_bin becomes DuckDB's time_bucket, which takes the same
arguments and agrees on every fixed-width bucket.
ColdFront also rewrites the JSON operators that DuckDB lacks into DuckDB's JSON functions, which return PostgreSQL's answers. The rewrite covers:
- the
#>,#>>,?,?|,?&,@>and<@operators. - the
json_extract_pathandjsonb_extract_pathfunctions and their_textforms. - the
jsonb_exists,jsonb_exists_anyandjsonb_exists_allfunctions. - the
jsonb_containsandjsonb_containedfunctions.
A path or key for these operators and functions must be a constant or a bound
parameter, not a column. A path can hold at most eight negative, signed or
zero-padded index elements, such as -1, +1 or 01. PostgreSQL has no
json form of ?, ?|, ?&, @> and <@, only a jsonb one. Cast the
view's json column to jsonb first, as in data::jsonb ? 'k'. Two kinds of
result differ from PostgreSQL's. DuckDB writes a returned container or number
in its own form, such as {"b":"c"}, or 1.5 for 1.50. The ->> operator
returns values in the same form. The @> and <@ operators never match an
integer against the same value written as a decimal. For example, a stored
{"p": 10.0} does not contain {"p": 10}, because DuckDB compares JSON
numbers by kind. ColdFront does not support most other jsonb functions on
tiered or decoupled data, such as jsonb_typeof, jsonb_set, jsonb_path_*,
jsonb_each and jsonb_object_keys. DuckDB either lacks these functions, or
its function of the same name differs in signature or result.
These limits apply only to reads that go through a tiered or iceberg-only view, which pg_duckdb plans entirely in DuckDB. Ordinary (non-tiered) PostgreSQL tables are untouched by ColdFront and keep full jsonb support, as does the hot partition table itself.
Bound parameters ($1 from a prepared statement, a driver's extended protocol,
or a plpgsql variable) work in such reads. DuckDB types most placeholders from
their context (ts > $1); one that is a direct argument of a DuckDB function
with several overloads (time_bucket's origin, so date_bin's) or any
argument of a table function (generate_series) it cannot, so ColdFront plans
such a read from the bound values on every execution instead of caching a
generic plan. The rewrite of #>, #>>, ?, ?|, ?&, @> and <@ also
needs the value of any parameter in an operand. ColdFront plans such a read
from the bound values on every execution as well. Under
plan_cache_mode = force_generic_plan those reads cannot run: the forced
generic plan has no values and fails with only works with DuckDB execution.
A hot-only read is the exception. When a SELECT reads a tiered view directly
(no join, CTE, or sub-query) and its WHERE provably restricts to the hot
tier, ColdFront rewrites it to read the hot partition table in plain
PostgreSQL: the full jsonb operator and function set works, and the query skips
DuckDB entirely. Such a read returns data as native jsonb rather than the
view's json. Reads that span tiers, are cold-only, or reach the view through
a join or sub-query stay in DuckDB, where the limits above apply.
Supported Array Types
A one-dimensional array of a supported type tiers as an Iceberg list of that type. The following table shows how each array type is stored and how the view reads it back:
| PG type | Iceberg/Parquet storage | Reads back as |
|---|---|---|
bigint[], integer[], real[], double precision[], boolean[], date[], time[], timestamp[], uuid[], bytea[], numeric(P,S)[] |
A list of the element's storage type. | The same type. |
smallint[] |
A list of INTEGER. |
integer[], as a smallint column reads as integer. |
text[] |
A list of VARCHAR. |
text[]. |
varchar(N)[], char(N)[] |
A list of VARCHAR. |
character varying[], with char(N) elements unpadded. |
The following arrays are refused when a tiered table is registered and when a decoupled table is created, with an error that names the column:
timestamptz[], because pg_duckdb cannot read atimestamptz[]column of a PostgreSQL table, which a tiered table's hot tier is; storetimestamp[]in UTC instead.jsonb[],json[]andinterval[], because those types are stored as text and read back through a cast that an array element does not get.- arrays of
vectororhalfvec, because a vector is itself stored as a list, and ColdFront stores an array as a list of scalar values.
An Iceberg list has one dimension and numbers its elements from 1, so ColdFront
refuses an array with more than one dimension or with a lower bound other than
1, such as '[0:2]={1,2,3}'. The hot table's check constraint refuses such an
array when it is written. A write to the cold tier refuses it too: a tiered
INSERT with an error that names the column, and a bound parameter with an
error that names no column. A write that reads the array from a PostgreSQL
table stores it renumbered from 1 instead. A tiered table with a column
declared with more than one dimension, such as integer[][], passes registration and is refused
at its first archive pass, because pg_duckdb reads a column by its declared
dimensions; a decoupled table refuses such a column when it is created. A
decoupled table's array type is spelled as its element type followed by [],
such as integer[]; the spellings integer[3] and integer ARRAY are
refused. ALTER TABLE ... ADD COLUMN cannot add an array column to a tiered
table, because duckdb-iceberg does not add a nested column to an Iceberg table.
A tiered INSERT accepts an array in any spelling, because its source runs in
PostgreSQL. A cold UPDATE and a decoupled INSERT run in DuckDB, which does
not read PostgreSQL's brace literal: a '{a,b}' literal reaches DuckDB as text
and fails to cast. Write the array as ARRAY['a', 'b'], or pass it as a bound
parameter.
Reads through a view run in DuckDB once the table has a cold tier, and the following array operations work there:
x = ANY(arr),arr @> ARRAY[...]andarr && ARRAY[...].- subscripts and slices, which count from 1, such as
arr[1]andarr[1:2]. array_length(arr, 1)andunnest(arr).
A brace literal and a bound array parameter fail in such a read, as they do for
a vector, and so does cardinality(), which DuckDB defines for maps only.