Skip to content

Commit 06a5edc

Browse files
authored
feat: Implement dbListObjects() for attached SQLite databases with schema prefix support (#690)
1 parent 6bb2a70 commit 06a5edc

File tree

5 files changed

+175
-2
lines changed

5 files changed

+175
-2
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ Collate:
105105
'dbIsValid_SQLiteConnection.R'
106106
'dbIsValid_SQLiteDriver.R'
107107
'dbIsValid_SQLiteResult.R'
108+
'dbListObjects_SQLiteConnection.R'
108109
'dbListResults_SQLiteConnection.R'
109110
'dbListTables_SQLiteConnection.R'
110111
'dbQuoteIdentifier_SQLiteConnection_SQL.R'

R/dbListObjects_SQLiteConnection.R

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#' @rdname SQLiteConnection-class
2+
#' @usage NULL
3+
dbListObjects_SQLiteConnection <- function(conn, prefix = NULL, ...) {
4+
if (!is.null(prefix)) {
5+
id <- as.list(dbUnquoteIdentifier(conn, dbQuoteIdentifier(conn, prefix))[[1]]@name)
6+
# `Id(schema = "mydb")` round-trips through dbQuoteIdentifier/dbUnquoteIdentifier
7+
# as a single-component identifier (table slot), so fall back to table if schema is absent.
8+
schema <- id[["schema"]] %||% id[["table"]]
9+
10+
sql <- sqliteListTablesQuery(conn, schema)
11+
rs <- dbSendQuery_SQLiteConnection_character(conn, sql)
12+
on.exit(dbClearResult_SQLiteResult(rs), add = TRUE)
13+
names <- dbFetch_SQLiteResult(rs)$name
14+
15+
tables <- lapply(names, function(t) Id(schema = schema, table = t))
16+
is_prefix <- rep(FALSE, length(tables))
17+
} else {
18+
rs <- dbSendQuery_SQLiteConnection_character(conn, "SELECT name FROM pragma_database_list;")
19+
on.exit(dbClearResult_SQLiteResult(rs), add = TRUE)
20+
schemas <- dbFetch_SQLiteResult(rs)$name
21+
dbClearResult_SQLiteResult(rs)
22+
on.exit(NULL, add = FALSE)
23+
schema_ids <- lapply(schemas, function(s) Id(schema = s))
24+
25+
table_names <- dbListTables_SQLiteConnection(conn)
26+
table_ids <- lapply(table_names, function(t) Id(table = t))
27+
28+
tables <- c(table_ids, schema_ids)
29+
is_prefix <- c(rep(FALSE, length(table_ids)), rep(TRUE, length(schema_ids)))
30+
}
31+
32+
data.frame(
33+
table = I(unname(tables)),
34+
is_prefix = is_prefix,
35+
stringsAsFactors = FALSE
36+
)
37+
}
38+
#' @rdname SQLiteConnection-class
39+
#' @export
40+
setMethod("dbListObjects", "SQLiteConnection", dbListObjects_SQLiteConnection)

man/SQLiteConnection-class.Rd

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# dbListObjects snapshot - empty memory db
2+
3+
Code
4+
dbListObjects(con)
5+
Output
6+
table is_prefix
7+
1 <Id> "main" TRUE
8+
9+
# dbListObjects snapshot - with prefix
10+
11+
Code
12+
dbListObjects(con, prefix = Id(schema = "main"))
13+
Output
14+
table is_prefix
15+
1 <Id> "main"."bar" FALSE
16+
2 <Id> "main"."foo" FALSE
17+
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
test_that("dbListObjects returns schemas as prefixes and tables directly", {
2+
con <- memory_db()
3+
on.exit(dbDisconnect(con), add = TRUE)
4+
5+
# No tables yet
6+
result <- dbListObjects(con)
7+
expect_s3_class(result, "data.frame")
8+
expect_named(result, c("table", "is_prefix"))
9+
# Should include at least "main" as a schema prefix
10+
schema_names <- vapply(result$table[result$is_prefix], function(x) x@name[["schema"]], character(1))
11+
expect_true("main" %in% schema_names)
12+
13+
# Add a table
14+
dbWriteTable(con, "mytable", data.frame(a = 1))
15+
result2 <- dbListObjects(con)
16+
table_names <- vapply(result2$table[!result2$is_prefix], function(x) x@name[["table"]], character(1))
17+
expect_true("mytable" %in% table_names)
18+
})
19+
20+
test_that("dbListObjects snapshot - empty memory db", {
21+
con <- memory_db()
22+
on.exit(dbDisconnect(con), add = TRUE)
23+
24+
expect_snapshot(dbListObjects(con))
25+
})
26+
27+
test_that("dbListObjects snapshot - with prefix", {
28+
con <- memory_db()
29+
on.exit(dbDisconnect(con), add = TRUE)
30+
31+
dbWriteTable(con, "foo", data.frame(x = 1))
32+
dbWriteTable(con, "bar", data.frame(y = 2))
33+
34+
expect_snapshot(dbListObjects(con, prefix = Id(schema = "main")))
35+
})
36+
37+
test_that("dbListObjects with schema prefix lists objects in that schema", {
38+
db1 <- tempfile(fileext = ".sqlite")
39+
db2 <- tempfile(fileext = ".sqlite")
40+
on.exit(
41+
{
42+
unlink(c(db1, db2), force = TRUE)
43+
},
44+
add = TRUE
45+
)
46+
47+
# Create db2 with a table and view
48+
con2 <- dbConnect(SQLite(), db2)
49+
dbExecute(con2, "CREATE TABLE attached_table (id INTEGER, name TEXT)")
50+
dbExecute(con2, "CREATE VIEW attached_view AS SELECT * FROM attached_table")
51+
dbDisconnect(con2)
52+
53+
# Connect to db1 and attach db2 as "mydb"
54+
con1 <- dbConnect(SQLite(), db1)
55+
on.exit(dbDisconnect(con1), add = TRUE)
56+
dbExecute(con1, "CREATE TABLE main_table (id INTEGER)")
57+
dbExecute(con1, paste0("ATTACH DATABASE '", db2, "' AS mydb"))
58+
59+
# Without prefix: schemas are listed as prefixes
60+
result <- dbListObjects(con1)
61+
schema_names <- vapply(result$table[result$is_prefix], function(x) x@name[["schema"]], character(1))
62+
expect_true("mydb" %in% schema_names)
63+
expect_true("main" %in% schema_names)
64+
65+
# With schema prefix: lists tables/views in attached database
66+
result_schema <- dbListObjects(con1, prefix = Id(schema = "mydb"))
67+
expect_s3_class(result_schema, "data.frame")
68+
expect_named(result_schema, c("table", "is_prefix"))
69+
expect_false(any(result_schema$is_prefix))
70+
71+
obj_names <- vapply(result_schema$table, function(x) x@name[["table"]], character(1))
72+
expect_setequal(obj_names, c("attached_table", "attached_view"))
73+
74+
# Verify schema is set in returned Ids
75+
obj_schemas <- vapply(result_schema$table, function(x) x@name[["schema"]], character(1))
76+
expect_true(all(obj_schemas == "mydb"))
77+
})
78+
79+
test_that("dbListObjects with main schema prefix lists main tables", {
80+
con <- memory_db()
81+
on.exit(dbDisconnect(con), add = TRUE)
82+
83+
dbWriteTable(con, "foo", data.frame(x = 1))
84+
dbWriteTable(con, "bar", data.frame(y = 2))
85+
86+
result <- dbListObjects(con, prefix = Id(schema = "main"))
87+
expect_false(any(result$is_prefix))
88+
obj_names <- vapply(result$table, function(x) x@name[["table"]], character(1))
89+
expect_setequal(obj_names, c("foo", "bar"))
90+
obj_schemas <- vapply(result$table, function(x) x@name[["schema"]], character(1))
91+
expect_true(all(obj_schemas == "main"))
92+
})
93+
94+
test_that("dbListObjects returns empty data frame for empty schema", {
95+
db1 <- tempfile(fileext = ".sqlite")
96+
db2 <- tempfile(fileext = ".sqlite")
97+
on.exit(unlink(c(db1, db2), force = TRUE), add = TRUE)
98+
99+
# Create empty db2
100+
con2 <- dbConnect(SQLite(), db2)
101+
dbDisconnect(con2)
102+
103+
con1 <- dbConnect(SQLite(), db1)
104+
on.exit(dbDisconnect(con1), add = TRUE)
105+
dbExecute(con1, paste0("ATTACH DATABASE '", db2, "' AS emptydb"))
106+
107+
result <- dbListObjects(con1, prefix = Id(schema = "emptydb"))
108+
expect_s3_class(result, "data.frame")
109+
expect_equal(nrow(result), 0L)
110+
})

0 commit comments

Comments
 (0)