Writes a data frame, an sf object or a lazy DuckDB table to a parquet file, or to a directory of hive
partitioned parquet files, locally, on R2 as r2://bucket/path or on S3 as s3://bucket/path. Rows get
sorted so that filters on the sort_by columns, and spatial filters on sf data, only read the row groups
they need. Lazy tables get written by DuckDB directly, without collecting the data into R.
Usage
data_to_parquet(
data,
path,
sort_by = NULL,
partition_by = NULL,
row_group_size = 10000L,
remove_stale = TRUE,
metadata = NULL,
compression = c("zstd", "snappy", "gzip", "brotli", "lz4", "lz4_raw", "uncompressed"),
purge_cache = TRUE
)Arguments
- data
data frame, sf object or lazy DuckDB table.
- path
local path,
r2://bucket/pathors3://bucket/pathof the parquet file, or of the directory withpartition_by. A file in a bucket has to end in.parquet. Existing files get overwritten.- sort_by
column names to sort by, optional. Spatial data gets sorted spatially after these.
- partition_by
column names to partition by, optional;
pathis then a directory.- row_group_size
rows per row group, default
10000, rounded up to a multiple of 2048 by DuckDB.- remove_stale
what happens to the files of an existing directory at
path:TRUE(default): parquet files in hive partition directories (key=value) get removed first, so no partitions of earlier writes remain, also if the partition columns changed. Other files are kept.FALSE: existing files are kept and only the files at the paths being written get replaced, e.g. to add partitions.
- metadata
named list of metadata to write into the parquet footer, see the Metadata section.
NULL(default): none of the source's metadata, with a warning if the source has any.list(): none, without the warning.
- compression
parquet compression codec:
"zstd"(default),"snappy","gzip","brotli","lz4","lz4_raw"or"uncompressed". zstd compresses well and decompresses fast, which suits files that get read remotely.- purge_cache
TRUE(default) orFALSE: purge the changed files from the Cloudflare cache of the bucket's custom domains when writing to R2, likeupload_parquet(). Ignored for S3 and local paths.
Value
(invisibly) path, normalized to r2://bucket/path or s3://bucket/path for buckets, ready for
parquet_tbl(), with the written files (local paths or bucket paths) in the files attribute.
Spatial data
sf objects get written as GeoParquet 2.0 with the native parquet GEOMETRY type, which stores the crs with
the column and a bounding box per row group. After the sort_by columns, rows get sorted along a Hilbert
curve so that each row group covers a compact area and sf::st_filter()
on lazy tables can skip row groups. The geometry column is always named geometry, following the
GeoParquet convention.
Invalid geometries get fixed via sf::st_make_valid(), keeping multi geometries multi, with a message, and
the repairs get recorded in the metadata:
processing: how many geometries were repaired, and with which version of GEOS (or s2 for geographic coordinates).geometry_repairs: the repaired rows, up to 1000, as an object withcolumns, the key columns, androws, the key values per row. The key is the declaredprimary_key, or else the first column or a column ending inIDif it identifies the rows; otherwise only the count gets recorded, with a warning.
Entries given in metadata are kept; repair the geometries before writing to avoid both.
Lazy tables with GEOMETRY columns get written as GeoParquet too, sorted by the column named geometry or
else the first GEOMETRY column, without repairing geometries. The extent of the Hilbert curve gets computed
in a separate query, so the source gets scanned twice, and a remote source downloads its geometry twice.
Empty geometries sort last.
Row groups
Row groups are what readers can skip, so their size balances the selectivity of filters against the size of the file metadata. DuckDB writes row groups in multiples of 2048 rows, so data with fewer rows ends up in a single row group and filters can't skip any part of it.
Partitioning
With partition_by, path is a directory with one subdirectory per value, like
year=2021/data_0.parquet. The partition values are only stored in the directory names, readers recover
them with hive partitioning, DuckDB as strings ("001" stays "001"). Partitioned files get written single
threaded, since multi threaded writes don't keep the sort order within partitions. They get written to a new
directory next to path first and moved into place once complete, so a dataset can be rewritten from a lazy
table reading it, and a failed write leaves the existing files as they were. A file at path gets replaced
by the directory. Lazy tables reading path see the new data and metadata afterwards, as long as the
columns stayed the same.
Writing to R2 and S3
Destinations in buckets get written to a temporary directory and uploaded via upload_parquet(), into a bucket
that has to exist (see create_r2_bucket()). Each file goes up in a single request from memory, so files have
to fit into memory and stay below 5 GB; partition large data with partition_by. A partitioned directory may
be named like a parquet file, parquet_tbl() tells files and directories apart. Stale files get removed before
the upload, so readers may see a partitioned dataset incomplete while it runs. Changed files get purged from
the Cloudflare cache (R2 only) unless purge_cache = FALSE. Partitioned writes write a manifest of the
directory, and all writes update the existing manifests of the directories above the destination, see
upload_parquet().
Metadata
metadata gets written into the parquet footer, where other tools can read it too (DuckDB's
parquet_kv_metadata, arrow's schema metadata). parquet_metadata() reads it back and collect_parquet()
attaches it to collected data. Conventional entries:
source(URL of the original data),license,notesanddescription.columns: alabelanddescriptionper column, e.g.columns = list(pop = list(label = "Population", description = "...")); a string is short for the label.labelattributes of data frame columns, as set by the haven or labelled packages, get added automatically.languagesandlanguage: every language the dataset holds text in, and the language of its plain text, see the Languages section.primary_key: the columns that together identify the rows, one like"DBUID"or several for long tables, likec("DAUID", "characteristic"). They have to exist, have no missing values and identify the rows, which gets checked before writing, at the cost of an extra pass over a lazy table. It gets written as a JSON array, and is how repaired geometries get listed.geois reserved for the GeoParquet metadata.
Single values get written as strings, anything else as JSON; NULL and NA entries get dropped with a
warning. Changing the metadata means writing the data again.
Languages
Text can be stored in several languages, under keys with the language as suffix: title_fr next to
title, or title_en and title_fr without a plain title; label_fr and description_fr in the
column entries, columns = list(pop = list(label_en = "Population", label_fr = "Population", description_fr = "...")), with or without a plain label; and a column holding another column's text in
a language is named the same way, name_fr next to name, or name_en and name_fr without a plain
name. Which suffixes mean a language is an explicit contract, nothing is guessed from the keys. The
languages of a dataset are
the
languagesentry: every language it holds text in, likec("en", "fr"), written as a JSON array;the
languageentry: the language of its plain text (title,label) and plain columns, as a language code like"en", only needed where there is plain text, and declared by that alone.
Only a declared suffix marks a variant: source_url next to source and rate_men next to rate are
entries and columns of their own, and title_fr is the French title only when the dataset declares
fr. Suffixed labels and descriptions in a language the metadata doesn't declare are refused, so a
bilingual dataset has to say languages = c("en", "fr"). Language codes are lowercase, two or three
letters with optional subtags after a hyphen ("en", "fra", "pt-br"), and a suffix has to match
exactly, name_FR is no translation. No language is privileged: a bilingual dataset can carry only
suffixed entries, labels and columns.
Readers get one language at a time: parquet_tbl() and select_language() select the columns of that
language under their plain names, collect_parquet() attaches its labels and parquet_metadata()
resolves the entries, see the Languages section of collect_parquet() for the fallbacks. A language the
dataset doesn't declare leaves it as stored.
Metadata doesn't get carried over by itself, since data read from parquet files describes something else
once it got changed. With metadata = NULL, writing a lazy table from parquet_tbl() (the files of the
left-hand table for joins) or data collected with collect_parquet() warns when the files it was read from
have metadata:
metadata = parquet_metadata(data)writes it again;metadata = modifyList(parquet_metadata(data), list(...))changes entries (aNULLdrops one);metadata = list()writes none.
Metadata read with parquet_metadata() describes the source, so its columns entries for columns the data
no longer has get dropped, and repaired geometries replace its processing and geometry_repairs entries.
A primary_key gets checked like any other.
Credentials
Credentials come from environment variables, e.g. set in .Renviron, read at call time. Public URLs and
local paths need none.
Paths on R2 (r2://bucket/path):
R2_ACCOUNT_ID: the R2 account id.R2_ACCESS_KEY_IDandR2_SECRET_ACCESS_KEY: the access key id and secret access key of an R2 API token with access to the bucket.R2_API_TOKEN, optional: the value of an API token with Zone Cache Purge permission (seecreate_r2_bucket()). Files that change on R2 then get purged from the Cloudflare cache of the bucket's custom domains, so that the new versions are served right away; the purge is skipped without it.R2PARQUET_PURGE_ORIGINS, optional: the origins of web pages reading the bucket from a browser, comma separated, likehttps://example.com,https://www.example.com. Cloudflare caches the responses to their requests separately perOrigin, which a purge of the plain URL misses, so each changed file also gets purged once per origin; copies cached for origins not listed are refreshed when the edge TTL runs out. This only concerns web apps, whose browsers send theOriginheader; requests without one, like those of this package, DuckDB or curl, get the plain URL's copy, which is always purged.
Paths on S3 (s3://bucket/path), on Amazon S3 or another S3 compatible service like MinIO, use the
variables AWS tools use:
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY, andAWS_SESSION_TOKENfor temporary credentials.AWS_REGIONorAWS_DEFAULT_REGION: the region, defaultus-east-1.AWS_ENDPOINT_URL_S3orAWS_ENDPOINT_URL: the endpoint of other S3 compatible services, likehttp://localhost:9000, which get addressed with path style URLs (endpoint/bucket/key).R2PARQUET_S3_URL_STYLE:pathorvhost, overrides the URL style.
AWS profiles and single sign-on are not read, aws configure export-credentials --format env prints the
variables for them.
