diff --git a/DESCRIPTION b/DESCRIPTION new file mode 100644 index 0000000..5de5c82 --- /dev/null +++ b/DESCRIPTION @@ -0,0 +1,42 @@ +Encoding: UTF-8 +Package: RPostgres +Version: 1.0-3 +Date: 2017-12-06 +Title: 'Rcpp' Interface to 'PostgreSQL' +Authors@R: c( + person("Hadley", "Wickham", role = "aut"), + person("Jeroen", "Ooms", role = "aut"), + person("Kirill", "Müller", role = "cre", email = "krlmlr+r@mailbox.org"), + person("RStudio", role = "cph"), + person("R Consortium", role = "cph"), + person("Tomoaki", "Nishiyama", role = "ctb", + comment = "Code for encoding vectors into strings derived from RPostgreSQL"), + person("Kungliga Tekniska Högskolan", role = "ctb", comment = "Source code for timegm") + ) +Description: + Fully 'DBI'-compliant 'Rcpp'-backed interface to 'PostgreSQL' , + an open-source relational database. +License: GPL-2 +LazyLoad: true +Depends: R (>= 3.1.0) +Imports: bit64, blob, DBI (>= 0.7), hms, methods, Rcpp (>= 0.11.4.2), + withr +Suggests: DBItest, testthat +LinkingTo: BH, plogr, Rcpp +Collate: 'PqDriver.R' 'PqConnection.R' 'PqResult.R' 'RPostgres-pkg.R' + 'RcppExports.R' 'default.R' 'quote.R' 'tables.R' + 'transactions.R' 'utils.R' +RoxygenNote: 6.0.1 +NeedsCompilation: yes +Packaged: 2017-12-06 00:17:52 UTC; muelleki +Author: Hadley Wickham [aut], + Jeroen Ooms [aut], + Kirill Müller [cre], + RStudio [cph], + R Consortium [cph], + Tomoaki Nishiyama [ctb] (Code for encoding vectors into strings derived + from RPostgreSQL), + Kungliga Tekniska Högskolan [ctb] (Source code for timegm) +Maintainer: Kirill Müller +Repository: CRAN +Date/Publication: 2017-12-06 10:21:43 UTC diff --git a/MD5 b/MD5 new file mode 100644 index 0000000..e187fa5 --- /dev/null +++ b/MD5 @@ -0,0 +1,81 @@ +488ff95ba8e2d7e9719b961276dff586 *DESCRIPTION +feec1bfbc596fa53152300f94a3a7ab7 *NAMESPACE +466d97216e230f9984d18d9eba02221e *NEWS.md +d3949d39c6e0d3ebb30f5f47e69ed7d2 *R/PqConnection.R +03a867755b841450b855879abbf62b30 *R/PqDriver.R +b91897c5beee9b99af6fd55051171a80 *R/PqResult.R +0b846aae48eacea9831cd61b50eedea2 *R/RPostgres-pkg.R +971e28b404b0401f5b8c38db70e85ff2 *R/RcppExports.R +ff99d2cac7decf472cd0bc8291a78e49 *R/default.R +4ab74e90a8c8573ecb483e069793bb28 *R/quote.R +178feaef07207f895f30fd44b038ace6 *R/tables.R +bbad0daadf44d8a9a43a0a720bef3803 *R/transactions.R +c3efac6c8ef323cae26e037cce6c0d16 *R/utils.R +f3da0de24104764362936f522e26ec6f *README.md +8131313a7c1ae9b3ca097dc23a33918d *configure +d41d8cd98f00b204e9800998ecf8427e *configure.win +254056c489b1f3e7cc8cdc9fe4a14255 *man/Postgres.Rd +fcfcf00fa9190113b6ee073f9fd11e2c *man/PqConnection-class.Rd +26ccd704aecacc6484f3ea5b2e3eaa5f *man/PqDriver-class.Rd +6ebae0927715384b8e90764e3ad2ba87 *man/PqResult-class.Rd +16132b2b8d5c154d5ad5f9f4bf63cc81 *man/RPostgres-package.Rd +3968c41c099acd7c228e85cc99655961 *man/dbConnect-PqDriver-method.Rd +dda833f4b2c036ce15b0555c49add570 *man/dbDataType.Rd +549393f4e48dad8a411932f23b543930 *man/postgres-query.Rd +8e99dd84756b338faa2981ff82a8bfc0 *man/postgres-tables.Rd +068c48521487f453e0c5b15bc6605cfe *man/postgres-transactions.Rd +0f380511ba5e72720224904f24e3a42b *man/postgresHasDefault.Rd +c4a8dbefb34cf9b1b599d26fc3ab3612 *man/quote.Rd +fac812734f22d35b7a19561ea33d2a7c *src/DbColumn.cpp +0d0f6283e1aee5ba374ac6c30ce35a57 *src/DbColumn.h +1bead3e4afde4eb4237507be9fd81801 *src/DbColumnDataSource.cpp +c1bd654b0f1d00b85c0213fb15266100 *src/DbColumnDataSource.h +b54f1bed5e031730d84ea6ac5678d57c *src/DbColumnDataSourceFactory.cpp +bdb2b7ed1a0c451b46b90f5ba0cae405 *src/DbColumnDataSourceFactory.h +1ba5afa61895d800dbe19155f7d2c492 *src/DbColumnDataType.h +4d4e148248bbebe297320f33a3244def *src/DbColumnStorage.cpp +00308bd348a82fa234bc3faf490c3bb5 *src/DbColumnStorage.h +e6793b8d19bb3599363801a69f689fb7 *src/DbConnection.cpp +91fd7039d2c543d73abf05c10937928a *src/DbConnection.h +52f74e46125617344b9ef6c064922057 *src/DbDataFrame.cpp +ce8cede04ec884229dfec09a95128ad9 *src/DbDataFrame.h +add0543c36d85ecd4d07d3aec205a3d1 *src/DbResult.cpp +a924adc6fcb47073a85a603d579bb3fc *src/DbResult.h +111e1efb50ff675feb507ea42a24b300 *src/Makevars.in +c5a5d51517569b4577b384cbbc412ad5 *src/Makevars.win +16024658adfb1ccb34b929298ac0396e *src/PqColumnDataSource.cpp +5b65a687e65c37ef8b4f6b0427313c9b *src/PqColumnDataSource.h +d94f34a4e740f7d2f7b1f46c5bbc9411 *src/PqColumnDataSourceFactory.cpp +5932e6e86bc3c43a31954bfd1dac8117 *src/PqColumnDataSourceFactory.h +8dd8d1fe7af6e996859ef849d6b72993 *src/PqDataFrame.cpp +3fb6f7104460055e29d87402ef538b55 *src/PqDataFrame.h +f0cc2e38a2445b24a32298045d42396b *src/PqResultImpl.cpp +68d9a7002c6ded7a86df79e6281b367c *src/PqResultImpl.h +4601b2b6e925fa6761186d25fa93f9f5 *src/PqResultSource.cpp +e2d8563ac79450e3f2347ebc5259ba0e *src/PqResultSource.h +74630eb1d3dd72f0e20c90f0deac0ba6 *src/RPostgres-init.c +5fd33ab4f8ba8480acb085aa8a5bf94d *src/RPostgres_types.h +57b657eeee6da528d4522e3c31781597 *src/RcppExports.cpp +861292357d8bb45348cbc0a2c3c00d2c *src/connection.cpp +40ca6054ebb1ae2e41a65c4f7d8ee5f3 *src/encode.cpp +dfb7deb192ff48e8d33f196f1afaa3d6 *src/encode.h +6b44efff9fefa69c04f41a3cbf3b1ce3 *src/encrypt.cpp +9a3d1653a1ced0d8daac4102701ec18a *src/integer64.h +ecf4e945c36fea5564e665ff5012c19d *src/logging.cpp +7291096e5ad632a53bf9483e3020c1ea *src/pch.h +99039714005147435bebbbd0a640d897 *src/result.cpp +e2f57a97ad5e5d484dc0de7d0656faea *src/win32/timegm.c +4111945e05f4e4a31c72f6ef63b1f1e6 *tests/testthat.R +a74d10622304ddd4ebf25b9b2a11b71c *tests/testthat/helper-DBItest.R +ac3971b62e24ae2cc633a12eb1aa0d3d *tests/testthat/helper-astyle.R +5f99bf46169a27c39db6b35611d0da42 *tests/testthat/helper-with_database_connection.R +f251e8d8c1249e774674be514615eeec *tests/testthat/helper-with_table.R +78eff1c41624d5eb4d7e5036b59b7d80 *tests/testthat/helper-without_rownames.R +000f052dbee7a744d411e507f6efbb9e *tests/testthat/test-DBItest.R +29890f7f72946cbf870c714ebeaee604 *tests/testthat/test-bigint.R +d0d355c427d7373ab8e6a85d655a381b *tests/testthat/test-data-type.R +3fb2c6809894d3fc84a1832ca0509707 *tests/testthat/test-dbConnect.R +9939d01b8b9c8b6c138e1466a1a74c09 *tests/testthat/test-dbGetQuery.R +dfc643c4d14d440fb7b02648551dca58 *tests/testthat/test-dbWriteTable.R +07ec38d74f52ade1ddc67997730ad1ca *tests/testthat/test-encoding.R +5170ffd2f4d60933fe94753bc048c7f2 *tools/winlibs.R diff --git a/NAMESPACE b/NAMESPACE new file mode 100644 index 0000000..1a9b079 --- /dev/null +++ b/NAMESPACE @@ -0,0 +1,44 @@ +# Generated by roxygen2: do not edit by hand + +export(Postgres) +export(postgresDefault) +export(postgresHasDefault) +exportClasses(PqConnection) +exportClasses(PqDriver) +exportClasses(PqResult) +exportMethods(dbBegin) +exportMethods(dbBind) +exportMethods(dbClearResult) +exportMethods(dbColumnInfo) +exportMethods(dbCommit) +exportMethods(dbConnect) +exportMethods(dbDataType) +exportMethods(dbDisconnect) +exportMethods(dbExistsTable) +exportMethods(dbFetch) +exportMethods(dbGetInfo) +exportMethods(dbGetRowCount) +exportMethods(dbGetRowsAffected) +exportMethods(dbGetStatement) +exportMethods(dbHasCompleted) +exportMethods(dbIsValid) +exportMethods(dbListFields) +exportMethods(dbListTables) +exportMethods(dbQuoteIdentifier) +exportMethods(dbQuoteLiteral) +exportMethods(dbQuoteString) +exportMethods(dbReadTable) +exportMethods(dbRemoveTable) +exportMethods(dbRollback) +exportMethods(dbSendQuery) +exportMethods(dbUnloadDriver) +exportMethods(dbWriteTable) +exportMethods(show) +exportMethods(sqlData) +import(DBI) +import(methods) +importFrom(Rcpp,evalCpp) +importFrom(bit64,integer64) +importFrom(blob,blob) +importFrom(hms,hms) +useDynLib(RPostgres, .registration = TRUE) diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..218ee75 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,10 @@ +# RPostgres 1.0-3 (2017-12-01) + +Initial release, compliant to the DBI specification. + +- Test almost all test cases of the DBI specification. +- Fully support parametrized queries. +- Spec-compliant transactions. +- 64-bit integers are now supported through the `bit64` package. This also means that numeric literals (as in `SELECT 1`) are returned as 64-bit integers. The `bigint` argument to `dbConnect()` allows overriding the data type on a per-connection basis. +- Correct handling of DATETIME and TIME columns. +- New default `row.names = FALSE`. diff --git a/R/PqConnection.R b/R/PqConnection.R new file mode 100644 index 0000000..c98c617 --- /dev/null +++ b/R/PqConnection.R @@ -0,0 +1,174 @@ +#' @include PqDriver.R +NULL + +#' PqConnection and methods. +#' +#' @keywords internal +#' @export +setClass("PqConnection", + contains = "DBIConnection", + slots = list(ptr = "externalptr", bigint = "character") +) + +# show() +#' @export +#' @rdname PqConnection-class +setMethod("show", "PqConnection", function(object) { + info <- dbGetInfo(object) + + if (info$host == "") { + host <- "socket" + } else { + host <- paste0(info$host, ":", info$port) + } + + cat(" ", info$dbname, "@", host, "\n", sep = "") +}) + +# dbIsValid() +#' @export +#' @rdname PqConnection-class +setMethod("dbIsValid", "PqConnection", function(dbObj, ...) { + connection_valid(dbObj@ptr) +}) + +# dbDisconnect() +#' @export +#' @rdname dbConnect-PqDriver-method +setMethod("dbDisconnect", "PqConnection", function(conn, ...) { + connection_release(conn@ptr) + invisible(TRUE) +}) + +# dbSendQuery() + +# dbSendStatement() + +# dbDataType() +#' @export +#' @rdname dbDataType +setMethod("dbDataType", "PqConnection", function(dbObj, obj, ...) { + if (is.data.frame(obj)) return(vapply(obj, dbDataType, "", dbObj = dbObj)) + get_data_type(obj) +}) + +get_data_type <- function(obj) { + if (is.factor(obj)) return("TEXT") + if (inherits(obj, "POSIXt")) return("TIMESTAMPTZ") + if (inherits(obj, "Date")) return("DATE") + if (inherits(obj, "difftime")) return("TIME") + switch(typeof(obj), + integer = "INTEGER", + double = "REAL", + character = "TEXT", + logical = "BOOLEAN", + list = "BYTEA", + stop("Unsupported type", call. = FALSE) + ) +} + +# dbQuoteString() + +# dbQuoteIdentifier() + +# dbWriteTable() + +# dbReadTable() + +# dbListTables() + +# dbExistsTable() + +# dbListFields() + +# dbRemoveTable() + +# dbGetInfo() +#' @export +#' @rdname PqConnection-class +setMethod("dbGetInfo", "PqConnection", function(dbObj, ...) { + connection_info(dbObj@ptr) +}) + +# dbBegin() + +# dbCommit() + +# dbRollback() + +# other + +#' Connect to a PostgreSQL database. +#' +#' Manually disconnecting a connection is not necessary with RPostgres, but +#' still recommended; +#' if you delete the object containing the connection, it will be automatically +#' disconnected during the next GC with a warning. +#' +#' @param drv `RPostgres::Postgres()` +#' @param dbname Database name. If `NULL`, defaults to the user name. +#' Note that this argument can only contain the database name, it will not +#' be parsed as a connection string (internally, `expand_dbname` is set to +#' `false` in the call to +#' [`PQconnectdbParams()`](https://www.postgresql.org/docs/9.6/static/libpq-connect.html)). +#' @param user,password User name and password. If `NULL`, will be +#' retrieved from `PGUSER` and `PGPASSWORD` envvars, or from the +#' appropriate line in `~/.pgpass`. See +#' for +#' more details. +#' @param host,port Host and port. If `NULL`, will be retrieved from +#' `PGHOST` and `PGPORT` env vars. +#' @param service Name of service to connect as. If `NULL`, will be +#' ignored. Otherwise, connection parameters will be loaded from the pg_service.conf +#' file and used. See +#' for details on this file and syntax. +#' @param ... Other name-value pairs that describe additional connection +#' options as described at +#' +#' @param bigint The R type that 64-bit integer types should be mapped to, +#' default is [bit64::integer64], which allows the full range of 64 bit +#' integers. +#' @param conn Connection to disconnect. +#' @export +#' @examples +#' if (postgresHasDefault()) { +#' library(DBI) +#' # Pass more arguments as necessary to dbConnect() +#' con <- dbConnect(RPostgres::Postgres()) +#' dbDisconnect(con) +#' } +setMethod("dbConnect", "PqDriver", + function(drv, dbname = NULL, + host = NULL, port = NULL, password = NULL, user = NULL, service = NULL, ..., + bigint = c("integer64", "integer", "numeric", "character")) { + + opts <- unlist(list(dbname = dbname, user = user, password = password, + host = host, port = as.character(port), service = service, client_encoding = "utf8", ...)) + if (!is.character(opts)) { + stop("All options should be strings", call. = FALSE) + } + bigint <- match.arg(bigint) + + if (length(opts) == 0) { + ptr <- connection_create(character(), character()) + } else { + ptr <- connection_create(names(opts), as.vector(opts)) + } + + con <- new("PqConnection", ptr = ptr, bigint = bigint) + dbExecute(con, "SET TIMEZONE='UTC'") + con + }) + + +#' Determine database type for R vector. +#' +#' @export +#' @param dbObj Postgres driver or connection. +#' @param obj Object to convert +#' @keywords internal +#' @rdname dbDataType +setMethod("dbDataType", "PqDriver", function(dbObj, obj, ...) { + if (is.data.frame(obj)) return(vapply(obj, dbDataType, "", dbObj = dbObj)) + get_data_type(obj) +}) diff --git a/R/PqDriver.R b/R/PqDriver.R new file mode 100644 index 0000000..6c694cd --- /dev/null +++ b/R/PqDriver.R @@ -0,0 +1,33 @@ +#' Postgres driver +#' +#' This driver never needs to be unloaded and hence `dbUnload()` is a +#' null-op. +#' +#' @export +#' @useDynLib RPostgres, .registration = TRUE +#' @importFrom Rcpp evalCpp +#' @import methods DBI +#' @examples +#' library(DBI) +#' RPostgres::Postgres() +Postgres <- function() { + new("PqDriver") +} + +#' PqDriver and methods. +#' +#' @export +#' @keywords internal +setClass("PqDriver", contains = "DBIDriver") + +#' @export +#' @rdname PqDriver-class +setMethod("dbUnloadDriver", "PqDriver", function(drv, ...) { + NULL +}) + +#' @rdname PqResult-class +#' @export +setMethod("dbIsValid", "PqDriver", function(dbObj, ...) { + TRUE +}) diff --git a/R/PqResult.R b/R/PqResult.R new file mode 100644 index 0000000..caa9062 --- /dev/null +++ b/R/PqResult.R @@ -0,0 +1,205 @@ +#' PostgreSQL results. +#' +#' @keywords internal +#' @include PqConnection.R +#' @export +setClass("PqResult", + contains = "DBIResult", + slots = list( + conn = "PqConnection", + ptr = "externalptr", + sql = "character", + bigint = "character" + ) +) + +#' @rdname PqResult-class +#' @export +setMethod("dbGetStatement", "PqResult", function(res, ...) { + if (!dbIsValid(res)) { + stop("Invalid result set.", call. = FALSE) + } + res@sql +}) + +#' @rdname PqResult-class +#' @export +setMethod("dbIsValid", "PqResult", function(dbObj, ...) { + result_valid(dbObj@ptr) +}) + +#' @rdname PqResult-class +#' @export +setMethod("dbGetRowCount", "PqResult", function(res, ...) { + result_rows_fetched(res@ptr) +}) + +#' @rdname PqResult-class +#' @export +setMethod("dbGetRowsAffected", "PqResult", function(res, ...) { + result_rows_affected(res@ptr) +}) + +#' @rdname PqResult-class +#' @export +setMethod("dbColumnInfo", "PqResult", function(res, ...) { + result_column_info(res@ptr) +}) + +#' Execute a SQL statement on a database connection +#' +#' To retrieve results a chunk at a time, use `dbSendQuery()`, +#' `dbFetch()`, then `dbClearResult()`. Alternatively, if you want all the +#' results (and they'll fit in memory) use `dbGetQuery()` which sends, +#' fetches and clears for you. +#' +#' @param conn A [PqConnection-class] created by [dbConnect()]. +#' @param statement An SQL string to execute +#' @param params A list of query parameters to be substituted into +#' a parameterised query. Query parameters are sent as strings, and the +#' correct type is imputed by PostgreSQL. If this fails, you can manually +#' cast the parameter with e.g. `"$1::bigint"`. +#' @param ... Another arguments needed for compatibility with generic ( +#' currently ignored). +#' @examples +#' # For running the examples on systems without PostgreSQL connection: +#' run <- postgresHasDefault() +#' +#' library(DBI) +#' if (run) db <- dbConnect(RPostgres::Postgres()) +#' if (run) dbWriteTable(db, "usarrests", datasets::USArrests, temporary = TRUE) +#' +#' # Run query to get results as dataframe +#' if (run) dbGetQuery(db, "SELECT * FROM usarrests LIMIT 3") +#' +#' # Send query to pull requests in batches +#' if (run) res <- dbSendQuery(db, "SELECT * FROM usarrests") +#' if (run) dbFetch(res, n = 2) +#' if (run) dbFetch(res, n = 2) +#' if (run) dbHasCompleted(res) +#' if (run) dbClearResult(res) +#' +#' if (run) dbRemoveTable(db, "usarrests") +#' +#' if (run) dbDisconnect(db) +#' @name postgres-query +NULL + +#' @export +#' @rdname postgres-query +setMethod("dbSendQuery", c("PqConnection", "character"), function(conn, statement, params = NULL, ...) { + statement <- enc2utf8(statement) + + rs <- new("PqResult", + conn = conn, + ptr = result_create(conn@ptr, statement), + sql = statement, + bigint = conn@bigint + ) + + if (!is.null(params)) { + dbBind(rs, params) + } + + rs +}) + +#' @param res Code a [PqResult-class] produced by +#' [DBI::dbSendQuery()]. +#' @param n Number of rows to return. If less than zero returns all rows. +#' @inheritParams DBI::sqlRownamesToColumn +#' @export +#' @rdname postgres-query +setMethod("dbFetch", "PqResult", function(res, n = -1, ..., row.names = FALSE) { + if (length(n) != 1) stopc("n must be scalar") + if (n < -1) stopc("n must be nonnegative or -1") + if (is.infinite(n)) n <- -1 + if (trunc(n) != n) stopc("n must be a whole number") + ret <- sqlColumnToRownames(result_fetch(res@ptr, n = n), row.names) + convert_bigint(ret, res@bigint) +}) + +convert_bigint <- function(ret, bigint) { + if (bigint == "integer64") return(ret) + fun <- switch(bigint, + integer = as.integer, + numeric = as.numeric, + character = as.character + ) + is_int64 <- which(vlapply(ret, inherits, "integer64")) + ret[is_int64] <- lapply(ret[is_int64], fun) + ret +} + +#' @rdname postgres-query +#' @export +setMethod("dbBind", "PqResult", function(res, params, ...) { + if (!is.null(names(params))) { + stop("Named parameters not supported", call. = FALSE) + } + if (!is.list(params)) params <- as.list(params) + lengths <- unique(viapply(params, length)) + if (length(lengths) > 1) { + stop("All parameters must have the same length.", call. = FALSE) + } + + params <- factor_to_string(params, warn = TRUE) + params <- posixlt_to_posixct(params) + params <- difftime_to_hms(params) + params <- prepare_for_binding(params) + result_bind(res@ptr, params) + invisible(res) +}) + +factor_to_string <- function(value, warn = FALSE) { + is_factor <- vlapply(value, is.factor) + if (warn && any(is_factor)) { + warning("Factors converted to character", call. = FALSE) + } + value[is_factor] <- lapply(value[is_factor], as.character) + value +} + +posixlt_to_posixct <- function(value) { + is_posixlt <- vlapply(value, inherits, "POSIXlt") + value[is_posixlt] <- lapply(value[is_posixlt], as.POSIXct) + value +} + +difftime_to_hms <- function(value) { + is_difftime <- vlapply(value, inherits, "difftime") + value[is_difftime] <- lapply(value[is_difftime], hms::as.hms) + value +} + +prepare_for_binding <- function(value) { + is_list <- vlapply(value, is.list) + value[!is_list] <- lapply(value[!is_list], as.character) + value[!is_list] <- lapply(value[!is_list], enc2utf8) + value[is_list] <- lapply(value[is_list], vcapply, function(x) { + if (is.null(x)) NA_character_ + else if (is.raw(x)) { + paste(sprintf("\\%.3o", as.integer(x)), collapse = "") + } else { + stop("Lists must contain raw vectors or NULL", call. = FALSE) + } + }) + value +} + +#' @rdname postgres-query +#' @export +setMethod("dbHasCompleted", "PqResult", function(res, ...) { + result_has_completed(res@ptr) +}) + +#' @rdname postgres-query +#' @export +setMethod("dbClearResult", "PqResult", function(res, ...) { + if (!dbIsValid(res)) { + warningc("Expired, result set already closed") + return(invisible(TRUE)) + } + result_release(res@ptr) + invisible(TRUE) +}) diff --git a/R/RPostgres-pkg.R b/R/RPostgres-pkg.R new file mode 100644 index 0000000..7724956 --- /dev/null +++ b/R/RPostgres-pkg.R @@ -0,0 +1,3 @@ +#' @importFrom hms hms +#' @importFrom bit64 integer64 +"_PACKAGE" diff --git a/R/RcppExports.R b/R/RcppExports.R new file mode 100644 index 0000000..53c1c60 --- /dev/null +++ b/R/RcppExports.R @@ -0,0 +1,91 @@ +# Generated by using Rcpp::compileAttributes() -> do not edit by hand +# Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 + +connection_create <- function(keys, values) { + .Call(`_RPostgres_connection_create`, keys, values) +} + +connection_valid <- function(con_) { + .Call(`_RPostgres_connection_valid`, con_) +} + +connection_release <- function(con_) { + invisible(.Call(`_RPostgres_connection_release`, con_)) +} + +connection_info <- function(con) { + .Call(`_RPostgres_connection_info`, con) +} + +connection_quote_string <- function(con, xs) { + .Call(`_RPostgres_connection_quote_string`, con, xs) +} + +connection_quote_identifier <- function(con, xs) { + .Call(`_RPostgres_connection_quote_identifier`, con, xs) +} + +connection_is_transacting <- function(con) { + .Call(`_RPostgres_connection_is_transacting`, con) +} + +connection_set_transacting <- function(con, transacting) { + invisible(.Call(`_RPostgres_connection_set_transacting`, con, transacting)) +} + +connection_copy_data <- function(con, sql, df) { + invisible(.Call(`_RPostgres_connection_copy_data`, con, sql, df)) +} + +encode_vector <- function(x) { + .Call(`_RPostgres_encode_vector`, x) +} + +encode_data_frame <- function(x) { + .Call(`_RPostgres_encode_data_frame`, x) +} + +encrypt_password <- function(password, user) { + .Call(`_RPostgres_encrypt_password`, password, user) +} + +init_logging <- function(log_level) { + invisible(.Call(`_RPostgres_init_logging`, log_level)) +} + +result_create <- function(con, sql, is_statement = FALSE) { + .Call(`_RPostgres_result_create`, con, sql, is_statement) +} + +result_release <- function(res) { + invisible(.Call(`_RPostgres_result_release`, res)) +} + +result_valid <- function(res_) { + .Call(`_RPostgres_result_valid`, res_) +} + +result_fetch <- function(res, n) { + .Call(`_RPostgres_result_fetch`, res, n) +} + +result_bind <- function(res, params) { + invisible(.Call(`_RPostgres_result_bind`, res, params)) +} + +result_has_completed <- function(res) { + .Call(`_RPostgres_result_has_completed`, res) +} + +result_rows_fetched <- function(res) { + .Call(`_RPostgres_result_rows_fetched`, res) +} + +result_rows_affected <- function(res) { + .Call(`_RPostgres_result_rows_affected`, res) +} + +result_column_info <- function(res) { + .Call(`_RPostgres_result_column_info`, res) +} + diff --git a/R/default.R b/R/default.R new file mode 100644 index 0000000..9ea7ced --- /dev/null +++ b/R/default.R @@ -0,0 +1,46 @@ +#' Check if default database is available. +#' +#' RPostgres examples and tests connect to a default database via +#' `dbConnect(`[RPostgres::Postgres()]`)`. This function checks if that +#' database is available, and if not, displays an informative message. +#' +#' @param ... Additional arguments passed on to [dbConnect()] +#' @export +#' @examples +#' if (postgresHasDefault()) { +#' db <- postgresDefault() +#' dbListTables(db) +#' dbDisconnect(db) +#' } +postgresHasDefault <- function(...) { + tryCatch({ + con <- connect_default(...) + dbDisconnect(con) + TRUE + }, error = function(...) { + message( + "Could not initialise default postgres database. If postgres is running\n", + "check that the environment variables PGHOST, PGPORT, \n", + "PGUSER, PGPASSWORD, and PGDATABASE, are defined and\n", + "point to your database." + ) + FALSE + }) +} + +#' `postgresDefault()` works similarly but returns a connection on success and +#' throws a testthat skip condition on failure, making it suitable for use in +#' tests. +#' @export +#' @rdname postgresHasDefault +postgresDefault <- function(...) { + tryCatch({ + connect_default(...) + }, error = function(...) { + testthat::skip("Test database not available") + }) +} + +connect_default <- function(...) { + dbConnect(Postgres(), ...) +} diff --git a/R/quote.R b/R/quote.R new file mode 100644 index 0000000..06d7688 --- /dev/null +++ b/R/quote.R @@ -0,0 +1,140 @@ +#' @include PqConnection.R +NULL + +#' Quote postgres strings and identifiers. +#' +#' @param conn A [PqConnection-class] created by `dbConnect()` +#' @param x A character to escaped +#' @param ... Other arguments needed for compatibility with generic +#' @examples +#' # For running the examples on systems without PostgreSQL connection: +#' run <- postgresHasDefault() +#' +#' library(DBI) +#' if (run) con <- dbConnect(RPostgres::Postgres()) +#' +#' x <- c("a", "b c", "d'e", "\\f") +#' if (run) dbQuoteString(con, x) +#' if (run) dbQuoteIdentifier(con, x) +#' if (run) dbDisconnect(con) +#' @name quote +NULL + +#' @export +#' @rdname quote +setMethod("dbQuoteString", c("PqConnection", "character"), function(conn, x, ...) { + if (length(x) == 0) return(SQL(character())) + res <- SQL(connection_quote_string(conn@ptr, enc2utf8(x))) + res +}) + +#' @export +#' @rdname quote +setMethod("dbQuoteString", c("PqConnection", "SQL"), function(conn, x, ...) { + x +}) + +#' @export +#' @rdname quote +setMethod("dbQuoteIdentifier", c("PqConnection", "character"), function(conn, x, ...) { + if (anyNA(x)) { + stop("Cannot pass NA to dbQuoteIdentifier()", call. = FALSE) + } + SQL(connection_quote_identifier(conn@ptr, x)) +}) + +#' @export +#' @rdname quote +setMethod("dbQuoteIdentifier", c("PqConnection", "SQL"), function(conn, x, ...) { + x +}) + +# locally for now, requires DBI > 0.7 +#' @rdname quote +setGeneric("dbQuoteLiteral", + def = function(conn, x, ...) standardGeneric("dbQuoteLiteral") +) + +#' @export +#' @rdname quote +setMethod("dbQuoteLiteral", c("PqConnection", "logical"), function(conn, x, ...) { + x <- as.character(x) + x[is.na(x)] <- "NULL" + SQL(x) +}) + +#' @export +#' @rdname quote +setMethod("dbQuoteLiteral", c("PqConnection", "integer"), function(conn, x, ...) { + ret <- paste0(as.character(x), "::int4") + ret[is.na(x)] <- "NULL" + SQL(ret) +}) + +#' @export +#' @rdname quote +setMethod("dbQuoteLiteral", c("PqConnection", "numeric"), function(conn, x, ...) { + ret <- paste0(as.character(x), "::float8") + ret[is.na(x)] <- "NULL" + SQL(ret) +}) + +#' @export +#' @rdname quote +setMethod("dbQuoteLiteral", c("PqConnection", "factor"), function(conn, x, ...) { + dbQuoteLiteral(conn, as.character(x)) +}) + +#' @export +#' @rdname quote +setMethod("dbQuoteLiteral", c("PqConnection", "Date"), function(conn, x, ...) { + ret <- paste0("'", as.character(x), "'::date") + ret[is.na(x)] <- "NULL" + SQL(ret) +}) + +#' @export +#' @rdname quote +setMethod("dbQuoteLiteral", c("PqConnection", "POSIXt"), function(conn, x, ...) { + ret <- paste0("'", as.character(x), "'::timestamp") + ret[is.na(x)] <- "NULL" + SQL(ret) +}) + +#' @export +#' @rdname quote +setMethod("dbQuoteLiteral", c("PqConnection", "difftime"), function(conn, x, ...) { + ret <- paste0(as.character(x), "::time") + ret[is.na(x)] <- "NULL" + SQL(ret) +}) + +#' @export +#' @rdname quote +setMethod("dbQuoteLiteral", c("PqConnection", "list"), function(conn, x, ...) { + quote_blob(x) +}) + +# Workaround, remove when blob > 1.1.0 is on CRAN +setOldClass("blob") + +#' @export +#' @rdname quote +#' @importFrom blob blob +setMethod("dbQuoteLiteral", c("PqConnection", "blob"), function(conn, x, ...) { + quote_blob(x) +}) + +quote_blob <- function(x) { + blob_data <- vcapply( + x, + function(x) { + if (is.null(x)) "NULL" + else if (is.raw(x)) paste0("E'\\\\x", paste(format(x), collapse = ""), "'") + else { + stop("Lists must contain raw vectors or NULL", call. = FALSE) + } + } + ) + SQL(blob_data) +} diff --git a/R/tables.R b/R/tables.R new file mode 100644 index 0000000..f6cd8ad --- /dev/null +++ b/R/tables.R @@ -0,0 +1,244 @@ +#' Convenience functions for reading/writing DBMS tables +#' +#' @param conn a [PqConnection-class] object, produced by +#' [DBI::dbConnect()] +#' @param name a character string specifying a table name. Names will be +#' automatically quoted so you can use any sequence of characters, not +#' just any valid bare table name. +#' @param value A data.frame to write to the database. +#' @inheritParams DBI::sqlCreateTable +#' @param overwrite a logical specifying whether to overwrite an existing table +#' or not. Its default is `FALSE`. +#' @param append a logical specifying whether to append to an existing table +#' in the DBMS. Its default is `FALSE`. +#' @param field.types character vector of named SQL field types where +#' the names are the names of new table's columns. If missing, types inferred +#' with [DBI::dbDataType()]). +#' @param copy If `TRUE`, serializes the data frame to a single string +#' and uses `COPY name FROM stdin`. This is fast, but not supported by +#' all postgres servers (e.g. Amazon's redshift). If `FALSE`, generates +#' a single SQL string. This is slower, but always supported. +#' +#' RPostgres does not use parameterised queries to insert rows because +#' benchmarks revealed that this was considerably slower than using a single +#' SQL string. +#' @examples +#' # For running the examples on systems without PostgreSQL connection: +#' run <- postgresHasDefault() +#' +#' library(DBI) +#' if (run) con <- dbConnect(RPostgres::Postgres()) +#' if (run) dbListTables(con) +#' if (run) dbWriteTable(con, "mtcars", mtcars, temporary = TRUE) +#' if (run) dbReadTable(con, "mtcars") +#' +#' if (run) dbListTables(con) +#' if (run) dbExistsTable(con, "mtcars") +#' +#' # A zero row data frame just creates a table definition. +#' if (run) dbWriteTable(con, "mtcars2", mtcars[0, ], temporary = TRUE) +#' if (run) dbReadTable(con, "mtcars2") +#' +#' if (run) dbDisconnect(con) +#' @name postgres-tables +NULL + +#' @export +#' @rdname postgres-tables +setMethod("dbWriteTable", c("PqConnection", "character", "data.frame"), + function(conn, name, value, ..., row.names = FALSE, overwrite = FALSE, append = FALSE, + field.types = NULL, temporary = FALSE, copy = TRUE) { + + if (is.null(row.names)) row.names <- FALSE + if ((!is.logical(row.names) && !is.character(row.names)) || length(row.names) != 1L) { + stopc("`row.names` must be a logical scalar or a string") + } + if (!is.logical(overwrite) || length(overwrite) != 1L || is.na(overwrite)) { + stopc("`overwrite` must be a logical scalar") + } + if (!is.logical(append) || length(append) != 1L || is.na(append)) { + stopc("`append` must be a logical scalar") + } + if (!is.logical(temporary) || length(temporary) != 1L) { + stopc("`temporary` must be a logical scalar") + } + if (overwrite && append) { + stopc("overwrite and append cannot both be TRUE") + } + if (append && !is.null(field.types)) { + stopc("Cannot specify field.types with append = TRUE") + } + + found <- dbExistsTable(conn, name) + if (found && !overwrite && !append) { + stop("Table ", name, " exists in database, and both overwrite and", + " append are FALSE", call. = FALSE) + } + if (found && overwrite) { + dbRemoveTable(conn, name) + } + + if (!found || overwrite) { + if (!is.null(field.types)) { + if (is.null(names(field.types))) + types <- structure(field.types, .Names = colnames(value)) + else + types <- field.types + } else { + types <- value + } + sql <- sqlCreateTable(conn, name, if (is.null(field.types)) value else types, + row.names = row.names, temporary = temporary) + dbExecute(conn, sql) + } + + if (nrow(value) > 0) { + value <- sqlData(conn, value, row.names = row.names, copy = copy) + if (!copy) { + sql <- sqlAppendTable(conn, name, value) + dbExecute(conn, sql) + } else { + fields <- dbQuoteIdentifier(conn, names(value)) + sql <- paste0( + "COPY ", dbQuoteIdentifier(conn, name), + " (", paste(fields, collapse = ", "), ")", + " FROM STDIN" + ) + connection_copy_data(conn@ptr, sql, value) + } + } + + invisible(TRUE) + } +) + + +#' @export +#' @inheritParams DBI::sqlRownamesToColumn +#' @rdname postgres-tables +setMethod("sqlData", "PqConnection", function(con, value, row.names = FALSE, copy = TRUE) { + if (is.null(row.names)) row.names <- FALSE + value <- sqlRownamesToColumn(value, row.names) + + # C code takes care of atomic vectors, just need to coerce objects + is_object <- vlapply(value, is.object) + is_posix <- vlapply(value, function(c) inherits(c, "POSIXt")) + is_difftime <- vlapply(value, function(c) inherits(c, "difftime")) + is_blob <- vlapply(value, function(c) is.list(c)) + is_whole_number <- vlapply(value, is_whole_number_vector) + + withr::with_options( + list(digits.secs = 6), + value[is_posix] <- lapply(value[is_posix], function(col) format_keep_na(col, usetz = T)) + ) + value[is_difftime] <- lapply(value[is_difftime], function(col) format_keep_na(hms::as.hms(col))) + value[is_blob] <- lapply( + value[is_blob], + function(col) { + vapply( + col, + function(x) { + if (is.null(x)) NA_character_ + else paste0("\\x", paste(format(x), collapse = "")) + }, + character(1) + ) + } + ) + + value[is_whole_number] <- lapply( + value[is_whole_number], + function(x) { + is_value <- which(!is.na(x)) + x[is_value] <- format(x[is_value], scientific = FALSE, na.encode = FALSE) + x + } + ) + + value[is_object] <- lapply(value[is_object], as.character) + value +}) + +format_keep_na <- function(x, ...) { + is_na <- is.na(x) + ret <- format(x, ...) + ret[is_na] <- NA + ret +} + + +#' @export +#' @param check.names If `TRUE`, the default, column names will be +#' converted to valid R identifiers. +#' @rdname postgres-tables +setMethod("dbReadTable", c("PqConnection", "character"), + function(conn, name, ..., check.names = TRUE, row.names = FALSE) { + + if (is.null(row.names)) row.names <- FALSE + if ((!is.logical(row.names) && !is.character(row.names)) || length(row.names) != 1L) { + stopc("`row.names` must be a logical scalar or a string") + } + + if (!is.logical(check.names) || length(check.names) != 1L) { + stopc("`check.names` must be a logical scalar") + } + + name <- dbQuoteIdentifier(conn, name) + out <- dbGetQuery(conn, paste("SELECT * FROM ", name), row.names = row.names) + + if (check.names) { + names(out) <- make.names(names(out), unique = TRUE) + } + + out + } +) + +#' @export +#' @rdname postgres-tables +setMethod("dbListTables", "PqConnection", function(conn, ...) { + query <- paste0( + "SELECT table_name FROM INFORMATION_SCHEMA.tables ", + "WHERE ", + "(table_schema = ANY(current_schemas(false)) OR table_type = 'LOCAL TEMPORARY')" + ) + dbGetQuery(conn, query)[[1]] +}) + +#' @export +#' @rdname postgres-tables +setMethod("dbExistsTable", c("PqConnection", "character"), function(conn, name, ...) { + stopifnot(length(name) == 1L) + name <- dbQuoteIdentifier(conn, name) + # Convert to plain string + name <- paste0(gsub('^"|"$', '', name)) + name <- dbQuoteString(conn, name) + + query <- paste0( + "SELECT COUNT(*) FROM INFORMATION_SCHEMA.tables WHERE table_name = ", + name, " ", + "AND ", + "(table_schema = ANY(current_schemas(false)) OR table_type = 'LOCAL TEMPORARY')" + ) + dbGetQuery(conn, query)[[1]] >= 1 +}) + +#' @export +#' @rdname postgres-tables +setMethod("dbRemoveTable", c("PqConnection", "character"), + function(conn, name, ...) { + name <- dbQuoteIdentifier(conn, name) + dbExecute(conn, paste("DROP TABLE ", name)) + invisible(TRUE) + } +) + +#' @export +#' @rdname postgres-tables +setMethod("dbListFields", c("PqConnection", "character"), + function(conn, name, ...) { + name <- dbQuoteString(conn, name) + dbGetQuery(conn, paste("SELECT column_name FROM information_schema.columns +WHERE table_name=", name))$column_name + } +) diff --git a/R/transactions.R b/R/transactions.R new file mode 100644 index 0000000..3ed7247 --- /dev/null +++ b/R/transactions.R @@ -0,0 +1,64 @@ +#' Transaction management. +#' +#' `dbBegin()` starts a transaction. `dbCommit()` and `dbRollback()` +#' end the transaction by either committing or rolling back the changes. +#' +#' @param conn a [PqConnection-class] object, produced by +#' [DBI::dbConnect()] +#' @param ... Unused, for extensibility. +#' @return A boolean, indicating success or failure. +#' @examples +#' # For running the examples on systems without PostgreSQL connection: +#' run <- postgresHasDefault() +#' +#' library(DBI) +#' if (run) con <- dbConnect(RPostgres::Postgres()) +#' if (run) dbWriteTable(con, "USarrests", datasets::USArrests, temporary = TRUE) +#' if (run) dbGetQuery(con, 'SELECT count(*) from "USarrests"') +#' +#' if (run) dbBegin(con) +#' if (run) dbExecute(con, 'DELETE from "USarrests" WHERE "Murder" > 1') +#' if (run) dbGetQuery(con, 'SELECT count(*) from "USarrests"') +#' if (run) dbRollback(con) +#' +#' # Rolling back changes leads to original count +#' if (run) dbGetQuery(con, 'SELECT count(*) from "USarrests"') +#' +#' if (run) dbRemoveTable(con, "USarrests") +#' if (run) dbDisconnect(con) +#' @name postgres-transactions +NULL + +#' @export +#' @rdname postgres-transactions +setMethod("dbBegin", "PqConnection", function(conn, ...) { + if (connection_is_transacting(conn@ptr)) { + stop("Nested transactions not supported.", call. = FALSE) + } + + dbExecute(conn, "BEGIN") + connection_set_transacting(conn@ptr, TRUE) + invisible(TRUE) +}) + +#' @export +#' @rdname postgres-transactions +setMethod("dbCommit", "PqConnection", function(conn, ...) { + if (!connection_is_transacting(conn@ptr)) { + stop("Call dbBegin() to start a transaction.", call. = FALSE) + } + dbExecute(conn, "COMMIT") + connection_set_transacting(conn@ptr, FALSE) + invisible(TRUE) +}) + +#' @export +#' @rdname postgres-transactions +setMethod("dbRollback", "PqConnection", function(conn, ...) { + if (!connection_is_transacting(conn@ptr)) { + stop("Call dbBegin() to start a transaction.", call. = FALSE) + } + dbExecute(conn, "ROLLBACK") + connection_set_transacting(conn@ptr, FALSE) + invisible(TRUE) +}) diff --git a/R/utils.R b/R/utils.R new file mode 100644 index 0000000..a023b0a --- /dev/null +++ b/R/utils.R @@ -0,0 +1,25 @@ +vlapply <- function(X, FUN, ..., USE.NAMES = TRUE) { + vapply(X = X, FUN = FUN, FUN.VALUE = logical(1L), ..., USE.NAMES = USE.NAMES) +} + +viapply <- function(X, FUN, ..., USE.NAMES = TRUE) { + vapply(X = X, FUN = FUN, FUN.VALUE = integer(1L), ..., USE.NAMES = USE.NAMES) +} + +vcapply <- function(X, FUN, ..., USE.NAMES = TRUE) { + vapply(X = X, FUN = FUN, FUN.VALUE = character(1L), ..., USE.NAMES = USE.NAMES) +} + +stopc <- function(...) { + stop(..., call. = FALSE, domain = NA) +} + +warningc <- function(...) { + warning(..., call. = FALSE, domain = NA) +} + +is_whole_number_vector <- function(x) { + if (!is.numeric(x)) return(FALSE) + is_value <- which(!is.na(x)) + isTRUE(all.equal(x[is_value] - trunc(x[is_value]), rep_len(0, length(is_value)))) +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb83776 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# RPostgres + +[![Travis-CI Build Status](https://travis-ci.org/r-dbi/RPostgres.png?branch=master)](https://travis-ci.org/r-dbi/RPostgres) [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/r-dbi/RPostgres?branch=master&svg=true)](https://ci.appveyor.com/project/r-dbi/RPostgres) [![codecov](https://codecov.io/gh/r-dbi/RPostgres/branch/master/graph/badge.svg)](https://codecov.io/gh/r-dbi/RPostgres) + +RPostgres is an DBI-compliant interface to the postgres database. It's a ground-up rewrite using C++ and Rcpp. Compared to PostgresSQL, it: + +* Has full support for parameterised queries via `dbSendQuery()`, and `dbBind()`. + +* Automatically cleans up open connections and result sets, ensuring that you + don't need to worry about leaking connections or memory. + +* Is a little faster, saving ~5 ms per query. (For refernce, it takes around 5ms + to retrive a 1000 x 25 result set from a local database, so this is + decent speed up for smaller queries.) + +* A simplified build process that relies on system libpq. + +## Installation + +RPostgres isn't available from CRAN yet, but you can get it from github with: + +```R +# install.packages("remotes") +remotes::install_github("r-dbi/RPostgres") +``` + +## Basic usage + +```R +library(DBI) +# Connect to the default postgres database +con <- dbConnect(RPostgres::Postgres()) + +dbListTables(con) +dbWriteTable(con, "mtcars", mtcars) +dbListTables(con) + +dbListFields(con, "mtcars") +dbReadTable(con, "mtcars") + +# You can fetch all results: +res <- dbSendQuery(con, "SELECT * FROM mtcars WHERE cyl = 4") +dbFetch(res) +dbClearResult(res) + +# Or a chunk at a time +res <- dbSendQuery(con, "SELECT * FROM mtcars WHERE cyl = 4") +while(!dbHasCompleted(res)){ + chunk <- dbFetch(res, n = 5) + print(nrow(chunk)) +} +# Clear the result +dbClearResult(res) + +# Disconnect from the database +dbDisconnect(con) +``` +## Connecting to a specific Postgres instance + +```R +library(DBI) +# Connect to a specific postgres database i.e. Heroku +con <- dbConnect(RPostgres::Postgres(),dbname = 'DATABASE_NAME', + host = 'HOST', # i.e. 'ec2-54-83-201-96.compute-1.amazonaws.com' + port = 5432, # or any other port specified by your DBA + user = 'USERNAME', + password = 'PASSWORD') + +``` + +## Design notes + +The original DBI design imagined that each package could instantiate X drivers, with each driver having Y connections and each connection having Z results. This turns out to be too general: a driver has no real state, for PostgreSQL each connection can only have one result set. In the RPostgres package there's only one class on the C side: a connection, which optionally contains a result set. On the R side, the driver class is just a dummy class with no contents (used only for dispatch), and both the connection and result objects point to the same external pointer. diff --git a/configure b/configure new file mode 100755 index 0000000..637bc10 --- /dev/null +++ b/configure @@ -0,0 +1,104 @@ +#!/bin/bash +# Anticonf (tm) script by Jeroen Ooms & Murat Tasan (2017) +# This script will prefer cflags (specifically includefile dirs) and lib dirs +# in the following order of precedence: +# (1) INCLUDE_DIR or LIB_DIR entered explicitly on the command line, e.g. +# R CMD INSTALL --configure-vars='INCLUDE_DIR=/.../include LIB_DIR=/.../lib' +# (2) Values found via 'pkg-config' for the libpq package. +# (3) Values found via 'pg_config' given a PostgreSQL installation. + +# Library settings +PKG_CONFIG_NAME="libpq" +PKG_DEB_NAME="libpq-dev" +PKG_RPM_NAME="postgresql-devel" +PKG_AMZ_RPM_NAMES="postgreql8-devel, psstgresql92-devel, postgresql93-devel, or postgresql94-devel" +PKG_CSW_NAME="postgresql_dev" +PKG_BREW_NAME="libpq" +PKG_TEST_HEADER="" +PKG_LIBS="-lpq" +PKG_LIBS_STATIC="-lpq -lssl -lcrypto -lldap" + +# pkg-config values (if available) +if [ $(command -v pkg-config) ]; then + PKGCONFIG_CFLAGS=$(pkg-config --cflags --silence-errors ${PKG_CONFIG_NAME}) + PKGCONFIG_LIBS=$(pkg-config --libs --silence-errors ${PKG_CONFIG_NAME}) + + # MacOS seems to ship a broken libpq.pc file + if [[ "$PKGCONFIG_CFLAGS" == *"Internal.sdk"* ]]; then + unset PKGCONFIG_CFLAGS + unset PKGCONFIG_LIBS + fi +fi + +# pg_config values (if available) +if [ $(command -v pg_config) ]; then + PG_INC_DIR=$(pg_config --includedir) + PG_LIB_DIR=$(pg_config --libdir) +fi + +# Note that cflags may be empty in case of success +if [ "$INCLUDE_DIR" ] || [ "$LIB_DIR" ]; then + echo "Found INCLUDE_DIR and/or LIB_DIR!" + PKG_CFLAGS="-I$INCLUDE_DIR $PKG_CFLAGS" + PKG_LIBS="-L$LIB_DIR $PKG_LIBS" +elif [ "$PKGCONFIG_CFLAGS" ] || [ "$PKGCONFIG_LIBS" ]; then + echo "Using pkg-config cflags and libs!" + PKG_CFLAGS=${PKGCONFIG_CFLAGS} + PKG_LIBS=${PKGCONFIG_LIBS} +elif [ "$PG_INC_DIR" ] || [ "$PG_LIB_DIR" ]; then + echo "Using pg_config includedir and libdir!" + PKG_CFLAGS="-I${PG_INC_DIR}" + PKG_LIBS="-L${PG_LIB_DIR} ${PKG_LIBS}" +elif [[ "$OSTYPE" == "darwin"* ]]; then + if [ $(command -v brew) ]; then + BREWDIR=$(brew --prefix) + else + curl -sfL "https://jeroen.github.io/autobrew/$PKG_BREW_NAME" > autobrew + source autobrew + fi + PKG_CFLAGS="-I$BREWDIR/opt/$PKG_BREW_NAME/include" + PKG_LIBS="-L$BREWDIR/opt/$PKG_BREW_NAME/lib $PKG_LIBS" +fi + +# For debugging +echo "Using PKG_CFLAGS=$PKG_CFLAGS" +echo "Using PKG_LIBS=$PKG_LIBS" + +# Find compiler +CC=$(${R_HOME}/bin/R CMD config CC) +CFLAGS=$(${R_HOME}/bin/R CMD config CFLAGS) +CPPFLAGS=$(${R_HOME}/bin/R CMD config CPPFLAGS) + +# Test configuration +echo "#include $PKG_TEST_HEADER" | ${CC} ${CPPFLAGS} ${PKG_CFLAGS} ${CFLAGS} -E -xc - >/dev/null 2>&1 || R_CONFIG_ERROR=1; + +# Customize the error +if [ $R_CONFIG_ERROR ]; then + echo "------------------------- ANTICONF ERROR ---------------------------" + echo "Configuration failed because $PKG_CONFIG_NAME was not found. Try installing:" + echo " * deb: $PKG_DEB_NAME (Debian, Ubuntu, etc)" + echo " * rpm: $PKG_RPM_NAME (Fedora, EPEL)" + echo " * rpm: $PKG_AMZ_RPM_NAMES (Amazon Linux)" + echo " * csw: $PKG_CSW_NAME (Solaris)" + echo " * brew: $PKG_BREW_NAME (OSX)" + echo "If $PKG_CONFIG_NAME is already installed, check that either:" + echo "(i) 'pkg-config' is in your PATH AND PKG_CONFIG_PATH contains" + echo " a $PKG_CONFIG_NAME.pc file; or" + echo "(ii) 'pg_config' is in your PATH." + echo "If neither can detect $PGK_CONFIG_NAME, you can set INCLUDE_DIR" + echo "and LIB_DIR manually via:" + echo "R CMD INSTALL --configure-vars='INCLUDE_DIR=... LIB_DIR=...'" + echo "--------------------------------------------------------------------" + exit 1; +fi + +# Write to Makevars +sed -e "s|@cflags@|$PKG_CFLAGS|" -e "s|@libs@|$PKG_LIBS|" src/Makevars.in > src/Makevars.new +if [ ! -f src/Makevars.new ] || (which diff > /dev/null && ! diff -q src/Makevars src/Makevars.new); then + mv src/Makevars.new src/Makevars +else + rm src/Makevars.new +fi + +# Success +exit 0 diff --git a/configure.win b/configure.win new file mode 100644 index 0000000..e69de29 diff --git a/man/Postgres.Rd b/man/Postgres.Rd new file mode 100644 index 0000000..f3b1b00 --- /dev/null +++ b/man/Postgres.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/PqDriver.R +\name{Postgres} +\alias{Postgres} +\title{Postgres driver} +\usage{ +Postgres() +} +\description{ +This driver never needs to be unloaded and hence \code{dbUnload()} is a +null-op. +} +\examples{ +library(DBI) +RPostgres::Postgres() +} diff --git a/man/PqConnection-class.Rd b/man/PqConnection-class.Rd new file mode 100644 index 0000000..de2b6db --- /dev/null +++ b/man/PqConnection-class.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/PqConnection.R +\docType{class} +\name{PqConnection-class} +\alias{PqConnection-class} +\alias{show,PqConnection-method} +\alias{dbIsValid,PqConnection-method} +\alias{dbGetInfo,PqConnection-method} +\title{PqConnection and methods.} +\usage{ +\S4method{show}{PqConnection}(object) + +\S4method{dbIsValid}{PqConnection}(dbObj, ...) + +\S4method{dbGetInfo}{PqConnection}(dbObj, ...) +} +\description{ +PqConnection and methods. +} +\keyword{internal} diff --git a/man/PqDriver-class.Rd b/man/PqDriver-class.Rd new file mode 100644 index 0000000..37cce55 --- /dev/null +++ b/man/PqDriver-class.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/PqDriver.R +\docType{class} +\name{PqDriver-class} +\alias{PqDriver-class} +\alias{dbUnloadDriver,PqDriver-method} +\title{PqDriver and methods.} +\usage{ +\S4method{dbUnloadDriver}{PqDriver}(drv, ...) +} +\description{ +PqDriver and methods. +} +\keyword{internal} diff --git a/man/PqResult-class.Rd b/man/PqResult-class.Rd new file mode 100644 index 0000000..32f57d1 --- /dev/null +++ b/man/PqResult-class.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/PqDriver.R, R/PqResult.R +\docType{methods} +\name{dbIsValid,PqDriver-method} +\alias{dbIsValid,PqDriver-method} +\alias{PqResult-class} +\alias{dbGetStatement,PqResult-method} +\alias{dbIsValid,PqResult-method} +\alias{dbGetRowCount,PqResult-method} +\alias{dbGetRowsAffected,PqResult-method} +\alias{dbColumnInfo,PqResult-method} +\title{PostgreSQL results.} +\usage{ +\S4method{dbIsValid}{PqDriver}(dbObj, ...) + +\S4method{dbGetStatement}{PqResult}(res, ...) + +\S4method{dbIsValid}{PqResult}(dbObj, ...) + +\S4method{dbGetRowCount}{PqResult}(res, ...) + +\S4method{dbGetRowsAffected}{PqResult}(res, ...) + +\S4method{dbColumnInfo}{PqResult}(res, ...) +} +\description{ +PostgreSQL results. +} +\keyword{internal} diff --git a/man/RPostgres-package.Rd b/man/RPostgres-package.Rd new file mode 100644 index 0000000..4b45c8e --- /dev/null +++ b/man/RPostgres-package.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/RPostgres-pkg.R +\docType{package} +\name{RPostgres-package} +\alias{RPostgres} +\alias{RPostgres-package} +\title{RPostgres: 'Rcpp' Interface to 'PostgreSQL'} +\description{ +Fully 'DBI'-compliant 'Rcpp'-backed interface to 'PostgreSQL' , +an open-source relational database. +} +\author{ +\strong{Maintainer}: Kirill Müller \email{krlmlr+r@mailbox.org} + +Authors: +\itemize{ + \item Hadley Wickham + \item Jeroen Ooms +} + +Other contributors: +\itemize{ + \item RStudio [copyright holder] + \item R Consortium [copyright holder] + \item Tomoaki Nishiyama (Code for encoding vectors into strings derived from RPostgreSQL) [contributor] + \item Kungliga Tekniska Högskolan (Source code for timegm) [contributor] +} + +} diff --git a/man/dbConnect-PqDriver-method.Rd b/man/dbConnect-PqDriver-method.Rd new file mode 100644 index 0000000..2591425 --- /dev/null +++ b/man/dbConnect-PqDriver-method.Rd @@ -0,0 +1,61 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/PqConnection.R +\docType{methods} +\name{dbDisconnect,PqConnection-method} +\alias{dbDisconnect,PqConnection-method} +\alias{dbConnect,PqDriver-method} +\title{Connect to a PostgreSQL database.} +\usage{ +\S4method{dbDisconnect}{PqConnection}(conn, ...) + +\S4method{dbConnect}{PqDriver}(drv, dbname = NULL, host = NULL, + port = NULL, password = NULL, user = NULL, service = NULL, ..., + bigint = c("integer64", "integer", "numeric", "character")) +} +\arguments{ +\item{conn}{Connection to disconnect.} + +\item{...}{Other name-value pairs that describe additional connection +options as described at +\url{http://www.postgresql.org/docs/9.6/static/libpq-connect.html#LIBPQ-PARAMKEYWORDS}} + +\item{drv}{\code{RPostgres::Postgres()}} + +\item{dbname}{Database name. If \code{NULL}, defaults to the user name. +Note that this argument can only contain the database name, it will not +be parsed as a connection string (internally, \code{expand_dbname} is set to +\code{false} in the call to +\href{https://www.postgresql.org/docs/9.6/static/libpq-connect.html}{PQconnectdbParams()}).} + +\item{host, port}{Host and port. If \code{NULL}, will be retrieved from +\code{PGHOST} and \code{PGPORT} env vars.} + +\item{user, password}{User name and password. If \code{NULL}, will be +retrieved from \code{PGUSER} and \code{PGPASSWORD} envvars, or from the +appropriate line in \code{~/.pgpass}. See +\url{http://www.postgresql.org/docs/9.6/static/libpq-pgpass.html} for +more details.} + +\item{service}{Name of service to connect as. If \code{NULL}, will be +ignored. Otherwise, connection parameters will be loaded from the pg_service.conf +file and used. See \url{http://www.postgresql.org/docs/9.6/static/libpq-pgservice.html} +for details on this file and syntax.} + +\item{bigint}{The R type that 64-bit integer types should be mapped to, +default is \link[bit64:integer64]{bit64::integer64}, which allows the full range of 64 bit +integers.} +} +\description{ +Manually disconnecting a connection is not necessary with RPostgres, but +still recommended; +if you delete the object containing the connection, it will be automatically +disconnected during the next GC with a warning. +} +\examples{ +if (postgresHasDefault()) { +library(DBI) +# Pass more arguments as necessary to dbConnect() +con <- dbConnect(RPostgres::Postgres()) +dbDisconnect(con) +} +} diff --git a/man/dbDataType.Rd b/man/dbDataType.Rd new file mode 100644 index 0000000..562e6cf --- /dev/null +++ b/man/dbDataType.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/PqConnection.R +\docType{methods} +\name{dbDataType,PqConnection-method} +\alias{dbDataType,PqConnection-method} +\alias{dbDataType,PqDriver-method} +\title{Determine database type for R vector.} +\usage{ +\S4method{dbDataType}{PqConnection}(dbObj, obj, ...) + +\S4method{dbDataType}{PqDriver}(dbObj, obj, ...) +} +\arguments{ +\item{dbObj}{Postgres driver or connection.} + +\item{obj}{Object to convert} +} +\description{ +Determine database type for R vector. +} +\keyword{internal} diff --git a/man/postgres-query.Rd b/man/postgres-query.Rd new file mode 100644 index 0000000..b7d029a --- /dev/null +++ b/man/postgres-query.Rd @@ -0,0 +1,80 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/PqResult.R +\docType{methods} +\name{postgres-query} +\alias{postgres-query} +\alias{dbSendQuery,PqConnection,character-method} +\alias{dbFetch,PqResult-method} +\alias{dbBind,PqResult-method} +\alias{dbHasCompleted,PqResult-method} +\alias{dbClearResult,PqResult-method} +\title{Execute a SQL statement on a database connection} +\usage{ +\S4method{dbSendQuery}{PqConnection,character}(conn, statement, params = NULL, + ...) + +\S4method{dbFetch}{PqResult}(res, n = -1, ..., row.names = FALSE) + +\S4method{dbBind}{PqResult}(res, params, ...) + +\S4method{dbHasCompleted}{PqResult}(res, ...) + +\S4method{dbClearResult}{PqResult}(res, ...) +} +\arguments{ +\item{conn}{A \linkS4class{PqConnection} created by \code{\link[=dbConnect]{dbConnect()}}.} + +\item{statement}{An SQL string to execute} + +\item{params}{A list of query parameters to be substituted into +a parameterised query. Query parameters are sent as strings, and the +correct type is imputed by PostgreSQL. If this fails, you can manually +cast the parameter with e.g. \code{"$1::bigint"}.} + +\item{...}{Another arguments needed for compatibility with generic ( +currently ignored).} + +\item{res}{Code a \linkS4class{PqResult} produced by +\code{\link[DBI:dbSendQuery]{DBI::dbSendQuery()}}.} + +\item{n}{Number of rows to return. If less than zero returns all rows.} + +\item{row.names}{Either \code{TRUE}, \code{FALSE}, \code{NA} or a string. + +If \code{TRUE}, always translate row names to a column called "row_names". +If \code{FALSE}, never translate row names. If \code{NA}, translate +rownames only if they're a character vector. + +A string is equivalent to \code{TRUE}, but allows you to override the +default name. + +For backward compatibility, \code{NULL} is equivalent to \code{FALSE}.} +} +\description{ +To retrieve results a chunk at a time, use \code{dbSendQuery()}, +\code{dbFetch()}, then \code{dbClearResult()}. Alternatively, if you want all the +results (and they'll fit in memory) use \code{dbGetQuery()} which sends, +fetches and clears for you. +} +\examples{ +# For running the examples on systems without PostgreSQL connection: +run <- postgresHasDefault() + +library(DBI) +if (run) db <- dbConnect(RPostgres::Postgres()) +if (run) dbWriteTable(db, "usarrests", datasets::USArrests, temporary = TRUE) + +# Run query to get results as dataframe +if (run) dbGetQuery(db, "SELECT * FROM usarrests LIMIT 3") + +# Send query to pull requests in batches +if (run) res <- dbSendQuery(db, "SELECT * FROM usarrests") +if (run) dbFetch(res, n = 2) +if (run) dbFetch(res, n = 2) +if (run) dbHasCompleted(res) +if (run) dbClearResult(res) + +if (run) dbRemoveTable(db, "usarrests") + +if (run) dbDisconnect(db) +} diff --git a/man/postgres-tables.Rd b/man/postgres-tables.Rd new file mode 100644 index 0000000..2cd1653 --- /dev/null +++ b/man/postgres-tables.Rd @@ -0,0 +1,102 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tables.R +\docType{methods} +\name{postgres-tables} +\alias{postgres-tables} +\alias{dbWriteTable,PqConnection,character,data.frame-method} +\alias{sqlData,PqConnection-method} +\alias{dbReadTable,PqConnection,character-method} +\alias{dbListTables,PqConnection-method} +\alias{dbExistsTable,PqConnection,character-method} +\alias{dbRemoveTable,PqConnection,character-method} +\alias{dbListFields,PqConnection,character-method} +\title{Convenience functions for reading/writing DBMS tables} +\usage{ +\S4method{dbWriteTable}{PqConnection,character,data.frame}(conn, name, value, + ..., row.names = FALSE, overwrite = FALSE, append = FALSE, + field.types = NULL, temporary = FALSE, copy = TRUE) + +\S4method{sqlData}{PqConnection}(con, value, row.names = FALSE, copy = TRUE) + +\S4method{dbReadTable}{PqConnection,character}(conn, name, ..., + check.names = TRUE, row.names = FALSE) + +\S4method{dbListTables}{PqConnection}(conn, ...) + +\S4method{dbExistsTable}{PqConnection,character}(conn, name, ...) + +\S4method{dbRemoveTable}{PqConnection,character}(conn, name, ...) + +\S4method{dbListFields}{PqConnection,character}(conn, name, ...) +} +\arguments{ +\item{conn}{a \linkS4class{PqConnection} object, produced by +\code{\link[DBI:dbConnect]{DBI::dbConnect()}}} + +\item{name}{a character string specifying a table name. Names will be +automatically quoted so you can use any sequence of characters, not +just any valid bare table name.} + +\item{value}{A data.frame to write to the database.} + +\item{...}{Other arguments used by individual methods.} + +\item{row.names}{Either \code{TRUE}, \code{FALSE}, \code{NA} or a string. + +If \code{TRUE}, always translate row names to a column called "row_names". +If \code{FALSE}, never translate row names. If \code{NA}, translate +rownames only if they're a character vector. + +A string is equivalent to \code{TRUE}, but allows you to override the +default name. + +For backward compatibility, \code{NULL} is equivalent to \code{FALSE}.} + +\item{overwrite}{a logical specifying whether to overwrite an existing table +or not. Its default is \code{FALSE}.} + +\item{append}{a logical specifying whether to append to an existing table +in the DBMS. Its default is \code{FALSE}.} + +\item{field.types}{character vector of named SQL field types where +the names are the names of new table's columns. If missing, types inferred +with \code{\link[DBI:dbDataType]{DBI::dbDataType()}}).} + +\item{temporary}{If \code{TRUE}, will generate a temporary table statement.} + +\item{copy}{If \code{TRUE}, serializes the data frame to a single string +and uses \code{COPY name FROM stdin}. This is fast, but not supported by +all postgres servers (e.g. Amazon's redshift). If \code{FALSE}, generates +a single SQL string. This is slower, but always supported. + +RPostgres does not use parameterised queries to insert rows because +benchmarks revealed that this was considerably slower than using a single +SQL string.} + +\item{con}{A database connection.} + +\item{check.names}{If \code{TRUE}, the default, column names will be +converted to valid R identifiers.} +} +\description{ +Convenience functions for reading/writing DBMS tables +} +\examples{ +# For running the examples on systems without PostgreSQL connection: +run <- postgresHasDefault() + +library(DBI) +if (run) con <- dbConnect(RPostgres::Postgres()) +if (run) dbListTables(con) +if (run) dbWriteTable(con, "mtcars", mtcars, temporary = TRUE) +if (run) dbReadTable(con, "mtcars") + +if (run) dbListTables(con) +if (run) dbExistsTable(con, "mtcars") + +# A zero row data frame just creates a table definition. +if (run) dbWriteTable(con, "mtcars2", mtcars[0, ], temporary = TRUE) +if (run) dbReadTable(con, "mtcars2") + +if (run) dbDisconnect(con) +} diff --git a/man/postgres-transactions.Rd b/man/postgres-transactions.Rd new file mode 100644 index 0000000..e8dfdd6 --- /dev/null +++ b/man/postgres-transactions.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/transactions.R +\docType{methods} +\name{postgres-transactions} +\alias{postgres-transactions} +\alias{dbBegin,PqConnection-method} +\alias{dbCommit,PqConnection-method} +\alias{dbRollback,PqConnection-method} +\title{Transaction management.} +\usage{ +\S4method{dbBegin}{PqConnection}(conn, ...) + +\S4method{dbCommit}{PqConnection}(conn, ...) + +\S4method{dbRollback}{PqConnection}(conn, ...) +} +\arguments{ +\item{conn}{a \linkS4class{PqConnection} object, produced by +\code{\link[DBI:dbConnect]{DBI::dbConnect()}}} + +\item{...}{Unused, for extensibility.} +} +\value{ +A boolean, indicating success or failure. +} +\description{ +\code{dbBegin()} starts a transaction. \code{dbCommit()} and \code{dbRollback()} +end the transaction by either committing or rolling back the changes. +} +\examples{ +# For running the examples on systems without PostgreSQL connection: +run <- postgresHasDefault() + +library(DBI) +if (run) con <- dbConnect(RPostgres::Postgres()) +if (run) dbWriteTable(con, "USarrests", datasets::USArrests, temporary = TRUE) +if (run) dbGetQuery(con, 'SELECT count(*) from "USarrests"') + +if (run) dbBegin(con) +if (run) dbExecute(con, 'DELETE from "USarrests" WHERE "Murder" > 1') +if (run) dbGetQuery(con, 'SELECT count(*) from "USarrests"') +if (run) dbRollback(con) + +# Rolling back changes leads to original count +if (run) dbGetQuery(con, 'SELECT count(*) from "USarrests"') + +if (run) dbRemoveTable(con, "USarrests") +if (run) dbDisconnect(con) +} diff --git a/man/postgresHasDefault.Rd b/man/postgresHasDefault.Rd new file mode 100644 index 0000000..07d50e7 --- /dev/null +++ b/man/postgresHasDefault.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/default.R +\name{postgresHasDefault} +\alias{postgresHasDefault} +\alias{postgresDefault} +\title{Check if default database is available.} +\usage{ +postgresHasDefault(...) + +postgresDefault(...) +} +\arguments{ +\item{...}{Additional arguments passed on to \code{\link[=dbConnect]{dbConnect()}}} +} +\description{ +RPostgres examples and tests connect to a default database via +\code{dbConnect(}\code{\link[RPostgres:Postgres]{RPostgres::Postgres()}}\code{)}. This function checks if that +database is available, and if not, displays an informative message. + +\code{postgresDefault()} works similarly but returns a connection on success and +throws a testthat skip condition on failure, making it suitable for use in +tests. +} +\examples{ +if (postgresHasDefault()) { + db <- postgresDefault() + dbListTables(db) + dbDisconnect(db) +} +} diff --git a/man/quote.Rd b/man/quote.Rd new file mode 100644 index 0000000..51f0fb8 --- /dev/null +++ b/man/quote.Rd @@ -0,0 +1,71 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/quote.R +\docType{methods} +\name{quote} +\alias{quote} +\alias{dbQuoteString,PqConnection,character-method} +\alias{dbQuoteString,PqConnection,SQL-method} +\alias{dbQuoteIdentifier,PqConnection,character-method} +\alias{dbQuoteIdentifier,PqConnection,SQL-method} +\alias{dbQuoteLiteral} +\alias{dbQuoteLiteral,PqConnection,logical-method} +\alias{dbQuoteLiteral,PqConnection,integer-method} +\alias{dbQuoteLiteral,PqConnection,numeric-method} +\alias{dbQuoteLiteral,PqConnection,factor-method} +\alias{dbQuoteLiteral,PqConnection,Date-method} +\alias{dbQuoteLiteral,PqConnection,POSIXt-method} +\alias{dbQuoteLiteral,PqConnection,difftime-method} +\alias{dbQuoteLiteral,PqConnection,list-method} +\alias{dbQuoteLiteral,PqConnection,blob-method} +\title{Quote postgres strings and identifiers.} +\usage{ +\S4method{dbQuoteString}{PqConnection,character}(conn, x, ...) + +\S4method{dbQuoteString}{PqConnection,SQL}(conn, x, ...) + +\S4method{dbQuoteIdentifier}{PqConnection,character}(conn, x, ...) + +\S4method{dbQuoteIdentifier}{PqConnection,SQL}(conn, x, ...) + +dbQuoteLiteral(conn, x, ...) + +\S4method{dbQuoteLiteral}{PqConnection,logical}(conn, x, ...) + +\S4method{dbQuoteLiteral}{PqConnection,integer}(conn, x, ...) + +\S4method{dbQuoteLiteral}{PqConnection,numeric}(conn, x, ...) + +\S4method{dbQuoteLiteral}{PqConnection,factor}(conn, x, ...) + +\S4method{dbQuoteLiteral}{PqConnection,Date}(conn, x, ...) + +\S4method{dbQuoteLiteral}{PqConnection,POSIXt}(conn, x, ...) + +\S4method{dbQuoteLiteral}{PqConnection,difftime}(conn, x, ...) + +\S4method{dbQuoteLiteral}{PqConnection,list}(conn, x, ...) + +\S4method{dbQuoteLiteral}{PqConnection,blob}(conn, x, ...) +} +\arguments{ +\item{conn}{A \linkS4class{PqConnection} created by \code{dbConnect()}} + +\item{x}{A character to escaped} + +\item{...}{Other arguments needed for compatibility with generic} +} +\description{ +Quote postgres strings and identifiers. +} +\examples{ +# For running the examples on systems without PostgreSQL connection: +run <- postgresHasDefault() + +library(DBI) +if (run) con <- dbConnect(RPostgres::Postgres()) + +x <- c("a", "b c", "d'e", "\\\\f") +if (run) dbQuoteString(con, x) +if (run) dbQuoteIdentifier(con, x) +if (run) dbDisconnect(con) +} diff --git a/src/DbColumn.cpp b/src/DbColumn.cpp new file mode 100644 index 0000000..aa9f9de --- /dev/null +++ b/src/DbColumn.cpp @@ -0,0 +1,116 @@ +#include "pch.h" +#include "DbColumn.h" +#include "DbColumnDataSource.h" +#include "DbColumnStorage.h" + + +DbColumn::DbColumn(DATA_TYPE dt, const int n_max_, DbColumnDataSourceFactory* factory, const int j) + : source(factory->create(j)), + n(0) +{ + if (dt == DT_BOOL) + dt = DT_UNKNOWN; + storage.push_back(new DbColumnStorage(dt, 0, n_max_, *source)); +} + +DbColumn::~DbColumn() { +} + +void DbColumn::set_col_value() { + DbColumnStorage* last = get_last_storage(); + DATA_TYPE dt = last->get_item_data_type(); + data_types_seen.insert(dt); + + DbColumnStorage* next = last->append_col(); + if (last != next) storage.push_back(next); +} + +void DbColumn::finalize(const int n_) { + n = n_; +} + +void DbColumn::warn_type_conflicts(const String& name) const { + std::set my_data_types_seen = data_types_seen; + DATA_TYPE dt = get_last_storage()->get_data_type(); + + switch (dt) { + case DT_REAL: + my_data_types_seen.erase(DT_INT); + break; + + case DT_INT64: + my_data_types_seen.erase(DT_INT); + break; + + default: + break; + } + + my_data_types_seen.erase(DT_UNKNOWN); + my_data_types_seen.erase(DT_BOOL); + my_data_types_seen.erase(dt); + + if (my_data_types_seen.size() == 0) return; + + String name_utf8 = name; + name_utf8.set_encoding(CE_UTF8); + + std::stringstream ss; + ss << "Column `" << name_utf8.get_cstring() << "`: " << + "mixed type, first seen values of type " << format_data_type(dt) << ", " << + "coercing other values of type "; + + bool first = true; + for (std::set::const_iterator it = my_data_types_seen.begin(); it != my_data_types_seen.end(); ++it) { + if (!first) ss << ", "; + else first = false; + ss << format_data_type(*it); + } + + warning(ss.str()); +} + +DbColumn::operator SEXP() const { + DATA_TYPE dt = get_last_storage()->get_data_type(); + SEXP ret = DbColumnStorage::allocate(n, dt); + int pos = 0; + for (size_t k = 0; k < storage.size(); ++k) { + const DbColumnStorage& current = storage[k]; + pos += current.copy_to(ret, dt, pos); + } + return ret; +} + +DATA_TYPE DbColumn::get_type() const { + const DATA_TYPE dt = get_last_storage()->get_data_type(); + return dt; +} + +const char* DbColumn::format_data_type(const DATA_TYPE dt) { + switch (dt) { + case DT_UNKNOWN: + return "unknown"; + case DT_BOOL: + return "boolean"; + case DT_INT: + return "integer"; + case DT_INT64: + return "integer64"; + case DT_REAL: + return "real"; + case DT_STRING: + return "string"; + case DT_BLOB: + return "blob"; + default: + return ""; + } +} + +DbColumnStorage* DbColumn::get_last_storage() { + return &storage.end()[-1]; +} + +const DbColumnStorage* DbColumn::get_last_storage() const { + return &storage.end()[-1]; +} diff --git a/src/DbColumn.h b/src/DbColumn.h new file mode 100644 index 0000000..6d4da73 --- /dev/null +++ b/src/DbColumn.h @@ -0,0 +1,40 @@ +#ifndef DB_COLUMN_H +#define DB_COLUMN_H + + +#include "DbColumnDataType.h" +#include "DbColumnDataSourceFactory.h" +#include +#include + +class DbColumnDataSourceFactory; +class DbColumnDataSource; +class DbColumnStorage; + +class DbColumn { +private: + boost::shared_ptr source; + boost::ptr_vector storage; + int n; + std::set data_types_seen; + +public: + DbColumn(DATA_TYPE dt_, const int n_max_, DbColumnDataSourceFactory* factory, const int j); + ~DbColumn(); + +public: + void set_col_value(); + void finalize(const int n_); + void warn_type_conflicts(const String& name) const; + + operator SEXP() const; + DATA_TYPE get_type() const; + static const char* format_data_type(const DATA_TYPE dt); + +private: + DbColumnStorage* get_last_storage(); + const DbColumnStorage* get_last_storage() const; +}; + + +#endif // DB_COLUMN_H diff --git a/src/DbColumnDataSource.cpp b/src/DbColumnDataSource.cpp new file mode 100644 index 0000000..e2f7f9c --- /dev/null +++ b/src/DbColumnDataSource.cpp @@ -0,0 +1,14 @@ +#include "pch.h" +#include "DbColumnDataSource.h" + +DbColumnDataSource::DbColumnDataSource(const int j_) : +j(j_) +{ +} + +DbColumnDataSource::~DbColumnDataSource() { +} + +int DbColumnDataSource::get_j() const { + return j; +} diff --git a/src/DbColumnDataSource.h b/src/DbColumnDataSource.h new file mode 100644 index 0000000..c0c8aac --- /dev/null +++ b/src/DbColumnDataSource.h @@ -0,0 +1,36 @@ +#ifndef DB_COLUMNDATASOURCE_H +#define DB_COLUMNDATASOURCE_H + +#include "DbColumnDataType.h" + +class DbColumnDataSource { + const int j; + +protected: + DbColumnDataSource(const int j); + +public: + virtual ~DbColumnDataSource(); + +public: + virtual DATA_TYPE get_data_type() const = 0; + virtual DATA_TYPE get_decl_data_type() const = 0; + + virtual bool is_null() const = 0; + + virtual int fetch_bool() const = 0; + virtual int fetch_int() const = 0; + virtual int64_t fetch_int64() const = 0; + virtual double fetch_real() const = 0; + virtual SEXP fetch_string() const = 0; + virtual SEXP fetch_blob() const = 0; + virtual double fetch_date() const = 0; + virtual double fetch_datetime_local() const = 0; + virtual double fetch_datetime() const = 0; + virtual double fetch_time() const = 0; + +protected: + int get_j() const; +}; + +#endif //DB_COLUMNDATASOURCE_H diff --git a/src/DbColumnDataSourceFactory.cpp b/src/DbColumnDataSourceFactory.cpp new file mode 100644 index 0000000..66dc58c --- /dev/null +++ b/src/DbColumnDataSourceFactory.cpp @@ -0,0 +1,8 @@ +#include "pch.h" +#include "DbColumnDataSourceFactory.h" + +DbColumnDataSourceFactory::DbColumnDataSourceFactory() { +} + +DbColumnDataSourceFactory::~DbColumnDataSourceFactory() { +} diff --git a/src/DbColumnDataSourceFactory.h b/src/DbColumnDataSourceFactory.h new file mode 100644 index 0000000..04fcff1 --- /dev/null +++ b/src/DbColumnDataSourceFactory.h @@ -0,0 +1,17 @@ +#ifndef DB_COLUMNDATASOURCEFACTORY_H +#define DB_COLUMNDATASOURCEFACTORY_H + +class DbColumnDataSource; + +class DbColumnDataSourceFactory { +protected: + DbColumnDataSourceFactory(); + +public: + virtual ~DbColumnDataSourceFactory(); + +public: + virtual DbColumnDataSource* create(const int j) = 0; +}; + +#endif //DB_COLUMNDATASOURCEFACTORY_H diff --git a/src/DbColumnDataType.h b/src/DbColumnDataType.h new file mode 100644 index 0000000..7b7f3fc --- /dev/null +++ b/src/DbColumnDataType.h @@ -0,0 +1,18 @@ +#ifndef RSQLITE_COLUMNDATATYPE_H +#define RSQLITE_COLUMNDATATYPE_H + +enum DATA_TYPE { + DT_UNKNOWN, + DT_BOOL, + DT_INT, + DT_INT64, + DT_REAL, + DT_STRING, + DT_BLOB, + DT_DATE, + DT_DATETIME, + DT_DATETIMETZ, + DT_TIME +}; + +#endif // RSQLITE_COLUMNDATATYPE_H diff --git a/src/DbColumnStorage.cpp b/src/DbColumnStorage.cpp new file mode 100644 index 0000000..32c5914 --- /dev/null +++ b/src/DbColumnStorage.cpp @@ -0,0 +1,312 @@ +#include "pch.h" +#include "DbColumnStorage.h" +#include "DbColumnDataSource.h" +#include "integer64.h" + + +using namespace Rcpp; + +DbColumnStorage::DbColumnStorage(DATA_TYPE dt_, const R_xlen_t capacity_, const int n_max_, + const DbColumnDataSource& source_) + : + i(0), + dt(dt_), + n_max(n_max_), + source(source_) +{ + data = allocate(get_new_capacity(capacity_), dt); +} + +DbColumnStorage::~DbColumnStorage() { +} + +DbColumnStorage* DbColumnStorage::append_col() { + if (source.is_null()) return append_null(); + return append_data(); +} + +DATA_TYPE DbColumnStorage::get_item_data_type() const { + return source.get_data_type(); +} + +DATA_TYPE DbColumnStorage::get_data_type() const { + if (dt == DT_UNKNOWN) return source.get_decl_data_type(); + return dt; +} + +SEXP DbColumnStorage::allocate(const R_xlen_t length, DATA_TYPE dt) { + SEXPTYPE type = sexptype_from_datatype(dt); + RObject class_ = class_from_datatype(dt); + + SEXP ret = Rf_allocVector(type, length); + if (!Rf_isNull(class_)) Rf_setAttrib(ret, R_ClassSymbol, class_); + set_attribs_from_datatype(ret, dt); + return ret; +} + +int DbColumnStorage::copy_to(SEXP x, DATA_TYPE dt, const int pos) const { + R_xlen_t n = Rf_xlength(x); + int src, tgt; + R_xlen_t capacity = get_capacity(); + for (src = 0, tgt = pos; src < capacity && src < i && tgt < n; ++src, ++tgt) { + copy_value(x, dt, tgt, src); + } + + for (; src < i && tgt < n; ++src, ++tgt) { + fill_default_value(x, dt, tgt); + } + + return src; +} + +R_xlen_t DbColumnStorage::get_capacity() const { + return Rf_xlength(data); +} + +R_xlen_t DbColumnStorage::get_new_capacity(const R_xlen_t desired_capacity) const { + if (n_max < 0) { + const R_xlen_t MIN_DATA_CAPACITY = 100; + return std::max(desired_capacity, MIN_DATA_CAPACITY); + } + else { + return std::max(desired_capacity, R_xlen_t(1)); + } +} + +DbColumnStorage* DbColumnStorage::append_null() { + if (i < get_capacity()) fill_default_value(); + ++i; + return this; +} + +void DbColumnStorage::fill_default_value() { + fill_default_value(data, dt, i); +} + +DbColumnStorage* DbColumnStorage::append_data() { + if (dt == DT_UNKNOWN) return append_data_to_new(dt); + if (i >= get_capacity()) return append_data_to_new(dt); + DATA_TYPE new_dt = source.get_data_type(); + if (dt == DT_INT && new_dt == DT_INT64) return append_data_to_new(DT_INT64); + if (dt == DT_INT && new_dt == DT_REAL) return append_data_to_new(DT_REAL); + + fetch_value(); + ++i; + return this; +} + +DbColumnStorage* DbColumnStorage::append_data_to_new(DATA_TYPE new_dt) { + if (new_dt == DT_UNKNOWN) new_dt = source.get_data_type(); + + R_xlen_t desired_capacity = (n_max < 0) ? (get_capacity() * 2) : (n_max - i); + + DbColumnStorage* spillover = new DbColumnStorage(new_dt, desired_capacity, n_max, source); + return spillover->append_data(); +} + +void DbColumnStorage::fetch_value() { + switch (dt) { + case DT_BOOL: + LOGICAL(data)[i] = source.fetch_bool(); + break; + + case DT_INT: + INTEGER(data)[i] = source.fetch_int(); + break; + + case DT_INT64: + INTEGER64(data)[i] = source.fetch_int64(); + break; + + case DT_REAL: + REAL(data)[i] = source.fetch_real(); + break; + + case DT_STRING: + SET_STRING_ELT(data, i, source.fetch_string()); + break; + + case DT_BLOB: + SET_VECTOR_ELT(data, i, source.fetch_blob()); + break; + + case DT_DATE: + REAL(data)[i] = source.fetch_date(); + break; + + case DT_DATETIME: + REAL(data)[i] = source.fetch_datetime_local(); + break; + + case DT_DATETIMETZ: + REAL(data)[i] = source.fetch_datetime(); + break; + + case DT_TIME: + REAL(data)[i] = source.fetch_time(); + break; + + default: + stop("NYI"); + } +} + +SEXPTYPE DbColumnStorage::sexptype_from_datatype(DATA_TYPE dt) { + switch (dt) { + case DT_UNKNOWN: + return NILSXP; + + case DT_BOOL: + return LGLSXP; + + case DT_INT: + return INTSXP; + + case DT_INT64: + return INT64SXP; + + case DT_REAL: + case DT_DATE: + case DT_DATETIME: + case DT_DATETIMETZ: + case DT_TIME: + return REALSXP; + + case DT_STRING: + return STRSXP; + + case DT_BLOB: + return VECSXP; + + default: + stop("Unknown type %d", dt); + } +} + +Rcpp::RObject DbColumnStorage::class_from_datatype(DATA_TYPE dt) { + switch (dt) { + case DT_INT64: + return CharacterVector::create("integer64"); + + case DT_BLOB: + return CharacterVector::create("blob"); + + case DT_DATE: + return CharacterVector::create("Date"); + + case DT_DATETIME: + case DT_DATETIMETZ: + return CharacterVector::create("POSIXct", "POSIXt"); + + case DT_TIME: + return CharacterVector::create("hms", "difftime"); + + default: + return R_NilValue; + } +} + +void DbColumnStorage::set_attribs_from_datatype(SEXP x, DATA_TYPE dt) { + switch (dt) { + case DT_TIME: + Rf_setAttrib(x, CharacterVector::create("units"), CharacterVector::create("secs")); + break; + + default: + ; + } +} + +void DbColumnStorage::fill_default_value(SEXP data, DATA_TYPE dt, R_xlen_t i) { + switch (dt) { + case DT_BOOL: + LOGICAL(data)[i] = NA_LOGICAL; + break; + + case DT_INT: + INTEGER(data)[i] = NA_INTEGER; + break; + + case DT_INT64: + INTEGER64(data)[i] = NA_INTEGER64; + break; + + case DT_REAL: + case DT_DATE: + case DT_DATETIME: + case DT_DATETIMETZ: + case DT_TIME: + REAL(data)[i] = NA_REAL; + break; + + case DT_STRING: + SET_STRING_ELT(data, i, NA_STRING); + break; + + case DT_BLOB: + SET_VECTOR_ELT(data, i, R_NilValue); + break; + + case DT_UNKNOWN: + stop("Not setting value for unknown data type"); + } +} + +void DbColumnStorage::copy_value(SEXP x, DATA_TYPE dt, const int tgt, const int src) const { + if (Rf_isNull(data)) { + fill_default_value(x, dt, tgt); + } + else { + switch (dt) { + case DT_BOOL: + LOGICAL(x)[tgt] = LOGICAL(data)[src]; + break; + + case DT_INT: + INTEGER(x)[tgt] = INTEGER(data)[src]; + break; + + case DT_INT64: + switch (TYPEOF(data)) { + case INTSXP: + INTEGER64(x)[tgt] = INTEGER(data)[src]; + break; + + case REALSXP: + INTEGER64(x)[tgt] = INTEGER64(data)[src]; + break; + } + break; + + case DT_REAL: + switch (TYPEOF(data)) { + case INTSXP: + REAL(x)[tgt] = INTEGER(data)[src]; + break; + + case REALSXP: + REAL(x)[tgt] = REAL(data)[src]; + break; + } + break; + + case DT_STRING: + SET_STRING_ELT(x, tgt, STRING_ELT(data, src)); + break; + + case DT_BLOB: + SET_VECTOR_ELT(x, tgt, VECTOR_ELT(data, src)); + break; + + case DT_DATE: + case DT_DATETIME: + case DT_DATETIMETZ: + case DT_TIME: + REAL(x)[tgt] = REAL(data)[src]; + break; + + default: + stop("NYI: default"); + } + } +} diff --git a/src/DbColumnStorage.h b/src/DbColumnStorage.h new file mode 100644 index 0000000..d99a12e --- /dev/null +++ b/src/DbColumnStorage.h @@ -0,0 +1,54 @@ +#ifndef DB_COLUMNSTORAGE_H +#define DB_COLUMNSTORAGE_H + + +#include "DbColumnDataType.h" + + +class DbColumnDataSource; + +class DbColumnStorage { + Rcpp::RObject data; + int i; + DATA_TYPE dt; + const int n_max; + const DbColumnDataSource& source; + +public: + DbColumnStorage(DATA_TYPE dt_, const R_xlen_t capacity_, const int n_max_, const DbColumnDataSource& source_); + ~DbColumnStorage(); + +public: + DbColumnStorage* append_col(); + + DATA_TYPE get_item_data_type() const; + DATA_TYPE get_data_type() const; + static SEXP allocate(const R_xlen_t length, DATA_TYPE dt); + int copy_to(SEXP x, DATA_TYPE dt, const int pos) const; + + // allocate() + static SEXPTYPE sexptype_from_datatype(DATA_TYPE type); + +private: + // append_col() + R_xlen_t get_capacity() const; + R_xlen_t get_new_capacity(const R_xlen_t desired_capacity) const; + + DbColumnStorage* append_null(); + void fill_default_value(); + + DbColumnStorage* append_data(); + DbColumnStorage* append_data_to_new(DATA_TYPE new_dt); + void fetch_value(); + + // allocate() + static Rcpp::RObject class_from_datatype(DATA_TYPE dt); + static void set_attribs_from_datatype(SEXP x, DATA_TYPE dt); + + // copy_to() + static void fill_default_value(SEXP data, DATA_TYPE dt, R_xlen_t i); + void copy_value(SEXP x, DATA_TYPE dt, const int tgt, const int src) const; +}; + + +#endif // DB_COLUMNSTORAGE_H diff --git a/src/DbConnection.cpp b/src/DbConnection.cpp new file mode 100644 index 0000000..d50b772 --- /dev/null +++ b/src/DbConnection.cpp @@ -0,0 +1,222 @@ +#include "pch.h" +#include "DbConnection.h" +#include "encode.h" + + +DbConnection::DbConnection(std::vector keys, std::vector values) : +pCurrentResult_(NULL), +transacting_(false) +{ + size_t n = keys.size(); + std::vector c_keys(n + 1), c_values(n + 1); + + for (size_t i = 0; i < n; ++i) { + c_keys[i] = keys[i].c_str(); + c_values[i] = values[i].c_str(); + } + c_keys[n] = NULL; + c_values[n] = NULL; + + pConn_ = PQconnectdbParams(&c_keys[0], &c_values[0], false); + + if (PQstatus(pConn_) != CONNECTION_OK) { + std::string err = PQerrorMessage(pConn_); + PQfinish(pConn_); + stop(err); + } + + PQsetClientEncoding(pConn_, "UTF-8"); +} + +DbConnection::~DbConnection() { + disconnect(); +} + +void DbConnection::disconnect() { + try { + PQfinish(pConn_); + pConn_ = NULL; + } catch (...) {} +} + +PGconn* DbConnection::conn() { + return pConn_; +} + +void DbConnection::set_current_result(const DbResult* pResult) { + // Cancels previous query, if needed. + if (pResult == pCurrentResult_) + return; + + if (pCurrentResult_ != NULL) { + if (pResult != NULL) + warning("Cancelling previous query"); + + cleanup_query(); + } + pCurrentResult_ = pResult; +} + +void DbConnection::cancel_query() { + check_connection(); + + // Cancel running query + PGcancel* cancel = PQgetCancel(pConn_); + if (cancel == NULL) { + warning("Failed to cancel running query"); + return; + } + + char errbuf[256]; + if (!PQcancel(cancel, errbuf, sizeof(errbuf))) { + warning(errbuf); + } + + PQfreeCancel(cancel); +} + +void DbConnection::finish_query() const { + // Clear pending results + PGresult* result; + while ((result = PQgetResult(pConn_)) != NULL) { + PQclear(result); + } +} + +bool DbConnection::is_current_result(const DbResult* pResult) { + return pCurrentResult_ == pResult; +} + +bool DbConnection::has_query() { + return pCurrentResult_ != NULL; +} + +void DbConnection::copy_data(std::string sql, List df) { + LOG_DEBUG << sql; + + R_xlen_t p = df.size(); + if (p == 0) + return; + + PGresult* pInit = PQexec(pConn_, sql.c_str()); + if (PQresultStatus(pInit) != PGRES_COPY_IN) { + PQclear(pInit); + conn_stop("Failed to initialise COPY"); + } + PQclear(pInit); + + + std::string buffer; + int n = Rf_length(df[0]); + // Sending row at-a-time is faster, presumable because it avoids copies + // of buffer. Sending data asynchronously appears to be no faster. + for (int i = 0; i < n; ++i) { + buffer.clear(); + encode_row_in_buffer(df, i, buffer); + + if (PQputCopyData(pConn_, buffer.data(), static_cast(buffer.size())) != 1) { + conn_stop("Failed to put data"); + } + } + + + if (PQputCopyEnd(pConn_, NULL) != 1) { + conn_stop("Failed to finish COPY"); + } + + PGresult* pComplete = PQgetResult(pConn_); + if (PQresultStatus(pComplete) != PGRES_COMMAND_OK) { + PQclear(pComplete); + conn_stop("COPY returned error"); + } + PQclear(pComplete); +} + +void DbConnection::check_connection() { + if (!pConn_) { + stop("Disconnected"); + } + + ConnStatusType status = PQstatus(pConn_); + if (status == CONNECTION_OK) return; + + // Status was bad, so try resetting. + PQreset(pConn_); + status = PQstatus(pConn_); + if (status == CONNECTION_OK) return; + + conn_stop("Lost connection to database"); +} + +List DbConnection::info() { + check_connection(); + + const char* dbnm = PQdb(pConn_); + const char* host = PQhost(pConn_); + const char* port = PQport(pConn_); + const char* user = PQuser(pConn_); + int pver = PQprotocolVersion(pConn_); + int sver = PQserverVersion(pConn_); + int pid = PQbackendPID(pConn_); + return + List::create( + _["dbname"] = dbnm == NULL ? "" : std::string(dbnm), + _["host"] = host == NULL ? "" : std::string(host), + _["port"] = port == NULL ? "" : std::string(port), + _["user"] = user == NULL ? "" : std::string(user), + _["protocol_version"] = pver, + _["server_version"] = sver, + _["pid"] = pid + ); +} + +SEXP DbConnection::quote_string(const String& x) { + // Returns a single CHRSXP + check_connection(); + + if (x == NA_STRING) + return get_null_string(); + + char* pq_escaped = PQescapeLiteral(pConn_, x.get_cstring(), static_cast(-1)); + SEXP escaped = Rf_mkCharCE(pq_escaped, CE_UTF8); + PQfreemem(pq_escaped); + + return escaped; +} + +SEXP DbConnection::quote_identifier(const String& x) { + // Returns a single CHRSXP + check_connection(); + + char* pq_escaped = PQescapeIdentifier(pConn_, x.get_cstring(), static_cast(-1)); + SEXP escaped = Rf_mkCharCE(pq_escaped, CE_UTF8); + PQfreemem(pq_escaped); + + return escaped; +} + +SEXP DbConnection::get_null_string() { + static RObject null = Rf_mkCharCE("NULL", CE_UTF8); + return null; +} + +bool DbConnection::is_transacting() const { + return transacting_; +} + +void DbConnection::set_transacting(bool transacting) { + transacting_ = transacting; +} + +void DbConnection::conn_stop(const char* msg) { + conn_stop(conn(), msg); +} + +void DbConnection::conn_stop(PGconn* conn, const char* msg) { + stop("%s: %s", msg, PQerrorMessage(conn)); +} + +void DbConnection::cleanup_query() { + cancel_query(); + finish_query(); +} diff --git a/src/DbConnection.h b/src/DbConnection.h new file mode 100644 index 0000000..fc8f4ed --- /dev/null +++ b/src/DbConnection.h @@ -0,0 +1,55 @@ +#ifndef __RPOSTGRES_PQ_CONNECTION__ +#define __RPOSTGRES_PQ_CONNECTION__ + +#include +#include + +class DbResult; + +// convenience typedef for shared_ptr to DbConnection +class DbConnection; +typedef boost::shared_ptr DbConnectionPtr; + +// DbConnection ---------------------------------------------------------------- + +class DbConnection : boost::noncopyable { + PGconn* pConn_; + const DbResult* pCurrentResult_; + bool transacting_; + +public: + DbConnection(std::vector keys, std::vector values); + virtual ~DbConnection(); + +public: + void disconnect(); + + PGconn* conn(); + + void set_current_result(const DbResult* pResult); + bool is_current_result(const DbResult* pResult); + bool has_query(); + + void copy_data(std::string sql, List df); + + void check_connection(); + List info(); + + SEXP quote_string(const String& x); + SEXP quote_identifier(const String& x); + static SEXP get_null_string(); + + bool is_transacting() const; + void set_transacting(bool transacting); + + void conn_stop(const char* msg); + static void conn_stop(PGconn* conn, const char* msg); + + void cleanup_query(); + void finish_query() const; + +private: + void cancel_query(); +}; + +#endif diff --git a/src/DbDataFrame.cpp b/src/DbDataFrame.cpp new file mode 100644 index 0000000..bb94e80 --- /dev/null +++ b/src/DbDataFrame.cpp @@ -0,0 +1,69 @@ +#include "pch.h" +#include "DbDataFrame.h" +#include "DbColumn.h" +#include "DbColumnStorage.h" +#include "DbColumnDataSource.h" +#include "DbColumnDataSourceFactory.h" +#include +#include + +DbDataFrame::DbDataFrame(DbColumnDataSourceFactory* factory_, std::vector names_, const int n_max_, + const std::vector& types_) + : n_max(n_max_), + i(0), + names(names_) +{ + factory.reset(factory_); + + data.reserve(types_.size()); + for (size_t j = 0; j < types_.size(); ++j) { + DbColumn x(types_[j], n_max, factory.get(), (int)j); + data.push_back(x); + } +} + +DbDataFrame::~DbDataFrame() { +} + +void DbDataFrame::set_col_values() { + std::for_each(data.begin(), data.end(), boost::bind(&DbColumn::set_col_value, _1)); +} + +bool DbDataFrame::advance() { + ++i; + + if (i % 1000 == 0) + checkUserInterrupt(); + + return (n_max < 0 || i < n_max); +} + +List DbDataFrame::get_data() { + // Throws away new data types + std::vector types_; + return get_data(types_); +} + +List DbDataFrame::get_data(std::vector& types_) { + // Trim back to what we actually used + finalize_cols(); + + types_.clear(); + std::transform(data.begin(), data.end(), std::back_inserter(types_), std::mem_fun_ref(&DbColumn::get_type)); + + boost::for_each(data, names, boost::bind(&DbColumn::warn_type_conflicts, _1, _2)); + + List out(data.begin(), data.end()); + out.attr("names") = names; + out.attr("class") = "data.frame"; + out.attr("row.names") = IntegerVector::create(NA_INTEGER, -i); + return out; +} + +size_t DbDataFrame::get_ncols() const { + return data.size(); +} + +void DbDataFrame::finalize_cols() { + std::for_each(data.begin(), data.end(), boost::bind(&DbColumn::finalize, _1, i)); +} diff --git a/src/DbDataFrame.h b/src/DbDataFrame.h new file mode 100644 index 0000000..c023fad --- /dev/null +++ b/src/DbDataFrame.h @@ -0,0 +1,35 @@ +#ifndef DB_DATAFRAME_H +#define DB_DATAFRAME_H + +#include +#include +#include "DbColumnDataType.h" + +class DbColumn; +class DbColumnDataSourceFactory; + +class DbDataFrame { + boost::scoped_ptr factory; + const int n_max; + int i; + boost::container::stable_vector data; + std::vector names; + +public: + DbDataFrame(DbColumnDataSourceFactory* factory, std::vector names, const int n_max_, const std::vector& types); + virtual ~DbDataFrame(); + +public: + void set_col_values(); + bool advance(); + + List get_data(); + List get_data(std::vector& types); + size_t get_ncols() const; + +private: + void finalize_cols(); +}; + + +#endif //DB_DATAFRAME_H diff --git a/src/DbResult.cpp b/src/DbResult.cpp new file mode 100644 index 0000000..5481da9 --- /dev/null +++ b/src/DbResult.cpp @@ -0,0 +1,63 @@ +#include "pch.h" +#include "DbResult.h" +#include "DbConnection.h" +#include "PqResultImpl.h" + + +DbResult::DbResult(const DbConnectionPtr& pConn, const std::string& sql) : +pConn_(pConn) +{ + pConn->check_connection(); + pConn->set_current_result(this); + + try { + impl.reset(new PqResultImpl(this, pConn->conn(), sql)); + } + catch (...) { + pConn->set_current_result(NULL); + throw; + } +} + +DbResult::~DbResult() { + try { + if (active()) { + pConn_->set_current_result(NULL); + } + } catch (...) {} +} + +void DbResult::bind(const List& params) { + return impl->bind(params); +} + +bool DbResult::active() const { + return pConn_->is_current_result(this); +} + +List DbResult::fetch(int n_max) { + if (!active()) + stop("Inactive result set"); + + return impl->fetch(n_max); +} + +int DbResult::n_rows_affected() { + return impl->n_rows_affected(); +} + +int DbResult::n_rows_fetched() { + return impl->n_rows_fetched(); +} + +bool DbResult::complete() { + return impl->complete(); +} + +List DbResult::get_column_info() { + return impl->get_column_info(); +} + +void DbResult::finish_query() { + pConn_->finish_query(); +} diff --git a/src/DbResult.h b/src/DbResult.h new file mode 100644 index 0000000..7390960 --- /dev/null +++ b/src/DbResult.h @@ -0,0 +1,42 @@ +#ifndef __RPOSTGRES_PQ_RESULT__ +#define __RPOSTGRES_PQ_RESULT__ + +#include +#include +#include + + +class DbConnection; +typedef boost::shared_ptr DbConnectionPtr; + +// DbResult -------------------------------------------------------------------- +// There is no object analogous to DbResult in libpq: this provides a result set +// like object for the R API. There is only ever one active result set (the +// most recent) for each connection. + +class PqResultImpl; + +class DbResult : boost::noncopyable { + DbConnectionPtr pConn_; + boost::scoped_ptr impl; + +public: + DbResult(const DbConnectionPtr& pConn, const std::string& sql); + ~DbResult(); + +public: + bool complete(); + bool active() const; + int n_rows_fetched(); + int n_rows_affected(); + + void bind(const List& params); + List fetch(int n_max = -1); + + List get_column_info(); + +public: + void finish_query(); +}; + +#endif diff --git a/src/Makevars.in b/src/Makevars.in new file mode 100644 index 0000000..627ef02 --- /dev/null +++ b/src/Makevars.in @@ -0,0 +1,2 @@ +PKG_CPPFLAGS=@cflags@ +PKG_LIBS=@libs@ diff --git a/src/Makevars.win b/src/Makevars.win new file mode 100644 index 0000000..9266481 --- /dev/null +++ b/src/Makevars.win @@ -0,0 +1,14 @@ +PKG_CPPFLAGS= -I../windows/libpq-9.5.2/include +PKG_LIBS= -L../windows/libpq-9.5.2/lib${R_ARCH} \ + -lpq -lssl -lcrypto -lwsock32 -lsecur32 -lws2_32 -lgdi32 -lcrypt32 -lwldap32 win32/timegm.o +CXX_STD=CXX11 + +$(SHLIB): win32/timegm.o + +$(OBJECTS): winlibs + +clean: + rm -f $(SHLIB) $(OBJECTS) win32/timegm.o + +winlibs: + "${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" "../tools/winlibs.R" diff --git a/src/PqColumnDataSource.cpp b/src/PqColumnDataSource.cpp new file mode 100644 index 0000000..76aa982 --- /dev/null +++ b/src/PqColumnDataSource.cpp @@ -0,0 +1,175 @@ +#include "pch.h" +#include +#include "PqColumnDataSource.h" +#include "PqResultSource.h" + +#if defined(WIN32) || defined(_WIN32) +#define timegm _mkgmtime +#endif + +PqColumnDataSource::PqColumnDataSource(PqResultSource* result_source_, const DATA_TYPE dt_, const int j) : +DbColumnDataSource(j), +result_source(result_source_), +dt(dt_) +{ +} + +PqColumnDataSource::~PqColumnDataSource() { +} + +DATA_TYPE PqColumnDataSource::get_data_type() const { + return dt; +} + +DATA_TYPE PqColumnDataSource::get_decl_data_type() const { + return dt; +} + +bool PqColumnDataSource::is_null() const { + LOG_VERBOSE; + return PQgetisnull(get_result(), 0, get_j()) != 0; +} + +int PqColumnDataSource::fetch_bool() const { + LOG_VERBOSE; + return (strcmp(get_result_value(), "t") == 0); +} + + +int PqColumnDataSource::fetch_int() const { + LOG_VERBOSE; + return atoi(get_result_value()); +} + +int64_t PqColumnDataSource::fetch_int64() const { + LOG_VERBOSE; + return boost::lexical_cast(get_result_value()); +} + +double PqColumnDataSource::fetch_real() const { + LOG_VERBOSE; + return atof(get_result_value()); +} + +SEXP PqColumnDataSource::fetch_string() const { + LOG_VERBOSE; + return Rf_mkCharCE(get_result_value(), CE_UTF8); +} + +SEXP PqColumnDataSource::fetch_blob() const { + LOG_VERBOSE; + const void* val = get_result_value(); + + size_t to_length = 0; + unsigned char* unescaped_blob = PQunescapeBytea(static_cast(val), &to_length); + + SEXP bytes = Rf_allocVector(RAWSXP, static_cast(to_length)); + memcpy(RAW(bytes), unescaped_blob, to_length); + + PQfreemem(unescaped_blob); + + return bytes; +} + +double PqColumnDataSource::fetch_date() const { + LOG_VERBOSE; + const char* val = get_result_value(); + struct tm date = tm(); + date.tm_isdst = -1; + date.tm_year = *val - 0x30; + date.tm_year *= 10; + date.tm_year += (*(++val) - 0x30); + date.tm_year *= 10; + date.tm_year += (*(++val) - 0x30); + date.tm_year *= 10; + date.tm_year += (*(++val) - 0x30) - 1900; + val++; + date.tm_mon = 10 * (*(++val) - 0x30); + date.tm_mon += (*(++val) - 0x30) - 1; + val++; + date.tm_mday = (*(++val) - 0x30) * 10; + date.tm_mday += (*(++val) - 0x30); + return static_cast(timegm(&date)) / (24.0 * 60 * 60); +} + +double PqColumnDataSource::fetch_datetime_local() const { + LOG_VERBOSE; + return convert_datetime(get_result_value(), true); +} + +double PqColumnDataSource::fetch_datetime() const { + LOG_VERBOSE; + return convert_datetime(get_result_value(), false); +} + +double PqColumnDataSource::fetch_time() const { + LOG_VERBOSE; + const char* val = get_result_value(); + int hour = (*val - 0x30) * 10; + hour += (*(++val) - 0x30); + val++; + int min = (*(++val) - 0x30) * 10; + min += (*(++val) - 0x30); + val++; + double sec = strtod(++val, NULL); + return static_cast(hour * 3600 + min * 60) + sec; +} + +double PqColumnDataSource::convert_datetime(const char* val, bool use_local) { + char* end; + struct tm date; + date.tm_isdst = -1; + date.tm_year = *val - 0x30; + date.tm_year *= 10; + date.tm_year += (*(++val) - 0x30); + date.tm_year *= 10; + date.tm_year += (*(++val) - 0x30); + date.tm_year *= 10; + date.tm_year += (*(++val) - 0x30) - 1900; + LOG_VERBOSE << date.tm_year; + + val++; + date.tm_mon = (*(++val) - 0x30) * 10; + date.tm_mon += (*(++val) - 0x30) - 1; + LOG_VERBOSE << date.tm_mon; + + val++; + date.tm_mday = (*(++val) - 0x30) * 10; + date.tm_mday += (*(++val) - 0x30); + LOG_VERBOSE << date.tm_mday; + + val++; + date.tm_hour = (*(++val) - 0x30) * 10; + date.tm_hour += (*(++val) - 0x30); + LOG_VERBOSE << date.tm_hour; + + val++; + date.tm_min = (*(++val) - 0x30) * 10; + date.tm_min += (*(++val) - 0x30); + LOG_VERBOSE << date.tm_min; + + val++; + double sec = strtod(++val, &end); + LOG_VERBOSE << sec; + + date.tm_sec = static_cast(sec); + LOG_VERBOSE << date.tm_sec; + + time_t time = use_local ? mktime(&date) : timegm(&date); + LOG_VERBOSE << time; + + double ret = static_cast(time) + (sec - date.tm_sec); + LOG_VERBOSE << ret; + + return ret; +} + +PGresult* PqColumnDataSource::get_result() const { + return result_source->get_result(); +} + +const char* PqColumnDataSource::get_result_value() const { + const char* val = PQgetvalue(get_result(), 0, get_j()); + LOG_VERBOSE << val; + return val; +} diff --git a/src/PqColumnDataSource.h b/src/PqColumnDataSource.h new file mode 100644 index 0000000..69eecde --- /dev/null +++ b/src/PqColumnDataSource.h @@ -0,0 +1,40 @@ +#ifndef RPOSTGRES_PQCOLUMNDATASOURCE_H +#define RPOSTGRES_PQCOLUMNDATASOURCE_H + +#include "DbColumnDataSource.h" + +class PqResultSource; +class PqColumnDataSourceFactory; + +class PqColumnDataSource : public DbColumnDataSource { + PqResultSource* result_source; + const DATA_TYPE dt; + +public: + PqColumnDataSource(PqResultSource* result_source_, const DATA_TYPE dt_, const int j); + virtual ~PqColumnDataSource(); + +public: + virtual DATA_TYPE get_data_type() const; + virtual DATA_TYPE get_decl_data_type() const; + + virtual bool is_null() const; + + virtual int fetch_bool() const; + virtual int fetch_int() const; + virtual int64_t fetch_int64() const; + virtual double fetch_real() const; + virtual SEXP fetch_string() const; + virtual SEXP fetch_blob() const; + virtual double fetch_date() const; + virtual double fetch_datetime_local() const; + virtual double fetch_datetime() const; + virtual double fetch_time() const; + +private: + static double convert_datetime(const char* val, bool use_local); + PGresult* get_result() const; + const char* get_result_value() const; +}; + +#endif //RPOSTGRES_PQCOLUMNDATASOURCE_H diff --git a/src/PqColumnDataSourceFactory.cpp b/src/PqColumnDataSourceFactory.cpp new file mode 100644 index 0000000..16831d3 --- /dev/null +++ b/src/PqColumnDataSourceFactory.cpp @@ -0,0 +1,16 @@ +#include "pch.h" +#include "PqColumnDataSourceFactory.h" +#include "PqColumnDataSource.h" + +PqColumnDataSourceFactory::PqColumnDataSourceFactory(PqResultSource* result_source_, const std::vector& types_) : +result_source(result_source_), +types(types_) +{ +} + +PqColumnDataSourceFactory::~PqColumnDataSourceFactory() { +} + +DbColumnDataSource* PqColumnDataSourceFactory::create(const int j) { + return new PqColumnDataSource(result_source, types[j], j); +} diff --git a/src/PqColumnDataSourceFactory.h b/src/PqColumnDataSourceFactory.h new file mode 100644 index 0000000..d33b2c4 --- /dev/null +++ b/src/PqColumnDataSourceFactory.h @@ -0,0 +1,21 @@ +#ifndef RPOSTGRES_PQCOLUMNDATASOURCEFACTORY_H +#define RPOSTGRES_PQCOLUMNDATASOURCEFACTORY_H + +#include "DbColumnDataSourceFactory.h" +#include "DbColumnDataType.h" + +class PqResultSource; + +class PqColumnDataSourceFactory : public DbColumnDataSourceFactory { + PqResultSource* result_source; + const std::vector types; + +public: + PqColumnDataSourceFactory(PqResultSource* result_source_, const std::vector& types_); + virtual ~PqColumnDataSourceFactory(); + +public: + virtual DbColumnDataSource* create(const int j); +}; + +#endif //RPOSTGRES_PQCOLUMNDATASOURCEFACTORY_H diff --git a/src/PqDataFrame.cpp b/src/PqDataFrame.cpp new file mode 100644 index 0000000..bf2101a --- /dev/null +++ b/src/PqDataFrame.cpp @@ -0,0 +1,13 @@ +#include "pch.h" +#include "PqDataFrame.h" +#include "PqColumnDataSourceFactory.h" + + +PqDataFrame::PqDataFrame(PqResultSource* result_source, const std::vector& names, const int n_max_, + const std::vector& types) : +DbDataFrame(new PqColumnDataSourceFactory(result_source, types), names, n_max_, types) +{ +} + +PqDataFrame::~PqDataFrame() { +} diff --git a/src/PqDataFrame.h b/src/PqDataFrame.h new file mode 100644 index 0000000..09ac18e --- /dev/null +++ b/src/PqDataFrame.h @@ -0,0 +1,15 @@ +#ifndef RPOSTGRES_PQDATAFRAME_H +#define RPOSTGRES_PQDATAFRAME_H + +#include "DbDataFrame.h" + +class PqResultSource; + +class PqDataFrame : public DbDataFrame { +public: + PqDataFrame(PqResultSource* result_source, const std::vector& names, const int n_max_, + const std::vector& types); + ~PqDataFrame(); +}; + +#endif //RPOSTGRES_PQDATAFRAME_H diff --git a/src/PqResultImpl.cpp b/src/PqResultImpl.cpp new file mode 100644 index 0000000..3b220af --- /dev/null +++ b/src/PqResultImpl.cpp @@ -0,0 +1,402 @@ +#include "pch.h" +#include "PqResultImpl.h" +#include "DbConnection.h" +#include "DbResult.h" +#include "DbColumnStorage.h" +#include "PqDataFrame.h" + +PqResultImpl::PqResultImpl(DbResult* pRes, PGconn* pConn, const std::string& sql) : +res(pRes), +pConn_(pConn), +pSpec_(prepare(pConn, sql)), +cache(pSpec_), +complete_(false), +ready_(false), +nrows_(0), +rows_affected_(0), +group_(0), +groups_(0), +pRes_(NULL) +{ + + LOG_DEBUG << sql; + + try { + if (cache.nparams_ == 0) { + bind(); + } + } catch (...) { + PQclear(pSpec_); + pSpec_ = NULL; + throw; + } +} + +PqResultImpl::~PqResultImpl() { + try { + PQclear(pSpec_); + } catch (...) {} +} + + + +// Cache /////////////////////////////////////////////////////////////////////// + +PqResultImpl::_cache::_cache(PGresult* spec) : +names_(get_column_names(spec)), +types_(get_column_types(spec)), +ncols_(names_.size()), +nparams_(PQnparams(spec)) +{ + for (int i = 0; i < nparams_; ++i) + LOG_VERBOSE << PQparamtype(spec, i); +} + + +std::vector PqResultImpl::_cache::get_column_names(PGresult* spec) { + std::vector names; + int ncols_ = PQnfields(spec); + names.reserve(ncols_); + + for (int i = 0; i < ncols_; ++i) { + names.push_back(std::string(PQfname(spec, i))); + } + + return names; +} + +std::vector PqResultImpl::_cache::get_column_types(PGresult* spec) { + std::vector types; + int ncols_ = PQnfields(spec); + types.reserve(ncols_); + + for (int i = 0; i < ncols_; ++i) { + Oid type = PQftype(spec, i); + // SELECT oid, typname FROM pg_type WHERE typtype = 'b' + switch (type) { + case 20: // BIGINT + types.push_back(DT_INT64); + break; + + case 21: // SMALLINT + case 23: // INTEGER + case 26: // OID + types.push_back(DT_INT); + break; + + case 1700: // DECIMAL + case 701: // FLOAT8 + case 700: // FLOAT + case 790: // MONEY + types.push_back(DT_REAL); + break; + + case 18: // CHAR + case 19: // NAME + case 25: // TEXT + case 114: // JSON + case 1042: // CHAR + case 1043: // VARCHAR + types.push_back(DT_STRING); + break; + case 1082: // DATE + types.push_back(DT_DATE); + break; + case 1083: // TIME + case 1266: // TIMETZOID + types.push_back(DT_TIME); + break; + case 1114: // TIMESTAMP + types.push_back(DT_DATETIME); + break; + case 1184: // TIMESTAMPTZOID + types.push_back(DT_DATETIMETZ); + break; + case 1186: // INTERVAL + case 3802: // JSONB + case 2950: // UUID + types.push_back(DT_STRING); + break; + + case 16: // BOOL + types.push_back(DT_BOOL); + break; + + case 17: // BYTEA + case 2278: // NULL + types.push_back(DT_BLOB); + break; + + case 705: // UNKNOWN + types.push_back(DT_STRING); + break; + + default: + types.push_back(DT_STRING); + warning("Unknown field type (%d) in column %s", type, PQfname(spec, i)); + } + } + + return types; +} + +PGresult* PqResultImpl::prepare(PGconn* conn, const std::string& sql) { + // Prepare query + PGresult* prep = PQprepare(conn, "", sql.c_str(), 0, NULL); + if (PQresultStatus(prep) != PGRES_COMMAND_OK) { + PQclear(prep); + DbConnection::conn_stop(conn, "Failed to prepare query"); + } + PQclear(prep); + + // Retrieve query specification + PGresult* spec = PQdescribePrepared(conn, ""); + if (PQresultStatus(spec) != PGRES_COMMAND_OK) { + PQclear(spec); + DbConnection::conn_stop(conn, "Failed to retrieve query result metadata"); + } + + return spec; +} + +void PqResultImpl::init(bool params_have_rows) { + ready_ = true; + nrows_ = 0; + complete_ = !params_have_rows; +} + + + +// Publics ///////////////////////////////////////////////////////////////////// + +bool PqResultImpl::complete() { + return complete_; +} + +int PqResultImpl::n_rows_fetched() { + return nrows_; +} + +int PqResultImpl::n_rows_affected() { + if (!ready_) return NA_INTEGER; + if (cache.ncols_ > 0) return 0; + return rows_affected_; +} + +void PqResultImpl::bind(const List& params) { + if (params.size() != cache.nparams_) { + stop("Query requires %i params; %i supplied.", + cache.nparams_, params.size()); + } + + if (params.size() == 0 && ready_) { + stop("Query does not require parameters."); + } + + set_params(params); + + if (params.length() > 0) { + SEXP first_col = params[0]; + groups_ = Rf_length(first_col); + } + else { + groups_ = 1; + } + group_ = 0; + + rows_affected_ = 0; + + bool has_params = bind_row(); + after_bind(has_params); +} + +List PqResultImpl::fetch(const int n_max) { + if (!ready_) + stop("Query needs to be bound before fetching"); + + int n = 0; + List out; + + if (n_max != 0) + out = fetch_rows(n_max, n); + else + out = peek_first_row(); + + return out; +} + +List PqResultImpl::get_column_info() { + peek_first_row(); + + CharacterVector names(cache.names_.begin(), cache.names_.end()); + + CharacterVector types(cache.ncols_); + for (size_t i = 0; i < cache.ncols_; i++) { + types[i] = Rf_type2char(DbColumnStorage::sexptype_from_datatype(cache.types_[i])); + } + + List out = Rcpp::List::create(names, types); + out.attr("row.names") = IntegerVector::create(NA_INTEGER, -cache.ncols_); + out.attr("class") = "data.frame"; + out.attr("names") = CharacterVector::create("name", "type"); + + return out; +} + + + +// Publics (custom) //////////////////////////////////////////////////////////// + + + + +// Privates //////////////////////////////////////////////////////////////////// + +void PqResultImpl::set_params(const List& params) { + params_ = params; +} + +bool PqResultImpl::bind_row() { + LOG_VERBOSE << "groups: " << group_ << "/" << groups_; + + if (group_ >= groups_) + return false; + + if (ready_ || group_ > 0) + res->finish_query(); + + std::vector c_params(cache.nparams_); + std::vector formats(cache.nparams_); + std::vector lengths(cache.nparams_); + for (int i = 0; i < cache.nparams_; ++i) { + if (TYPEOF(params_[i]) == VECSXP) { + List param(params_[i]); + if (!Rf_isNull(param[group_])) { + Rbyte* param_value = RAW(param[group_]); + c_params[i] = reinterpret_cast(param_value); + formats[i] = 1; + lengths[i] = Rf_length(param[group_]); + } + } + else { + CharacterVector param(params_[i]); + if (param[group_] != NA_STRING) { + c_params[i] = CHAR(param[group_]); + } + } + } + + if (!PQsendQueryPrepared(pConn_, "", cache.nparams_, &c_params[0], + &lengths[0], &formats[0], 0)) + conn_stop("Failed to send query"); + + if (!PQsetSingleRowMode(pConn_)) + conn_stop("Failed to set single row mode"); + + return true; +} + +void PqResultImpl::after_bind(bool params_have_rows) { + init(params_have_rows); + if (params_have_rows) + step(); +} + +List PqResultImpl::fetch_rows(const int n_max, int& n) { + n = (n_max < 0) ? 100 : n_max; + + PqDataFrame data(this, cache.names_, n_max, cache.types_); + + if (complete_ && data.get_ncols() == 0) { + warning("Don't need to call dbFetch() for statements, only for queries"); + } + + while (!complete_) { + LOG_VERBOSE << nrows_ << "/" << n; + + data.set_col_values(); + step(); + nrows_++; + if (!data.advance()) + break; + } + + LOG_VERBOSE << nrows_; + return data.get_data(); +} + +void PqResultImpl::step() { + while (step_run()) + ; +} + +bool PqResultImpl::step_run() { + LOG_VERBOSE; + + pRes_ = PQgetResult(pConn_); + + // We're done, but we need to call PQgetResult until it returns NULL + if (PQresultStatus(pRes_) == PGRES_TUPLES_OK) { + PGresult* next = PQgetResult(pConn_); + while (next != NULL) { + PQclear(next); + next = PQgetResult(pConn_); + } + } + + if (pRes_ == NULL) { + PQclear(pRes_); + stop("No active query"); + } + + ExecStatusType status = PQresultStatus(pRes_); + + switch (status) { + case PGRES_FATAL_ERROR: + { + PQclear(pRes_); + conn_stop("Failed to fetch row"); + return false; + } + case PGRES_SINGLE_TUPLE: + return false; + default: + return step_done(); + } +} + +bool PqResultImpl::step_done() { + char* tuples = PQcmdTuples(pRes_); + rows_affected_ += atoi(tuples); + + ++group_; + bool more_params = bind_row(); + + if (!more_params) + complete_ = true; + + LOG_VERBOSE << "group: " << group_ << ", more_params: " << more_params; + return more_params; +} + +List PqResultImpl::peek_first_row() { + PqDataFrame data(this, cache.names_, 1, cache.types_); + + if (!complete_) + data.set_col_values(); + // Not calling data.advance(), remains a zero-row data frame + + return data.get_data(); +} + +void PqResultImpl::conn_stop(const char* msg) const { + DbConnection::conn_stop(pConn_, msg); +} + +void PqResultImpl::bind() { + bind(List()); +} + +PGresult* PqResultImpl::get_result() { + return pRes_; +} diff --git a/src/PqResultImpl.h b/src/PqResultImpl.h new file mode 100644 index 0000000..070546d --- /dev/null +++ b/src/PqResultImpl.h @@ -0,0 +1,79 @@ +#ifndef RPOSTGRES_PQRESULTIMPL_H +#define RPOSTGRES_PQRESULTIMPL_H + +#include +#include +#include "DbColumnDataType.h" +#include "PqResultSource.h" + +class DbResult; + +class PqResultImpl : boost::noncopyable, public PqResultSource { + // Back pointer for query cancellation + DbResult* res; + + // Wrapped pointer + PGconn* pConn_; + PGresult* pSpec_; + + // Cache + struct _cache { + const std::vector names_; + const std::vector types_; + const size_t ncols_; + const int nparams_; + + _cache(PGresult* spec); + + static std::vector get_column_names(PGresult* spec); + static std::vector get_column_types(PGresult* spec); + } cache; + + // State + bool complete_; + bool ready_; + int nrows_; + int rows_affected_; + List params_; + int group_, groups_; + PGresult* pRes_; + +public: + PqResultImpl(DbResult* pRes, PGconn* pConn, const std::string& sql); + ~PqResultImpl(); + +private: + static PGresult* prepare(PGconn* conn, const std::string& sql); + void init(bool params_have_rows); + +public: + bool complete(); + int n_rows_fetched(); + int n_rows_affected(); + void bind(const List& params); + List fetch(const int n_max); + + List get_column_info(); + +private: + void set_params(const List& params); + bool bind_row(); + void after_bind(bool params_have_rows); + + List fetch_rows(int n_max, int& n); + void step(); + bool step_run(); + bool step_done(); + List peek_first_row(); + +private: + void conn_stop(const char* msg) const; + + void bind(); + +public: + // PqResultSource + PGresult* get_result(); +}; + +#endif //RPOSTGRES_PQRESULTIMPL_H diff --git a/src/PqResultSource.cpp b/src/PqResultSource.cpp new file mode 100644 index 0000000..7c34513 --- /dev/null +++ b/src/PqResultSource.cpp @@ -0,0 +1,8 @@ +#include "pch.h" +#include "PqResultSource.h" + +PqResultSource::PqResultSource() { +} + +PqResultSource::~PqResultSource() { +} diff --git a/src/PqResultSource.h b/src/PqResultSource.h new file mode 100644 index 0000000..ba31b31 --- /dev/null +++ b/src/PqResultSource.h @@ -0,0 +1,13 @@ +#ifndef RPOSTGRES_PQRESULTSOURCE_H +#define RPOSTGRES_PQRESULTSOURCE_H + +class PqResultSource { +public: + PqResultSource(); + virtual ~PqResultSource(); + +public: + virtual PGresult* get_result() = 0; +}; + +#endif //RPOSTGRES_PQRESULTSOURCE_H diff --git a/src/RPostgres-init.c b/src/RPostgres-init.c new file mode 100644 index 0000000..1620e89 --- /dev/null +++ b/src/RPostgres-init.c @@ -0,0 +1,28 @@ +#include + +#ifdef _WIN32 +#include +#endif + +// From http://www.postgresql.org/docs/9.4/static/libpq-connect.html: +// On Windows, there is a way to improve performance if a single database +// connection is repeatedly started and shutdown. Internally, libpq calls +// WSAStartup() and WSACleanup() for connection startup and shutdown, +// respectively. WSAStartup() increments an internal Windows library reference +// count which is decremented by WSACleanup(). When the reference count is just +// one, calling WSACleanup() frees all resources and all DLLs are unloaded. +// This is an expensive operation. To avoid this, an application can manually +// call WSAStartup() so resources will not be freed when the last database +// connection is closed. + +void R_init_mypackage(DllInfo *info) { +#ifdef _WIN32 + WSAStartup(MAKEWORD(1, 0), NULL); +#endif +} + +void R_unload_mylib(DllInfo *info) { +#ifdef _WIN32 + WSACleanup(); +#endif +} diff --git a/src/RPostgres_types.h b/src/RPostgres_types.h new file mode 100644 index 0000000..57c4af7 --- /dev/null +++ b/src/RPostgres_types.h @@ -0,0 +1,19 @@ +#include "pch.h" + +#ifndef __RPOSTGRES_TYPES__ +#define __RPOSTGRES_TYPES__ + +#include "DbConnection.h" +#include "DbResult.h" + +namespace Rcpp { + +template<> +DbConnection* as(SEXP x); + +template<> +DbResult* as(SEXP x); + +} + +#endif diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp new file mode 100644 index 0000000..72dd868 --- /dev/null +++ b/src/RcppExports.cpp @@ -0,0 +1,286 @@ +// Generated by using Rcpp::compileAttributes() -> do not edit by hand +// Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 + +#include "RPostgres_types.h" +#include + +using namespace Rcpp; + +// connection_create +XPtr connection_create(std::vector keys, std::vector values); +RcppExport SEXP _RPostgres_connection_create(SEXP keysSEXP, SEXP valuesSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< std::vector >::type keys(keysSEXP); + Rcpp::traits::input_parameter< std::vector >::type values(valuesSEXP); + rcpp_result_gen = Rcpp::wrap(connection_create(keys, values)); + return rcpp_result_gen; +END_RCPP +} +// connection_valid +bool connection_valid(XPtr con_); +RcppExport SEXP _RPostgres_connection_valid(SEXP con_SEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< XPtr >::type con_(con_SEXP); + rcpp_result_gen = Rcpp::wrap(connection_valid(con_)); + return rcpp_result_gen; +END_RCPP +} +// connection_release +void connection_release(XPtr con_); +RcppExport SEXP _RPostgres_connection_release(SEXP con_SEXP) { +BEGIN_RCPP + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< XPtr >::type con_(con_SEXP); + connection_release(con_); + return R_NilValue; +END_RCPP +} +// connection_info +List connection_info(DbConnection* con); +RcppExport SEXP _RPostgres_connection_info(SEXP conSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< DbConnection* >::type con(conSEXP); + rcpp_result_gen = Rcpp::wrap(connection_info(con)); + return rcpp_result_gen; +END_RCPP +} +// connection_quote_string +CharacterVector connection_quote_string(DbConnection* con, CharacterVector xs); +RcppExport SEXP _RPostgres_connection_quote_string(SEXP conSEXP, SEXP xsSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< DbConnection* >::type con(conSEXP); + Rcpp::traits::input_parameter< CharacterVector >::type xs(xsSEXP); + rcpp_result_gen = Rcpp::wrap(connection_quote_string(con, xs)); + return rcpp_result_gen; +END_RCPP +} +// connection_quote_identifier +CharacterVector connection_quote_identifier(DbConnection* con, CharacterVector xs); +RcppExport SEXP _RPostgres_connection_quote_identifier(SEXP conSEXP, SEXP xsSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< DbConnection* >::type con(conSEXP); + Rcpp::traits::input_parameter< CharacterVector >::type xs(xsSEXP); + rcpp_result_gen = Rcpp::wrap(connection_quote_identifier(con, xs)); + return rcpp_result_gen; +END_RCPP +} +// connection_is_transacting +bool connection_is_transacting(DbConnection* con); +RcppExport SEXP _RPostgres_connection_is_transacting(SEXP conSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< DbConnection* >::type con(conSEXP); + rcpp_result_gen = Rcpp::wrap(connection_is_transacting(con)); + return rcpp_result_gen; +END_RCPP +} +// connection_set_transacting +void connection_set_transacting(DbConnection* con, bool transacting); +RcppExport SEXP _RPostgres_connection_set_transacting(SEXP conSEXP, SEXP transactingSEXP) { +BEGIN_RCPP + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< DbConnection* >::type con(conSEXP); + Rcpp::traits::input_parameter< bool >::type transacting(transactingSEXP); + connection_set_transacting(con, transacting); + return R_NilValue; +END_RCPP +} +// connection_copy_data +void connection_copy_data(DbConnection* con, std::string sql, List df); +RcppExport SEXP _RPostgres_connection_copy_data(SEXP conSEXP, SEXP sqlSEXP, SEXP dfSEXP) { +BEGIN_RCPP + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< DbConnection* >::type con(conSEXP); + Rcpp::traits::input_parameter< std::string >::type sql(sqlSEXP); + Rcpp::traits::input_parameter< List >::type df(dfSEXP); + connection_copy_data(con, sql, df); + return R_NilValue; +END_RCPP +} +// encode_vector +std::string encode_vector(RObject x); +RcppExport SEXP _RPostgres_encode_vector(SEXP xSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< RObject >::type x(xSEXP); + rcpp_result_gen = Rcpp::wrap(encode_vector(x)); + return rcpp_result_gen; +END_RCPP +} +// encode_data_frame +std::string encode_data_frame(List x); +RcppExport SEXP _RPostgres_encode_data_frame(SEXP xSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< List >::type x(xSEXP); + rcpp_result_gen = Rcpp::wrap(encode_data_frame(x)); + return rcpp_result_gen; +END_RCPP +} +// encrypt_password +String encrypt_password(String password, String user); +RcppExport SEXP _RPostgres_encrypt_password(SEXP passwordSEXP, SEXP userSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< String >::type password(passwordSEXP); + Rcpp::traits::input_parameter< String >::type user(userSEXP); + rcpp_result_gen = Rcpp::wrap(encrypt_password(password, user)); + return rcpp_result_gen; +END_RCPP +} +// init_logging +void init_logging(const std::string& log_level); +RcppExport SEXP _RPostgres_init_logging(SEXP log_levelSEXP) { +BEGIN_RCPP + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< const std::string& >::type log_level(log_levelSEXP); + init_logging(log_level); + return R_NilValue; +END_RCPP +} +// result_create +XPtr result_create(XPtr con, std::string sql, bool is_statement); +RcppExport SEXP _RPostgres_result_create(SEXP conSEXP, SEXP sqlSEXP, SEXP is_statementSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< XPtr >::type con(conSEXP); + Rcpp::traits::input_parameter< std::string >::type sql(sqlSEXP); + Rcpp::traits::input_parameter< bool >::type is_statement(is_statementSEXP); + rcpp_result_gen = Rcpp::wrap(result_create(con, sql, is_statement)); + return rcpp_result_gen; +END_RCPP +} +// result_release +void result_release(XPtr res); +RcppExport SEXP _RPostgres_result_release(SEXP resSEXP) { +BEGIN_RCPP + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< XPtr >::type res(resSEXP); + result_release(res); + return R_NilValue; +END_RCPP +} +// result_valid +bool result_valid(XPtr res_); +RcppExport SEXP _RPostgres_result_valid(SEXP res_SEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< XPtr >::type res_(res_SEXP); + rcpp_result_gen = Rcpp::wrap(result_valid(res_)); + return rcpp_result_gen; +END_RCPP +} +// result_fetch +List result_fetch(DbResult* res, const int n); +RcppExport SEXP _RPostgres_result_fetch(SEXP resSEXP, SEXP nSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< DbResult* >::type res(resSEXP); + Rcpp::traits::input_parameter< const int >::type n(nSEXP); + rcpp_result_gen = Rcpp::wrap(result_fetch(res, n)); + return rcpp_result_gen; +END_RCPP +} +// result_bind +void result_bind(DbResult* res, List params); +RcppExport SEXP _RPostgres_result_bind(SEXP resSEXP, SEXP paramsSEXP) { +BEGIN_RCPP + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< DbResult* >::type res(resSEXP); + Rcpp::traits::input_parameter< List >::type params(paramsSEXP); + result_bind(res, params); + return R_NilValue; +END_RCPP +} +// result_has_completed +bool result_has_completed(DbResult* res); +RcppExport SEXP _RPostgres_result_has_completed(SEXP resSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< DbResult* >::type res(resSEXP); + rcpp_result_gen = Rcpp::wrap(result_has_completed(res)); + return rcpp_result_gen; +END_RCPP +} +// result_rows_fetched +int result_rows_fetched(DbResult* res); +RcppExport SEXP _RPostgres_result_rows_fetched(SEXP resSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< DbResult* >::type res(resSEXP); + rcpp_result_gen = Rcpp::wrap(result_rows_fetched(res)); + return rcpp_result_gen; +END_RCPP +} +// result_rows_affected +int result_rows_affected(DbResult* res); +RcppExport SEXP _RPostgres_result_rows_affected(SEXP resSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< DbResult* >::type res(resSEXP); + rcpp_result_gen = Rcpp::wrap(result_rows_affected(res)); + return rcpp_result_gen; +END_RCPP +} +// result_column_info +List result_column_info(DbResult* res); +RcppExport SEXP _RPostgres_result_column_info(SEXP resSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< DbResult* >::type res(resSEXP); + rcpp_result_gen = Rcpp::wrap(result_column_info(res)); + return rcpp_result_gen; +END_RCPP +} + +static const R_CallMethodDef CallEntries[] = { + {"_RPostgres_connection_create", (DL_FUNC) &_RPostgres_connection_create, 2}, + {"_RPostgres_connection_valid", (DL_FUNC) &_RPostgres_connection_valid, 1}, + {"_RPostgres_connection_release", (DL_FUNC) &_RPostgres_connection_release, 1}, + {"_RPostgres_connection_info", (DL_FUNC) &_RPostgres_connection_info, 1}, + {"_RPostgres_connection_quote_string", (DL_FUNC) &_RPostgres_connection_quote_string, 2}, + {"_RPostgres_connection_quote_identifier", (DL_FUNC) &_RPostgres_connection_quote_identifier, 2}, + {"_RPostgres_connection_is_transacting", (DL_FUNC) &_RPostgres_connection_is_transacting, 1}, + {"_RPostgres_connection_set_transacting", (DL_FUNC) &_RPostgres_connection_set_transacting, 2}, + {"_RPostgres_connection_copy_data", (DL_FUNC) &_RPostgres_connection_copy_data, 3}, + {"_RPostgres_encode_vector", (DL_FUNC) &_RPostgres_encode_vector, 1}, + {"_RPostgres_encode_data_frame", (DL_FUNC) &_RPostgres_encode_data_frame, 1}, + {"_RPostgres_encrypt_password", (DL_FUNC) &_RPostgres_encrypt_password, 2}, + {"_RPostgres_init_logging", (DL_FUNC) &_RPostgres_init_logging, 1}, + {"_RPostgres_result_create", (DL_FUNC) &_RPostgres_result_create, 3}, + {"_RPostgres_result_release", (DL_FUNC) &_RPostgres_result_release, 1}, + {"_RPostgres_result_valid", (DL_FUNC) &_RPostgres_result_valid, 1}, + {"_RPostgres_result_fetch", (DL_FUNC) &_RPostgres_result_fetch, 2}, + {"_RPostgres_result_bind", (DL_FUNC) &_RPostgres_result_bind, 2}, + {"_RPostgres_result_has_completed", (DL_FUNC) &_RPostgres_result_has_completed, 1}, + {"_RPostgres_result_rows_fetched", (DL_FUNC) &_RPostgres_result_rows_fetched, 1}, + {"_RPostgres_result_rows_affected", (DL_FUNC) &_RPostgres_result_rows_affected, 1}, + {"_RPostgres_result_column_info", (DL_FUNC) &_RPostgres_result_column_info, 1}, + {NULL, NULL, 0} +}; + +RcppExport void R_init_RPostgres(DllInfo *dll) { + R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); + R_useDynamicSymbols(dll, FALSE); +} diff --git a/src/connection.cpp b/src/connection.cpp new file mode 100644 index 0000000..e94ecff --- /dev/null +++ b/src/connection.cpp @@ -0,0 +1,109 @@ +#include "pch.h" +#include "RPostgres_types.h" + + +// [[Rcpp::export]] +XPtr connection_create( + std::vector keys, + std::vector values +) { + LOG_VERBOSE; + + DbConnectionPtr* pConn = new DbConnectionPtr( + new DbConnection(keys, values) + ); + + return XPtr(pConn, true); +} + +// [[Rcpp::export]] +bool connection_valid(XPtr con_) { + DbConnectionPtr* con = con_.get(); + return con; +} + +// [[Rcpp::export]] +void connection_release(XPtr con_) { + if (!connection_valid(con_)) { + warning("Already disconnected"); + return; + } + + DbConnectionPtr* con = con_.get(); + if (con->get()->has_query()) { + warning("%s\n%s", + "There is a result object still in use.", + "The connection will be automatically released when it is closed" + ); + } + + con->get()->disconnect(); + con_.release(); +} + +// [[Rcpp::export]] +List connection_info(DbConnection* con) { + return con->info(); +} + +// Quoting + +// [[Rcpp::export]] +CharacterVector connection_quote_string(DbConnection* con, CharacterVector xs) { + R_xlen_t n = xs.size(); + CharacterVector output(n); + + for (R_xlen_t i = 0; i < n; ++i) { + String x = xs[i]; + output[i] = con->quote_string(x); + } + + return output; +} + +// [[Rcpp::export]] +CharacterVector connection_quote_identifier(DbConnection* con, CharacterVector xs) { + R_xlen_t n = xs.size(); + CharacterVector output(n); + + for (R_xlen_t i = 0; i < n; ++i) { + String x = xs[i]; + output[i] = con->quote_identifier(x); + } + + return output; +} + +// Transactions + +// [[Rcpp::export]] +bool connection_is_transacting(DbConnection* con) { + return con->is_transacting(); +} + +// [[Rcpp::export]] +void connection_set_transacting(DbConnection* con, bool transacting) { + con->set_transacting(transacting); +} + +// Specific functions + +// [[Rcpp::export]] +void connection_copy_data(DbConnection* con, std::string sql, List df) { + return con->copy_data(sql, df); +} + + +// as() override + +namespace Rcpp { + +template<> +DbConnection* as(SEXP x) { + DbConnectionPtr* connection = (DbConnectionPtr*)(R_ExternalPtrAddr(x)); + if (!connection) + stop("Invalid connection"); + return connection->get(); +} + +} diff --git a/src/encode.cpp b/src/encode.cpp new file mode 100644 index 0000000..f5c4ad5 --- /dev/null +++ b/src/encode.cpp @@ -0,0 +1,143 @@ +#include "pch.h" +#include "encode.h" + + +// [[Rcpp::export]] +std::string encode_vector(RObject x) { + std::string buffer; + + int n = Rf_length(x); + for (int i = 0; i < n; ++i) { + encode_in_buffer(x, i, buffer); + if (i != n - 1) + buffer.push_back('\n'); + } + + return buffer; +} + +void encode_row_in_buffer(List x, int i, std::string& buffer, + std::string fieldDelim, + std::string lineDelim) { + int p = Rf_length(x); + for (int j = 0; j < p; ++j) { + RObject xj(x[j]); + encode_in_buffer(xj, i, buffer); + if (j != p - 1) + buffer.append(fieldDelim); + } + buffer.append(lineDelim); +} + +// [[Rcpp::export]] +std::string encode_data_frame(List x) { + if (Rf_length(x) == 0) + return (""); + int n = Rf_length(x[0]); + + std::string buffer; + for (int i = 0; i < n; ++i) { + encode_row_in_buffer(x, i, buffer); + } + + return buffer; +} + +// ============================================================================= +// Derived from EncodeElementS in RPostgreSQL +// Written by: tomoakin@kenroku.kanazawa-u.ac.jp +// License: GPL-2 + +void encode_in_buffer(RObject x, int i, std::string& buffer) { + switch (TYPEOF(x)) { + case LGLSXP: { + int value = LOGICAL(x)[i]; + if (value == TRUE) { + buffer.append("true"); + } else if (value == FALSE) { + buffer.append("false"); + } else { + buffer.append("\\N"); + } + break; + } + case INTSXP: { + int value = INTEGER(x)[i]; + if (value == NA_INTEGER) { + buffer.append("\\N"); + } else { + char buf[32]; + snprintf(buf, 32, "%d", value); + buffer.append(buf); + } + break; + } + case REALSXP: { + double value = REAL(x)[i]; + if (!R_FINITE(value)) { + if (ISNA(value)) { + buffer.append("\\N"); + } else if (ISNAN(value)) { + buffer.append("NaN"); + } else if (value > 0) { + buffer.append("Infinity"); + } else { + buffer.append("-Infinity"); + } + } else { + char buf[15 + 1 + 1 + 4 + 1]; // minus + decimal + exponent + \0 + snprintf(buf, 22, "%.15g", value); + buffer.append(buf); + } + break; + } + case STRSXP: { + RObject value = STRING_ELT(x, i); + if (value == NA_STRING) { + buffer.append("\\N"); + } else { + const char* s = Rf_translateCharUTF8(STRING_ELT(x, i)); + escape_in_buffer(s, buffer); + } + break; + } + default: + stop("Don't know how to handle vector of type %s.", + Rf_type2char(TYPEOF(x))); + } +} + + +// Escape postgresql special characters +// http://www.postgresql.org/docs/9.4/static/sql-copy.html#AEN71914 +void escape_in_buffer(const char* string, std::string& buffer) { + size_t len = strlen(string); + + for (size_t i = 0; i < len; ++i) { + switch (string[i]) { + case '\b': + buffer.append("\\b"); + break; + case '\f': + buffer.append("\\f"); + break; + case '\n': + buffer.append("\\n"); + break; + case '\r': + buffer.append("\\r"); + break; + case '\t': + buffer.append("\\t"); + break; + case '\v': + buffer.append("\\v"); + break; + case '\\': + buffer.append("\\\\"); + break; + default: + buffer.push_back(string[i]); + } + } +} diff --git a/src/encode.h b/src/encode.h new file mode 100644 index 0000000..3c489ab --- /dev/null +++ b/src/encode.h @@ -0,0 +1,13 @@ +#ifndef __RPOSTGRES_ENCODE__ +#define __RPOSTGRES_ENCODE__ + +// Defined in encode.cpp ------------------------------------------------------- + +void escape_in_buffer(const char* string, std::string& buffer); +void encode_in_buffer(RObject x, int i, std::string& buffer); +void encode_row_in_buffer(List x, int i, std::string& buffer, + std::string fieldDelim = "\t", + std::string lineDelim = "\n"); +std::string encode_data_frame(List x); + +#endif diff --git a/src/encrypt.cpp b/src/encrypt.cpp new file mode 100644 index 0000000..2b97057 --- /dev/null +++ b/src/encrypt.cpp @@ -0,0 +1,12 @@ +#include "pch.h" + + +// [[Rcpp::export]] +String encrypt_password(String password, String user) { + char* encrypted = PQencryptPassword(password.get_cstring(), user.get_cstring()); + + String copy(encrypted); + PQfreemem(encrypted); + + return copy; +} diff --git a/src/integer64.h b/src/integer64.h new file mode 100644 index 0000000..47f9b23 --- /dev/null +++ b/src/integer64.h @@ -0,0 +1,12 @@ +#ifndef RPOSTGRES_INTEGER64_H +#define RPOSTGRES_INTEGER64_H + +#define INT64SXP REALSXP + +#define NA_INTEGER64 (0x8000000000000000) + +inline int64_t* INTEGER64(SEXP x) { + return reinterpret_cast(REAL(x)); +} + +#endif // RPOSTGRES_INTEGER64_H diff --git a/src/logging.cpp b/src/logging.cpp new file mode 100644 index 0000000..672619f --- /dev/null +++ b/src/logging.cpp @@ -0,0 +1,8 @@ +#include "pch.h" +#include + + +// [[Rcpp::export]] +void init_logging(const std::string& log_level) { + plog::init_r(log_level); +} diff --git a/src/pch.h b/src/pch.h new file mode 100644 index 0000000..9b640c2 --- /dev/null +++ b/src/pch.h @@ -0,0 +1,6 @@ +#include +#include + +#include + +using namespace Rcpp; diff --git a/src/result.cpp b/src/result.cpp new file mode 100644 index 0000000..b8624a9 --- /dev/null +++ b/src/result.cpp @@ -0,0 +1,63 @@ +#include "pch.h" +#include "RPostgres_types.h" + + +// [[Rcpp::export]] +XPtr result_create(XPtr con, std::string sql, bool is_statement = false) { + (void)is_statement; + DbResult* res = new DbResult(*con, sql); + return XPtr(res, true); +} + +// [[Rcpp::export]] +void result_release(XPtr res) { + res.release(); +} + +// [[Rcpp::export]] +bool result_valid(XPtr res_) { + DbResult* res = res_.get(); + return res != NULL && res->active(); +} + +// [[Rcpp::export]] +List result_fetch(DbResult* res, const int n) { + return res->fetch(n); +} + +// [[Rcpp::export]] +void result_bind(DbResult* res, List params) { + res->bind(params); +} + +// [[Rcpp::export]] +bool result_has_completed(DbResult* res) { + return res->complete(); +} + +// [[Rcpp::export]] +int result_rows_fetched(DbResult* res) { + return res->n_rows_fetched(); +} + +// [[Rcpp::export]] +int result_rows_affected(DbResult* res) { + return res->n_rows_affected(); +} + +// [[Rcpp::export]] +List result_column_info(DbResult* res) { + return res->get_column_info(); +} + +namespace Rcpp { + +template<> +DbResult* as(SEXP x) { + DbResult* result = (DbResult*)(R_ExternalPtrAddr(x)); + if (!result) + stop("Invalid result set"); + return result; +} + +} diff --git a/src/win32/timegm.c b/src/win32/timegm.c new file mode 100644 index 0000000..3bdbe13 --- /dev/null +++ b/src/win32/timegm.c @@ -0,0 +1,76 @@ +/* + * Copyright (c) 1997 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the Institute nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + + + +#include + +#if defined(WIN32) && !defined(_WIN64) + +static int +is_leap(unsigned y) { + y += 1900; + return (y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0); +} + +time_t + _mkgmtime32(struct tm *tm) { + static const unsigned ndays[2][12] = { + {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, + {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} + }; + time_t res = 0; + int i; + + for (i = 70; i < tm->tm_year; ++i) + res += is_leap(i) ? 366 : 365; + + for (i = 0; i < tm->tm_mon; ++i) + res += ndays[is_leap(tm->tm_year)][i]; + res += tm->tm_mday - 1; + res *= 24; + res += tm->tm_hour; + res *= 60; + res += tm->tm_min; + res *= 60; + res += tm->tm_sec; + return res; +} + +#else + +void dummy_to_prevent_empty_unit_warning(){} + +#endif /* WIN32 */ + + diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..b5bf92a --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,4 @@ +library(testthat) +library(RPostgres) + +test_check("RPostgres") diff --git a/tests/testthat/helper-DBItest.R b/tests/testthat/helper-DBItest.R new file mode 100644 index 0000000..334d8ad --- /dev/null +++ b/tests/testthat/helper-DBItest.R @@ -0,0 +1,15 @@ +library(DBI) + +DBItest::make_context( + Postgres(), + NULL, + name = "RPostgres", + tweaks = DBItest::tweaks( + placeholder_pattern = "$1", + date_cast = function(x) paste0("date '", x, "'"), + time_cast = function(x) paste0("time '", x, "'"), + timestamp_cast = function(x) paste0("timestamp '", x, "'"), + is_null_check = function(x) paste0("(", x, "::text IS NULL)"), + blob_cast = function(x) paste0("(", x, "::bytea)") + ) +) diff --git a/tests/testthat/helper-astyle.R b/tests/testthat/helper-astyle.R new file mode 100644 index 0000000..17c0bd1 --- /dev/null +++ b/tests/testthat/helper-astyle.R @@ -0,0 +1,33 @@ +vcapply <- function(X, FUN, ..., USE.NAMES = TRUE) { + vapply(X = X, FUN = FUN, FUN.VALUE = character(1L), ..., USE.NAMES = USE.NAMES) +} + +astyle <- function(extra_args = character()) { + astyle_cmd <- "astyle" + if (Sys.which(astyle_cmd) == "") { + skip("astyle not found") + } + if (!requireNamespace("rprojroot", quietly = TRUE)) { + skip("rprojroot not installed") + } + + astyle_args <- c( + "-n", + "--indent=spaces=2", + "--unpad-paren", + "--pad-header", + "--pad-oper", + "--min-conditional-indent=0", + "--align-pointer=type", + "--align-reference=type" + ) + + src_path <- rprojroot::find_package_root_file("src") + src_files <- dir(src_path, "[.](?:cpp|h)$", recursive = TRUE, full.names = TRUE) + astyle_files <- grep("(?:RcppExports[.](?:cpp|h)|static_assert[.]h)", src_files, value = TRUE, invert = TRUE) + output <- system2(astyle_cmd, c(astyle_args, astyle_files, extra_args), stdout = TRUE, stderr = TRUE) + unchanged <- grepl("^Unchanged", output) + if (any(!unchanged)) { + warning(paste(output[!unchanged], collapse = "\n")) + } +} diff --git a/tests/testthat/helper-with_database_connection.R b/tests/testthat/helper-with_database_connection.R new file mode 100644 index 0000000..d5ac8f3 --- /dev/null +++ b/tests/testthat/helper-with_database_connection.R @@ -0,0 +1,10 @@ +#' Execute an R expression with access to a database connection. +#' +#' @param expr expression. Any R expression. +#' @param con PqConnection. A database connection, by default. +#' \code{dbConnect(RPostgres::Postgres())}. +#' @return the return value of the evaluated \code{expr}. +with_database_connection <- function(expr, con = postgresDefault()) { + context <- list2env(list(con = con), parent = parent.frame()) + eval(substitute(expr), envir = context) +} diff --git a/tests/testthat/helper-with_table.R b/tests/testthat/helper-with_table.R new file mode 100644 index 0000000..fa22915 --- /dev/null +++ b/tests/testthat/helper-with_table.R @@ -0,0 +1,10 @@ +#' Run an expression that creates and touches a table, then clean up. +#' +#' @param con PqConnection. The database connection. +#' @param tbl character. The table name. +#' @param expr expression. The R expression to execute. +#' @return the return value of the \code{expr}. +with_table <- function(con, tbl, expr) { + on.exit(DBI::dbRemoveTable(con, tbl), add = TRUE) + force(expr) +} diff --git a/tests/testthat/helper-without_rownames.R b/tests/testthat/helper-without_rownames.R new file mode 100644 index 0000000..93bb356 --- /dev/null +++ b/tests/testthat/helper-without_rownames.R @@ -0,0 +1,4 @@ +without_rownames <- function(df) { + row.names(df) <- NULL + df +} diff --git a/tests/testthat/test-DBItest.R b/tests/testthat/test-DBItest.R new file mode 100644 index 0000000..b0a4b17 --- /dev/null +++ b/tests/testthat/test-DBItest.R @@ -0,0 +1,12 @@ +if (postgresHasDefault() && identical(Sys.getenv("NOT_CRAN"), "true")) { + +DBItest::test_all(c( + # deliberately skipped, not required with upcoming version of DBI + "get_info_driver", + "get_info_connection", + "get_info_result", + + NULL +)) + +} diff --git a/tests/testthat/test-bigint.R b/tests/testthat/test-bigint.R new file mode 100644 index 0000000..63b51f4 --- /dev/null +++ b/tests/testthat/test-bigint.R @@ -0,0 +1,22 @@ +context("bigint") + +test_that("integer", { + con <- postgresDefault(bigint = "integer") + on.exit(dbDisconnect(con)) + + expect_identical(dbGetQuery(con, "SELECT COUNT(*) FROM (SELECT 1) A")[[1]], 1L) +}) + +test_that("numeric", { + con <- postgresDefault(bigint = "numeric") + on.exit(dbDisconnect(con)) + + expect_identical(dbGetQuery(con, "SELECT COUNT(*) FROM (SELECT 1) A")[[1]], 1.0) +}) + +test_that("character", { + con <- postgresDefault(bigint = "character") + on.exit(dbDisconnect(con)) + + expect_identical(dbGetQuery(con, "SELECT COUNT(*) FROM (SELECT 1) A")[[1]], "1") +}) diff --git a/tests/testthat/test-data-type.R b/tests/testthat/test-data-type.R new file mode 100644 index 0000000..2b983db --- /dev/null +++ b/tests/testthat/test-data-type.R @@ -0,0 +1,12 @@ +context("dbDataType") + +# Taken from DBI +test_that("dbDataType works on a data frame", { + con <- postgresDefault() + on.exit(dbDisconnect(con)) + + df <- data.frame(x = 1:10, y = 1:10 / 2) + types <- dbDataType(con, df) + + expect_equal(types, c(x = "INTEGER", y = "REAL")) +}) diff --git a/tests/testthat/test-dbConnect.R b/tests/testthat/test-dbConnect.R new file mode 100644 index 0000000..b210cb0 --- /dev/null +++ b/tests/testthat/test-dbConnect.R @@ -0,0 +1,53 @@ +context("Connection") + +test_that("querying closed connection throws error", { + db <- postgresDefault() + dbDisconnect(db) + expect_error(dbSendQuery(db, "select * from foo"), "not valid") +}) + +test_that("warn if previous result set is invalidated", { + con <- postgresDefault() + on.exit(dbDisconnect(con)) + + rs1 <- dbSendQuery(con, "SELECT 1 + 1") + + expect_warning(rs2 <- dbSendQuery(con, "SELECT 1 + 1"), "Cancelling previous query") + expect_false(dbIsValid(rs1)) + + dbClearResult(rs2) +}) + +test_that("no warning if previous result set is closed", { + con <- postgresDefault() + on.exit(dbDisconnect(con)) + + rs1 <- dbSendQuery(con, "SELECT 1 + 1") + dbClearResult(rs1) + + expect_warning(rs2 <- dbSendQuery(con, "SELECT 1 + 1"), NA) + dbClearResult(rs2) +}) + +test_that("warning if close connection with open results", { + con <- postgresDefault() + + rs1 <- dbSendQuery(con, "SELECT 1 + 1") + + expect_warning(dbDisconnect(con), "still in use") +}) + +test_that("passing other options parameters", { + con <- postgresDefault(application_name = "apple") + on.exit(dbDisconnect(con)) + + pid <- dbGetInfo(con)$pid + r <- dbGetQuery(con, "SELECT application_name FROM pg_stat_activity WHERE pid=$1", + list(pid)) + expect_identical(r$application_name, "apple") +}) + +test_that("error if passing unkown parameters", { + skip_on_cran() + expect_error(dbConnect(Postgres(), fruit = "apple"), 'invalid connection option "fruit"') +}) diff --git a/tests/testthat/test-dbGetQuery.R b/tests/testthat/test-dbGetQuery.R new file mode 100644 index 0000000..873d9c0 --- /dev/null +++ b/tests/testthat/test-dbGetQuery.R @@ -0,0 +1,57 @@ +context("dbGetQuery") + +test_that("special characters work", { + con <- postgresDefault() + + angstrom <- enc2utf8("\\u00e5") + + dbExecute(con, "CREATE TEMPORARY TABLE test1 (x TEXT)") + dbExecute(con, "INSERT INTO test1 VALUES ('\\u00e5')") + + expect_equal(dbGetQuery(con, "SELECT * FROM test1")$x, angstrom) + expect_equal(dbGetQuery(con, "SELECT * FROM test1 WHERE x = '\\u00e5'")$x, + angstrom) +}) + + +# Not generic enough for DBItest +test_that("JSONB format is recognized", { + con <- postgresDefault() + + n_json <- dbGetQuery(con, "SELECT count(*) FROM pg_type WHERE typname = 'jsonb' AND typtype = 'b'")[[1]] + if (as.integer(n_json) == 0) skip("No jsonb type installed") + + jsonb <- '{\"name\": \"mike\"}' + + dbExecute(con, "CREATE TEMPORARY TABLE test2 (data JSONB)") + dbExecute(con, paste0("INSERT INTO test2(data) values ('", jsonb, "');")) + + expect_warning( + expect_equal(dbGetQuery(con, "SELECT * FROM test2")$data, jsonb), + NA + ) + + dbDisconnect(con) +}) + + +test_that("uuid format is recognized", { + con <- postgresDefault() + + dbExecute(con, "CREATE TEMPORARY TABLE fuutab + ( + fuu UUID, + name VARCHAR(255) NOT NULL + );") + + uuid <- "c44352c0-72bd-11e5-a7f3-0002a5d5c51b" + + dbExecute(con, paste0("INSERT INTO fuutab(fuu, name) values ('", uuid, "', 'bob');")) + + expect_warning( + expect_equal(dbGetQuery(con, "SELECT * FROM fuutab")$fuu, uuid), + NA + ) + + dbDisconnect(con) +}) diff --git a/tests/testthat/test-dbWriteTable.R b/tests/testthat/test-dbWriteTable.R new file mode 100644 index 0000000..fe645a7 --- /dev/null +++ b/tests/testthat/test-dbWriteTable.R @@ -0,0 +1,89 @@ +context("dbWriteTable") + +if (postgresHasDefault()) { + +with_database_connection({ + describe("Writing to the database", { + test_that("writing to a database table is successful", { + with_table(con, "beaver2", { + dbWriteTable(con, "beaver2", beaver2, temporary = TRUE) + expect_equal(dbReadTable(con, "beaver2"), beaver2) + }) + }) + + test_that("writing to a database table with character features is successful", { + with_table(con, "iris", { + iris2 <- transform(iris, Species = as.character(Species)) + dbWriteTable(con, "iris", iris2, temporary = TRUE) + expect_equal(dbReadTable(con, "iris"), iris2) + }) + }) + }) + + describe("Appending to the database", { + test_that("append to a database table is successful", { + with_table(con, "beaver2", { + dbWriteTable(con, "beaver2", beaver2, temporary = TRUE) + dbWriteTable(con, "beaver2", beaver2, append = TRUE, temporary = TRUE) + expect_equal(dbReadTable(con, "beaver2"), rbind(beaver2, beaver2)) + }) + }) + + test_that("append to a database table with character features is successful", { + with_table(con, "iris", { + iris2 <- transform(iris, Species = as.character(Species)) + dbWriteTable(con, "iris", iris2, temporary = TRUE) + dbWriteTable(con, "iris", iris2, append = TRUE, temporary = TRUE) + expect_equal(dbReadTable(con, "iris"), rbind(iris2, iris2)) + }) + }) + }) + + describe("Usage of the field.types argument", { + test_that("New table creation respects the field.types argument", { + with_table(con, "iris", { + iris2 <- transform( + iris, + Petal.Width = as.integer(Petal.Width), + Species = as.character(Species) + ) + field.types <- c("real", "double precision", "numeric", "bigint", "text") + + dbWriteTable(con, "iris", iris2, field.types = field.types, temporary = TRUE) + + iris3 <- transform( + iris2, + Petal.Width = bit64::as.integer64(Petal.Width) + ) + expect_equal(dbReadTable(con, "iris"), iris3) + + # http://stackoverflow.com/questions/2146705/select-datatype-of-the-field-in-postgres + types <- DBI::dbGetQuery(con, + paste("select column_name, data_type from information_schema.columns ", + "where table_name = 'iris'")) + expected <- data.frame(column_name = colnames(iris2), + data_type = field.types, stringsAsFactors = FALSE) + types <- without_rownames(types[order(types$column_name), ]) + expected <- without_rownames(expected[order(expected$column_name), ]) + + expect_equal(types, expected) + }) + }) + + test_that("Appending fails when using the field.types argument", { + with_table(con, "iris", { + iris2 <- transform(iris, Petal.Width = as.integer(Petal.Width), + Species = as.character(Species)) + field.types <- c("real", "double precision", "numeric", "bigint", "text") + + dbWriteTable(con, "iris", iris2, field.types = field.types, temporary = TRUE) + expect_error( + dbWriteTable(con, "iris", iris2, field.types = field.types, append = TRUE, temporary = TRUE), + "field[.]types" + ) + }) + }) + }) +}) + +} diff --git a/tests/testthat/test-encoding.R b/tests/testthat/test-encoding.R new file mode 100644 index 0000000..ef8f987 --- /dev/null +++ b/tests/testthat/test-encoding.R @@ -0,0 +1,23 @@ +context("Encoding") + +# Specific to RPostgres +test_that("NAs encoded as NULLs", { + expect_equal(encode_vector(NA), "\\N") + expect_equal(encode_vector(NA_integer_), "\\N") + expect_equal(encode_vector(NA_real_), "\\N") + expect_equal(encode_vector(NA_character_), "\\N") +}) + +# Specific to RPostgres +test_that("special floating point values encoded correctly", { + expect_equal(encode_vector(NaN), "NaN") + expect_equal(encode_vector(Inf), "Infinity") + expect_equal(encode_vector(-Inf), "-Infinity") +}) + +# Specific to RPostgres +test_that("special string values are escaped", { + expect_equal(encode_vector("\n"), "\\n") + expect_equal(encode_vector("\r"), "\\r") + expect_equal(encode_vector("\b"), "\\b") +}) diff --git a/tools/winlibs.R b/tools/winlibs.R new file mode 100644 index 0000000..7f29ebf --- /dev/null +++ b/tools/winlibs.R @@ -0,0 +1,8 @@ +# Build against static libpq +if(!file.exists("../windows/libpq-9.5.2/include/libpq-fe.h")){ + if(getRversion() < "3.3.0") setInternet2() + download.file("https://github.com/rwinlib/libpq/archive/v9.5.2.zip", "lib.zip", quiet = TRUE) + dir.create("../windows", showWarnings = FALSE) + unzip("lib.zip", exdir = "../windows") + unlink("lib.zip") +}