Skip to content

Commit c69bb95

Browse files
zacdav-dbZac Davies
andauthored
Fix DBI binary standard writes (#246)
Co-authored-by: Zac Davies <zachary.davies+data@databricks.com>
1 parent 9fabe75 commit c69bb95

4 files changed

Lines changed: 114 additions & 42 deletions

File tree

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# brickster (development version)
22

3+
- Fixed `dbWriteTable()` and `dbAppendTable()` standard-path writes for binary columns, which now use Databricks `BINARY` types and `X'...'` literals when no staging volume is configured (#245)
34
- Fixed `git_source()` erroring when `type` was left at its default
45
- Fixed Unity Catalog volume file requests so `db_volume_*` paths containing spaces are encoded correctly (#231)
56
- `db_cluster_events()` now forwards the `event_types` argument to the API, which was previously ignored

R/databricks-dbi.R

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,17 @@ db_clean_table_name <- function(name) {
10611061
gsub('^\"|\"$', '', name)
10621062
}
10631063

1064+
# Check whether an R column represents Databricks binary values.
1065+
db_is_binary_column <- function(x) {
1066+
if (inherits(x, "blob") || is.raw(x)) {
1067+
return(TRUE)
1068+
}
1069+
1070+
is.list(x) &&
1071+
purrr::every(x, \(value) is.raw(value) || is.null(value)) &&
1072+
purrr::some(x, is.raw)
1073+
}
1074+
10641075
#' Map R data types to Databricks SQL types
10651076
#' @param dbObj A DatabricksConnection object
10661077
#' @param obj R object(s) to get SQL types for
@@ -1072,6 +1083,10 @@ setMethod("dbDataType", "DatabricksConnection", function(dbObj, obj, ...) {
10721083
purrr::map_chr(
10731084
obj,
10741085
function(x) {
1086+
if (db_is_binary_column(x)) {
1087+
return("BINARY")
1088+
}
1089+
10751090
switch(
10761091
class(x)[1],
10771092
logical = "BOOLEAN",
@@ -1584,31 +1599,51 @@ db_create_table_from_data <- function(
15841599
#' @keywords internal
15851600
db_generate_typed_values_sql <- function(conn, data) {
15861601
# Convert each row to SQL values with proper typing
1587-
row_values <- apply(data, 1, function(row) {
1588-
values <- purrr::map2_chr(row, names(data), function(val, col_name) {
1589-
col_data <- data[[col_name]]
1590-
1591-
if (is.na(val)) {
1592-
"NULL"
1593-
} else if (is.logical(col_data)) {
1594-
if (as.logical(val)) "TRUE" else "FALSE"
1595-
} else if (is.numeric(col_data)) {
1596-
# Don't quote numeric values to preserve type
1597-
as.character(val)
1598-
} else if (is.character(col_data)) {
1599-
# Quote string values and escape single quotes
1600-
db_escape_string_literal(conn, val)
1601-
} else {
1602-
# Default to quoted string for other types
1603-
db_escape_string_literal(conn, as.character(val))
1604-
}
1602+
row_values <- purrr::pmap_chr(data, function(...) {
1603+
row <- list(...)
1604+
values <- purrr::imap_chr(row, function(val, col_name) {
1605+
db_format_typed_value_sql(conn, val, data[[col_name]])
16051606
})
16061607
paste0("(", paste(values, collapse = ", "), ")")
16071608
})
16081609

16091610
paste(row_values, collapse = ", ")
16101611
}
16111612

1613+
# Format a single R value for inline SQL VALUES.
1614+
db_format_typed_value_sql <- function(conn, val, col_data) {
1615+
if (db_is_missing_sql_value(val)) {
1616+
"NULL"
1617+
} else if (db_is_binary_column(col_data)) {
1618+
db_binary_literal(val)
1619+
} else if (is.logical(col_data)) {
1620+
if (as.logical(val)) "TRUE" else "FALSE"
1621+
} else if (is.numeric(col_data)) {
1622+
# Don't quote numeric values to preserve type
1623+
as.character(val)
1624+
} else if (is.character(col_data)) {
1625+
# Quote string values and escape single quotes
1626+
db_escape_string_literal(conn, val)
1627+
} else {
1628+
# Default to quoted string for other types
1629+
db_escape_string_literal(conn, as.character(val))
1630+
}
1631+
}
1632+
1633+
# Check whether a row value should be rendered as SQL NULL.
1634+
db_is_missing_sql_value <- function(val) {
1635+
is.null(val) || (length(val) == 1L && is.na(val))
1636+
}
1637+
1638+
# Format raw bytes as a Databricks binary literal.
1639+
db_binary_literal <- function(val) {
1640+
if (!is.raw(val)) {
1641+
cli::cli_abort("Binary columns must contain raw vectors or `NULL` values.")
1642+
}
1643+
1644+
paste0("X'", paste(toupper(as.character(val)), collapse = ""), "'")
1645+
}
1646+
16121647
#' Escape string literals for inline SQL VALUES
16131648
#' @keywords internal
16141649
db_escape_string_literal <- function(conn, val) {

R/databricks-dbplyr.R

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -228,30 +228,7 @@ copy_to.DatabricksConnection <- function(
228228
#' @returns SQL VALUES clause
229229
#' @keywords internal
230230
db_generate_typed_values_sql_for_view <- function(con, data) {
231-
# Convert each row to SQL values with proper typing
232-
row_values <- apply(data, 1, function(row) {
233-
values <- purrr::map2_chr(row, names(data), function(val, col_name) {
234-
col_data <- data[[col_name]]
235-
236-
if (is.na(val)) {
237-
"NULL"
238-
} else if (is.logical(col_data)) {
239-
if (as.logical(val)) "TRUE" else "FALSE"
240-
} else if (is.numeric(col_data)) {
241-
# Don't quote numeric values to preserve type
242-
as.character(val)
243-
} else if (is.character(col_data)) {
244-
# Quote string values and escape single quotes
245-
db_escape_string_literal(con, val)
246-
} else {
247-
# Default to quoted string for other types
248-
db_escape_string_literal(con, as.character(val))
249-
}
250-
})
251-
paste0("(", paste(values, collapse = ", "), ")")
252-
})
253-
254-
paste(row_values, collapse = ", ")
231+
db_generate_typed_values_sql(con, data)
255232
}
256233

257234
# Slightly modified version of sparklyr/R/dplyr_sql_translation.R (thank you!)

tests/testthat/test-databricks-dbi-offline-helpers.R

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,65 @@ test_that("dbWriteTable routes to standard path when volume staging is not prefe
204204
expect_false(state$show_progress)
205205
})
206206

207+
test_that("dbWriteTable standard path supports binary columns", {
208+
con <- make_dbi_test_con(show_progress = FALSE)
209+
value <- data.frame(id = 1:3)
210+
value$payload <- I(list(as.raw(c(0, 15, 255)), raw(0), NULL))
211+
state <- new.env(parent = emptyenv())
212+
state$sql <- character(0)
213+
214+
local_mocked_bindings(
215+
dbExistsTable = function(...) FALSE,
216+
db_should_use_volume_method = function(...) FALSE,
217+
dbExecute = function(conn, statement, ...) {
218+
state$sql <- c(state$sql, statement)
219+
0L
220+
},
221+
db_sql_exec_and_wait = function(statement, ...) {
222+
state$sql <- c(state$sql, statement)
223+
list(status = list(state = "SUCCEEDED"))
224+
},
225+
.package = "brickster"
226+
)
227+
228+
expect_invisible(dbWriteTable(con, "tbl_binary", value, overwrite = TRUE))
229+
expect_identical(
230+
state$sql[[1]],
231+
"CREATE OR REPLACE TABLE `tbl_binary` (`id` INT, `payload` BINARY)"
232+
)
233+
expect_identical(
234+
state$sql[[2]],
235+
paste0(
236+
"INSERT INTO `tbl_binary` (`id`, `payload`) VALUES ",
237+
"(1, X'000FFF'), (2, X''), (3, NULL)"
238+
)
239+
)
240+
})
241+
242+
test_that("dbAppendTable standard path supports binary columns", {
243+
con <- make_dbi_test_con(show_progress = FALSE)
244+
value <- data.frame(id = 4L)
245+
value$payload <- I(list(as.raw(171)))
246+
state <- new.env(parent = emptyenv())
247+
state$sql <- NULL
248+
249+
local_mocked_bindings(
250+
dbExistsTable = function(...) TRUE,
251+
db_should_use_volume_method = function(...) FALSE,
252+
db_sql_exec_and_wait = function(statement, ...) {
253+
state$sql <- statement
254+
list(status = list(state = "SUCCEEDED"))
255+
},
256+
.package = "brickster"
257+
)
258+
259+
expect_invisible(dbAppendTable(con, "tbl_binary", value))
260+
expect_identical(
261+
state$sql,
262+
"INSERT INTO `tbl_binary` (`id`, `payload`) VALUES (4, X'AB')"
263+
)
264+
})
265+
207266
test_that("dbWriteTable handles row.names consistently for character and Id signatures", {
208267
con <- make_dbi_test_con()
209268
state <- new.env(parent = emptyenv())

0 commit comments

Comments
 (0)