Skip to content

Commit 6e290f8

Browse files
committed
Type the lazy __index__ consistently with the in-memory writer
writeDuckDBTableParquet()/buildTableSelectSQL() emitted the __index__ column as an uncast (offset + row_number()), i.e. always BIGINT, while the in-memory writer narrows the index (or widens it via index_max). So the same resource got a different __index__ type depending on which path wrote it, and a cross-path append (in-memory int32 part 0 + lazy BIGINT part 1) produced a schema-inconsistent, unreadable resource. The lazy path now chooses the index integer type up front and CASTs it in SQL, matching the in-memory writer: narrow by range on a fresh write, honor index_max when supplied, and pin to part 0's on-disk type on append (read via readParquetSchema). index_max is threaded through the existing ... to writeDuckDBTableParquet; row count is already computed on this path, so there is no extra scan. Adds a .duckdbIntTypeName() Arrow->DuckDB type-name helper. Tests cover lazy narrowing, index_max = Inf -> int64, and append pinning to part 0. No behavior change for readers; small resources now narrow on the lazy path as they already did in-memory (BIGINT -> smallest int). Bump version to 0.99.5; update NEWS.
1 parent 97bbf8c commit 6e290f8

5 files changed

Lines changed: 97 additions & 8 deletions

File tree

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Package: DuckDBDataFrame
2-
Version: 0.99.4
2+
Version: 0.99.5
33
Date: 2026-07-22
44
Title: DuckDB-Backed DataFrame and Table Structures
55
Description:

NEWS.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
# DuckDBDataFrame 0.99.5
2+
3+
## Bug fixes
4+
5+
- The lazy SQL write path (`writeDuckDBTableParquet()` / `buildTableSelectSQL()`)
6+
now types the `__index__` column the same way the in-memory writer does,
7+
instead of always emitting a BIGINT `row_number()`. It `CAST`s the index to a
8+
type chosen by range (narrowed on a fresh write, `index_max` honored, or pinned
9+
to part 0's on-disk type on append), so a resource written or appended across
10+
both write paths keeps one consistent `__index__` type. Previously an
11+
in-memory part 0 (narrowed) plus a lazy append (BIGINT) produced a
12+
schema-inconsistent, unreadable resource, and the same table had a different
13+
index type depending on which path wrote it.
14+
115
# DuckDBDataFrame 0.99.4
216

317
## Bug fixes

R/parquet-io.R

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -420,11 +420,25 @@ function(query_sql, target_path, order_cols = NULL, partition_by = NULL,
420420
}, character(1L))
421421
}
422422

423+
.duckdbIntTypeName <- function(arrow_type) {
424+
switch(arrowTypeToName(arrow_type),
425+
"int8" = "TINYINT",
426+
"int16" = "SMALLINT",
427+
"int32" = "INTEGER",
428+
"int64" = "BIGINT",
429+
"uint8" = "UTINYINT",
430+
"uint16" = "USMALLINT",
431+
"uint32" = "UINTEGER",
432+
"uint64" = "UBIGINT",
433+
stop("unsupported __index__ integer type: ",
434+
arrowTypeToName(arrow_type)))
435+
}
436+
423437
#' @importFrom dbplyr sql_render
424438
#' @importFrom DBI dbQuoteIdentifier
425439
buildTableSelectSQL <-
426440
function(x, indexcol = NULL, keycol = NULL, dimtbl = NULL, offset = 0L,
427-
conn = dbconn(x))
441+
index_type = NULL, conn = dbconn(x))
428442
{
429443
if (!inherits(x, "DuckDBTable"))
430444
stop("'x' must be a DuckDBTable")
@@ -438,9 +452,11 @@ function(x, indexcol = NULL, keycol = NULL, dimtbl = NULL, offset = 0L,
438452

439453
if (!is.null(indexcol)) {
440454
qidx <- as.character(dbQuoteIdentifier(conn, indexcol))
441-
select_parts <- c(select_parts,
442-
sprintf("(%s + row_number() OVER (ORDER BY (SELECT 1))) AS %s",
443-
format(offset, scientific = FALSE, trim = TRUE), qidx))
455+
idx_expr <- sprintf("(%s + row_number() OVER (ORDER BY (SELECT 1)))",
456+
format(offset, scientific = FALSE, trim = TRUE))
457+
if (!is.null(index_type))
458+
idx_expr <- sprintf("CAST(%s AS %s)", idx_expr, index_type)
459+
select_parts <- c(select_parts, sprintf("%s AS %s", idx_expr, qidx))
444460
output_names <- c(output_names, indexcol)
445461
}
446462

@@ -501,7 +517,7 @@ function(x, indexcol = NULL, keycol = NULL, dimtbl = NULL, offset = 0L,
501517
writeDuckDBTableParquet <-
502518
function(x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL,
503519
append = FALSE, offset = 0L, part = NULL, part_digits = 0L,
504-
cluster_by = NULL, ...)
520+
cluster_by = NULL, index_max = NULL, ...)
505521
{
506522
if (!inherits(x, "DuckDBTable"))
507523
stop("'x' must be a DuckDBTable")
@@ -513,9 +529,24 @@ function(x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL,
513529
create = TRUE)
514530

515531
conn <- dbconn(x)
532+
533+
n <- nrow(x)
534+
index_type <- NULL
535+
if (!is.null(indexcol)) {
536+
idx_arrow <- if (isTRUE(append)) {
537+
readParquetSchema(prep$path,
538+
columns = indexcol)$GetFieldByName(indexcol)$type
539+
} else if (!is.null(index_max)) {
540+
arrowIntType(c(0, index_max))
541+
} else {
542+
arrowIntType(c(0, prep$offset + n))
543+
}
544+
index_type <- .duckdbIntTypeName(idx_arrow)
545+
}
546+
516547
built <- buildTableSelectSQL(x, indexcol = indexcol, keycol = keycol,
517548
dimtbl = dimtbl, offset = prep$offset,
518-
conn = conn)
549+
index_type = index_type, conn = conn)
519550
# A clustering key (cluster_by) chooses the physical row order to make
520551
# row-group zonemaps prune range queries on its columns.
521552
spec <- .asClusterSpec(cluster_by)
@@ -530,7 +561,6 @@ function(x, path, indexcol = "__index__", keycol = "__name__", dimtbl = NULL,
530561
order_cols = order_cols)
531562
DBI::dbExecute(conn, copy_sql)
532563

533-
n <- nrow(x)
534564
sample_n <- min(100L, max(1L, n))
535565
sample_df <- as.data.frame(arrow::read_parquet(prep$pq_path))
536566
if (nrow(sample_df) > sample_n)

man/parquet-io.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-parquet-io.R

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,3 +424,47 @@ test_that("reading wide numeric types warns about possible precision loss", {
424424
expect_no_warning(DuckDBDataFrame(tf2, datacols = "d", keycol = "id"),
425425
message = "precision")
426426
})
427+
428+
.f1IndexType <- function(dir) {
429+
fs <- sort(list.files(dir, pattern = "parquet$", recursive = TRUE,
430+
full.names = TRUE))
431+
vapply(fs, function(p)
432+
arrow::ParquetFileReader$create(p)$GetSchema()$
433+
GetFieldByName("__index__")$type$ToString(), character(1L))
434+
}
435+
436+
test_that("lazy writeParquet narrows __index__ and honors index_max", {
437+
skip_if_not_installed("arrow")
438+
src <- tempfile(fileext = ".parquet"); on.exit(unlink(src), add = TRUE)
439+
arrow::write_parquet(data.frame(v = 1:5), src)
440+
ddf <- DuckDBDataFrame(src)
441+
442+
# Small lazy write narrows (uint8), no longer defaults to int64/BIGINT.
443+
out <- tempfile()
444+
writeDuckDBTableParquet(ddf, out, indexcol = "__index__", keycol = NULL)
445+
expect_identical(unname(.f1IndexType(out)), "uint8")
446+
447+
# index_max = Inf forces int64 (parity with the in-memory writer).
448+
out2 <- tempfile()
449+
writeDuckDBTableParquet(ddf, out2, indexcol = "__index__", keycol = NULL,
450+
index_max = Inf)
451+
expect_identical(unname(.f1IndexType(out2)), "int64")
452+
})
453+
454+
test_that("lazy append pins __index__ to part 0's type (schema-consistent parts)", {
455+
skip_if_not_installed("arrow")
456+
src <- tempfile(fileext = ".parquet"); on.exit(unlink(src), add = TRUE)
457+
arrow::write_parquet(data.frame(v = 1:5), src)
458+
ddf <- DuckDBDataFrame(src)
459+
460+
dir <- tempfile()
461+
writeDuckDBTableParquet(ddf, dir, indexcol = "__index__", keycol = NULL,
462+
part = 0L, part_digits = 2L, append = FALSE,
463+
index_max = Inf) # part 0 int64
464+
writeDuckDBTableParquet(ddf, dir, indexcol = "__index__", keycol = NULL,
465+
offset = 5, part = 1L, part_digits = 2L,
466+
append = TRUE) # no index_max
467+
types <- .f1IndexType(dir)
468+
expect_length(types, 2L)
469+
expect_true(all(types == "int64")) # append pinned to part 0, not narrowed
470+
})

0 commit comments

Comments
 (0)