This page documents every DuckDB type as it crosses to R through Arrow and back: the Arrow type the engine exports it as, what the two R readers make of that, which Arrow type writes it again, and which R functions keep Arrow's types on the way in. See duckdb_types for a description of the direct conversion to R vectors.
The routes
Reading.
Every function that returns Arrow hands out the engine's own export, under the connection's settings:
dbGetQueryArrow(), dbFetchArrow() and dbFetchArrowChunk() after dbSendQueryArrow(), dbReadTableArrow(),
duckdb_fetch_arrow() and duckdb_fetch_record_batch() after dbSendQuery(arrow = TRUE), and arrow::to_arrow().
No R vector exists until a reader converts the stream,
and the two readers differ, so each entry below names both:
nanoarrow's as.data.frame(), and arrow's as.data.frame() of arrow::as_arrow_table().
The export settings are DuckDB's, set with SET, and each changes the Arrow type of some columns:
arrow_lossless_conversion = trueexports each type that has no exact Arrow counterpart as an extension type that names it, so that DuckDB, or another Arrow consumer that knows the name, gets the type back. Where nanoarrow falls back to the storage of one, the entries below say so.arrow_large_buffer_size = truegives strings, binary data and lists 64-bit offsets, aslarge_string,large_binaryandlarge_list, which both readers convert as they convert the others.arrow_output_version,'1.0'by default, gates the newer layouts. From'1.4', binary data exports asbinary_view, andproduce_arrow_string_view = trueandarrow_output_list_view = truetake effect, asstring_viewandlist_view; with an older version those two change nothing. From'1.5', aDECIMALup to width 9 exports asdecimal32and up to width 18 asdecimal64. nanoarrow convertsstring_viewandbinary_view.
Writing.
Only the routes that let DuckDB scan the Arrow data keep its types:
duckdb_register_arrow(), and arrow::to_duckdb(), which calls it.
duckdb_register_arrow() takes whatever arrow::Scanner$create() scans.
Nothing is copied: the result is a view, and CREATE TABLE ... AS SELECT * FROM it writes a table.
A registered arrow Table is scanned by every query, and a registered RecordBatchReader is a limitation.
Every other route converts through an R data frame, so a column lands as the type its R vector writes (see duckdb_types).
dbWriteTableArrow(), dbCreateTableArrow() and dbAppendTableArrow() are DBI's defaults, which do that batch by batch.
dbBindArrow() converts the same way, then binds by position.
Reading
Numbers
BOOLEANexports asbooland reads aslogical; witharrow_lossless_conversion, asarrow.bool8, which nanoarrow reads as theintegerof its storage.TINYINT,SMALLINT,UTINYINT,USMALLINTexport asint8,int16,uint8anduint16, and read asinteger, exactly.INTEGERexports asint32and reads asinteger, exactly but for the minimum.UINTEGERexports asuint32. nanoarrow reads it asnumeric; arrow reads it asintegerwhen every value fits, and asnumericotherwise.BIGINTexports asint64. nanoarrow reads it asnumeric, exact up to 2^53. arrow reads it asintegerwhen every value fits and asbit64::integer64otherwise, exactly but for the minimum, or asinteger64always underoptions(arrow.int64_downcast = FALSE).UBIGINTexports asuint64, and both read it asnumeric, exact up to 2^53; its rounding past that is a limitation.HUGEINT,UHUGEINTexport asdecimal128(38, 0), and both read them asnumeric; their rounding is a limitation, and so is aUHUGEINTof 2^127 or more reading as negative. Witharrow_lossless_conversionthey export asarrow.opaque, which carries every value. Their text reads them exactly (see duckdb_types).BIGNUMexports asarrow.opaqueunder either setting; nanoarrow reads its storage bytes as ablob.DECIMAL(width, scale)exports asdecimal128(width, scale), or narrower from output version 1.5, and both read it asnumeric; its rounding is a limitation.FLOAT,DOUBLEexport asfloatanddouble, and read asnumeric.
Text and binary
VARCHARexports asstringand reads ascharacter.BLOBexports asbinary; nanoarrow reads it as ablob::blob, and arrow as anarrow_binarylist of raw vectors.BITexports asbinary, the bytes DuckDB stores for the bit string, and reads as abloborarrow_binaryof those bytes. Witharrow_lossless_conversionit exports asarrow.opaque, which nanoarrow reads as the same bytes. Its text reads it as0and1(see duckdb_types).UUIDexports asstring, lowercase and hyphenated, and reads ascharacter. Witharrow_lossless_conversionit exports asarrow.uuid.
Dates and times
DATEexports asdate32and reads asDate.TIMEexports astime64('us')and reads ashms.TIME_NSexports astime64('ns')and reads ashms.TIMETZexports as thetime64('us')of its local time and reads ashms; the offset it drops is a limitation. Witharrow_lossless_conversionit exports asarrow.opaque, which keeps the offset.TIMESTAMP_S,TIMESTAMP_MS,TIMESTAMP,TIMESTAMP_NSexport astimestampin their own unit, without a zone, and read asPOSIXct, whose double cannot hold aTIMESTAMP_NS's nanoseconds, a limitation. The two readers label the same instant differently: nanoarrow gives it the zoneUTC, so it prints the stored clock, and arrow gives it none, so it prints in R's session zone, a different clock outside UTC.TIMESTAMPTZexports astimestamp('us', zone), where the zone is DuckDB'sTimeZonesetting, and both readers read it as aPOSIXctlabelled with that zone.INTERVALexports asinterval_month_day_nano.
Enums and nested types
ENUMexports as a dictionary of its values; nanoarrow reads it ascharacter, and arrow asfactor.ARRAYexports as afixed_size_listand reads as a list of vectors (vctrs::list_of()orarrow_fixed_size_list), without thearray = "matrix"thatdbGetQuery()needs.LISTexports aslistand reads as a list of vectors (vctrs::list_of()orarrow_list).MAPexports asmapand reads as a list of key and value data frames.STRUCTexports asstructand reads as a data frame column, a tibble in arrow's case.UNIONexports as asparse_union. nanoarrow reads it as a data frame with a column per member,NAwhere the value is another member's.
Everything else
NULL, untyped, exports asint32and reads asNA_integer_, as it does throughdbGetQuery().JSONexports asstringand reads ascharacter; witharrow_lossless_conversionit exports asarrow.json, which nanoarrow reads ascharacter.INETexports as a struct whoseaddressis theHUGEINTthe engine stores, as adecimal128(38, 0), which both read as a double, the same asdbGetQuery()does, and its text reads the address (see duckdb_types). Witharrow_lossless_conversionthat field becomesarrow.opaque.
Writing
Arrow types
Each Arrow type lands as one DuckDB type when DuckDB scans it:
bool,int8toint64,uint8touint64,float,doubleland asBOOLEAN, the integer type of the same width and sign,FLOATandDOUBLE.decimal32,decimal64,decimal128land asDECIMALof the same width and scale.string,large_string,string_viewland asVARCHAR, andbinary,large_binary,binary_view,fixed_size_binaryasBLOB.date32,date64land asDATE.time32andtime64('us')land asTIME, andtime64('ns')asTIME_NS.timestampwithout a zone lands asTIMESTAMP_S,TIMESTAMP_MS,TIMESTAMPorTIMESTAMP_NSby its unit, and with a zone asTIMESTAMPTZ, the same instant.durationlands asINTERVALin any unit.interval_monthsandinterval_month_day_nanoland asINTERVAL, each part kept.list,large_list,list_viewland asLIST, andfixed_size_listasARRAY.structlands asSTRUCT,mapasMAP, andsparse_unionasUNION.A dictionary lands as
VARCHAR.nalands as a column of typeNULL.The extension types land as the DuckDB type they name:
arrow.uuidasUUID,arrow.jsonasJSON,arrow.bool8asBOOLEAN, andarrow.opaqueas the DuckDB type in its metadata. What GeoArrow WKB lands as is under Geometry, and the other GeoArrow encodings are a limitation.
R classes, through Arrow
An R vector reaches DuckDB through Arrow as the Arrow type its package infers,
which differs from what dbWriteTable() gives for some classes.
Where nothing is said below, nanoarrow and arrow infer the type dbWriteTable() writes.
factorinfers a dictionary and lands asVARCHAR, wheredbWriteTable()writesENUM. arrow lands anorderedone asVARCHARtoo.POSIXctinfers atimestampwith its zone, or R's session zone where it has none, and lands asTIMESTAMPTZ, wheredbWriteTable()writes a plainTIMESTAMP.difftimeinfers adurationand lands asINTERVALin hours and below, so 2 days land as48:00:00, wheredbWriteTable()keeps the days.hmsinferstime32and lands asTIME, the one R route to that type; its truncation is a limitation.A plain list of vectors lands as
LISTthrough arrow.A matrix column lands as
ARRAYthrough nanoarrow.
Geometry
GEOMETRY and the spatial extension's own types through Arrow, and where the geoarrow and sf packages meet them:
GEOMETRYexports asgeoarrow.wkb, with the column's CRS in the field's metadata, as PROJJSON where the core orspatialknows the CRS, and as its identifier otherwise:OGC:CRS84exports as PROJJSON withoutspatial, andEPSG:4267only with it. It staysgeoarrow.wkbunder every export setting,arrow_lossless_conversionincluded;arrow_large_buffer_sizemakes its storagelarge_binary, and anarrow_output_versionfrom'1.4'makes itbinary_view. With the geoarrow package loaded, both readers convert it to ageoarrow_vctrin each of those layouts, arrow the view one included.sf reads a result through GeoArrow in one call. With geoarrow loaded,
sf::st_as_sf(dbGetQueryArrow(con, sql))gives ansfwhose geometries and CRS equal the source's, in the large and view layouts too, and so dosf::st_as_sf()of the result'sarrow::as_arrow_table(), andsf::st_as_sfc()of thegeoarrow_vctrcolumn ofas.data.frame().The
spatialextension's own types cross as their storage.POINT_2Dand the other point and box types export as astruct,LINESTRING_2DandLINESTRING_3Das alistof point structs,POLYGON_2DandPOLYGON_3Das a list of those lists, andWKB_BLOBasbinary, witharrow_lossless_conversiontoo, and each reader converts them as it converts those Arrow types.GeoArrow WKB writes
GEOMETRYwith its CRS. Encode the geometry column as WKB, asgeoarrow::as_geoarrow_vctr(sf::st_geometry(x), schema = geoarrow::geoarrow_wkb(crs = sf::st_crs(x))), or as awk::as_wkb()column, which nanoarrow infers asgeoarrow.wkbonce geoarrow is loaded. Registerarrow::as_arrow_table(nanoarrow::as_nanoarrow_array_stream(df))withduckdb_register_arrow(), and aCREATE TABLE ... AS SELECTfrom the view writes aGEOMETRYcolumn with the CRS, which reads back into ansfequal to the one written. Withspatialloaded, the type names the CRS by its identifier, asGEOMETRY('EPSG:4267'); without it, it keeps the PROJJSON it came as, andsf::st_crs()reads the same CRS from both. WKB without a CRS lands as plainGEOMETRY, and so do the large and view layouts DuckDB's own export makes.A
geoarrow_vctrcolumn holds indices. Thegeoarrow_vctrthatas.data.frame()gives for a geometry in an Arrow result is an integer vector of indices into the Arrow data it holds, and writing it back is a limitation.
Limitations and reference
The limitations are listed in the handbook, in usage/arrow-types/.
The routes through R vectors are documented in duckdb_types,
and how a stream behaves, when it drains and what invalidates it, is documented in the handbook's integrations/.
Every entry on this page was measured on DuckDB 1.5.5, nanoarrow 0.9.0 and arrow 25.0.1
in experiments/2026-09-27-arrow-types/
and experiments/2026-09-28-type-rereview/,
and geometry also with geoarrow 0.4.4 and sf 1.1-3 in experiments/2026-09-27-geoarrow/.