Shape / columns · All operations
Detect column types from the data
A variant of Change a column's type
Scan the values of each column and set it to the type its data fits.
Status
- In arcform
Runs as a SQL step
arcform carries it as a
sql:step today.
The DuckDB step
Set each column to the type its values fit, and recast any table to its detected types
CREATE TABLE raw AS SELECT * FROM (VALUES
('1', '2026-01-03', '1.5', 'x', '4.5', 'o-1'),
('22', '2026-02-10', '2', '7', '10', 'o-2'),
(NULL, NULL, '3.25', NULL, NULL, NULL)
) t(whole, day, fraction, word, "unit price", "order");
-- a second table with no date column, where a type that is absent must not stop the recast, and a column with no value at all,
-- which stays VARCHAR
CREATE TABLE raw2 AS SELECT * FROM (VALUES ('1', '1.5', 'a', NULL::VARCHAR), ('2', '2', 'b', NULL)) t(n, price, label, note);
-- a table with no rows has no value to detect a type from, so it recasts to itself, its columns as they are
CREATE TABLE raw3 (code VARCHAR, qty VARCHAR);
-- a table of the same name in another schema, sharing a column name, must not add a column to the detection
CREATE SCHEMA other;
CREATE TABLE other.raw2 (label VARCHAR);
-- DuckDB 2.0.0 has no quote-identifier function, so a name is quoted by doubling its quotes
CREATE TEMP MACRO quoted(name) AS '"' || replace(name, '"', '""') || '"';
-- one table macro detects any table by name: every column is unpivoted as text, nulls kept so a column with no value stays
-- (as VARCHAR), and each is matched to its position among the table's columns in the current database and schema only
-- a value fits BIGINT only if casting it and printing it again gives the same text (TRY_CAST('1.5' AS BIGINT) is 2)
CREATE TEMP MACRO detect_types(tbl) AS TABLE
SELECT c.column_index, u.col,
CASE WHEN count(u.v) = 0 THEN 'VARCHAR'
WHEN count(*) FILTER (WHERE TRY_CAST(u.v AS BIGINT)::VARCHAR = u.v) = count(u.v) THEN 'BIGINT'
WHEN count(TRY_CAST(u.v AS DATE)) = count(u.v) THEN 'DATE'
WHEN count(TRY_CAST(u.v AS DOUBLE)) = count(u.v) THEN 'DOUBLE'
ELSE 'VARCHAR' END AS detected_type
FROM (SELECT * FROM (SELECT COLUMNS(*)::VARCHAR FROM query_table(tbl))
UNPIVOT INCLUDE NULLS (v FOR col IN (COLUMNS(*)))) u
JOIN duckdb_columns() c ON c.table_name = tbl AND c.column_name = u.col
AND c.database_name = current_database() AND c.schema_name = current_schema()
GROUP BY c.column_index, u.col;
-- the select list is written from the detection in the table's own column order, with every name quoted; a table with no
-- rows detects nothing, and its statement reads it as it is rather than being NULL
CREATE TEMP MACRO recast_sql(tbl) AS (
SELECT coalesce('SELECT ' || string_agg('TRY_CAST(' || quoted(col) || ' AS ' || detected_type || ') AS ' || quoted(col),
', ' ORDER BY column_index), 'SELECT *') || ' FROM ' || quoted(tbl)
FROM detect_types(tbl));
-- query() takes only a constant, so the statements are kept in a variable for it to read
SET VARIABLE recast = MAP {'raw': recast_sql('raw'), 'raw2': recast_sql('raw2'), 'raw3': recast_sql('raw3')};
CREATE TEMP TABLE result AS SELECT * FROM query(getvariable('recast')['raw']);
CREATE TEMP TABLE result2 AS SELECT * FROM query(getvariable('recast')['raw2']);
CREATE TEMP TABLE result3 AS SELECT * FROM query(getvariable('recast')['raw3']);
SELECT * FROM detect_types('raw') ORDER BY column_index;
DESCRIBE result;
DESCRIBE result2;
DESCRIBE result3;
SELECT CASE WHEN ((SELECT list((column_name, column_type)) FROM (DESCRIBE result)) = [('whole', 'BIGINT'), ('day', 'DATE'), ('fraction', 'DOUBLE'), ('word', 'VARCHAR'), ('unit price', 'DOUBLE'), ('order', 'VARCHAR')] AND (SELECT sum(whole) = 23 AND max(day) = DATE '2026-02-10' AND sum(fraction) = 6.75 AND count(word) = 2 AND sum("unit price") = 14.5 AND string_agg("order", ',') = 'o-1,o-2' AND count(*) = 3 FROM result) AND (SELECT list((column_name, column_type)) FROM (DESCRIBE result2)) = [('n', 'BIGINT'), ('price', 'DOUBLE'), ('label', 'VARCHAR'), ('note', 'VARCHAR')] AND (SELECT sum(n) = 3 AND sum(price) = 3.5 FROM result2) AND (SELECT list((column_name, column_type)) FROM (DESCRIBE result3)) = [('code', 'VARCHAR'), ('qty', 'VARCHAR')] AND (SELECT count(*) = 0 FROM result3)) THEN 'as expected' ELSE error('the tables were not recast to the detected types in their own column order with their values intact, or a table without a date column, a column with no value or a table with no rows was not recast') END AS verdict;- The statement builds its own data.
- It needs nothing installed beyond DuckDB itself.
- The statement checks its own result and raises an error when it is wrong: “the tables were not recast to the detected types in their own column order with their values intact, or a table without a date column, a column with no value or a table with no rows was not recast”.
- It passed on DuckDB
v2.0.0 (Cyanoptera) d54cbbe3df.
What other tools call it
Meridian's classification; an independent blind rating agreed on 95.7% of 324 rows (variant, version 3).
Alteryx Designer (1)
Auto Fieldread 2026-09-27Scan every record and set each field to the smallest type and size that fits its data
KNIME Analytics Platform (1)
Column Auto Type Castread 2026-09-27Convert string columns to numeric or date type when every value parses
RapidMiner Studio (1)
Guess Typesread 2026-09-27Re-detect the value type of each column from its contents
Trifacta (2)
Lock typeread 2026-09-27Lock a column to its current data type so type inference stops changing it
Unlock typeread 2026-09-27Unlock a column's data type so it is inferred again
Background
DuckDB's CSV reader detects each column's type from a sample of the file, by default 20,480 rows.[1] In DuckDB, a column that fits no other candidate type falls back to VARCHAR, since everything can be cast to VARCHAR.[1]
When to use it
RapidMiner's Guess Types operator re-guesses value types after preprocessing, as when a prefix such as id_ has been cut from values that are otherwise numbers.[2]
With its quickscan option on, KNIME's Column Auto Type Cast decides each type from the first rows only, and may fail during execution if later rows contradict the inferred type.[3] DuckDB samples a .gz compressed CSV file or stdin only from the beginning of the file, because it cannot jump to other locations in it.[1] Setting DuckDB's sample_size parameter to -1 makes the CSV reader read the entire file for sampling.[1] When a wrong guess would be costly, scan the whole column or state the type yourself: a sample says nothing about the rows it did not read. (Meridian's view)
When the data cannot resolve a date ambiguity, DuckDB falls back on a list of preferred formats, in which ISO 8601 is preferred over MM-DD-YYYY.[1] Alteryx's Auto Field tool assigns a string type rather than a numeric one to a field in which any record starts with zero.[4] Check identifiers that begin with a zero, such as some postcodes and account numbers, before accepting a numeric type detected for them. (Meridian's view)
The method
DuckDB's CSV sniffer first detects the file's dialect, then the type of each column, then whether the file has a header row.[1] DuckDB tries to convert the sampled values of each column to each candidate type, and removes a candidate from that column's set whenever a conversion fails.[1] After all samples have been handled, DuckDB chooses the remaining candidate type with the highest priority.[1] DuckDB's auto_type_candidates option sets which candidate types are considered, and VARCHAR is always considered as the fallback.[1] Quoting makes no difference to DuckDB's type detection, so "42" and 42 are typed alike.[1]
KNIME's Column Auto Type Cast checks first whether the values are dates, then integer, long, double, and finally string.[3] KNIME's Column Auto Type Cast converts a string column to a numeric or date type if and only if all of its entries could be converted.[3] Alteryx's Auto Field tool reads through all of the records of an input and sets each field type to the smallest possible size for the data in the column.[4] RapidMiner's Guess Types operator changes the value types of the attributes it re-guesses and has no impact on the values themselves.[2]
DuckDB also uses detected types to find a header: it treats the first row as a header when that row's types deviate from those of the other rows.[1] A header cannot be detected when every column is VARCHAR, so DuckDB then assumes the file has a header unless the header option is set to false.[1]
Compared with the alternatives
DuckDB's detected types can be overridden with the types option, as a list of types in column order or as a map from column name to type.[1] DuckDB's all_varchar option disables type detection entirely, so every column stays VARCHAR as it occurs in the CSV file.[1] When DuckDB's COPY statement reads a CSV file into a table, the table's own column types are used instead of detected ones.[1]
Sources
- [1]CSV Auto Detection – DuckDBdocumentation
- [2]Guess Types - Rapidminer Documentationdocumentation
- [3]Column Auto Type Castdocumentation
- [4]Auto Field Tooldocumentation