Skip to content

Commit b50d686

Browse files
authored
Add parens for MS access joins (#1688)
Fixes #1576
1 parent e2d611d commit b50d686

File tree

7 files changed

+108
-23
lines changed

7 files changed

+108
-23
lines changed

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ S3method(sql_query_join,DBIConnection)
293293
S3method(sql_query_join,MariaDBConnection)
294294
S3method(sql_query_join,MySQL)
295295
S3method(sql_query_join,MySQLConnection)
296+
S3method(sql_query_multi_join,ACCESS)
296297
S3method(sql_query_multi_join,DBIConnection)
297298
S3method(sql_query_rows,DBIConnection)
298299
S3method(sql_query_save,"Microsoft SQL Server")

NEWS.md

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

3+
* MS Access now correctly generates SQL for multiple joins by adding required parentheses (#1576).
34
* `.data$col`, `.data[[col]]`, `.env$var`, and `.env$[[var]]` now work correctly inside `across()` (#1520).
45
* New `.sql` pronoun makes it a little easier to use known SQL functions in packages, requiring only `@importFrom dbplyr .sql` (#1117).
56
* `join_by(between())` now correctly handles column renames (#1572).

R/backend-access.R

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,49 @@ sql_escape_datetime.ACCESS <- function(con, x) {
205205
supports_window_clause.ACCESS <- function(con) {
206206
TRUE
207207
}
208+
209+
#' @export
210+
sql_query_multi_join.ACCESS <- function(
211+
con,
212+
x,
213+
joins,
214+
table_names,
215+
by_list,
216+
select,
217+
...,
218+
lvl = 0
219+
) {
220+
if (vctrs::vec_duplicate_any(table_names)) {
221+
cli_abort("{.arg table_names} must be unique.")
222+
}
223+
224+
from <- dbplyr_sql_subquery(con, x, name = table_names[[1]], lvl = lvl)
225+
names <- table_names[-1]
226+
tables <- joins$table
227+
types <- toupper(paste0(joins$type, " JOIN"))
228+
229+
n_joins <- length(types)
230+
231+
# MS Access requires: ((t1 JOIN t2 ON ...) JOIN t3 ON ...)
232+
# N joins need N opening parens, each ON clause followed by closing paren
233+
open_parens <- strrep("(", n_joins)
234+
from <- sql(paste0(open_parens, from))
235+
236+
for (i in seq_len(n_joins)) {
237+
table <- dbplyr_sql_subquery(con, tables[[i]], name = names[[i]], lvl = lvl)
238+
by <- joins$by[[i]]
239+
on <- sql_join_tbls(con, by = by, na_matches = by$na_matches)
240+
241+
from <- sql(paste0(
242+
paste0(from, "\n"),
243+
paste0(types[[i]], " ", table, "\n"),
244+
paste0("ON ", on, ")")
245+
))
246+
}
247+
248+
clauses <- list(
249+
sql_clause_select(con, select),
250+
sql_clause_from(from)
251+
)
252+
sql_format_clauses(clauses, lvl = lvl, con = con)
253+
}

R/db-sql.R

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -531,36 +531,27 @@ sql_query_multi_join.DBIConnection <- function(
531531
}
532532

533533
from <- dbplyr_sql_subquery(con, x, name = table_names[[1]], lvl = lvl)
534-
535-
join_table_queries <- purrr::map2(
536-
joins$table,
537-
table_names[-1],
538-
function(table, name) {
539-
dbplyr_sql_subquery(con, table, name = name, lvl = lvl)
540-
}
541-
)
534+
names <- table_names[-1]
535+
tables <- joins$table
542536
types <- toupper(paste0(joins$type, " JOIN"))
543-
join_clauses <- purrr::map2(
544-
types,
545-
join_table_queries,
546-
\(join_kw, from) sql_clause(join_kw, from)
547-
)
548537

549-
on_clauses <- purrr::map(
550-
joins$by,
551-
function(by) {
552-
on <- sql_join_tbls(con, by = by, na_matches = by$na_matches)
553-
sql_clause("ON", on, sep = " AND", parens = TRUE, lvl = 1)
554-
}
555-
)
556-
join_on_clauses <- vctrs::vec_interleave(join_clauses, on_clauses)
538+
n_joins <- length(types)
539+
out <- vector("list", n_joins * 2)
540+
541+
for (i in seq_len(n_joins)) {
542+
table <- dbplyr_sql_subquery(con, tables[[i]], name = names[[i]], lvl = lvl)
543+
out[[2 * i - 1]] <- sql_clause(types[[i]], table)
544+
545+
by <- joins$by[[i]]
546+
on <- sql_join_tbls(con, by = by, na_matches = by$na_matches)
547+
out[[2 * i]] <- sql_clause("ON", on, sep = " AND", parens = TRUE, lvl = 1)
548+
}
557549

558550
clauses <- list2(
559551
sql_clause_select(con, select),
560552
sql_clause_from(from),
561-
!!!join_on_clauses
553+
!!!out
562554
)
563-
564555
sql_format_clauses(clauses, lvl = lvl, con = con)
565556
}
566557

R/sql.R

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ sql <- function(...) {
1212
structure(x, class = c("sql", "character"))
1313
}
1414

15+
sql_c <- function(...) {
16+
sql(paste0(unlist(list(...)), collapse = ""))
17+
}
18+
1519
#' @export
1620
`[.sql` <- function(x, i) {
1721
sql(NextMethod())

tests/testthat/_snaps/backend-access.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,31 @@
1616
SELECT TOP 6 `df`.*
1717
FROM `df`
1818

19+
# multiple joins use parens #1576
20+
21+
Code
22+
inner_join(left_join(lf1, lf2, by = "x"), lf3, by = "x")
23+
Output
24+
<SQL>
25+
SELECT `lf1`.*, `b`, `c`
26+
FROM ((`lf1`
27+
LEFT JOIN `lf2`
28+
ON `lf1`.`x` = `lf2`.`x`)
29+
INNER JOIN `lf3`
30+
ON `lf1`.`x` = `lf3`.`x`)
31+
32+
---
33+
34+
Code
35+
left_join(inner_join(left_join(lf1, lf2, by = "x"), lf3, by = "x"), lf4, by = "x")
36+
Output
37+
<SQL>
38+
SELECT `lf1`.*, `b`, `c`, `d`
39+
FROM (((`lf1`
40+
LEFT JOIN `lf2`
41+
ON `lf1`.`x` = `lf2`.`x`)
42+
INNER JOIN `lf3`
43+
ON `lf1`.`x` = `lf3`.`x`)
44+
LEFT JOIN `lf4`
45+
ON `lf1`.`x` = `lf4`.`x`)
46+

tests/testthat/test-backend-access.R

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,17 @@ test_that("queries translate correctly", {
109109
mf <- lazy_frame(x = 1, con = simulate_access())
110110
expect_snapshot(mf |> head())
111111
})
112+
113+
test_that("multiple joins use parens #1576", {
114+
lf1 <- lazy_frame(x = 1, a = 1, .name = "lf1", con = simulate_access())
115+
lf2 <- lazy_frame(x = 1, b = 1, .name = "lf2", con = simulate_access())
116+
lf3 <- lazy_frame(x = 1, c = 1, .name = "lf3", con = simulate_access())
117+
lf4 <- lazy_frame(x = 1, d = 1, .name = "lf4", con = simulate_access())
118+
119+
expect_snapshot(left_join(lf1, lf2, by = "x") |> inner_join(lf3, by = "x"))
120+
expect_snapshot(
121+
left_join(lf1, lf2, by = "x") |>
122+
inner_join(lf3, by = "x") |>
123+
left_join(lf4, by = "x")
124+
)
125+
})

0 commit comments

Comments
 (0)