diff --git a/DESCRIPTION b/DESCRIPTION index 848bad2..8c68d80 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: ResultModelManager Title: Result Model Manager (RMM) for OHDSI packages -Version: 0.1.0 +Version: 0.1.1 Authors@R: person("Jamie", "Gilbert", , "gilbert@ohdsi.org", role = c("aut", "cre")) Description: Database data model management utilities for OHDSI packages. @@ -17,7 +17,8 @@ Imports: ParallelLogger, checkmate, DBI, - pool + pool, + readr Suggests: testthat (>= 3.0.0), RSQLite, diff --git a/NAMESPACE b/NAMESPACE index c99495f..f1e51f7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,7 @@ export(ConnectionHandler) export(DataMigrationManager) export(PooledConnectionHandler) +export(generateSqlSchema) import(DatabaseConnector) import(R6) import(checkmate) @@ -13,3 +14,4 @@ importFrom(SqlRender,render) importFrom(SqlRender,translate) importFrom(pool,dbPool) importFrom(pool,poolClose) +importFrom(readr,read_csv) diff --git a/NEWS.md b/NEWS.md index 467788f..964e8a1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,10 @@ -# ResultModelManager 0.0.1 +# ResultModelManager 0.1.1 + +Changes: +1. Added snakeCaseToCamelCase parameter to public in connectionHandlers so it can be defined once if required + +2. Added schema generator function that creates sql from csv files with table defs + +# ResultModelManager 0.1.0 Initial version \ No newline at end of file diff --git a/R/ConnectionHandler.R b/R/ConnectionHandler.R index 975c92b..5a3726a 100644 --- a/R/ConnectionHandler.R +++ b/R/ConnectionHandler.R @@ -21,7 +21,8 @@ #' #' @field connectionDetails DatabaseConnector connectionDetails object #' @field con DatabaseConnector connection object -#' @field isActive Is connection active or not +#' @field isActive Is connection active or not#' +#' @field snakeCaseToCamelCase (Optional) Boolean. return the results columns in camel case (default) #' #' @import checkmate #' @import R6 @@ -35,13 +36,15 @@ ConnectionHandler <- R6::R6Class( connectionDetails = NULL, con = NULL, isActive = FALSE, + snakeCaseToCamelCase = TRUE, #' #' @param connectionDetails DatabaseConnector::connectionDetails class #' @param loadConnection Boolean option to load connection right away - initialize = function(connectionDetails, loadConnection = TRUE) { + #' @param snakeCaseToCamelCase (Optional) Boolean. return the results columns in camel case (default) + initialize = function(connectionDetails, loadConnection = TRUE, snakeCaseToCamelCase = TRUE) { checkmate::assertClass(connectionDetails, "connectionDetails") self$connectionDetails <- connectionDetails - + self$snakeCaseToCamelCase <- snakeCaseToCamelCase if (loadConnection) { self$initConnection() } @@ -75,11 +78,13 @@ ConnectionHandler <- R6::R6Class( #' Connects automatically if it isn't yet loaded #' @returns DatabaseConnector Connection instance getConnection = function() { - if (is.null(self$con)) + if (is.null(self$con)) { self$initConnection() + } - if (!self$dbIsValid()) + if (!self$dbIsValid()) { self$initConnection() + } return(self$con) }, @@ -128,14 +133,14 @@ ConnectionHandler <- R6::R6Class( #' You may wish to ignore it. #' @param ... Additional query parameters #' @returns boolean TRUE if connection is valid - queryDb = function(sql, snakeCaseToCamelCase = TRUE, overrideRowLimit = FALSE, ...) { + queryDb = function(sql, snakeCaseToCamelCase = self$snakeCaseToCamelCase, overrideRowLimit = FALSE, ...) { # Limit row count is intended for web applications that may cause a denial of service if they consume too many # resources. limitRowCount <- as.integer(Sys.getenv("LIMIT_ROW_COUNT")) if (!is.na(limitRowCount) & limitRowCount > 0 & !overrideRowLimit) { sql <- SqlRender::render("SELECT TOP @limit_row_count * FROM (@query) result;", - query = gsub(";$", "", sql), # Remove last semi-colon - limit_row_count = limitRowCount + query = gsub(";$", "", sql), # Remove last semi-colon + limit_row_count = limitRowCount ) } sql <- self$renderTranslateSql(sql, ...) @@ -186,7 +191,7 @@ ConnectionHandler <- R6::R6Class( #' Does not translate or render sql. #' @param sql sql query string #' @param snakeCaseToCamelCase (Optional) Boolean. return the results columns in camel case (default) - queryFunction = function(sql, snakeCaseToCamelCase = TRUE) { + queryFunction = function(sql, snakeCaseToCamelCase = self$snakeCaseToCamelCase) { DatabaseConnector::querySql(self$getConnection(), sql, snakeCaseToCamelCase = snakeCaseToCamelCase) }, @@ -200,4 +205,3 @@ ConnectionHandler <- R6::R6Class( } ) ) - diff --git a/R/DataMigrationManager.R b/R/DataMigrationManager.R index eb5ebb7..94ed384 100644 --- a/R/DataMigrationManager.R +++ b/R/DataMigrationManager.R @@ -213,14 +213,14 @@ DataMigrationManager <- R6::R6Class( # load list of migrations migrations <- self$getStatus() # execute migrations that haven't been executed yet - migrations <- migrations[!migrations$executed,] + migrations <- migrations[!migrations$executed, ] if (nrow(migrations) > 0) { if (is.null(stopMigrationVersion)) { stopMigrationVersion <- max(migrations$migrationOrder) } for (i in 1:nrow(migrations)) { - migration <- migrations[i,] + migration <- migrations[i, ] if (isTRUE(migration$migrationOrder <= stopMigrationVersion)) { private$executeMigration(migration) } @@ -250,10 +250,11 @@ DataMigrationManager <- R6::R6Class( # Load, render, translate and execute sql if (self$isPackage()) { sql <- SqlRender::loadRenderTranslateSql(file.path(self$migrationPath, migration$migrationFile), - dbms = private$connectionDetails$dbms, - database_schema = self$databaseSchema, - table_prefix = self$tablePrefix, - packageName = self$packageName) + dbms = private$connectionDetails$dbms, + database_schema = self$databaseSchema, + table_prefix = self$tablePrefix, + packageName = self$packageName + ) private$connectionHandler$executeSql(sql) } else { # Check to see if a file for database platform exists @@ -264,8 +265,9 @@ DataMigrationManager <- R6::R6Class( sql <- SqlRender::readSql(file.path(self$migrationPath, "sql_server", migration$migrationFile)) } private$connectionHandler$executeSql(sql, - database_schema = self$databaseSchema, - table_prefix = self$tablePrefix) + database_schema = self$databaseSchema, + table_prefix = self$tablePrefix + ) } private$logInfo("Saving migration: ", migration$migrationFile) # Save migration in set of migrations @@ -275,10 +277,10 @@ DataMigrationManager <- R6::R6Class( VALUES ('@migration_file', @order); " private$connectionHandler$executeSql(iSql, - database_schema = self$databaseSchema, - migration_file = migration$migrationFile, - table_prefix = self$tablePrefix, - order = migration$migrationOrder + database_schema = self$databaseSchema, + migration_file = migration$migrationFile, + table_prefix = self$tablePrefix, + order = migration$migrationOrder ) private$logInfo("Migration complete ", migration$migrationFile) }, @@ -293,8 +295,9 @@ DataMigrationManager <- R6::R6Class( );" private$connectionHandler$executeSql(sql, - database_schema = self$databaseSchema, - table_prefix = self$tablePrefix) + database_schema = self$databaseSchema, + table_prefix = self$tablePrefix + ) private$logInfo("Migrations table created") }, getCompletedMigrations = function() { @@ -306,8 +309,9 @@ DataMigrationManager <- R6::R6Class( {DEFAULT @migration = migration} SELECT migration_file, migration_order FROM @database_schema.@table_prefix@migration ORDER BY migration_order;" migrationsExecuted <- private$connectionHandler$queryDb(sql, - database_schema = self$databaseSchema, - table_prefix = self$tablePrefix) + database_schema = self$databaseSchema, + table_prefix = self$tablePrefix + ) return(migrationsExecuted) }, @@ -329,7 +333,6 @@ DataMigrationManager <- R6::R6Class( ParallelLogger::logError(...) } }, - logInfo = function(...) { if (isUnitTest() | isRmdCheck()) { writeLines(text = .makeMessage(...)) diff --git a/R/PooledConnectionHandler.R b/R/PooledConnectionHandler.R index e117cfd..ed91c7d 100644 --- a/R/PooledConnectionHandler.R +++ b/R/PooledConnectionHandler.R @@ -70,7 +70,7 @@ PooledConnectionHandler <- R6::R6Class( #' Overrides ConnectionHandler Call. Does not translate or render sql. #' @param sql sql query string #' @param snakeCaseToCamelCase (Optional) Boolean. return the results columns in camel case (default) - queryFunction = function(sql, snakeCaseToCamelCase = TRUE) { + queryFunction = function(sql, snakeCaseToCamelCase = self$snakeCaseToCamelCase) { data <- DatabaseConnector::dbGetQuery(self$getConnection(), sql) if (snakeCaseToCamelCase) { colnames(data) <- SqlRender::snakeCaseToCamelCase(colnames(data)) diff --git a/R/SchemaGenerator.R b/R/SchemaGenerator.R new file mode 100644 index 0000000..7feb3dc --- /dev/null +++ b/R/SchemaGenerator.R @@ -0,0 +1,92 @@ +# Copyright 2022 Observational Health Data Sciences and Informatics +# +# This file is part of CohortDiagnostics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +.writeFieldDefinition <- function(field) { + field <- as.list(field) + str <- paste("\t", field$columnName, toupper(field$dataType)) + + if (field$primaryKey == "yes") { + str <- paste(str, "NOT NULL") + } + + str +} + +#' Schema generator +#' @description +#' Take a csv schema definition and create a basic sql script with it. +#' +#' @param csvFilepath Path to schema file. Csv file must have the columns: +#' "table_name", "colum_name", "data_type", "is_required", "primary_key" +#' Note - +#' @param sqlOutputPath File to write sql to. +#' @param overwrite Boolean - overwrite existing file? +#' @export +#' +#' @importFrom readr read_csv +#' @return +#' string containing the sql for the table +generateSqlSchema <- function(csvFilepath, + sqlOutputPath = NULL, + overwrite = FALSE) { + if (!is.null(sqlOutputPath) && (file.exists(sqlOutputPath) & !overwrite)) { + stop("Output file ", sqlOutputPath, "already exists. Set overwrite = TRUE to continue") + } + + checkmate::assertFileExists(csvFilepath) + schemaDefinition <- readr::read_csv(csvFilepath, show_col_types = FALSE) + colnames(schemaDefinition) <- SqlRender::snakeCaseToCamelCase(colnames(schemaDefinition)) + requiredFields <- c("tableName", "columnName", "dataType", "isRequired", "primaryKey") + checkmate::assertNames(colnames(schemaDefinition), must.include = requiredFields) + + tableSqlStr <- " +CREATE TABLE @database_schema.@table_prefix@table_name ( + @table_fields +); +" + fullScript <- "" + defs <- "{DEFAULT @table_prefix = ''}\n" + + for (table in unique(schemaDefinition$tableName)) { + tableFields <- schemaDefinition[schemaDefinition$tableName == table, ] + fieldDefinitions <- apply(tableFields, 1, .writeFieldDefinition) + + primaryKeyFields <- tableFields[tableFields$primaryKey == "yes", ] + if (nrow(primaryKeyFields)) { + pkeyField <- paste0("\tPRIMARY KEY(", paste(primaryKeyFields$columnName, collapse = ","), ")") + fieldDefinitions <- c(fieldDefinitions, pkeyField) + } + + fieldDefinitions <- paste(fieldDefinitions, collapse = ",\n") + tableString <- SqlRender::render(tableSqlStr, + table_name = paste0("@", table), + table_fields = fieldDefinitions + ) + + tableDefStr <- paste0("{DEFAULT @", table, " = ", table, "}\n") + defs <- paste0(defs, tableDefStr) + + fullScript <- paste(fullScript, tableString) + } + + # Get fields for each table + lines <- paste(defs, fullScript) + if (!is.null(sqlOutputPath)) { + writeLines(lines, sqlOutputPath) + } + + lines +} diff --git a/extras/ResultModelManager.pdf b/extras/ResultModelManager.pdf index 2181949..c11f98a 100644 Binary files a/extras/ResultModelManager.pdf and b/extras/ResultModelManager.pdf differ diff --git a/man/ConnectionHandler.Rd b/man/ConnectionHandler.Rd index e238ad5..3689df0 100644 --- a/man/ConnectionHandler.Rd +++ b/man/ConnectionHandler.Rd @@ -24,7 +24,9 @@ Allows a connection to cleanly be opened and closed and stored within class/obje \item{\code{con}}{DatabaseConnector connection object} -\item{\code{isActive}}{Is connection active or not} +\item{\code{isActive}}{Is connection active or not#'} + +\item{\code{snakeCaseToCamelCase}}{(Optional) Boolean. return the results columns in camel case (default)} } \if{html}{\out{}} } @@ -50,7 +52,11 @@ Allows a connection to cleanly be opened and closed and stored within class/obje \if{latex}{\out{\hypertarget{method-ConnectionHandler-new}{}}} \subsection{Method \code{new()}}{ \subsection{Usage}{ -\if{html}{\out{
ResultModelManager::ConnectionHandler$dbIsValid()
ResultModelManager::ConnectionHandler$executeFunction()
ResultModelManager::ConnectionHandler$executeSql()
ResultModelManager::ConnectionHandler$finalize()
ResultModelManager::ConnectionHandler$getConnection()
ResultModelManager::ConnectionHandler$queryDb()
ResultModelManager::ConnectionHandler$renderTranslateSql()
ResultModelManager::ConnectionHandler$dbIsValid()
ResultModelManager::ConnectionHandler$executeFunction()
ResultModelManager::ConnectionHandler$executeSql()
ResultModelManager::ConnectionHandler$finalize()
ResultModelManager::ConnectionHandler$getConnection()
ResultModelManager::ConnectionHandler$queryDb()
ResultModelManager::ConnectionHandler$renderTranslateSql()