Skip to content

Commit 80b745c

Browse files
detulehadley
andcommitted
SQL Server: Index local temp tables (#600)
Update handling ( listing, writing ) of local temp tables. --------- Co-authored-by: Hadley Wickham <[email protected]>
1 parent 9aba357 commit 80b745c

File tree

7 files changed

+316
-11
lines changed

7 files changed

+316
-11
lines changed

NAMESPACE

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ S3method(odbcListColumns,OdbcConnection)
2121
S3method(odbcListObjectTypes,default)
2222
S3method(odbcListObjects,OdbcConnection)
2323
S3method(odbcPreviewObject,OdbcConnection)
24+
export(isTempTable)
2425
export(odbc)
2526
export(odbcConnectionActions)
2627
export(odbcConnectionColumns)

NEWS.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
# odbc (development version)
2+
* SQL Server: Improved handling for local temp tables in dbWrite, dbAppendTable,
3+
dbExistTable (@detule, #600)
4+
* Teradata: Improved handling for temp tables (@detule and @But2ene, #589, 590)
5+
* Oracle: Fix regression when falling back to odbcConnectionColumns to
6+
describe column data types (@detule, #587)
27

38
# odbc 1.3.5
49

R/Table.R

+3-2
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,8 @@ createFields <- function(con, fields, field.types, row.names) {
208208
setMethod(
209209
"dbExistsTable", c("OdbcConnection", "Id"),
210210
function(conn, name, ...) {
211-
name@name[["table"]] %in% odbcConnectionTables(conn,
211+
dbExistsTable(
212+
conn,
212213
name = id_field(name, "table"),
213214
catalog_name = id_field(name, "catalog"),
214215
schema_name = id_field(name, "schema")
@@ -231,6 +232,6 @@ setMethod(
231232
"dbExistsTable", c("OdbcConnection", "character"),
232233
function(conn, name, ...) {
233234
stopifnot(length(name) == 1)
234-
df <- odbcConnectionTables(conn, name = name)
235+
df <- odbcConnectionTables(conn, name = name, ...)
235236
NROW(df) > 0
236237
})

R/db.R

+121-9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,45 @@
1+
2+
#' Helper method used to determine if a table identifier is that
3+
#' of a temporary table.
4+
#'
5+
#' Currently implemented only for select back-ends where
6+
#' we have a use for it (SQL Server, for example). Generic, in case
7+
#' we develop a broader use case.
8+
#' @param conn OdbcConnection
9+
#' @param name Table name
10+
#' @param ... additional parameters to methods
11+
#' @rdname isTempTable
12+
#' @export
13+
setGeneric(
14+
"isTempTable",
15+
valueClass = "logical",
16+
function(conn, name, ...) {
17+
standardGeneric("isTempTable")
18+
}
19+
)
20+
21+
#' @rdname isTempTable
22+
setMethod(
23+
"isTempTable",
24+
c("OdbcConnection", "Id"),
25+
function(conn, name, ...) {
26+
isTempTable(conn,
27+
name = id_field(name, "table"),
28+
catalog_name = id_field(name, "catalog"),
29+
schema_name = id_field(name, "schema"),
30+
...)
31+
}
32+
)
33+
34+
#' @rdname isTempTable
35+
setMethod(
36+
"isTempTable",
37+
c("OdbcConnection", "SQL"),
38+
function(conn, name, ...) {
39+
isTempTable(conn, dbUnquoteIdentifier(conn, name)[[1]], ...)
40+
}
41+
)
42+
143
# Oracle --------------------------------------------------------------------
244

345
# Simple class prototype to avoid messages about unknown classes from setMethod
@@ -203,19 +245,89 @@ setMethod("sqlCreateTable", "DB2/AIX64",
203245

204246
# Microsoft SQL Server ---------------------------------------------------------
205247

206-
# Simple class prototype to avoid messages about unknown classes from setMethod
248+
#' Simple class prototype to avoid messages about unknown classes from setMethod
249+
#' @rdname SQLServer
250+
#' @usage NULL
207251
setClass("Microsoft SQL Server", where = class_cache)
208252

209-
# For SQL Server, conn@quote will return the quotation mark, however
210-
# both quotation marks as well as square bracket are used interchangeably for
211-
# delimited identifiers. See:
212-
# https://learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-ver16
213-
# Therefore strip the brackets first, and then call the DBI method that strips
214-
# the quotation marks.
215-
# TODO: the generic implementation in DBI should take a quote char as
216-
# parameter.
253+
#' SQL Server specific implementation.
254+
#'
255+
#' For SQL Server, `conn@quote` will return the quotation mark, however
256+
#' both quotation marks as well as square bracket are used interchangeably for
257+
#' delimited identifiers. See:
258+
#' \url{https://learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-ver16}
259+
#' Therefore strip the brackets first, and then call the DBI method that strips
260+
#' the quotation marks.
261+
#' TODO: the generic implementation in DBI should take a quote char as
262+
#' parameter.
263+
#'
264+
#' @rdname SQLServer
265+
#' @docType methods
266+
#' @inheritParams DBI::dbUnquoteIdentifier
267+
#' @usage NULL
217268
setMethod("dbUnquoteIdentifier", c("Microsoft SQL Server", "SQL"),
218269
function(conn, x, ...) {
219270
x <- gsub("(\\[)([^\\.]+?)(\\])", "\\2", x)
220271
callNextMethod( conn, x, ... )
221272
})
273+
274+
#' SQL Server specific implementation.
275+
#'
276+
#' Local temp tables are stored as
277+
#' \code{ [tempdb].[dbo].[#name]________(padding using underscores)[numeric identifier] }
278+
#'
279+
#' True if:
280+
#' - If catalog_name is supplied it must equal "temdb" or "%" ( wildcard )
281+
#' - Name must start with "#" followd by a non-"#" character
282+
#' @rdname SQLServer
283+
#' @usage NULL
284+
setMethod("isTempTable", c("Microsoft SQL Server", "character"),
285+
function(conn, name, catalog_name = NULL, schema_name = NULL, ...) {
286+
if ( !is.null(catalog_name) &&
287+
catalog_name != "%" &&
288+
length(catalog_name ) > 0 &&
289+
catalog_name != "tempdb" ) {
290+
return(FALSE)
291+
}
292+
293+
if ( !grepl("^[#][^#]", name ) ) {
294+
return(FALSE)
295+
}
296+
return(TRUE)
297+
})
298+
299+
#' SQL server specific dbExistsTable implementation that accounts for
300+
#' local temp tables.
301+
#'
302+
#' If we can identify that the name is that of a local temp table
303+
#' then adjust the identifier and query appropriately.
304+
#'
305+
#' Note, the implementation here is such that it assumes the metadata attribute is
306+
#' set such that catalog functions accept wildcard entries.
307+
#'
308+
#' Driver note. OEM driver will return correctly for
309+
#' name, \code{catalog_name = "tempdb"} in some circumstances. For exmaple
310+
#' if the name has no underscores to beginwith. FreeTDS, will not index
311+
#' the table correctly unless name is adjusted ( allowed trailing wildcards to
312+
#' accomodate trailing underscores and postfix ).
313+
#'
314+
#' Therefore, in all cases query for \code{name___%}.
315+
#' @rdname SQLServer
316+
#' @docType methods
317+
#' @aliases dbExistsTable
318+
#' @inherit DBI::dbExistsTable
319+
#' @usage NULL
320+
setMethod(
321+
"dbExistsTable", c("Microsoft SQL Server", "character"),
322+
function(conn, name, ...) {
323+
stopifnot(length(name) == 1)
324+
if ( isTempTable( conn, name, ... ) )
325+
{
326+
name <- paste0(name, "\\_\\_\\_%");
327+
df <- odbcConnectionTables(conn, name, catalog_name = "tempdb", schema_name = "dbo")
328+
}
329+
else {
330+
df <- odbcConnectionTables(conn, name = name, ...)
331+
}
332+
NROW(df) > 0
333+
})

man/SQLServer.Rd

+131
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/isTempTable.Rd

+27
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-SQLServer.R

+28
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,32 @@ test_that("SQLServer", {
234234
expect_equal(nrow(dbFetch(rs, n = 0)), 0)
235235
expect_equal(nrow(dbFetch(rs, n = 10)), 2)
236236
})
237+
238+
test_that("isTempTable tests", {
239+
con <- DBItest:::connect(DBItest:::get_default_context())
240+
expect_true( isTempTable(con, "#myTmp"))
241+
expect_true( isTempTable(con, "#myTmp", catalog_name = "tempdb" ))
242+
expect_true( isTempTable(con, "#myTmp", catalog_name = "%" ))
243+
expect_true( isTempTable(con, "#myTmp", catalog_name = NULL ))
244+
expect_true( !isTempTable(con, "##myTmp"))
245+
expect_true( !isTempTable(con, "#myTmp", catalog_name = "abc" ))
246+
})
247+
248+
test_that("dbExistsTable accounts for local temp tables", {
249+
con <- DBItest:::connect(DBItest:::get_default_context())
250+
tbl_name <- "#myTemp"
251+
tbl_name2 <- "##myTemp"
252+
tbl_name3 <- "#myTemp2"
253+
DBI::dbExecute(con, paste0("CREATE TABLE ", tbl_name, " (
254+
id int not null,
255+
primary key (id) )"), immediate = TRUE)
256+
expect_true( dbExistsTable( con, tbl_name) )
257+
expect_true( dbExistsTable( con, tbl_name, catalog_name = "tempdb") )
258+
# Fail because not recognized as temp table ( catalog not tempdb )
259+
expect_true( !dbExistsTable( con, tbl_name, catalog_name = "abc") )
260+
# Fail because not recognized as temp table ( second char "#" )
261+
expect_true( !dbExistsTable( con, tbl_name2, catalog_name = "tempdb" ) )
262+
# Fail because table not actually present
263+
expect_true( !dbExistsTable( con, tbl_name3, catalog_name = "tempdb" ) )
264+
})
237265
})

0 commit comments

Comments
 (0)