The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
When an organisation’s data moves into Apache Iceberg, R stops being a first-class citizen and becomes the thing you export CSVs to.
R is the only major data language without an Apache Iceberg client. Apache governs implementations in Java, Python (PyIceberg), Rust and Go. There is none for R. Until now the only route was to read Iceberg tables through DuckDB as an intermediary, which means no writes, no schema access, no snapshot management, no catalog integration and no partition information.
Meanwhile Snowflake, Databricks, BigQuery, AWS and Dremio have all
standardised on Iceberg as the open table format. Parquet is well served
in R by arrow, and Delta Lake has a community Rust binding;
Iceberg had nothing.
icebergr talks to Iceberg directly, through iceberg-rust,
the Apache-governed Rust implementation, via extendr. Arrow
is the interchange layer throughout, so data crosses from Rust into R
over the Arrow C stream interface without a serialisation round
trip.
CRAN is the intended destination; icebergr is not there
yet. See CRAN status for what has to clear
first. Until then, install from GitHub.
# install.packages("pak")
pak::pak("PursuitOfDataScience/icebergr")Installing from source compiles Apache Iceberg’s Rust implementation,
so you need a Rust toolchain (rustc >= 1.92):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shExpect the first build to take a while: a default install compiles 264 crates.
Optional backends are off by default because each substantially enlarges that tree:
# Object storage (S3)
ICEBERGR_CARGO_FEATURES=s3 R CMD INSTALL --preclean .
# AWS Glue catalog (implies s3)
ICEBERGR_CARGO_FEATURES=glue R CMD INSTALL --preclean .Either of those needs network access, even from the CRAN tarball. The vendored crates bundled there cover the default build; the roughly one hundred extra crates these features pull in — the AWS SDK among them — would have tripled the tarball, so cargo fetches them from crates.io instead.
Everything below runs offline against a locally generated table.
library(icebergr)
tbl <- icebergr_example_table()
tbl
#> <icebergr_table>
#> table: db.events
#> location: /tmp/RtmpXXXXXX/icebergr-warehouse/db/events
#> format: v2
#> snapshot: 4964299904926817223
#> columns: 5
#> id <int>
#> event <string>
#> amount <double>
#> day <date>
#> recorded_at <timestamptz>Read it, pushing the filter and the projection down into the scan:
icebergr_collect(
icebergr_scan(tbl, filter = id > 1000 & amount > 900, select = c("id", "amount"))
)Pushdown is the whole performance argument for Iceberg over reading
raw Parquet, so it is worth being able to verify rather than
assume. icebergr_scan_plan() shows which files a scan would
touch, before reading any of them:
nrow(icebergr_scan_plan(icebergr_scan(tbl)))
#> [1] 2
nrow(icebergr_scan_plan(icebergr_scan(tbl, filter = id > 1000)))
#> [1] 1Travel back through snapshot history:
history <- icebergr_snapshots(tbl)
history[, c("snapshot_id", "operation", "added_records")]
# The state before the most recent append
icebergr_collect(icebergr_scan(tbl, snapshot_id = history$snapshot_id[[1]]))
# Or by time. `as_of` is a *commit* time, not a time in the data, so take it
# from the history: this table was written just now.
icebergr_collect(icebergr_scan(tbl, as_of = history$timestamp[[1]]))Append new data:
tbl <- icebergr_append(tbl, data.frame(
id = 9001L,
event = "purchase",
amount = 42.5,
day = as.Date("2024-07-01"),
recorded_at = as.POSIXct("2024-07-01 09:00:00", tz = "UTC")
))Connect to a real catalog. Credentials come from the environment, never from arguments:
Sys.setenv(ICEBERGR_REST_TOKEN = "...")
catalog <- icebergr_catalog("rest", uri = "https://catalog.example.com")
icebergr_list_namespaces(catalog)
icebergr_list_tables(catalog, "analytics")
tbl <- icebergr_table(catalog, "analytics.events")
icebergr_schema(tbl)
icebergr_partitions(tbl)See vignette("catalog-configuration") for REST, Glue and
S3 setup.
Built against iceberg-rust 0.10.0.
Reads and writes table spec v1 and v2. v3 metadata is
parsed, but v3-specific features are not exposed.
icebergr_spec_support() reports this matrix for your own
build, resolved against the optional features you compiled in.
✅ works · ⚙️ needs a build flag · 🦀 missing upstream in
iceberg-rust · 🚧 out of scope for 0.1.0
📖 Reading
| ✅ | Read a table into Arrow / a tibble |
| ✅ | Predicate pushdown — filters prune whole files and row groups |
| ✅ | Projection pushdown — only selected columns leave disk |
| ✅ | Row group and row-level scan pruning |
| ✅ | Merge-on-read tables — positional and equality deletes
applied. iceberg-rust refuses an equality delete keyed on a
list or map column; every other column type is
handled |
| ✅ | Inspect the file plan before reading, with
icebergr_scan_plan() |
| ✅ | Nested types — struct and list read and
write. A map column can be created, but writing map
values from R needs the arrow package, since
nanoarrow cannot build a map array alone. Note
nanoarrow::na_map() needs its key type built non-nullable:
na_map(na_string(nullable = FALSE), …) |
| 🦀 | Row limit pushdown — limit bounds
decoding, not planning |
| 🦀 | Pushdown on a nested field — read the parent column and filter in R |
| ✅ | decimal filters — row-level selection is disabled for
these scans, because iceberg-rust 0.10.0 drops every row of
an ordering comparison on a decimal; file and row group pruning still
apply |
🕰️ Time travel
| ✅ | Snapshot history |
| ✅ | Read a snapshot by id |
| ✅ | Read as of a timestamp — resolved against Iceberg’s snapshot log, so a rollback’s abandoned snapshot is not selected |
| ✅ | Read the schema as of a snapshot, and filter and select by the names it had |
✍️ Writing
| ✅ | Append rows |
| ✅ | Create a table (unpartitioned) and a namespace |
| ✅ | Register an existing table — from a metadata file named
<version>-<uuid>.metadata.json, as every engine
writes them; a renamed file reads but cannot be appended to |
| ✅ | Spec v1 tables read, filter and append, and stay v1 |
| 🚧 | Appending to a partitioned table — refused before anything is written, since a partition value per row is not computed. Reading one works |
| 🦀 | Row-level deletes and MERGE / upsert — reading
such tables works. iceberg-rust can write an equality
delete file but has no transaction action that commits one |
| 🦀 | Overwrite writes — fast_append is the only way
iceberg-rust 0.10.0 can add files |
| 🚧 | Partitioned table creation — available upstream, not exposed here |
🗂️ Catalogs and metadata
| ✅ | REST catalog |
| ✅ | In-process memory catalog, for local warehouses |
| ✅ | Schema and partition spec inspection, including a partitioned table’s spec |
| ✅ | Table properties, and icebergr_reload() to see another
session’s commits |
| 🚧 | Setting table properties — needs an update_properties
transaction |
| ⚙️ | AWS Glue — build with the glue Cargo feature |
| ⚙️ | Object storage (S3) — build with the s3 Cargo
feature |
| 🦀 | Hadoop / filesystem catalog — none exists upstream; use
type = "memory" |
🚧 Not in 0.1.0 — schema evolution, partition
evolution, snapshot expiry, dbplyr lazy verbs, table
encryption. Compaction is 🦀: it needs a rewrite action
iceberg-rust 0.10.0 does not have.
A correct narrow surface beats a broad buggy one. Anything marked 🦀 or 🚧 raises an informative error rather than failing obscurely.
R types survive the Arrow round trip as follows:
| R type | Iceberg type | Round trip |
|---|---|---|
integer |
int |
Unchanged |
double |
double |
Unchanged, including Inf and -Inf.
NaN returns as NA: R’s
is.na(NaN) is TRUE, so it is written as a
null. |
character |
string |
Unchanged, UTF-8 preserved |
logical |
boolean |
Unchanged |
Date |
date |
Unchanged |
POSIXct |
timestamptz |
Instant preserved; normalised to UTC |
bit64::integer64 |
long |
Unchanged, full 64-bit precision. Reading a long back
needs bit64 installed; without it Arrow’s
int64 narrows to a double. |
factor |
string |
Returns character. Iceberg has no
dictionary type, so levels cannot be carried. |
POSIXct |
timestamp_ns |
Readable, writable and filterable, but a POSIXct is a
double of seconds, so sub-microsecond precision is lost.
nanoarrow warns on every such read — it triggers on the
nanosecond count exceeding 2^53, which any present-day instant does, so
the warning appears even when nothing was actually lost. |
| data frame column | struct |
Unchanged, and comes back as a data frame column. Iceberg cannot push a filter or a projection down onto a nested field, so read the parent column and subset it in R. |
vctrs::list_of |
list |
Unchanged, and comes back as a list_of column. |
Snapshot ids are character, not numeric. Iceberg assigns them as random 64-bit integers and an R numeric holds only 53 bits, so a double round trip would silently select the wrong snapshot.
CRAN is the target. The package is built to submit:
configure and configure.win,
Makevars.in templates, vendored dependencies via
tools/vendor.R, an offline --offline build,
-j2 to stay inside CRAN’s parallelism limit, a
CARGO_HOME confined to the build tree, and a per-crate
LICENSE.note inventory. CI runs that exact path on every
commit, so the submission build is exercised continuously rather than
assembled at the last minute.
Two things have to clear first. Both are tractable; neither is settled.
1. Vendored size — measured, not estimated. The
source tarball is 10.7 MB, almost all of it
vendor.tar.xz. That is over CRAN’s 10 MB guideline, which
explicitly allows requesting more; the request is in cran-comments.md,
made with the arithmetic shown rather than asserted.
It was 31.5 MB until tools/vendor.R learned to reduce
the vendor tree rather than just compress it. cargo vendor
writes every entry in Cargo.lock, and a lockfile is a union
over every platform and every optional feature, so most of what it
writes is never compiled: of 442 crates, 270 are
compiled on at least one platform R runs on and 264 on
any single machine. The other 172 — the optional Glue and S3 backends,
and the crates belonging to operating systems R does not run on — cannot
be deleted, because cargo resolves the whole lock graph before
it filters targets or selects features and fails outright on a missing
package. But it never reads the ones it does not select, so they ship as
their manifest and their licence rather than as source. With that plus
four smaller sweeps (test corpora, files cargo ignores,
#[cfg(test)] modules inside src/, and the 219
of 246 windows-sys API modules behind disabled features),
373.8 MB of vendored sources become 97.3 MB.
What is left is not reducible without giving something up.
iceberg declares [features] default = [] and
has no optional dependencies, so tokio,
reqwest, parquet, twelve arrow-*
crates and apache-avro are unconditional. Compressed
individually the ten largest crates are 4.45 MB of the 10.46 MB archive
and the remaining 432 directories average 22 KB, so there is no longer a
single item to attack.
One consequence for anyone enabling an optional backend:
ICEBERGR_CARGO_FEATURES builds fetch their extra crates
from crates.io, because the bundled archive covers the default build. A
default install still touches the network at no point, and CI runs that
offline path on every commit.
2. Minimum Rust version. iceberg-rust
0.10.0 declares rustc 1.94 and edition 2024, and bumps its
minimum most releases. This one is now measured rather than assumed:
CRAN’s Windows build farm carries 1.92.0, and the first submission
failed to install there because of it.
The declared floor is not a real one. Four crates in the tree ask for
1.94 — iceberg, iceberg-catalog-rest,
iceberg-catalog-glue and fastnum, with nothing
else above 1.91.1 — and none of them uses a language or library feature
newer than 1.92. The whole tree compiles, and the test suite passes, on
1.92.0. Cargo treats a dependency’s rust-version as a hard
error rather than a warning, so src/Makevars{,.win} pass
--ignore-rust-version and tools/msrv.R gates
on the floor the package is genuinely tested against, which
DESCRIPTION states as 1.92.
Pinning an earlier iceberg-rust stays the fallback for
the day a release really does need something newer:
iceberg-rust |
Declared MSRV | Cost of pinning it |
|---|---|---|
| 0.10.0 (current) | 1.94, builds on 1.92 | — |
| 0.9.1 | 1.92 | Loses with_runtime; the runtime is then inherited from
the calling context, which is where we already are |
| 0.8.0 | 1.88 | Predates the storage-factory refactor; needs real binding changes |
Edition 2024 itself only needs 1.85, which is early 2025, so the edition is not the constraint — the rolling minimum is.
Precedent worth knowing. polars was on
CRAN in 2023 with a non-vendored, network-fetching build that current
policy disallows, and is on r-universe today. That is a caution about
how a heavy Rust package fails a submission, not proof that one
must.
FEASIBILITY.md
has the full analysis, the measurements behind these numbers, and what
remains unverified.
GPL (>= 3). Bundled Rust crates keep their own licences, listed in
inst/NOTICE and LICENSE.note.
Apache, Apache Iceberg and Iceberg are trademarks of The Apache
Software Foundation. icebergr is an independent community
package, not affiliated with or endorsed by the ASF, and is not one of
the official Iceberg clients.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.