@@ -1046,6 +1046,64 @@ applyOracleClientEnv <- function(explicitDir = Sys.getenv("EXPLORATORY_ORACLE_CL
10461046# *.AL32UTF8 -> correct UTF-8.
10471047# So the character set has to be forced, not merely defaulted: a user who set NLS_LANG for the
10481048# ODBC based Oracle data source would otherwise silently corrupt Oracle (OCI) results.
1049+ # Parses the Oracle (OCI) additional parameters field ("key1=value1;key2=value2") into a list
1050+ # of arguments for ROracle::dbConnect.
1051+ #
1052+ # Unlike the ODBC based Oracle data source, where unrecognized keys are still handed to the
1053+ # driver, ROracle's dbConnect swallows anything it does not declare: the method signature is
1054+ # (username, password, dbname, prefetch, bulk_read, bulk_write, stmt_cache,
1055+ # external_credentials, sysdba, ...) and the ... is dropped rather than forwarded. A typo would
1056+ # therefore be silently ignored, so unknown keys are rejected here instead.
1057+ #
1058+ # Types are converted because ROracle rejects strings outright ("argument 'prefetch' must be a
1059+ # single logical value and cannot be 'TRUE'"), and it also refuses stmt_cache unless prefetch is
1060+ # enabled ("prefetch should be enabled for statement cache"), so that pairing is checked here to
1061+ # give a clearer message than the driver's.
1062+ ORACLE_OCI_LOGICAL_PARAMS <- c(" prefetch" , " external_credentials" , " sysdba" )
1063+ ORACLE_OCI_INTEGER_PARAMS <- c(" bulk_read" , " bulk_write" , " stmt_cache" )
1064+
1065+ parseOracleOCIAdditionalParams <- function (additionalParams ) {
1066+ if (is.null(additionalParams ) || length(additionalParams ) == 0 ||
1067+ is.na(additionalParams [[1 ]]) || trimws(additionalParams [[1 ]]) == " " ) {
1068+ return (list ())
1069+ }
1070+ entries <- strsplit(as.character(additionalParams [[1 ]]), " ;" , fixed = TRUE )[[1 ]]
1071+ entries <- trimws(entries )
1072+ entries <- entries [entries != " " ]
1073+ supported <- c(ORACLE_OCI_LOGICAL_PARAMS , ORACLE_OCI_INTEGER_PARAMS )
1074+ result <- list ()
1075+ for (entry in entries ) {
1076+ if (! grepl(" =" , entry , fixed = TRUE )) {
1077+ stop(paste0(" Invalid Oracle (OCI) additional parameter '" , entry ,
1078+ " '. Use the form key=value, separated by semicolons." ))
1079+ }
1080+ key <- trimws(sub(" =.*$" , " " , entry ))
1081+ value <- trimws(sub(" ^[^=]*=" , " " , entry ))
1082+ if (! (key %in% supported )) {
1083+ stop(paste0(" Unknown Oracle (OCI) additional parameter '" , key , " '. Supported parameters are: " ,
1084+ paste(supported , collapse = " , " ), " ." ))
1085+ }
1086+ if (key %in% ORACLE_OCI_LOGICAL_PARAMS ) {
1087+ upperValue <- toupper(value )
1088+ if (! (upperValue %in% c(" TRUE" , " FALSE" , " T" , " F" ))) {
1089+ stop(paste0(" Oracle (OCI) additional parameter '" , key , " ' must be TRUE or FALSE, but was '" , value , " '." ))
1090+ }
1091+ result [[key ]] <- upperValue %in% c(" TRUE" , " T" )
1092+ } else {
1093+ number <- suppressWarnings(as.integer(value ))
1094+ if (is.na(number )) {
1095+ stop(paste0(" Oracle (OCI) additional parameter '" , key , " ' must be a whole number, but was '" , value , " '." ))
1096+ }
1097+ result [[key ]] <- number
1098+ }
1099+ }
1100+ # ROracle fails the connection outright when a statement cache is requested without prefetch.
1101+ if (! is.null(result $ stmt_cache ) && result $ stmt_cache > 0 && ! isTRUE(result $ prefetch )) {
1102+ stop(" Oracle (OCI) additional parameter 'stmt_cache' requires 'prefetch=TRUE'." )
1103+ }
1104+ result
1105+ }
1106+
10491107oracleOCINlsLang <- function (currentNlsLang = Sys.getenv(" NLS_LANG" )) {
10501108 if (is.null(currentNlsLang ) || length(currentNlsLang ) == 0 || is.na(currentNlsLang [[1 ]]) ||
10511109 currentNlsLang [[1 ]] == " " ) {
@@ -1062,7 +1120,8 @@ oracleOCINlsLang <- function(currentNlsLang = Sys.getenv("NLS_LANG")) {
10621120# Single source of truth for the Oracle (OCI) connection pool key so that getDBConnection
10631121# and clearDBConnection can never drift apart. Every component is normalized here because
10641122# paste() silently drops NULL, which would produce a key that no clear call can match.
1065- oracleOCIPoolKey <- function (host , port , databaseName , username , timezone , bulkRead , connectionString ) {
1123+ oracleOCIPoolKey <- function (host , port , databaseName , username , timezone , bulkRead , connectionString ,
1124+ additionalParams = " " ) {
10661125 normalize <- function (value ) {
10671126 if (is.null(value ) || length(value ) == 0 || is.na(value [[1 ]])) " " else as.character(value [[1 ]])
10681127 }
@@ -1071,7 +1130,8 @@ oracleOCIPoolKey <- function(host, port, databaseName, username, timezone, bulkR
10711130 timezone <- " UTC" # matches the default applied when connecting.
10721131 }
10731132 paste(" oracleoci" , normalize(host ), normalize(port ), normalize(databaseName ),
1074- normalize(username ), timezone , normalize(bulkRead ), normalize(connectionString ), sep = " :" )
1133+ normalize(username ), timezone , normalize(bulkRead ), normalize(connectionString ),
1134+ normalize(additionalParams ), sep = " :" )
10751135}
10761136
10771137# Oracle rejects a trailing semicolon through OCI, unlike most SQL consoles, so drop one
@@ -1969,7 +2029,8 @@ getDBConnection <- function(type, host = NULL, port = "", databaseName = "", use
19692029 if (timezone == " " ) {
19702030 timezone <- " UTC" # if timezone is not provided use UTC as default, same as the ODBC based Oracle data source.
19712031 }
1972- key <- oracleOCIPoolKey(host , port , databaseName , username , timezone , bulkRead , connectionString )
2032+ key <- oracleOCIPoolKey(host , port , databaseName , username , timezone , bulkRead , connectionString ,
2033+ additionalParams )
19732034 # Only reuse a pooled connection when connection pooling is on (e.g. while the data source
19742035 # dialog is open), and always probe it first. Same reasoning as the oracle branch above. (tam#36429)
19752036 conn <- NULL
@@ -2025,15 +2086,23 @@ getDBConnection <- function(type, host = NULL, port = "", databaseName = "", use
20252086 on.exit({
20262087 if (previousNlsLang == " " ) Sys.unsetenv(" NLS_LANG" ) else Sys.setenv(NLS_LANG = previousNlsLang )
20272088 }, add = TRUE )
2028- conn <- ROracle :: dbConnect(
2029- ROracle :: Oracle(unicode_as_utf8 = TRUE ),
2030- username = username ,
2031- password = password ,
2032- dbname = dbname ,
2033- # Rows fetched per OCI round trip. This is the main reason this data source is faster
2034- # than the ODBC based one, so it is exposed on the connection as a user setting.
2035- bulk_read = bulkReadRows
2036- )
2089+ # Additional parameters are validated and type converted before they reach the driver.
2090+ # bulk_read has its own field on the connection, so that field wins and an additional
2091+ # parameter of the same name is ignored rather than silently overriding the visible value.
2092+ extraArgs <- parseOracleOCIAdditionalParams(additionalParams )
2093+ extraArgs $ bulk_read <- NULL
2094+ conn <- do.call(ROracle :: dbConnect , c(
2095+ list (
2096+ ROracle :: Oracle(unicode_as_utf8 = TRUE ),
2097+ username = username ,
2098+ password = password ,
2099+ dbname = dbname ,
2100+ # Rows fetched per OCI round trip. This is the main reason this data source is faster
2101+ # than the ODBC based one, so it is exposed on the connection as a user setting.
2102+ bulk_read = bulkReadRows
2103+ ),
2104+ extraArgs
2105+ ))
20372106 if (user_env $ pool_connection ) {
20382107 connection_pool [[key ]] <- conn
20392108 }
@@ -2140,7 +2209,8 @@ clearDBConnection <- function(type, host = NULL, port = NULL, databaseName, user
21402209 key <- paste(" oracle" , host , port , databaseName , username , timezone , sep = " :" )
21412210 } else if (type %in% c(" oracleoci" )) {
21422211 # Built through the same helper getDBConnection uses, so the keys cannot drift apart.
2143- key <- oracleOCIPoolKey(host , port , databaseName , username , timezone , bulkRead , connectionString )
2212+ key <- oracleOCIPoolKey(host , port , databaseName , username , timezone , bulkRead , connectionString ,
2213+ additionalParams )
21442214 }
21452215 rm(list = key , envir = connection_pool ) # Even if there is no matching key, this is harmless.
21462216}
@@ -2309,13 +2379,15 @@ queryMySQL <- function(host, port, databaseName, username, password, numOfRows =
23092379# ' @param connectionString (optional) TNS alias, full connect descriptor, or EZConnect string.
23102380# ' When provided it is used as is and host, port and databaseName are ignored.
23112381queryOracleOCI <- function (host = " " , port = 1521 , databaseName = " " , username = " " , password = " " ,
2312- numOfRows = - 1 , query , timezone = " " , bulkRead = 200000 , connectionString = " " , ... ) {
2382+ numOfRows = - 1 , query , timezone = " " , bulkRead = 200000 , connectionString = " " ,
2383+ additionalParams = " " , ... ) {
23132384 if (! requireNamespace(" ROracle" )) { stop(" package ROracle must be installed." ) }
23142385 if (! requireNamespace(" DBI" )) { stop(" package DBI must be installed." ) }
23152386
23162387 conn <- getDBConnection(type = " oracleoci" , host = host , port = port , databaseName = databaseName ,
23172388 username = username , password = password , timezone = timezone ,
2318- bulkRead = bulkRead , connectionString = connectionString )
2389+ bulkRead = bulkRead , connectionString = connectionString ,
2390+ additionalParams = additionalParams )
23192391 tryCatch({
23202392 query <- convertUserInputToUtf8(query )
23212393 # set envir = parent.frame() to get variables from users environment, not package environment
@@ -2328,7 +2400,8 @@ queryOracleOCI <- function(host = "", port = 1521, databaseName = "", username =
23282400 }, error = function (err ) {
23292401 # clear connection in pool so that new connection will be used for the next try
23302402 clearDBConnection(" oracleoci" , host , port , databaseName , username , timezone = timezone ,
2331- bulkRead = bulkRead , connectionString = connectionString )
2403+ bulkRead = bulkRead , connectionString = connectionString ,
2404+ additionalParams = additionalParams )
23322405 stop(err )
23332406 })
23342407 ROracle :: dbClearResult(resultSet )
0 commit comments