Describes a read without performing it. Pass the result to
icebergr_collect() or as.data.frame() to materialise it.
Usage
icebergr_scan(
tbl,
filter = NULL,
select = NULL,
limit = NULL,
snapshot_id = NULL,
as_of = NULL,
batch_size = NULL,
case_sensitive = TRUE
)Arguments
- tbl
An
icebergr_tablefromicebergr_table().- filter
An unquoted R expression, pushed down to Iceberg. See Pushdown below for what can be expressed.
- select
Character vector of columns to read.
NULLreads all of them.- limit
Maximum number of rows to return, or
NULLfor no limit. See Pushdown for an important caveat.- snapshot_id
Read this snapshot instead of the current one. A character id from
icebergr_snapshots().- as_of
Read the table as it was at this time, a
POSIXctorDate. Resolved against the table's snapshot log to the snapshot that was current at that moment, so a snapshot a rollback abandoned, or one that only ever existed on another branch, is not selected even though it carries a matching timestamp. Cannot be combined withsnapshot_id.- batch_size
Rows per Arrow batch, at least 1, or
NULLfor the default. Affects memory use, not results.- case_sensitive
Whether column names in
filterandselectare matched case-sensitively. WhenFALSE, each name is resolved to the table's own spelling before the scan is planned, soselect = "ID"reads the column the table callsid.An exact match always wins. Iceberg column names are case-sensitive, so a table may hold both
idandID; asking forIDreadsID, not whichever the two happen to be ordered. A name that matches no column exactly and more than one case-insensitively is ambiguous, and is an error rather than a silent choice between them.
Pushdown
filter and select are pushed down into scan planning, which is the whole
performance argument for Iceberg over reading raw Parquet: manifests carry
per-file statistics, so entire files and row groups are skipped before any
bytes are read. Inspect the effect with icebergr_scan_plan().
limit is not pushed down: iceberg-rust has no row limit in its scan
API, so the same files are planned and rows are counted as batches arrive.
It bounds how much is decoded and converted, not how much is planned.
Filters may use ==, !=, <, <=, >, >=, &, |, !, %in%,
is.na(), is.nan() and startsWith(). A bare name is read as a column when
the table has a column of that name, and otherwise evaluated in the calling
environment, so filter = year == target works with a local target. That
holds wherever the name appears: an Iceberg predicate compares a column with a
value, so a filter naming a column on both sides, such as a > b, is refused
rather than reading b from the calling environment. Anything more elaborate
should be applied in R after collecting.
A filter keeps the rows R's own evaluation of it would keep, NA and NaN
included, although Iceberg's rules for them differ: no comparison matches a
NaN, is.na() is TRUE for one, x == 0 matches -0 as well as 0, and
!(x %in% c(1, 2)) keeps the rows where x is NA, since %in% is never
NA in R. The exception is an ordering comparison between strings, which
follows Iceberg's byte order; that agrees with R only in the C locale.
startsWith() is pushed down only against a string column, since Iceberg
defines a prefix comparison for no other type.
A filter on a decimal column is pushed down, but with iceberg-rust's
row-level selection turned off for that scan: in 0.10 that stage drops every
row of an ordering comparison against a decimal, so price > 2.25 returned
nothing at all. File and row-group pruning still apply, so such a scan is a
little less selective and still correct.
Column names and time travel
Iceberg records a schema per snapshot, so filter and select are resolved
against the schema of the snapshot actually being read: the one named by
snapshot_id or as_of, and otherwise the current snapshot. A column another
engine has since renamed or dropped is therefore still nameable as of a
snapshot that had it, and one added afterwards is refused for a snapshot that
did not. icebergr_schema() takes the same snapshot_id and reports what
those columns are.
The current snapshot's schema is usually the table's current schema, but not
straight after another engine changes the schema without writing anything,
such as an ALTER TABLE ... ADD COLUMN. Until the next commit, a read still
has the columns the current snapshot was written with, under their old names,
while icebergr_schema() already reports the new ones.
Examples
tbl <- icebergr_example_table(rows = 10)
# Projection and predicate pushdown
scan <- icebergr_scan(tbl, filter = id > 1000 & amount > 900, select = c("id", "amount"))
scan
#> <icebergr_scan>
#> table: db.events
#> select: id, amount
#> filter: id > 1000 & amount > 900 (pushed down)
#> Use icebergr_collect() to read it.
icebergr_collect(scan)
#> # A tibble: 2 × 2
#> id amount
#> <int> <dbl>
#> 1 1009 944.
#> 2 1010 1000
# A local variable is usable in a filter: a bare name is read as a column
# only when the table has one of that name.
cutoff <- 1005
icebergr_collect(icebergr_scan(tbl, filter = id > cutoff, select = "id"))
#> # A tibble: 5 × 1
#> id
#> <int>
#> 1 1006
#> 2 1007
#> 3 1008
#> 4 1009
#> 5 1010
# Time travel, to the state before the second append
history <- icebergr_snapshots(tbl)
nrow(icebergr_collect(icebergr_scan(tbl, snapshot_id = history$snapshot_id[[1]])))
#> [1] 10
nrow(icebergr_collect(icebergr_scan(tbl, as_of = history$timestamp[[1]])))
#> [1] 10