[med-svn] [r-cran-dbitest] 01/06: New upstream version 1.5
Andreas Tille
tille at debian.org
Sun Oct 1 21:44:01 UTC 2017
This is an automated email from the git hooks/post-receive script.
tille pushed a commit to branch master
in repository r-cran-dbitest.
commit 37c930ab01bed8e43c9d4d47ef2bc939006cc56b
Author: Andreas Tille <tille at debian.org>
Date: Sun Oct 1 23:32:39 2017 +0200
New upstream version 1.5
---
DESCRIPTION | 66 +--
MD5 | 179 +++---
NAMESPACE | 8 +-
NEWS.md | 63 +++
R/context.R | 5 +-
R/expectations.R | 48 +-
R/import-dbi.R | 7 +-
R/run.R | 30 +-
R/spec-.R | 62 +-
R/spec-all.R | 11 +
R/spec-compliance-methods.R | 24 +-
R/spec-compliance-read-only.R | 18 -
R/spec-compliance.R | 1 -
R/spec-connection-connect.R | 23 -
R/spec-connection-data-type.R | 33 +-
R/spec-connection-disconnect.R | 43 ++
R/spec-connection-get-info.R | 25 +-
R/spec-connection.R | 2 +-
R/spec-driver-class.R | 7 -
R/spec-driver-connect.R | 35 ++
R/spec-driver-constructor.R | 21 +-
R/spec-driver-data-type.R | 159 ++++--
R/spec-driver.R | 3 +-
R/spec-getting-started.R | 13 +-
R/spec-meta-bind-.R | 126 +----
R/spec-meta-bind-multi-row.R | 70 ---
R/spec-meta-bind-runner.R | 101 ++++
R/spec-meta-bind-tester-extra.R | 21 +
R/spec-meta-bind.R | 443 +++++++++------
R/spec-meta-column-info.R | 17 +-
R/spec-meta-get-row-count.R | 122 +++-
R/spec-meta-get-rows-affected.R | 89 ++-
R/spec-meta-get-statement.R | 62 +-
R/spec-meta-has-completed.R | 88 +++
R/spec-meta-is-valid-connection.R | 16 -
R/spec-meta-is-valid-result.R | 21 -
R/spec-meta-is-valid.R | 61 ++
R/spec-meta.R | 9 +-
R/spec-result-clear-result.R | 62 ++
R/spec-result-create-table-with-data-type.R | 54 +-
R/spec-result-execute.R | 69 +++
R/spec-result-fetch.R | 312 ++++++----
R/spec-result-get-query.R | 197 +++++--
R/spec-result-roundtrip.R | 609 +++++++-------------
R/spec-result-send-query.R | 120 ++--
R/spec-result-send-statement.R | 102 ++++
R/spec-result.R | 9 +-
R/spec-sql-exists-table.R | 117 ++++
R/spec-sql-list-fields.R | 23 +-
R/spec-sql-list-tables.R | 92 ++-
R/spec-sql-quote-identifier.R | 163 ++++--
R/spec-sql-quote-string.R | 142 ++++-
R/spec-sql-read-table.R | 303 ++++++++++
R/spec-sql-read-write-roundtrip.R | 241 --------
R/spec-sql-read-write-table.R | 137 -----
R/spec-sql-remove-table.R | 161 ++++++
R/spec-sql-write-table.R | 755 +++++++++++++++++++++++++
R/spec-sql.R | 6 +-
R/spec-stress-connection.R | 41 +-
R/spec-stress-driver.R | 34 --
R/spec-stress.R | 1 -
R/spec-transaction-begin-commit-rollback.R | 192 +++++++
R/spec-transaction-begin-commit.R | 98 ----
R/spec-transaction-begin-rollback.R | 10 -
R/spec-transaction-with-transaction.R | 133 ++++-
R/spec-transaction.R | 3 +-
R/spec.R | 16 +-
R/test-all.R | 12 +-
R/tweaks.R | 71 ++-
R/utils.R | 136 ++++-
build/vignette.rds | Bin 202 -> 200 bytes
inst/doc/test.html | 8 +-
man/DBIspec-wip.Rd | 306 +---------
man/DBIspec.Rd | 191 +------
man/DBItest-package.Rd | 3 +-
man/context.Rd | 1 -
man/make_placeholder_fun.Rd | 1 -
man/spec_connection_disconnect.Rd | 21 +
man/spec_driver_connect.Rd | 31 +
man/spec_driver_data_type.Rd | 45 ++
man/spec_meta_bind.Rd | 115 ++++
man/spec_meta_get_row_count.Rd | 29 +
man/spec_meta_get_rows_affected.Rd | 21 +
man/spec_meta_get_statement.Rd | 16 +
man/spec_meta_has_completed.Rd | 32 ++
man/spec_meta_is_valid.Rd | 25 +
man/spec_result_clear_result.Rd | 24 +
man/spec_result_create_table_with_data_type.Rd | 16 +
man/spec_result_execute.Rd | 33 ++
man/spec_result_fetch.Rd | 46 ++
man/spec_result_get_query.Rd | 54 ++
man/spec_result_roundtrip.Rd | 51 ++
man/spec_result_send_query.Rd | 35 ++
man/spec_result_send_statement.Rd | 35 ++
man/spec_sql_exists_table.Rd | 42 ++
man/spec_sql_list_tables.Rd | 33 ++
man/spec_sql_quote_identifier.Rd | 47 ++
man/spec_sql_quote_string.Rd | 45 ++
man/spec_sql_read_table.Rd | 74 +++
man/spec_sql_remove_table.Rd | 38 ++
man/spec_sql_write_table.Rd | 129 +++++
man/spec_transaction_begin_commit_rollback.Rd | 57 ++
man/spec_transaction_with_transaction.Rd | 31 +
man/test_all.Rd | 11 +-
man/test_compliance.Rd | 1 -
man/test_connection.Rd | 1 -
man/test_data_type.Rd | 50 ++
man/test_driver.Rd | 1 -
man/test_getting_started.Rd | 1 -
man/test_meta.Rd | 1 -
man/test_result.Rd | 1 -
man/test_sql.Rd | 1 -
man/test_stress.Rd | 1 -
man/test_transaction.Rd | 1 -
man/tweaks.Rd | 43 +-
tests/testthat/test-consistency.R | 28 +
tests/testthat/test-tweaks.R | 2 +-
117 files changed, 5711 insertions(+), 2620 deletions(-)
diff --git a/DESCRIPTION b/DESCRIPTION
index 375793f..b88b9eb 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -1,54 +1,56 @@
Package: DBItest
Title: Testing 'DBI' Back Ends
-Version: 1.4
-Date: 2016-12-02
+Version: 1.5
+Date: 2017-06-18
Authors at R: c( person(given = "Kirill", family = "Müller", role =
c("aut", "cre"), email = "krlmlr+r at mailbox.org"),
person("RStudio", role = "cph") )
Description: A helper that tests 'DBI' back ends for conformity
to the interface.
Depends: R (>= 3.0.0)
-Imports: DBI (>= 0.4-9), methods, R6, testthat (>= 1.0.2), withr
-Suggests: devtools, knitr, lintr, rmarkdown
+Imports: blob, DBI (>= 0.4-9), desc, hms, methods, R6, testthat (>=
+ 1.0.2), withr
+Suggests: knitr, lintr, rmarkdown
License: LGPL (>= 2)
LazyData: true
Encoding: UTF-8
BugReports: https://github.com/rstats-db/DBItest/issues
-RoxygenNote: 5.0.1.9000
+RoxygenNote: 6.0.1
VignetteBuilder: knitr
Collate: 'DBItest.R' 'context.R' 'expectations.R' 'import-dbi.R'
'import-testthat.R' 'run.R' 's4.R' 'spec.R'
- 'spec-getting-started.R' 'spec-driver-class.R'
- 'spec-driver-constructor.R' 'spec-driver-data-type.R'
- 'spec-driver-get-info.R' 'spec-driver.R'
- 'spec-connection-connect.R' 'spec-connection-data-type.R'
- 'spec-connection-get-info.R' 'spec-connection.R'
- 'spec-result-send-query.R' 'spec-result-fetch.R'
- 'spec-result-get-query.R'
+ 'spec-getting-started.R' 'spec-compliance-methods.R'
+ 'spec-driver-constructor.R' 'spec-driver-class.R'
+ 'spec-driver-data-type.R' 'spec-connection-data-type.R'
'spec-result-create-table-with-data-type.R'
- 'spec-result-roundtrip.R' 'spec-result.R'
- 'spec-sql-quote-string.R' 'spec-sql-quote-identifier.R'
- 'spec-sql-read-write-table.R' 'spec-sql-read-write-roundtrip.R'
- 'spec-sql-list-tables.R' 'spec-sql-list-fields.R' 'spec-sql.R'
- 'spec-meta-is-valid-connection.R' 'spec-meta-is-valid-result.R'
- 'spec-meta-get-statement.R' 'spec-meta-column-info.R'
+ 'spec-driver-connect.R' 'spec-connection-disconnect.R'
+ 'spec-result-send-query.R' 'spec-result-fetch.R'
+ 'spec-result-roundtrip.R' 'spec-result-clear-result.R'
+ 'spec-result-get-query.R' 'spec-result-send-statement.R'
+ 'spec-result-execute.R' 'spec-sql-quote-string.R'
+ 'spec-sql-quote-identifier.R' 'spec-sql-read-table.R'
+ 'spec-sql-write-table.R' 'spec-sql-list-tables.R'
+ 'spec-sql-exists-table.R' 'spec-sql-remove-table.R'
+ 'spec-meta-bind-runner.R' 'spec-meta-bind-tester-extra.R'
+ 'spec-meta-bind.R' 'spec-meta-bind-.R' 'spec-meta-is-valid.R'
+ 'spec-meta-has-completed.R' 'spec-meta-get-statement.R'
'spec-meta-get-row-count.R' 'spec-meta-get-rows-affected.R'
- 'spec-meta-get-info-result.R' 'spec-meta-bind.R'
- 'spec-meta-bind-multi-row.R' 'spec-meta-bind-.R' 'spec-meta.R'
- 'spec-transaction-begin-commit.R'
- 'spec-transaction-begin-rollback.R'
- 'spec-transaction-with-transaction.R' 'spec-transaction.R'
- 'spec-compliance-methods.R' 'spec-compliance-read-only.R'
- 'spec-compliance.R' 'spec-stress-driver.R'
- 'spec-stress-connection.R' 'spec-stress.R' 'spec-.R'
- 'test-all.R' 'test-getting-started.R' 'test-driver.R'
- 'test-connection.R' 'test-result.R' 'test-sql.R' 'test-meta.R'
- 'test-transaction.R' 'test-compliance.R' 'test-stress.R'
- 'tweaks.R' 'utf8.R' 'utils.R'
+ 'spec-transaction-begin-commit-rollback.R'
+ 'spec-transaction-with-transaction.R' 'spec-driver-get-info.R'
+ 'spec-connection-get-info.R' 'spec-sql-list-fields.R'
+ 'spec-meta-column-info.R' 'spec-meta-get-info-result.R'
+ 'spec-driver.R' 'spec-connection.R' 'spec-result.R'
+ 'spec-sql.R' 'spec-meta.R' 'spec-transaction.R'
+ 'spec-compliance.R' 'spec-stress-connection.R' 'spec-stress.R'
+ 'spec-all.R' 'spec-.R' 'test-all.R' 'test-getting-started.R'
+ 'test-driver.R' 'test-connection.R' 'test-result.R'
+ 'test-sql.R' 'test-meta.R' 'test-transaction.R'
+ 'test-compliance.R' 'test-stress.R' 'tweaks.R' 'utf8.R'
+ 'utils.R'
NeedsCompilation: no
-Packaged: 2016-12-03 07:33:52 UTC; muelleki
+Packaged: 2017-06-18 21:52:02 UTC; muelleki
Author: Kirill Müller [aut, cre],
RStudio [cph]
Maintainer: Kirill Müller <krlmlr+r at mailbox.org>
Repository: CRAN
-Date/Publication: 2016-12-03 09:38:00
+Date/Publication: 2017-06-19 09:04:40 UTC
diff --git a/MD5 b/MD5
index f090858..7c37781 100644
--- a/MD5
+++ b/MD5
@@ -1,60 +1,65 @@
-5fc4ec7955f22b177ff6ec1324b83ca4 *DESCRIPTION
-81cd946d3de4c7b9dda883b122c6d728 *NAMESPACE
-d8c88467640b146998eb53ae514c26e0 *NEWS.md
+1245c831423fc02893cba904ec8c6a0e *DESCRIPTION
+c2778d0fdb65dfda6196920fc40b9592 *NAMESPACE
+1297e2e3d373cdc5ebd926d0dfb5727f *NEWS.md
f956e8e1290d2316d720831804645f34 *R/DBItest.R
-5aff7b5a0aca622d131ff7d6d1721c95 *R/context.R
-ac14ecef161e0215338bdbec18e6efa6 *R/expectations.R
-33d6bb1e697558fa783c48b4fadeafcd *R/import-dbi.R
+34b240efcc81cd529578dd0ae94d9d4a *R/context.R
+d5685a8b06a7bc18eb306206ade486e9 *R/expectations.R
+2456d9af5a745bbc589664a9807cb618 *R/import-dbi.R
d1e36fe1f7d910ebe6ded24c9a2052b3 *R/import-testthat.R
-ae71a7343fa7585c7140c8f9f38d6f95 *R/run.R
+620d45fb2dbdd989f71962ac20548e54 *R/run.R
392269cd7ac8df3e6fbc56b0bea5538b *R/s4.R
-c8d4595c9679057b2d2062ccb04d7be2 *R/spec-.R
-50461b143db937afc2ac9a4a5f6f0d36 *R/spec-compliance-methods.R
-540f3c072b3b4c11d6399ada69511ea2 *R/spec-compliance-read-only.R
-9713a34fea629c2af551b92b2286a3ae *R/spec-compliance.R
-d2b51be02b7918e4a12e5dd5ed43f3a5 *R/spec-connection-connect.R
-613cfa69e2a310ea3c12fa6afda715d7 *R/spec-connection-data-type.R
-c4c5e3d7f0dd8051a9f34d88ae8b68c9 *R/spec-connection-get-info.R
-4b89a1600c91ceefb43e95fdf6cf3786 *R/spec-connection.R
-d2c103c881337a6f77e795661626e22b *R/spec-driver-class.R
-e39149bf7d88ad365c717b0fb9cbb9d9 *R/spec-driver-constructor.R
-7c0ff9831cd7e3755b9c75016468f4cf *R/spec-driver-data-type.R
+1834d84891a386f20fed471804ba9af7 *R/spec-.R
+0f31c304333be84f7316c3e4b97f3c0d *R/spec-all.R
+06780ae0e6199d4a6f2bcf930bbb62a6 *R/spec-compliance-methods.R
+972cfc91acad25db5b17773e92da485d *R/spec-compliance.R
+0edfb3ab996e0e1fd47f208b02e81fad *R/spec-connection-data-type.R
+4be7c2d1297094effddfb76bacaf6382 *R/spec-connection-disconnect.R
+983bcf8185cfa4639f6154d049bcff7a *R/spec-connection-get-info.R
+a7d5ca6edad9ad39cfce44daabdc4a54 *R/spec-connection.R
+bba2f83db6655029c093f677317a9a5f *R/spec-driver-class.R
+a5de28228f154ad01aa1fda9b1b2a45b *R/spec-driver-connect.R
+514dc60d2d73d95c8ef04aa04b16737c *R/spec-driver-constructor.R
+67e621f97e3edbe3deb6069874284363 *R/spec-driver-data-type.R
978caf381fa28ccf9cc86f6e6dd1829e *R/spec-driver-get-info.R
-1e89ea5b2cab75c520a4b84d0f7a266f *R/spec-driver.R
-47c6653a4b7f3631a81be59e1e19066a *R/spec-getting-started.R
-53e20873912387b7e072b97d3fa7a827 *R/spec-meta-bind-.R
-6c85ffbcf0862780619becd6e91d4387 *R/spec-meta-bind-multi-row.R
-b6ddec2116deb42841879e3d92ce75b2 *R/spec-meta-bind.R
-d5daa12e58b8f4a105fb78d56fb185b5 *R/spec-meta-column-info.R
+c9021aed2d2038ce94985e8a32dec17c *R/spec-driver.R
+efc53f22877e18c2c0a9c3075de644c6 *R/spec-getting-started.R
+36943fabb95767725ed0e8612932d8ac *R/spec-meta-bind-.R
+10f95280bda787682afa81de099f75d0 *R/spec-meta-bind-runner.R
+edad73f8c65e82e0b0e525775e2b36fb *R/spec-meta-bind-tester-extra.R
+1e8844d8153d746e28190c849e23604e *R/spec-meta-bind.R
+6cef2e7c2dfebbbb157dcc4f8b6db2f9 *R/spec-meta-column-info.R
8baa8e35cca961dbf729f32e64772a8c *R/spec-meta-get-info-result.R
-98ef128fd9c3696a2096e1736566bd09 *R/spec-meta-get-row-count.R
-7c0cc8f4eefe301c9dbd996e7f9edc05 *R/spec-meta-get-rows-affected.R
-c65042cf47857b9d1b80456ca4d9ec2f *R/spec-meta-get-statement.R
-8144ef9733640935ce1b6a6cbc1aa0db *R/spec-meta-is-valid-connection.R
-d719d353cf89f3bec0fadff81faa9d1d *R/spec-meta-is-valid-result.R
-a6c9971e4d04a4d2dbe08e756c1478d6 *R/spec-meta.R
-b8f98188cd517505409fb51f7849a5c1 *R/spec-result-create-table-with-data-type.R
-df1faf90c7d8b2276bee75efb0690337 *R/spec-result-fetch.R
-f0988ce5d7f08d595f891942a7f29724 *R/spec-result-get-query.R
-49967036adccaeef8ab7bf3c210f1259 *R/spec-result-roundtrip.R
-471c08dd04893fece4639075ae9c884a *R/spec-result-send-query.R
-9e8f4e7b38f18382a3693675bdc00a3d *R/spec-result.R
-415dda2703baf2b2181839623551a498 *R/spec-sql-list-fields.R
-cc10b00b566802e74094323f4aa6f1b8 *R/spec-sql-list-tables.R
-4a4308ae632ee0cbeb28facacf391724 *R/spec-sql-quote-identifier.R
-c87d982d3ec048485c876eeb11a86404 *R/spec-sql-quote-string.R
-ee39cce95d7c16246bedeee7f6c0c46e *R/spec-sql-read-write-roundtrip.R
-b142b6fa4661c19ca31024ed3cd7f57f *R/spec-sql-read-write-table.R
-96328be15900ec078468ae49cf55963d *R/spec-sql.R
-0667076b1e04bbdbc42de060e47a9446 *R/spec-stress-connection.R
-cb3218a7f9285a392f2d692d24bddac3 *R/spec-stress-driver.R
-73cf07d48bda9a36aa75ea52b2e1d0c4 *R/spec-stress.R
-536357374e5a2c280ab5b3de409d3b5f *R/spec-transaction-begin-commit.R
-28d4aa077a503dc5443b6d615608a33b *R/spec-transaction-begin-rollback.R
-8a72283fb233f4f036bc7f0c667e438b *R/spec-transaction-with-transaction.R
-7e1873b2a47fc22f7113db9e3a1e4ff5 *R/spec-transaction.R
-4ebba52dcbf2f992f73001f7e4c992b4 *R/spec.R
-42930e1c30af1764d829eb72b5f7377b *R/test-all.R
+74c358255ccfdd7996f1e9590c97f0fe *R/spec-meta-get-row-count.R
+aa5770fc1932c29a4fa3dd7282f9914b *R/spec-meta-get-rows-affected.R
+2de48f7148473480fcc00569e5dda94c *R/spec-meta-get-statement.R
+940f2a46d267d5c1e5c92363c2c785ce *R/spec-meta-has-completed.R
+fe580adc31ab99c23239c7cf27cfade3 *R/spec-meta-is-valid.R
+646b4c99f1282473f003703fc8945202 *R/spec-meta.R
+22c4dbe38db7b7b1d986884c2f64456b *R/spec-result-clear-result.R
+0d74571fddd43736be206871afc6d4cb *R/spec-result-create-table-with-data-type.R
+edbdfa32d432d88b2a6708b5775a4e09 *R/spec-result-execute.R
+6d0d6ea9abfc359d5f68c85cf09b7a9b *R/spec-result-fetch.R
+849ca1b0185d36d5d2d9ebb97067efbb *R/spec-result-get-query.R
+61e1515f84caaa4f7d6decc65f73ff08 *R/spec-result-roundtrip.R
+f6b3a5fc1b1140ccbda87f149135be1d *R/spec-result-send-query.R
+032c1ebecb0d5fd83ca9949fde8ab0f7 *R/spec-result-send-statement.R
+731f38402ebc536101493e18e9d6d4c5 *R/spec-result.R
+16b21fb31401dfa924a562c8e47de532 *R/spec-sql-exists-table.R
+5bb59f7831543044a99275394abdf7d2 *R/spec-sql-list-fields.R
+8fd8c3ca87c3fbb5a7275dcde2d54591 *R/spec-sql-list-tables.R
+26aa547943a60ca1234fb4d66d758134 *R/spec-sql-quote-identifier.R
+1fca815ed3b90f7a02f75dc12995a7b2 *R/spec-sql-quote-string.R
+6832c24d5ac16e1b495a75cf695021cb *R/spec-sql-read-table.R
+3ad10e6c7693e3a1113be5eadb22b071 *R/spec-sql-remove-table.R
+d713fef9d76f1cef6842ddc6a5cdc7d2 *R/spec-sql-write-table.R
+72c51ae33337d0c3de4cb0cf5b17289c *R/spec-sql.R
+520f2727c9e6d8344d2014efa2be31b5 *R/spec-stress-connection.R
+a0df2a1b835bf1a6382da15019229c14 *R/spec-stress.R
+7f7dc1a68ab9cacd3ef14f771c723356 *R/spec-transaction-begin-commit-rollback.R
+198aa308e2344a1e767585c591610fab *R/spec-transaction-with-transaction.R
+1a22ea9de2430d087a469690f421a33b *R/spec-transaction.R
+c5a5d9501278eb4d6cc2da3073c55bcd *R/spec.R
+5f5348b2d1f011bcb0aaf2363acdaa57 *R/test-all.R
0f3c8fb591881f71dc343a2bbd3cf898 *R/test-compliance.R
5306ca1f57b4cec62b057c38b9acce72 *R/test-connection.R
0395879bafa2c627d4c3f954baf1c3e5 *R/test-driver.R
@@ -64,31 +69,59 @@ a11b4257b6ee210f7840be0cc32a1fc8 *R/test-meta.R
c23e4b8e51ad324d42d65f45e10eaacc *R/test-sql.R
4f098cdffca1949c4cbe9f96fc270c88 *R/test-stress.R
16036f6126b6d6184b02c9f2978eb75a *R/test-transaction.R
-a184497d73f437ab2c1a6506331ff7d3 *R/tweaks.R
+9e35d01ed22e883b5166095d232773c8 *R/tweaks.R
5bb7302537533dc370d8aa96bb6d1dfd *R/utf8.R
-964c6a1fb51ac4953eec954e40eb0ea9 *R/utils.R
+1468998e72e9e9f2aa9407b458efa685 *R/utils.R
2ac48354a76e3430802052eff9620bac *README.md
-ae2fd6ea55bce774f817836f579f6f5b *build/vignette.rds
+7d7ae464019d1910adc2ab02227e99c1 *build/vignette.rds
1994b1ff1f8a4d1ded48cd7a04a8d770 *inst/doc/test.Rmd
-c05c2b2466a3d5d513106443f4f768ff *inst/doc/test.html
-95db7c3f60b0a8dd95d9b2fe8badb4a6 *man/DBIspec-wip.Rd
-91e6a04a3b0565e65f5f012bbac89b39 *man/DBIspec.Rd
-2c210cf5334383ea31c9fb433cda01ee *man/DBItest-package.Rd
-6dc3d568a3939a41158e4d4ce0cc4244 *man/context.Rd
-4567f06e67179b2ef9f5821dc3039922 *man/make_placeholder_fun.Rd
-6b085b1c60ce27e321737430f7ce9da6 *man/test_all.Rd
-fed4f17ab866041d1096aed7d83ca042 *man/test_compliance.Rd
-c6520953292eb0086be311cb0dc58163 *man/test_connection.Rd
-5fe3efacc98502dbf6e29aabc06f5e1a *man/test_driver.Rd
-067b47bf6df2eedf248c9576f181e7aa *man/test_getting_started.Rd
-5cafd1a20f6cc2a2797a65e869d8642c *man/test_meta.Rd
-fa7bfdec7b1354fb1c436b43c6abd817 *man/test_result.Rd
-8d7d087982f2d0738aad95b1809e009e *man/test_sql.Rd
-04b8e7396de9b11026b4778c07792885 *man/test_stress.Rd
-a501fdb79764314a9bd20ba58154d8e2 *man/test_transaction.Rd
-2eafca654541bff02abd622dc12e12a5 *man/tweaks.Rd
+74bdb117c2241a1e04ff843b5ea20e6b *inst/doc/test.html
+f4991d67bed9b353e9d345df60f0644a *man/DBIspec-wip.Rd
+02423d348a28bc746dbdcbad995a6133 *man/DBIspec.Rd
+742938944bd94771b0908461884e4335 *man/DBItest-package.Rd
+db14a08cc7e54abf4f37c7925f6dcd22 *man/context.Rd
+d1ce288efebb76c3a37173ed08b424aa *man/make_placeholder_fun.Rd
+f11d528da585cc8c980d8666c97f91a5 *man/spec_connection_disconnect.Rd
+ac236b13318f49da5ddb0a934848f4d1 *man/spec_driver_connect.Rd
+90198413c2c473ea8c8588b916cf2117 *man/spec_driver_data_type.Rd
+7e60c90549131ef6e5a191454c00e607 *man/spec_meta_bind.Rd
+ad7ccec1026426affbc1711e0ba4e530 *man/spec_meta_get_row_count.Rd
+23cb6913cfe73a45170e2389684ec2e0 *man/spec_meta_get_rows_affected.Rd
+eac7a0e0375fa2253ed7e8730c35c496 *man/spec_meta_get_statement.Rd
+31d4934206413dc389b1b2047c345532 *man/spec_meta_has_completed.Rd
+d889652b2cc25de411f0bd510add2139 *man/spec_meta_is_valid.Rd
+eed88eee1d1057b5abd0771690408060 *man/spec_result_clear_result.Rd
+2ba0de7b65ae593739878145833394f2 *man/spec_result_create_table_with_data_type.Rd
+874fcfa70384f03fb4c6252fc3e5e6af *man/spec_result_execute.Rd
+04e289bdc54621cfb998dcc8c0a12704 *man/spec_result_fetch.Rd
+60c4c439b0067834cb16c0b8485ff42b *man/spec_result_get_query.Rd
+663bf753c23037884a427f488b6e6ed2 *man/spec_result_roundtrip.Rd
+d2f0bab545567ea1fc8b4afe0265d587 *man/spec_result_send_query.Rd
+d4528b1b3fb332b7b39fcbf135996c9b *man/spec_result_send_statement.Rd
+1fd29617a4e9cd794aa027dbb38d0110 *man/spec_sql_exists_table.Rd
+3080f65296f19a0cd19fd14caf96531d *man/spec_sql_list_tables.Rd
+d5881e833c7dcd04dfbc0effd2884988 *man/spec_sql_quote_identifier.Rd
+485367a0a71df587658b8f5d6d82f8b5 *man/spec_sql_quote_string.Rd
+27a0bba8fe0115024cd42554df8c468c *man/spec_sql_read_table.Rd
+7f26eca7aaa65ffa83d28cf3448d7a57 *man/spec_sql_remove_table.Rd
+b6ecc86e4fbcfa49da41c28432ca99eb *man/spec_sql_write_table.Rd
+5bb241217ca33f7b17c781a033357ca4 *man/spec_transaction_begin_commit_rollback.Rd
+2d13edd8dda77b2a1a16800d56ba2f30 *man/spec_transaction_with_transaction.Rd
+ed4bf19f8656de19b97a630af4e162d5 *man/test_all.Rd
+4bc0f9d09ca8eab9f2ff83b304001a05 *man/test_compliance.Rd
+bbe6d24725d121c50cdbc5e9c785ed7f *man/test_connection.Rd
+6712e8eb176e63f271e253332d708f78 *man/test_data_type.Rd
+63ce8e8a0cce917b24f47941e628d4d5 *man/test_driver.Rd
+52a1adcb43614ab4575ff339f55269b9 *man/test_getting_started.Rd
+568916d526f9bd636aa357378a11f44c *man/test_meta.Rd
+dfa395bce7c4be8e3912a3198a09446e *man/test_result.Rd
+9f4e497e73fafd1041d505563306d217 *man/test_sql.Rd
+51d757c4b4410d6305e909c7c0c2f50c *man/test_stress.Rd
+fb4fefc8dead49d3218bf51e8c81c3ea *man/test_transaction.Rd
+54002550af0573ae2456320ffbba470a *man/tweaks.Rd
e66cc0201e7914ca0a08d2401d1ac8a8 *tests/testthat.R
+e6e5e686b137cce397617a031718c1ff *tests/testthat/test-consistency.R
3675efbbcc4ee2129cfe7b52a10fd282 *tests/testthat/test-context.R
4c438214a5f4b238d0832ce8b8c9a0ba *tests/testthat/test-lint.R
-361b9b8cea0450bd4dc41916dd4da39a *tests/testthat/test-tweaks.R
+8d740e8ffa890ea201a4be8119408ef1 *tests/testthat/test-tweaks.R
1994b1ff1f8a4d1ded48cd7a04a8d770 *vignettes/test.Rmd
diff --git a/NAMESPACE b/NAMESPACE
index 296315f..abf65bd 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -1,5 +1,6 @@
# Generated by roxygen2: do not edit by hand
+S3method("$",DBItest_tweaks)
S3method(format,DBItest_tweaks)
S3method(print,DBItest_tweaks)
export(get_default_context)
@@ -12,13 +13,16 @@ export(test_driver)
export(test_getting_started)
export(test_meta)
export(test_result)
+export(test_some)
export(test_sql)
export(test_stress)
export(test_transaction)
export(tweaks)
import(testthat)
+importFrom(DBI,SQL)
importFrom(DBI,dbBegin)
importFrom(DBI,dbBind)
+importFrom(DBI,dbBreak)
importFrom(DBI,dbCallProc)
importFrom(DBI,dbClearResult)
importFrom(DBI,dbColumnInfo)
@@ -26,7 +30,6 @@ importFrom(DBI,dbCommit)
importFrom(DBI,dbConnect)
importFrom(DBI,dbDataType)
importFrom(DBI,dbDisconnect)
-importFrom(DBI,dbDriver)
importFrom(DBI,dbExecute)
importFrom(DBI,dbExistsTable)
importFrom(DBI,dbFetch)
@@ -49,7 +52,7 @@ importFrom(DBI,dbRollback)
importFrom(DBI,dbSendQuery)
importFrom(DBI,dbSendStatement)
importFrom(DBI,dbSetDataMappings)
-importFrom(DBI,dbUnloadDriver)
+importFrom(DBI,dbWithTransaction)
importFrom(DBI,dbWriteTable)
importFrom(methods,extends)
importFrom(methods,findMethod)
@@ -58,4 +61,5 @@ importFrom(methods,getClasses)
importFrom(methods,hasMethod)
importFrom(methods,is)
importFrom(stats,setNames)
+importFrom(withr,with_output_sink)
importFrom(withr,with_temp_libpaths)
diff --git a/NEWS.md b/NEWS.md
index 21d6f43..8490a6e 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,3 +1,66 @@
+# DBItest 1.5 (2017-06-18)
+
+Finalize specification. Most tests now come with a corresponding prose, only those where the behavior is not finally decided don't have a prose version yet (#88).
+
+New tests
+---------
+
+- Test behavior of methods in presence of placeholders (#120).
+- Test column name mismatch behavior for appending tables (#93).
+- Test that `dbBind()` against factor works but raises a warning (#91).
+- Test roundtrip of alternating empty and non-empty strings (#42).
+- Test multiple columns of different types in one statement or table (#35).
+- Test `field.types` argument to `dbWriteTable()` (#12).
+- Added tests for invalid or closed connection argument to all methods that expect a connection as first argument (#117).
+- Enabled test that tests a missing `dbDisconnect()`.
+- Add test for unambiguous escaping of identifiers (rstats-db/RSQLite#123).
+- Reenable tests for visibility (#89).
+- Fix and specify 64-bit roundtrip test.
+- 64-bit integers only need to be coercible to `numeric` and `character` (#74).
+- Added roundtrip test for time values (#14).
+- Added tweaks for handling date, time, timestamp, ... (#53, #76).
+- Test that `dbFetch()` on update-only query returns warning (#66).
+
+Adapted tests
+-------------
+
+- `NULL` is a valid value for the `row.names` argument, same as `FALSE`.
+- A column named `row_names` receives no special handling (#54).
+- A warning (not an error anymore) is expected when calling `dbDisconnect()` on a closed or invalid connection.
+- `row.names = FALSE` is now the default for methods that read or write tables.
+- Add `NA` to beginning and end of columns in table roundtrip tests (#24).
+- Stricter tests for confusion of named and unnamed SQL parameters and placeholders (#107).
+- Also check names of all returned data frames.
+- The return value for all calls to `dbGetQuery()`, `dbFetch()`, and `dbReadTable()` is now checked for consistency (all columns have the same length, length matches number of rows) (#126).
+- Removed stress tests that start a new session.
+- Allow `hms` (or other subclasses of `difftime`) to be returned as time class (#135, @jimhester).
+- Test that dates are of type `numeric` (#99, @jimhester).
+- Replace `POSIXlt` by `POSIXct` (#100, @jimhester).
+- Use `"PST8PDT"` instead of `"PST"` as time zone (#110, @thrasibule).
+- Added tests for support of `blob` objects (input and output), but backends are not required to return `blob` objects (#98).
+- The `logical_return`, `date_typed` and `timestamp_typed` tweaks are respected by the bind tests.
+- Fixed tests involving time comparison; now uses UTC timezone and compares against a `difftime`.
+- Tests for roundtrip of character values now includes tabs, in addition to many other special characters (#85).
+- Make sure at least one table exists in the `dbListTables()` test.
+- Fix roundtrip tests for raw columns: now expecting `NULL` and not `NA` entries for SQL NULL values.
+- Fix `expect_equal_df()` for list columns.
+- Testing that a warning is given if the user forgets to call `dbDisconnect()` or `dbClearResult()` (#103).
+- Numeric roundtrip accepts conversion of `NaN` to `NA` (#79).
+
+Internal
+--------
+
+- Fix R CMD check errors.
+- Internal consistency checks (#114).
+- Skip patterns that don't match any of the tests now raise a warning (#84).
+- New `test_some()` to test individual tests (#136).
+- Use desc instead of devtools (#40).
+- All unexpected warnings are now reported as test failures (#113).
+- `DBItest_tweaks` class gains a `$` method, accessing an undefined tweak now raises an error.
+- The arguments of the `tweaks()` function now have default values that further describe their intended usage.
+- New `with_closed_connection()`, `with_invalid_connection()`, `with_result()` and `with_remove_test_table()` helpers, and `expect_visible()`, `expect_inbisible_true()`, and `expect_equal_df()` expectations for more concise tests.
+
+
# DBItest 1.4 (2016-12-02)
## DBI specification
diff --git a/R/context.R b/R/context.R
index 4d2a482..0c73ddb 100644
--- a/R/context.R
+++ b/R/context.R
@@ -67,7 +67,10 @@ package_name <- function(ctx) {
}
connect <- function(ctx) {
- do.call(dbConnect, c(list(ctx$drv), ctx$connect_args))
+ connect_call <- as.call(c(list(quote(dbConnect), ctx$drv), ctx$connect_args))
+ connect_fun <- function() {}
+ body(connect_fun) <- connect_call
+ connect_fun()
}
.ctx_env <- new.env(parent = emptyenv())
diff --git a/R/expectations.R b/R/expectations.R
index f14cae0..c7728fc 100644
--- a/R/expectations.R
+++ b/R/expectations.R
@@ -25,11 +25,53 @@ has_method <- function(method_name) {
}
}
+expect_visible <- function(code) {
+ ret <- withVisible(code)
+ expect_true(ret$visible)
+ ret$value
+}
+
expect_invisible_true <- function(code) {
ret <- withVisible(code)
expect_true(ret$value)
- # Cannot test for visibility of return value yet (#89)
- return()
- expect_false(ret$visible)
+ test_that("Visibility", {
+ expect_false(ret$visible)
+ })
+
+ invisible(ret$value)
+}
+
+expect_equal_df <- function(actual, expected) {
+ factor_cols <- vapply(expected, is.factor, logical(1L))
+ expected[factor_cols] <- lapply(expected[factor_cols], as.character)
+
+ asis_cols <- vapply(expected, inherits, "AsIs", FUN.VALUE = logical(1L))
+ expected[asis_cols] <- lapply(expected[asis_cols], unclass)
+
+ list_cols <- vapply(expected, is.list, logical(1L))
+
+ if (!any(list_cols)) {
+ order_actual <- order(actual)
+ order_expected <- order(expected)
+ } else {
+ expect_false(all(list_cols))
+ expect_equal(anyDuplicated(actual[!list_cols]), 0)
+ expect_equal(anyDuplicated(expected[!list_cols]), 0)
+ order_actual <- order(actual[!list_cols])
+ order_expected <- order(expected[!list_cols])
+ }
+
+ has_rownames_actual <- is.character(attr(actual, "row.names"))
+ has_rownames_expected <- is.character(attr(expected, "row.names"))
+ expect_equal(has_rownames_actual, has_rownames_expected)
+
+ if (has_rownames_actual) {
+ expect_equal(sort(row.names(actual)), sort(row.names(expected)))
+ }
+
+ actual <- unrowname(actual[order_actual, ])
+ expected <- unrowname(expected[order_expected, ])
+
+ expect_identical(actual, expected)
}
diff --git a/R/import-dbi.R b/R/import-dbi.R
index 9a27aec..2e1d404 100644
--- a/R/import-dbi.R
+++ b/R/import-dbi.R
@@ -1,12 +1,13 @@
# The imports below were generated using the following call:
# @import.gen::importFrom("DBI")
-#' @importFrom DBI dbBegin dbBind dbCallProc dbClearResult dbColumnInfo
-#' @importFrom DBI dbCommit dbConnect dbDataType dbDisconnect dbDriver
+#' @importFrom DBI dbBegin dbBind dbBreak dbCallProc dbClearResult dbColumnInfo
+#' @importFrom DBI dbCommit dbConnect dbDataType dbDisconnect
#' @importFrom DBI dbExecute dbExistsTable dbFetch dbGetDBIVersion
#' @importFrom DBI dbGetInfo dbGetQuery dbGetRowCount dbGetRowsAffected
#' @importFrom DBI dbGetStatement dbHasCompleted dbIsValid
#' @importFrom DBI dbListConnections dbListFields dbListTables
#' @importFrom DBI dbQuoteIdentifier dbQuoteString dbReadTable dbRemoveTable
#' @importFrom DBI dbRollback dbSendQuery dbSendStatement dbSetDataMappings
-#' @importFrom DBI dbUnloadDriver dbWriteTable
+#' @importFrom DBI dbWithTransaction dbWriteTable
+#' @importFrom DBI SQL
NULL
diff --git a/R/run.R b/R/run.R
index 4389634..61bf6f8 100644
--- a/R/run.R
+++ b/R/run.R
@@ -13,8 +13,8 @@ run_tests <- function(ctx, tests, skip, test_suite) {
tests <- tests[!vapply(tests, is.null, logical(1L))]
- skip_rx <- paste0(paste0("(?:^", skip, "$)"), collapse = "|")
- skip_flag <- grepl(skip_rx, names(tests), perl = TRUE)
+ skipped <- get_skip_names(skip)
+ skip_flag <- names(tests) %in% skipped
ok <- vapply(seq_along(tests), function(test_idx) {
test_name <- names(tests)[[test_idx]]
@@ -36,11 +36,35 @@ run_tests <- function(ctx, tests, skip, test_suite) {
ok
}
+get_skip_names <- function(skip) {
+ if (length(skip) == 0L) return(character())
+ names_all <- names(spec_all)
+ names_all <- names_all[names_all != ""]
+ skip_flags_all <- lapply(paste0("(?:^", skip, "$)"), grepl, names_all, perl = TRUE)
+ skip_used <- vapply(skip_flags_all, any, logical(1L))
+ if (!all(skip_used)) {
+ warning("Unused skip expressions: ", paste(skip[!skip_used], collapse = ", "),
+ call. = FALSE)
+ }
+
+ skip_flag_all <- Reduce(`|`, skip_flags_all)
+ skip_tests <- names_all[skip_flag_all]
+
+ skip_tests
+}
+
patch_test_fun <- function(test_fun, desc) {
- body_of_test_fun <- body(test_fun)
+ body_of_test_fun <- wrap_all_statements_with_expect_no_warning(body(test_fun))
+
eval(bquote(
function(ctx) {
test_that(.(desc), .(body_of_test_fun))
}
))
}
+
+wrap_all_statements_with_expect_no_warning <- function(block) {
+ stopifnot(identical(block[[1]], quote(`{`)))
+ block[-1] <- lapply(block[-1], function(x) eval(bquote(quote(expect_warning(.(x), NA)))))
+ block
+}
diff --git a/R/spec-.R b/R/spec-.R
index 57eb1c6..d816ee8 100644
--- a/R/spec-.R
+++ b/R/spec-.R
@@ -9,49 +9,61 @@
#
# Output: Files R/test-xxx-1.R and R/test-xxx-2.R, and @include directives to stdout
+##### All
+#' @include spec-all.R
+##### Stress
#' @include spec-stress.R
#' @include spec-stress-connection.R
-#' @include spec-stress-driver.R
+##### Aggregators
#' @include spec-compliance.R
-#' @include spec-compliance-read-only.R
-#' @include spec-compliance-methods.R
#' @include spec-transaction.R
-#' @include spec-transaction-with-transaction.R
-#' @include spec-transaction-begin-rollback.R
-#' @include spec-transaction-begin-commit.R
#' @include spec-meta.R
-#' @include spec-meta-bind-.R
-#' @include spec-meta-bind-multi-row.R
-#' @include spec-meta-bind.R
+#' @include spec-sql.R
+#' @include spec-result.R
+#' @include spec-connection.R
+#' @include spec-driver.R
+##### Later
#' @include spec-meta-get-info-result.R
+#' @include spec-meta-column-info.R
+#' @include spec-sql-list-fields.R
+#' @include spec-connection-get-info.R
+#' @include spec-driver-get-info.R
+##### Method specs
+#' @include spec-transaction-with-transaction.R
+#' @include spec-transaction-begin-commit-rollback.R
#' @include spec-meta-get-rows-affected.R
#' @include spec-meta-get-row-count.R
-#' @include spec-meta-column-info.R
#' @include spec-meta-get-statement.R
-#' @include spec-meta-is-valid-result.R
-#' @include spec-meta-is-valid-connection.R
-#' @include spec-sql.R
-#' @include spec-sql-list-fields.R
+#' @include spec-meta-has-completed.R
+#' @include spec-meta-is-valid.R
+#' @include spec-meta-bind-.R
+#' @include spec-meta-bind.R
+#' @include spec-meta-bind-tester-extra.R
+#' @include spec-meta-bind-runner.R
+#' @include spec-sql-remove-table.R
+#' @include spec-sql-exists-table.R
#' @include spec-sql-list-tables.R
-#' @include spec-sql-read-write-roundtrip.R
-#' @include spec-sql-read-write-table.R
+#' @include spec-sql-write-table.R
+#' @include spec-sql-read-table.R
#' @include spec-sql-quote-identifier.R
#' @include spec-sql-quote-string.R
-#' @include spec-result.R
-#' @include spec-result-roundtrip.R
-#' @include spec-result-create-table-with-data-type.R
+#' @include spec-result-execute.R
+#' @include spec-result-send-statement.R
#' @include spec-result-get-query.R
+#' @include spec-result-clear-result.R
+#' @include spec-result-roundtrip.R
#' @include spec-result-fetch.R
#' @include spec-result-send-query.R
-#' @include spec-connection.R
-#' @include spec-connection-get-info.R
+#' @include spec-connection-disconnect.R
+#' @include spec-driver-connect.R
+#' @include spec-result-create-table-with-data-type.R
#' @include spec-connection-data-type.R
-#' @include spec-connection-connect.R
-#' @include spec-driver.R
-#' @include spec-driver-get-info.R
#' @include spec-driver-data-type.R
-#' @include spec-driver-constructor.R
+##### Class specs
#' @include spec-driver-class.R
+##### Soft specs
+#' @include spec-driver-constructor.R
+#' @include spec-compliance-methods.R
#' @include spec-getting-started.R
#' @include spec.R
NULL
diff --git a/R/spec-all.R b/R/spec-all.R
new file mode 100644
index 0000000..69f482b
--- /dev/null
+++ b/R/spec-all.R
@@ -0,0 +1,11 @@
+spec_all <- c(
+ spec_getting_started,
+ spec_driver,
+ spec_connection,
+ spec_result,
+ spec_sql,
+ spec_meta,
+ spec_transaction,
+ spec_compliance,
+ spec_stress
+)
diff --git a/R/spec-compliance-methods.R b/R/spec-compliance-methods.R
index a45c621..2308013 100644
--- a/R/spec-compliance-methods.R
+++ b/R/spec-compliance-methods.R
@@ -1,10 +1,22 @@
-#' @template dbispec-sub-wip
+#' @template dbispec-sub
#' @format NULL
-#' @section Full compliance:
-#' \subsection{All of DBI}{
+#' @section DBI classes and methods:
spec_compliance_methods <- list(
- #' The package defines three classes that implement the required methods.
+ #' A backend defines three classes,
compliance = function(ctx) {
+ #' which are subclasses of
+ expect_identical(
+ names(key_methods),
+ c(
+ #' [DBIDriver-class],
+ "Driver",
+ #' [DBIConnection-class],
+ "Connection",
+ #' and [DBIResult-class].
+ "Result"
+ )
+ )
+
pkg <- package_name(ctx)
where <- asNamespace(pkg)
@@ -20,6 +32,9 @@ spec_compliance_methods <- list(
class <- classes[[1]]
+ #' The backend provides implementation for all methods
+ #' of these base classes
+ #' that are defined but not implemented by DBI.
mapply(function(method, args) {
expect_has_class_method(method, class, args, where)
}, names(key_methods[[name]]), key_methods[[name]])
@@ -36,7 +51,6 @@ spec_compliance_methods <- list(
Map(expect_ellipsis_in_formals, methods, names(methods))
},
- #' }
NULL
)
diff --git a/R/spec-compliance-read-only.R b/R/spec-compliance-read-only.R
deleted file mode 100644
index ae06a86..0000000
--- a/R/spec-compliance-read-only.R
+++ /dev/null
@@ -1,18 +0,0 @@
-#' @template dbispec-sub-wip
-#' @format NULL
-#' @section Full compliance:
-#' \subsection{Read-only access}{
-spec_compliance_read_only <- list(
- spec_compliance_methods,
-
- #' Writing to the database fails. (You might need to set up a separate
- #' test context just for this test.)
- read_only = function(ctx) {
- with_connection({
- expect_error(dbWriteTable(con, "test", data.frame(a = 1)))
- })
- },
-
- #' }
- NULL
-)
diff --git a/R/spec-compliance.R b/R/spec-compliance.R
index 9d7cf75..f661234 100644
--- a/R/spec-compliance.R
+++ b/R/spec-compliance.R
@@ -2,7 +2,6 @@
#' @format NULL
spec_compliance <- c(
spec_compliance_methods,
- spec_compliance_read_only,
NULL
)
diff --git a/R/spec-connection-connect.R b/R/spec-connection-connect.R
deleted file mode 100644
index 8db85f0..0000000
--- a/R/spec-connection-connect.R
+++ /dev/null
@@ -1,23 +0,0 @@
-#' @template dbispec-sub-wip
-#' @format NULL
-#' @section Connection:
-#' \subsection{Construction: `dbConnect("DBIDriver")` and `dbDisconnect("DBIConnection", "ANY")`}{
-spec_connection_connect <- list(
- #' Can connect and disconnect, connection object inherits from
- #' "DBIConnection".
- can_connect_and_disconnect = function(ctx) {
- con <- connect(ctx)
- expect_s4_class(con, "DBIConnection")
- expect_true(dbDisconnect(con))
- },
-
- #' Repeated disconnect throws warning.
- cannot_disconnect_twice = function(ctx) {
- con <- connect(ctx)
- dbDisconnect(con)
- expect_warning(dbDisconnect(con))
- },
-
- #' }
- NULL
-)
diff --git a/R/spec-connection-data-type.R b/R/spec-connection-data-type.R
index ac41127..bce4dcf 100644
--- a/R/spec-connection-data-type.R
+++ b/R/spec-connection-data-type.R
@@ -1,36 +1,9 @@
-#' @template dbispec-sub-wip
-#' @format NULL
-#' @section Connection:
-#' \subsection{`dbDataType("DBIConnection", "ANY")`}{
spec_connection_data_type <- list(
- #' SQL Data types exist for all basic R data types. dbDataType() does not
- #' throw an error and returns a nonempty atomic character
data_type_connection = function(ctx) {
- con <- connect(ctx)
- check_conn_data_type <- function(value) {
- eval(bquote({
- expect_is(dbDataType(con, .(value)), "character")
- expect_equal(length(dbDataType(con, .(value))), 1L)
- expect_match(dbDataType(con, .(value)), ".")
- }))
- }
-
- expect_conn_has_data_type <- function(value) {
- eval(bquote(
- expect_error(check_conn_data_type(.(value)), NA)))
- }
-
- expect_conn_has_data_type(logical(1))
- expect_conn_has_data_type(integer(1))
- expect_conn_has_data_type(numeric(1))
- expect_conn_has_data_type(character(1))
- expect_conn_has_data_type(Sys.Date())
- expect_conn_has_data_type(Sys.time())
- if (!isTRUE(ctx$tweaks$omit_blob_tests)) {
- expect_conn_has_data_type(list(raw(1)))
- }
+ with_connection({
+ test_data_type(ctx, con)
+ })
},
- #' }
NULL
)
diff --git a/R/spec-connection-disconnect.R b/R/spec-connection-disconnect.R
new file mode 100644
index 0000000..1273d70
--- /dev/null
+++ b/R/spec-connection-disconnect.R
@@ -0,0 +1,43 @@
+#' spec_connection_disconnect
+#' @usage NULL
+#' @format NULL
+#' @keywords NULL
+spec_connection_disconnect <- list(
+ disconnect_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbDisconnect)), c("conn", "..."))
+ },
+
+ #' @return
+ can_disconnect = function(ctx) {
+ con <- connect(ctx)
+ #' `dbDisconnect()` returns `TRUE`, invisibly.
+ expect_invisible_true(dbDisconnect(con))
+ },
+
+ #' @section Specification:
+ cannot_forget_disconnect = function(ctx) {
+ expect_warning(gc(), NA)
+ connect(ctx)
+ #' A warning is issued on garbage collection when a connection has been
+ #' released without calling `dbDisconnect()`.
+ expect_warning(gc())
+ },
+
+ #' A warning is issued immediately when calling `dbDisconnect()` on an
+ #' already disconnected
+ disconnect_closed_connection = function(ctx) {
+ with_closed_connection({
+ expect_warning(dbDisconnect(con))
+ })
+ },
+
+ #' or invalid connection.
+ disconnect_invalid_connection = function(ctx) {
+ with_invalid_connection({
+ expect_warning(dbDisconnect(con))
+ })
+ },
+
+ NULL
+)
diff --git a/R/spec-connection-get-info.R b/R/spec-connection-get-info.R
index bdd9afa..16e90d5 100644
--- a/R/spec-connection-get-info.R
+++ b/R/spec-connection-get-info.R
@@ -5,22 +5,21 @@
spec_connection_get_info <- list(
#' Return value of dbGetInfo has necessary elements
get_info_connection = function(ctx) {
- con <- connect(ctx)
- on.exit(expect_error(dbDisconnect(con), NA), add = TRUE)
+ with_connection({
+ info <- dbGetInfo(con)
+ expect_is(info, "list")
+ info_names <- names(info)
- info <- dbGetInfo(con)
- expect_is(info, "list")
- info_names <- names(info)
+ necessary_names <-
+ c("db.version", "dbname", "username", "host", "port")
- necessary_names <-
- c("db.version", "dbname", "username", "host", "port")
+ for (name in necessary_names) {
+ eval(bquote(
+ expect_true(.(name) %in% info_names)))
+ }
- for (name in necessary_names) {
- eval(bquote(
- expect_true(.(name) %in% info_names)))
- }
-
- expect_false("password" %in% info_names)
+ expect_false("password" %in% info_names)
+ })
},
#' }
diff --git a/R/spec-connection.R b/R/spec-connection.R
index b3dec97..9dc712c 100644
--- a/R/spec-connection.R
+++ b/R/spec-connection.R
@@ -1,7 +1,7 @@
#' @template dbispec
#' @format NULL
spec_connection <- c(
- spec_connection_connect,
+ spec_connection_disconnect,
spec_connection_data_type,
spec_connection_get_info
)
diff --git a/R/spec-driver-class.R b/R/spec-driver-class.R
index c0f728e..1c4cf40 100644
--- a/R/spec-driver-class.R
+++ b/R/spec-driver-class.R
@@ -1,14 +1,7 @@
-#' @template dbispec-sub
-#' @format NULL
-#' @section Driver:
spec_driver_class <- list(
inherits_from_driver = function(ctx) {
- #' Each DBI backend implements a \dfn{driver class},
- #' which must be an S4 class and inherit from the `DBIDriver` class.
expect_s4_class(ctx$drv, "DBIDriver")
},
- #' This section describes the construction of, and the methods defined for,
- #' this driver class.
NULL
)
diff --git a/R/spec-driver-connect.R b/R/spec-driver-connect.R
new file mode 100644
index 0000000..8a7d3fc
--- /dev/null
+++ b/R/spec-driver-connect.R
@@ -0,0 +1,35 @@
+#' spec_driver_connect
+#' @usage NULL
+#' @format NULL
+#' @keywords NULL
+spec_driver_connect <- list(
+ connect_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbConnect)), c("drv", "..."))
+ },
+
+ #' @return
+ can_connect = function(ctx) {
+ con <- expect_visible(connect(ctx))
+ #' `dbConnect()` returns an S4 object that inherits from [DBIConnection-class].
+ expect_s4_class(con, "DBIConnection")
+ dbDisconnect(con)
+ #' This object is used to communicate with the database engine.
+ },
+
+ #' @section Specification:
+ #' DBI recommends using the following argument names for authentication
+ #' parameters, with `NULL` default:
+ #' - `user` for the user name (default: current user)
+ #' - `password` for the password
+ #' - `host` for the host name (default: local connection)
+ #' - `port` for the port number (default: local connection)
+ #' - `dbname` for the name of the database on the host, or the database file
+ #' name
+ #'
+ #' The defaults should provide reasonable behavior, in particular a
+ #' local connection for `host = NULL`. For some DBMS (e.g., PostgreSQL),
+ #' this is different to a TCP/IP connection to `localhost`.
+
+ NULL
+)
diff --git a/R/spec-driver-constructor.R b/R/spec-driver-constructor.R
index 1c11392..f024441 100644
--- a/R/spec-driver-constructor.R
+++ b/R/spec-driver-constructor.R
@@ -1,22 +1,20 @@
#' @template dbispec-sub
#' @format NULL
-#' @section Driver:
-#' \subsection{Construction}{
+#' @section Construction of the DBIDriver object:
spec_driver_constructor <- list(
constructor = function(ctx) {
pkg_name <- package_name(ctx)
- #' The backend must support creation of an instance of this driver class
+ #' The backend must support creation of an instance of its [DBIDriver-class]
+ #' subclass
#' with a \dfn{constructor function}.
#' By default, its name is the package name without the leading \sQuote{R}
#' (if it exists), e.g., `SQLite` for the \pkg{RSQLite} package.
default_constructor_name <- gsub("^R", "", pkg_name)
- #' For the automated tests, the constructor name can be tweaked using the
- #' `constructor_name` tweak.
+ #' However, backend authors may choose a different name.
constructor_name <- ctx$tweaks$constructor_name %||% default_constructor_name
- #'
#' The constructor must be exported, and
pkg_env <- getNamespace(pkg_name)
eval(bquote(
@@ -28,19 +26,12 @@ spec_driver_constructor <- list(
constructor <- get(constructor_name, mode = "function", pkg_env)
#' that is callable without arguments.
- #' For the automated tests, unless the
- #' `constructor_relax_args` tweak is set to `TRUE`,
+ expect_that(constructor, all_args_have_default_values())
+ #' DBI recommends to define a constructor with an empty argument list.
if (!isTRUE(ctx$tweaks$constructor_relax_args)) {
- #' an empty argument list is expected.
expect_that(constructor, arglist_is_empty())
- } else {
- #' Otherwise, an argument list where all arguments have default values
- #' is also accepted.
- expect_that(constructor, all_args_have_default_values())
}
- #'
},
- #' }
NULL
)
diff --git a/R/spec-driver-data-type.R b/R/spec-driver-data-type.R
index 2f1dbc9..9eba5be 100644
--- a/R/spec-driver-data-type.R
+++ b/R/spec-driver-data-type.R
@@ -1,60 +1,115 @@
-#' @template dbispec-sub
+#' spec_driver_data_type
+#' @usage NULL
#' @format NULL
-#' @section Driver:
-#' \subsection{`dbDataType("DBIDriver", "ANY")`}{
+#' @keywords NULL
+#' @inherit test_data_type
spec_driver_data_type <- list(
- #' The backend can override the [DBI::dbDataType()] generic
- #' for its driver class.
+ data_type_formals = function(ctx) {
+ # <establish formals of described function>
+ expect_equal(names(formals(dbDataType)), c("dbObj", "obj", "..."))
+ },
+
data_type_driver = function(ctx) {
- #' This generic expects an arbitrary object as second argument
- #' and returns a corresponding SQL type
- check_driver_data_type <- function(value) {
- eval(bquote({
- #' as atomic
- expect_equal(length(dbDataType(ctx$drv, .(value))), 1L)
- #' character value
- expect_is(dbDataType(ctx$drv, .(value)), "character")
- #' with at least one character.
- expect_match(dbDataType(ctx$drv, .(value)), ".")
- #' As-is objects (i.e., wrapped by [base::I()]) must be
- #' supported and return the same results as their unwrapped counterparts.
- expect_identical(dbDataType(ctx$drv, I(.(value))),
- dbDataType(ctx$drv, .(value)))
- }))
- }
+ test_data_type(ctx, ctx$drv)
+ },
- #'
- #' To query the values returned by the default implementation,
- #' run `example(dbDataType, package = "DBI")`.
- #' If the backend needs to override this generic,
- #' it must accept all basic R data types as its second argument, namely
- expect_driver_has_data_type <- function(value) {
- eval(bquote(
- expect_error(check_driver_data_type(.(value)), NA)))
- }
+ NULL
+)
+
+#' test_data_type
+#' @param ctx,dbObj Arguments to internal test function
+test_data_type <- function(ctx, dbObj) {
+ #' @return
+ #' `dbDataType()` returns the SQL type that corresponds to the `obj` argument
+ check_data_type <- function(value) {
+ eval(bquote({
+ #' as a non-empty
+ expect_match(dbDataType(dbObj, .(value)), ".")
+ #' character string.
+ if (!is.data.frame(value)) {
+ expect_equal(length(dbDataType(dbObj, .(value))), 1L)
+ } else {
+ #' For data frames, a character vector with one element per column
+ #' is returned.
+ expect_equal(length(dbDataType(dbObj, value)), .(ncol(value)))
+ }
+ expect_is(dbDataType(dbObj, .(value)), "character")
+ expect_visible(dbDataType(dbObj, .(value)))
+ }))
+ }
- #' [base::logical()],
- expect_driver_has_data_type(logical(1))
- #' [base::integer()],
- expect_driver_has_data_type(integer(1))
- #' [base::numeric()],
- expect_driver_has_data_type(numeric(1))
- #' [base::character()],
- expect_driver_has_data_type(character(1))
- #' dates (see [base::Dates()]),
- expect_driver_has_data_type(Sys.Date())
- #' date-time (see [base::DateTimeClasses()]),
- expect_driver_has_data_type(Sys.time())
- #' and [base::difftime()].
- expect_driver_has_data_type(Sys.time() - Sys.time())
- #' It also must accept lists of `raw` vectors
- #' and map them to the BLOB (binary large object) data type.
+ #' An error is raised for invalid values for the `obj` argument such as a
+ #' `NULL` value.
+ expect_error(dbDataType(dbObj, NULL))
+
+ #' @section Specification:
+ #' The backend can override the [dbDataType()] generic
+ #' for its driver class.
+ #'
+ #' This generic expects an arbitrary object as second argument.
+ #' To query the values returned by the default implementation,
+ #' run `example(dbDataType, package = "DBI")`.
+ #' If the backend needs to override this generic,
+ #' it must accept all basic R data types as its second argument, namely
+ expect_has_data_type <- function(value) {
+ eval(bquote(
+ expect_error(check_data_type(.(value)), NA)))
+ }
+
+ expected_data_types <- list(
+ #' [logical],
+ logical(1),
+ #' [integer],
+ integer(1),
+ #' [numeric],
+ numeric(1),
+ #' [character],
+ character(1),
+ #' dates (see [Dates]),
+ Sys.Date(),
+ #' date-time (see [DateTimeClasses]),
+ Sys.time(),
+ #' and [difftime].
+ Sys.time() - Sys.time(),
+ #' If the database supports blobs,
+ if (!isTRUE(ctx$tweaks$omit_blob_tests)) {
+ #' this method also must accept lists of [raw] vectors,
+ list(as.raw(1:10))
+ },
if (!isTRUE(ctx$tweaks$omit_blob_tests)) {
- expect_driver_has_data_type(list(raw(1)))
+ #' and [blob::blob] objects.
+ blob::blob(as.raw(1:10))
}
- #' The behavior for other object types is not specified.
- },
+ )
- #' }
- NULL
-)
+ lapply(
+ compact(expected_data_types),
+ expect_has_data_type
+ )
+
+ expect_has_data_type(data.frame(a = 1, b = "2", stringsAsFactors = FALSE))
+
+ #' As-is objects (i.e., wrapped by [I()]) must be
+ #' supported and return the same results as their unwrapped counterparts.
+ lapply(
+ compact(expected_data_types),
+ function(value) {
+ if (!is.null(value)) {
+ eval(bquote(
+ expect_error(
+ expect_identical(dbDataType(dbObj, I(.(value))),
+ dbDataType(dbObj, .(value))),
+ NA)))
+ }
+ }
+ )
+
+ #' The SQL data type for [factor]
+ expect_identical(dbDataType(dbObj, letters),
+ dbDataType(dbObj, factor(letters)))
+ #' and [ordered] is the same as for character.
+ expect_identical(dbDataType(dbObj, letters),
+ dbDataType(dbObj, ordered(letters)))
+
+ #' The behavior for other object types is not specified.
+}
diff --git a/R/spec-driver.R b/R/spec-driver.R
index 0ea3ed0..ee03b4c 100644
--- a/R/spec-driver.R
+++ b/R/spec-driver.R
@@ -4,5 +4,6 @@ spec_driver <- c(
spec_driver_class,
spec_driver_constructor,
spec_driver_data_type,
- spec_driver_get_info
+ spec_driver_get_info,
+ spec_driver_connect
)
diff --git a/R/spec-getting-started.R b/R/spec-getting-started.R
index e25f293..5a5b3da 100644
--- a/R/spec-getting-started.R
+++ b/R/spec-getting-started.R
@@ -1,14 +1,15 @@
#' @template dbispec
#' @format NULL
-#' @section Getting started:
+#' @section Definition:
spec_getting_started <- list(
package_dependencies = function(ctx) {
- #' A DBI backend is an R package,
- pkg <- get_pkg(ctx)
+ #' A DBI backend is an R package
+ pkg_path <- get_pkg_path(ctx)
- pkg_imports <- devtools::parse_deps(pkg$imports)$name
+ pkg_deps_df <- desc::desc_get_deps(pkg_path)
+ pkg_imports <- pkg_deps_df[pkg_deps_df[["type"]] == "Imports", ][["package"]]
- #' which should import the \pkg{DBI}
+ #' which imports the \pkg{DBI}
expect_true("DBI" %in% pkg_imports)
#' and \pkg{methods}
expect_true("methods" %in% pkg_imports)
@@ -20,7 +21,7 @@ spec_getting_started <- list(
#' For better or worse, the names of many existing backends start with
#' \sQuote{R}, e.g., \pkg{RSQLite}, \pkg{RMySQL}, \pkg{RSQLServer}; it is up
- #' to the package author to adopt this convention or not.
+ #' to the backend author to adopt this convention or not.
expect_match(pkg_name, "^R")
},
diff --git a/R/spec-meta-bind-.R b/R/spec-meta-bind-.R
index cc46e75..45bd544 100644
--- a/R/spec-meta-bind-.R
+++ b/R/spec-meta-bind-.R
@@ -34,7 +34,9 @@ test_select_bind_one <- function(con, placeholder_fun, values,
}
new_extra_imp <- function(extra) {
- if (length(extra) == 0)
+ if (is.environment(extra))
+ extra$new()
+ else if (length(extra) == 0)
new_extra_imp_one("none")
else if (length(extra) == 1)
new_extra_imp_one(extra)
@@ -47,12 +49,6 @@ new_extra_imp <- function(extra) {
new_extra_imp_one <- function(extra) {
extra_imp <- switch(
extra,
- return_value = BindTesterExtraReturnValue,
- too_many = BindTesterExtraTooMany,
- not_enough = BindTesterExtraNotEnough,
- wrong_name = BindTesterExtraWrongName,
- unequal_length = BindTesterExtraUnequalLength,
- repeated = BindTesterExtraRepeated,
none = BindTesterExtra,
stop("Unknown extra: ", extra, call. = FALSE)
)
@@ -60,112 +56,6 @@ new_extra_imp_one <- function(extra) {
extra_imp$new()
}
-# BindTesterExtra ---------------------------------------------------------
-
-BindTesterExtra <- R6::R6Class(
- "BindTesterExtra",
- portable = TRUE,
-
- public = list(
- check_return_value = function(bind_res, res) invisible(NULL),
- patch_bind_values = identity,
- requires_names = function() FALSE,
- is_repeated = function() FALSE
- )
-)
-
-
-# BindTesterExtraReturnValue ----------------------------------------------
-
-BindTesterExtraReturnValue <- R6::R6Class(
- "BindTesterExtraReturnValue",
- inherit = BindTesterExtra,
- portable = TRUE,
-
- public = list(
- check_return_value = function(bind_res, res) {
- expect_false(bind_res$visible)
- expect_identical(res, bind_res$value)
- }
- )
-)
-
-
-# BindTesterExtraTooMany --------------------------------------------------
-
-BindTesterExtraTooMany <- R6::R6Class(
- "BindTesterExtraTooMany",
- inherit = BindTesterExtra,
- portable = TRUE,
-
- public = list(
- patch_bind_values = function(bind_values) {
- c(bind_values, bind_values[[1L]])
- }
- )
-)
-
-
-# BindTesterExtraNotEnough --------------------------------------------------
-
-BindTesterExtraNotEnough <- R6::R6Class(
- "BindTesterExtraNotEnough",
- inherit = BindTesterExtra,
- portable = TRUE,
-
- public = list(
- patch_bind_values = function(bind_values) {
- bind_values[-1L]
- }
- )
-)
-
-
-# BindTesterExtraWrongName ------------------------------------------------
-
-BindTesterExtraWrongName <- R6::R6Class(
- "BindTesterExtraWrongName",
- inherit = BindTesterExtra,
- portable = TRUE,
-
- public = list(
- patch_bind_values = function(bind_values) {
- stats::setNames(bind_values, paste0("bogus", names(bind_values)))
- },
-
- requires_names = function() TRUE
- )
-)
-
-
-# BindTesterExtraUnequalLength --------------------------------------------
-
-BindTesterExtraUnequalLength <- R6::R6Class(
- "BindTesterExtraUnequalLength",
- inherit = BindTesterExtra,
- portable = TRUE,
-
- public = list(
- patch_bind_values = function(bind_values) {
- bind_values[[2]] <- bind_values[[2]][-1]
- bind_values
- }
- )
-)
-
-
-# BindTesterExtraRepeated -------------------------------------------------
-
-BindTesterExtraRepeated <- R6::R6Class(
- "BindTesterExtraRepeated",
- inherit = BindTesterExtra,
- portable = TRUE,
-
- public = list(
- is_repeated = function() TRUE
- )
-)
-
# BindTester --------------------------------------------------------------
@@ -222,17 +112,11 @@ BindTester <- R6::R6Class(
},
bind = function(res, bind_values) {
- error_bind_values <- extra_obj$patch_bind_values(bind_values)
-
- if (!identical(bind_values, error_bind_values)) {
- expect_error(dbBind(res, error_bind_values))
- return(FALSE)
- }
+ bind_values <- extra_obj$patch_bind_values(bind_values)
bind_res <- withVisible(dbBind(res, bind_values))
extra_obj$check_return_value(bind_res, res)
-
- TRUE
+ invisible()
},
compare = function(rows, values) {
diff --git a/R/spec-meta-bind-multi-row.R b/R/spec-meta-bind-multi-row.R
deleted file mode 100644
index e46dcea..0000000
--- a/R/spec-meta-bind-multi-row.R
+++ /dev/null
@@ -1,70 +0,0 @@
-#' @template dbispec-sub-wip
-#' @format NULL
-#' @section Parametrised queries and statements:
-#' \subsection{`dbBind("DBIResult")`}{
-spec_meta_bind_multi_row <- list(
- #' Binding of multi-row integer values.
- bind_multi_row = function(ctx) {
- with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, list(1:3))
- })
- },
-
- #' Binding of multi-row integer values with zero rows.
- bind_multi_row_zero_length = function(ctx) {
- with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, list(integer(), integer()))
- })
- },
-
- #' Binding of multi-row integer values with unequal length.
- bind_multi_row_unequal_length = function(ctx) {
- with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, list(1:3, 2:4), extra = "unequal_length")
- })
- },
-
- #' Binding of multi-row statements.
- bind_multi_row_statement = function(ctx) {
- with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, list(1:3), query = FALSE)
- })
- },
-
- #' }
- NULL
-)
-
-#' @noRd
-#' @details
-list(
- #' Binding of multi-row integer values with group column.
- bind_multi_row_group_column = function(ctx) {
- with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, list(1:3), extra = "group_column")
- })
- },
-
- #' Binding of multi-row integer values with group column and zero rows.
- bind_multi_row_group_column_zero_length = function(ctx) {
- with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, list(integer(), integer()), extra = "group_column")
- })
- },
-
- #' Binding of multi-row integer values, groupwise fetching.
- bind_multi_row_groupwise_fetch = function(ctx) {
- with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, list(1:3), extra = "groupwise_fetch")
- })
- },
-
- #' Binding of multi-row integer values, groupwise fetching, with group column.
- bind_multi_row_group_column_groupwise_fetch = function(ctx) {
- with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, list(1:3), extra = c("group_column", "groupwise_fetch"))
- })
- },
-
- NULL
-)
diff --git a/R/spec-meta-bind-runner.R b/R/spec-meta-bind-runner.R
new file mode 100644
index 0000000..fbe6d8b
--- /dev/null
+++ b/R/spec-meta-bind-runner.R
@@ -0,0 +1,101 @@
+run_bind_tester <- list()
+
+#' spec_meta_bind
+#' @name spec_meta_bind
+#' @usage NULL
+#' @format NULL
+#' @keywords NULL
+#' @section Specification:
+#' \pkg{DBI} clients execute parametrized statements as follows:
+#'
+run_bind_tester$fun <- function() {
+ if ((extra_obj$requires_names() %in% TRUE) && is.null(names(placeholder))) {
+ # test only valid for named placeholders
+ return()
+ }
+
+ if ((extra_obj$requires_names() %in% FALSE) && !is.null(names(placeholder))) {
+ # test only valid for unnamed placeholders
+ return()
+ }
+
+ #' 1. Call [dbSendQuery()] or [dbSendStatement()] with a query or statement
+ #' that contains placeholders,
+ #' store the returned [DBIResult-class] object in a variable.
+ #' Mixing placeholders (in particular, named and unnamed ones) is not
+ #' recommended.
+ if (is_query())
+ res <- send_query()
+ else
+ res <- send_statement()
+
+ #' It is good practice to register a call to [dbClearResult()] via
+ #' [on.exit()] right after calling `dbSendQuery()` or `dbSendStatement()`
+ #' (see the last enumeration item).
+ if (extra_obj$is_premature_clear()) dbClearResult(res)
+ else on.exit(expect_error(dbClearResult(res), NA))
+
+ #' Until `dbBind()` has been called, the returned result set object has the
+ #' following behavior:
+ #' - [dbFetch()] raises an error (for `dbSendQuery()`)
+ if (is_query()) expect_error(dbFetch(res))
+ #' - [dbGetRowCount()] returns zero (for `dbSendQuery()`)
+ if (is_query()) expect_equal(dbGetRowCount(res), 0)
+ #' - [dbGetRowsAffected()] returns an integer `NA` (for `dbSendStatement()`)
+ if (!is_query()) expect_identical(dbGetRowsAffected(res), NA_integer_)
+ #' - [dbIsValid()] returns `TRUE`
+ expect_true(dbIsValid(res))
+ #' - [dbHasCompleted()] returns `FALSE`
+ expect_false(dbHasCompleted(res))
+
+ #' 1. Construct a list with parameters
+ #' that specify actual values for the placeholders.
+ bind_values <- values
+ #' The list must be named or unnamed,
+ #' depending on the kind of placeholders used.
+ #' Named values are matched to named parameters, unnamed values
+ #' are matched by position in the list of parameters.
+ if (!is.null(names(placeholder))) {
+ names(bind_values) <- names(placeholder)
+ }
+ #' All elements in this list must have the same lengths and contain values
+ #' supported by the backend; a [data.frame] is internally stored as such
+ #' a list.
+ #' The parameter list is passed to a call to `dbBind()` on the `DBIResult`
+ #' object.
+ bind(res, bind_values)
+
+ # Safety net: returning early if dbBind() should have thrown an error but
+ # didn't
+ if (!identical(bind_values, extra_obj$patch_bind_values(bind_values)))
+ return()
+ if (extra_obj$is_premature_clear())
+ return()
+
+ #' 1. Retrieve the data or the number of affected rows from the `DBIResult` object.
+ retrieve <- function() {
+ #' - For queries issued by `dbSendQuery()`,
+ #' call [dbFetch()].
+ if (is_query()) {
+ rows <- check_df(dbFetch(res))
+ compare(rows, values)
+ } else {
+ #' - For statements issued by `dbSendStatements()`,
+ #' call [dbGetRowsAffected()].
+ #' (Execution begins immediately after the `dbBind()` call,
+ #' the statement is processed entirely before the function returns.)
+ rows_affected <- dbGetRowsAffected(res)
+ compare_affected(rows_affected, values)
+ }
+ }
+
+ if (!extra_obj$is_untouched()) retrieve()
+
+ #' 1. Repeat 2. and 3. as necessary.
+ if (extra_obj$is_repeated()) {
+ bind(res, bind_values)
+ retrieve()
+ }
+
+ #' 1. Close the result set via [dbClearResult()].
+}
diff --git a/R/spec-meta-bind-tester-extra.R b/R/spec-meta-bind-tester-extra.R
new file mode 100644
index 0000000..3f7fd3c
--- /dev/null
+++ b/R/spec-meta-bind-tester-extra.R
@@ -0,0 +1,21 @@
+BindTesterExtra <- R6::R6Class(
+ "BindTesterExtra",
+ portable = TRUE,
+
+ public = list(
+ check_return_value = function(bind_res, res) invisible(NULL),
+ patch_bind_values = identity,
+ requires_names = function() NA,
+ is_repeated = function() FALSE,
+ is_premature_clear = function() FALSE,
+ is_untouched = function() FALSE
+ )
+)
+
+new_bind_tester_extra <- function(...) {
+ R6::R6Class(
+ inherit = BindTesterExtra,
+ portable = TRUE,
+ public = list(...)
+ )
+}
diff --git a/R/spec-meta-bind.R b/R/spec-meta-bind.R
index 1cf80bf..0f898b1 100644
--- a/R/spec-meta-bind.R
+++ b/R/spec-meta-bind.R
@@ -1,200 +1,287 @@
-run_bind_tester <- list()
-
-#' @template dbispec-sub
+#' spec_meta_bind
+#' @usage NULL
#' @format NULL
-#' @section Parametrized queries and statements:
-#' \pkg{DBI} supports parametrized (or prepared) queries and statements
-#' via the [DBI::dbBind()] generic.
-#' Parametrized queries are different from normal queries
-#' in that they allow an arbitrary number of placeholders,
-#' which are later substituted by actual values.
-#' Parametrized queries (and statements) serve two purposes:
-#'
-#' - The same query can be executed more than once with different values.
-#' The DBMS may cache intermediate information for the query,
-#' such as the execution plan,
-#' and execute it faster.
-#' - Separation of query syntax and parameters protects against SQL injection.
-#'
-#' The placeholder format is currently not specified by \pkg{DBI};
-#' in the future, a uniform placeholder syntax may be supported.
-#' Consult the backend documentation for the supported formats.
-#' For automated testing, backend authors specify the placeholder syntax with
-#' the `placeholder_pattern` tweak.
-#' Known examples are:
-#'
-#' - `?` (positional matching in order of appearance) in \pkg{RMySQL} and \pkg{RSQLite}
-#' - `$1` (positional matching by index) in \pkg{RPostgres} and \pkg{RSQLite}
-#' - `:name` and `$name` (named matching) in \pkg{RSQLite}
-#'
-#' \pkg{DBI} clients execute parametrized statements as follows:
-#'
-run_bind_tester$fun <- function() {
- if (extra_obj$requires_names() && is.null(names(placeholder))) {
- # wrong_name test only valid for named placeholders
- return()
- }
-
- # FIXME
- #' 1. Call [DBI::dbSendQuery()] or [DBI::dbSendStatement()] with a query or statement
- #' that contains placeholders,
- #' store the returned \code{\linkS4class{DBIResult}} object in a variable.
- #' Mixing placeholders (in particular, named and unnamed ones) is not
- #' recommended.
- if (is_query())
- res <- send_query()
- else
- res <- send_statement()
- #' It is good practice to register a call to [DBI::dbClearResult()] via
- #' [on.exit()] right after calling `dbSendQuery()`, see the last
- #' enumeration item.
- on.exit(expect_error(dbClearResult(res), NA))
-
- #' 1. Construct a list with parameters
- #' that specify actual values for the placeholders.
- bind_values <- values
- #' The list must be named or unnamed,
- #' depending on the kind of placeholders used.
- #' Named values are matched to named parameters, unnamed values
- #' are matched by position.
- if (!is.null(names(placeholder))) {
- names(bind_values) <- names(placeholder)
- }
- #' All elements in this list must have the same lengths and contain values
- #' supported by the backend; a [data.frame()] is internally stored as such
- #' a list.
- # FIXME
-
- #' The parameter list is passed a call to [dbBind()] on the `DBIResult`
- #' object.
- if (!bind(res, bind_values))
- return()
-
- #' 1. Retrieve the data or the number of affected rows from the `DBIResult` object.
- retrieve <- function() {
- #' - For queries issued by `dbSendQuery()`,
- #' call [DBI::dbFetch()].
- if (is_query()) {
- rows <- dbFetch(res)
- compare(rows, values)
- } else {
- #' - For statements issued by `dbSendStatements()`,
- #' call [DBI::dbGetRowsAffected()].
- #' (Execution begins immediately after the `dbBind()` call,
- #' the statement is processed entirely before the function returns.
- #' Calls to `dbFetch()` are ignored.)
- rows_affected <- dbGetRowsAffected(res)
- compare_affected(rows_affected, values)
- }
- }
- retrieve()
-
- #' 1. Repeat 2. and 3. as necessary.
- if (extra_obj$is_repeated()) {
- bind(res, bind_values)
- retrieve()
- }
+#' @keywords NULL
+spec_meta_bind <- list(
+ bind_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbBind)), c("res", "params", "..."))
+ },
- #' 1. Close the result set via [DBI::dbClearResult()].
-}
+ #' @return
+ bind_return_value = function(ctx) {
+ extra <- new_bind_tester_extra(
+ check_return_value = function(bind_res, res) {
+ #' `dbBind()` returns the result set,
+ expect_identical(res, bind_res$value)
+ #' invisibly,
+ expect_false(bind_res$visible)
+ }
+ )
+ with_connection({
+ #' for queries issued by [dbSendQuery()]
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = extra)
+ })
+ with_connection({
+ #' and also for data manipulation statements issued by
+ #' [dbSendStatement()].
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = extra, query = FALSE)
+ })
+ },
-#' @template dbispec-sub-wip
-#' @format NULL
-#' @section Parametrised queries and statements:
-#' \subsection{`dbBind("DBIResult")`}{
-spec_meta_bind <- list(
- #' Empty binding with check of
- #' return value.
bind_empty = function(ctx) {
with_connection({
- res <- dbSendQuery(con, "SELECT 1")
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
+ with_result(
+ #' Calling `dbBind()` for a query without parameters
+ dbSendQuery(con, "SELECT 1"),
+ #' raises an error.
+ expect_error(dbBind(res, list()))
+ )
+ })
+ },
- bind_res <- withVisible(dbBind(res, list()))
- expect_false(bind_res$visible)
- expect_identical(res, bind_res$value)
+ bind_too_many = function(ctx) {
+ extra <- new_bind_tester_extra(
+ patch_bind_values = function(bind_values) {
+ #' Binding too many
+ c(bind_values, bind_values[[1L]])
+ }
+ )
+ with_connection({
+ expect_error(
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = extra)
+ )
})
},
- #' Binding of integer values raises an
- #' error if connection is closed.
- bind_error = function(ctx) {
- con <- connect(ctx)
- dbDisconnect(con)
- expect_error(test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L))
+ bind_not_enough = function(ctx) {
+ extra <- new_bind_tester_extra(
+ patch_bind_values = function(bind_values) {
+ #' or not enough values,
+ bind_values[-1L]
+ }
+ )
+ with_connection({
+ expect_error(
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = extra)
+ )
+ })
},
- #' Binding of integer values with check of
- #' return value.
- bind_return_value = function(ctx) {
+ bind_wrong_name = function(ctx) {
+ extra <- new_bind_tester_extra(
+ patch_bind_values = function(bind_values) {
+ #' or parameters with wrong names
+ stats::setNames(bind_values, paste0("bogus", names(bind_values)))
+ },
+
+ requires_names = function() TRUE
+ )
with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = "return_value")
+ expect_error(
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = extra)
+ )
})
},
- #' Binding of integer values with too many
- #' values.
- bind_too_many = function(ctx) {
+ bind_multi_row_unequal_length = function(ctx) {
+ extra <- new_bind_tester_extra(
+ patch_bind_values = function(bind_values) {
+ #' or unequal length,
+ bind_values[[2]] <- bind_values[[2]][-1]
+ bind_values
+ }
+ )
with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = "too_many")
+ #' also raises an error.
+ expect_error(
+ test_select_bind(
+ con, ctx$tweaks$placeholder_pattern, list(1:3, 2:4),
+ extra = extra, query = FALSE
+ )
+ )
})
},
- #' Binding of integer values with too few
- #' values.
- bind_not_enough = function(ctx) {
+ #' If the placeholders in the query are named,
+ bind_named_param_unnamed_placeholders = function(ctx) {
+ extra <- new_bind_tester_extra(
+ patch_bind_values = function(bind_values) {
+ #' all parameter values must have names
+ stats::setNames(bind_values, NULL)
+ },
+
+ requires_names = function() TRUE
+ )
+ with_connection({
+ expect_error(
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = extra)
+ )
+ })
+ },
+
+ bind_named_param_empty_placeholders = function(ctx) {
+ extra <- new_bind_tester_extra(
+ patch_bind_values = function(bind_values) {
+ #' (which must not be empty
+ names(bind_values)[[1]] <- ""
+ },
+
+ requires_names = function() TRUE
+ )
+ with_connection({
+ expect_error(
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, list(1L, 2L), extra = extra)
+ )
+ })
+ },
+
+ bind_named_param_na_placeholders = function(ctx) {
+ extra <- new_bind_tester_extra(
+ patch_bind_values = function(bind_values) {
+ #' or `NA`),
+ names(bind_values)[[1]] <- NA
+ },
+
+ requires_names = function() TRUE
+ )
+ with_connection({
+ expect_error(
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, list(1L, 2L), extra = extra)
+ )
+ })
+ },
+
+ #' and vice versa,
+ bind_unnamed_param_named_placeholders = function(ctx) {
+ extra <- new_bind_tester_extra(
+ patch_bind_values = function(bind_values) {
+ stats::setNames(bind_values, letters[seq_along(bind_values)])
+ },
+
+ requires_names = function() FALSE
+ )
+ with_connection({
+ #' otherwise an error is raised.
+ expect_error(
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = extra)
+ )
+ })
+ },
+
+ #' The behavior for mixing placeholders of different types
+ #' (in particular mixing positional and named placeholders)
+ #' is not specified.
+ #'
+
+ bind_premature_clear = function(ctx) {
+ extra <- new_bind_tester_extra(
+ #' Calling `dbBind()` on a result set already cleared by [dbClearResult()]
+ is_premature_clear = function() TRUE
+ )
+ with_connection({
+ #' also raises an error.
+ expect_error(
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = extra)
+ )
+ })
+ },
+
+ #' @section Specification:
+ #' The elements of the `params` argument do not need to be scalars,
+ bind_multi_row = function(ctx) {
+ with_connection({
+ #' vectors of arbitrary length
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, list(1:3))
+ })
+ },
+
+ bind_multi_row_zero_length = function(ctx) {
+ with_connection({
+ #' (including length 0)
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, list(integer(), integer()))
+ })
+
+ #' are supported.
+ # This behavior is tested as part of run_bind_tester$fun
+ #' For queries, calling `dbFetch()` binding such parameters returns
+ #' concatenated results, equivalent to binding and fetching for each set
+ #' of values and connecting via [rbind()].
+ },
+
+ bind_multi_row_statement = function(ctx) {
with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = "not_enough")
+ # This behavior is tested as part of run_bind_tester$fun
+ #' For data manipulation statements, `dbGetRowsAffected()` returns the
+ #' total number of rows affected if binding non-scalar parameters.
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, list(1:3), query = FALSE)
})
},
- #' Binding of integer values, repeated.
bind_repeated = function(ctx) {
+ extra <- new_bind_tester_extra(
+ #' `dbBind()` also accepts repeated calls on the same result set
+ is_repeated = function() TRUE
+ )
+
+ with_connection({
+ #' for both queries
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = extra)
+ })
+
with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = "repeated")
+ #' and data manipulation statements,
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = extra, query = FALSE)
})
},
- #' Binding of integer values with wrong names.
- bind_wrong_name = function(ctx) {
+ bind_repeated_untouched = function(ctx) {
+ extra <- new_bind_tester_extra(
+ #' even if no results are fetched between calls to `dbBind()`.
+ is_repeated = function() TRUE,
+ is_untouched = function() TRUE
+ )
+
with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = "wrong_name")
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = extra)
+ })
+
+ with_connection({
+ test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L, extra = extra, query = FALSE)
})
},
- #' Binding of integer values.
+ #'
+ #' At least the following data types are accepted:
+ #' - [integer]
bind_integer = function(ctx) {
with_connection({
test_select_bind(con, ctx$tweaks$placeholder_pattern, 1L)
})
},
- #' Binding of numeric values.
+ #' - [numeric]
bind_numeric = function(ctx) {
with_connection({
test_select_bind(con, ctx$tweaks$placeholder_pattern, 1.5)
})
},
- #' Binding of logical values.
+ #' - [logical] for Boolean values (some backends may return an integer)
bind_logical = function(ctx) {
with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, TRUE)
- })
- },
-
- #' Binding of logical values (coerced to integer).
- bind_logical_int = function(ctx) {
- with_connection({
test_select_bind(
con, ctx$tweaks$placeholder_pattern, TRUE,
- transform_input = function(x) as.character(as.integer(x)))
+ type = NULL,
+ transform_input = ctx$tweaks$logical_return,
+ transform_output = ctx$tweaks$logical_return
+ )
})
},
- #' Binding of `NULL` values.
+ #' - [NA]
bind_null = function(ctx) {
with_connection({
test_select_bind(
@@ -204,22 +291,48 @@ spec_meta_bind <- list(
})
},
- #' Binding of character values.
+ #' - [character]
bind_character = function(ctx) {
with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, texts)
+ test_select_bind(
+ con,
+ ctx$tweaks$placeholder_pattern,
+ texts
+ )
+ })
+ },
+
+ #' - [factor] (bound as character,
+ bind_factor = function(ctx) {
+ with_connection({
+ #' with warning)
+ expect_warning(
+ test_select_bind(
+ con,
+ ctx$tweaks$placeholder_pattern,
+ lapply(texts, factor)
+ )
+ )
})
},
- #' Binding of date values.
+ #' - [Date]
bind_date = function(ctx) {
+ if (!isTRUE(ctx$tweaks$date_typed)) {
+ skip("tweak: !date_typed")
+ }
+
with_connection({
test_select_bind(con, ctx$tweaks$placeholder_pattern, Sys.Date())
})
},
- #' Binding of [POSIXct] timestamp values.
+ #' - [POSIXct] timestamps
bind_timestamp = function(ctx) {
+ if (!isTRUE(ctx$tweaks$timestamp_typed)) {
+ skip("tweak: !timestamp_typed")
+ }
+
with_connection({
data_in <- as.POSIXct(round(Sys.time()))
test_select_bind(
@@ -231,19 +344,23 @@ spec_meta_bind <- list(
})
},
- #' Binding of [POSIXlt] timestamp values.
+ #' - [POSIXlt] timestamps
bind_timestamp_lt = function(ctx) {
+ if (!isTRUE(ctx$tweaks$timestamp_typed)) {
+ skip("tweak: !timestamp_typed")
+ }
+
with_connection({
data_in <- as.POSIXlt(round(Sys.time()))
test_select_bind(
con, ctx$tweaks$placeholder_pattern, data_in,
type = dbDataType(con, data_in),
transform_input = as.POSIXct,
- transform_output = identity)
+ transform_output = as.POSIXct)
})
},
- #' Binding of raw values.
+ #' - lists of [raw] for blobs (with `NULL` entries for SQL NULL values)
bind_raw = function(ctx) {
if (isTRUE(ctx$tweaks$omit_blob_tests)) {
skip("tweak: omit_blob_tests")
@@ -253,25 +370,25 @@ spec_meta_bind <- list(
test_select_bind(
con, ctx$tweaks$placeholder_pattern, list(list(as.raw(1:10))),
type = NULL,
- transform_input = identity,
- transform_output = identity)
+ transform_input = blob::as.blob,
+ transform_output = blob::as.blob)
})
},
- #' Binding of statements.
- bind_statement = function(ctx) {
- with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, list(1), query = FALSE)
- })
- },
+ #' - objects of type [blob::blob]
+ bind_blob = function(ctx) {
+ if (isTRUE(ctx$tweaks$omit_blob_tests)) {
+ skip("tweak: omit_blob_tests")
+ }
- #' Repeated binding of statements.
- bind_statement_repeated = function(ctx) {
with_connection({
- test_select_bind(con, ctx$tweaks$placeholder_pattern, list(1), query = FALSE, extra = "repeated")
+ test_select_bind(
+ con, ctx$tweaks$placeholder_pattern, list(blob::blob(as.raw(1:10))),
+ type = NULL,
+ transform_input = identity,
+ transform_output = blob::as.blob)
})
},
- #' }
NULL
)
diff --git a/R/spec-meta-column-info.R b/R/spec-meta-column-info.R
index 880aefc..d99c100 100644
--- a/R/spec-meta-column-info.R
+++ b/R/spec-meta-column-info.R
@@ -7,13 +7,16 @@ spec_meta_column_info <- list(
column_info = function(ctx) {
with_connection({
query <- "SELECT 1 as a, 1.5 as b, NULL"
- expect_warning(res <- dbSendQuery(con, query), NA)
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
- ci <- dbColumnInfo(res)
- expect_is(ci, "data.frame")
- expect_identical(colnames(ci), c("name", "type"))
- expect_identical(ci$name[1:2], c("a", "b"))
- expect_is(ci$type, "character")
+ with_result(
+ dbSendQuery(con, query),
+ {
+ ci <- dbColumnInfo(res)
+ expect_is(ci, "data.frame")
+ expect_identical(colnames(ci), c("name", "type"))
+ expect_identical(ci$name[1:2], c("a", "b"))
+ expect_is(ci$type, "character")
+ }
+ )
})
},
diff --git a/R/spec-meta-get-row-count.R b/R/spec-meta-get-row-count.R
index 0fb3ba2..c2af346 100644
--- a/R/spec-meta-get-row-count.R
+++ b/R/spec-meta-get-row-count.R
@@ -1,48 +1,108 @@
-#' @template dbispec-sub-wip
+#' spec_meta_get_row_count
+#' @usage NULL
#' @format NULL
-#' @section Meta:
-#' \subsection{`dbGetRowCount("DBIResult")`}{
+#' @keywords NULL
spec_meta_get_row_count <- list(
- #' Row count information is correct.
- row_count = function(ctx) {
+ get_row_count_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbGetRowCount)), c("res", "..."))
+ },
+
+ #' @return
+ #' `dbGetRowCount()` returns a scalar number (integer or numeric),
+ #' the number of rows fetched so far.
+ row_count_query = function(ctx) {
with_connection({
query <- "SELECT 1 as a"
- res <- dbSendQuery(con, query)
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
- rc <- dbGetRowCount(res)
- expect_equal(rc, 0L)
- dbFetch(res)
- rc <- dbGetRowCount(res)
- expect_equal(rc, 1L)
+ with_result(
+ #' After calling [dbSendQuery()],
+ dbSendQuery(con, query),
+ {
+ rc <- dbGetRowCount(res)
+ #' the row count is initially zero.
+ expect_equal(rc, 0L)
+ #' After a call to [dbFetch()] without limit,
+ check_df(dbFetch(res))
+ rc <- dbGetRowCount(res)
+ #' the row count matches the total number of rows returned.
+ expect_equal(rc, 1L)
+ }
+ )
})
with_connection({
query <- union(.ctx = ctx, "SELECT 1 as a", "SELECT 2", "SELECT 3")
- res <- dbSendQuery(con, query)
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
- rc <- dbGetRowCount(res)
- expect_equal(rc, 0L)
- dbFetch(res, 2L)
- rc <- dbGetRowCount(res)
- expect_equal(rc, 2L)
- dbFetch(res)
- rc <- dbGetRowCount(res)
- expect_equal(rc, 3L)
+ with_result(
+ dbSendQuery(con, query),
+ {
+ rc <- dbGetRowCount(res)
+ expect_equal(rc, 0L)
+ #' Fetching a limited number of rows
+ check_df(dbFetch(res, 2L))
+ #' increases the number of rows by the number of rows returned,
+ rc <- dbGetRowCount(res)
+ expect_equal(rc, 2L)
+ #' even if fetching past the end of the result set.
+ check_df(dbFetch(res, 2L))
+ rc <- dbGetRowCount(res)
+ expect_equal(rc, 3L)
+ }
+ )
})
with_connection({
+ #' For queries with an empty result set,
query <- union(
- .ctx = ctx, "SELECT * FROM (SELECT 1 as a) a WHERE (0 = 1)")
- res <- dbSendQuery(con, query)
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
- rc <- dbGetRowCount(res)
- expect_equal(rc, 0L)
- dbFetch(res)
- rc <- dbGetRowCount(res)
- expect_equal(rc, 0L)
+ .ctx = ctx, "SELECT * FROM (SELECT 1 as a) a WHERE (0 = 1)"
+ )
+ with_result(
+ dbSendQuery(con, query),
+ {
+ rc <- dbGetRowCount(res)
+ #' zero is returned
+ expect_equal(rc, 0L)
+ check_df(dbFetch(res))
+ rc <- dbGetRowCount(res)
+ #' even after fetching.
+ expect_equal(rc, 0L)
+ }
+ )
+ })
+ },
+
+ row_count_statement = function(ctx) {
+ with_connection({
+ name <- random_table_name()
+
+ with_remove_test_table(name = name, {
+ query <- paste0("CREATE TABLE ", name, " (a integer)")
+ with_result(
+ #' For data manipulation statements issued with
+ #' [dbSendStatement()],
+ dbSendStatement(con, query),
+ {
+ rc <- dbGetRowCount(res)
+ #' zero is returned before
+ expect_equal(rc, 0L)
+ expect_warning(check_df(dbFetch(res)))
+ rc <- dbGetRowCount(res)
+ #' and after calling `dbFetch()`.
+ expect_equal(rc, 0L)
+ }
+ )
+ })
+ })
+ },
+
+ get_row_count_error = function(ctx) {
+ with_connection({
+ res <- dbSendQuery(con, "SELECT 1")
+ dbClearResult(res)
+ #' Attempting to get the row count for a result set cleared with
+ #' [dbClearResult()] gives an error.
+ expect_error(dbGetRowCount(res))
})
},
- #' }
NULL
)
diff --git a/R/spec-meta-get-rows-affected.R b/R/spec-meta-get-rows-affected.R
index 0d4ce77..c08342a 100644
--- a/R/spec-meta-get-rows-affected.R
+++ b/R/spec-meta-get-rows-affected.R
@@ -1,42 +1,75 @@
-#' @template dbispec-sub-wip
+#' spec_meta_get_rows_affected
+#' @usage NULL
#' @format NULL
-#' @section Meta:
-#' \subsection{`dbGetRowsAffected("DBIResult")`}{
+#' @keywords NULL
spec_meta_get_rows_affected <- list(
- #' Information on affected rows is correct.
- rows_affected = function(ctx) {
- with_connection({
- expect_error(dbGetQuery(con, "SELECT * FROM iris"))
- on.exit(expect_error(dbExecute(con, "DROP TABLE iris"), NA),
- add = TRUE)
+ get_rows_affected_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbGetRowsAffected)), c("res", "..."))
+ },
- iris <- get_iris(ctx)
- dbWriteTable(con, "iris", iris)
+ #' @return
+ #' `dbGetRowsAffected()` returns a scalar number (integer or numeric),
+ #' the number of rows affected by a data manipulation statement
+ rows_affected_statement = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ dbWriteTable(con, "test", data.frame(a = 1:10))
- local({
query <- paste0(
- "DELETE FROM iris WHERE (",
- dbQuoteIdentifier(con, "Species"),
- " = ", dbQuoteString(con, "versicolor"),
- ")")
- res <- dbSendStatement(con, query)
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
- ra <- dbGetRowsAffected(res)
-
- expect_identical(ra, sum(iris$Species == "versicolor"))
+ "DELETE FROM ", dbQuoteIdentifier(con, "test"), " ",
+ "WHERE a < 6"
+ )
+ with_result(
+ #' issued with [dbSendStatement()].
+ dbSendStatement(con, query),
+ {
+ rc <- dbGetRowsAffected(res)
+ #' The value is available directly after the call
+ expect_equal(rc, 5L)
+ expect_warning(check_df(dbFetch(res)))
+ rc <- dbGetRowsAffected(res)
+ #' and does not change after calling [dbFetch()].
+ expect_equal(rc, 5L)
+ }
+ )
})
+ })
+ },
- local({
- query <- "DELETE FROM iris WHERE (0 = 1)"
- res <- dbSendStatement(con, query)
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
- ra <- dbGetRowsAffected(res)
+ rows_affected_query = function(ctx) {
+ with_connection({
+ query <- "SELECT 1 as a"
+ with_result(
+ #' For queries issued with [dbSendQuery()],
+ dbSendQuery(con, query),
+ {
+ rc <- dbGetRowsAffected(res)
+ #' zero is returned before
+ expect_equal(rc, 0L)
+ check_df(dbFetch(res))
+ rc <- dbGetRowsAffected(res)
+ #' and after the call to `dbFetch()`.
+ expect_equal(rc, 0L)
+ }
+ )
+ })
+ },
- expect_identical(ra, 0L)
+ get_rows_affected_error = function(ctx) {
+ with_connection({
+ query <- paste0(
+ "CREATE TABLE ", dbQuoteIdentifier(con, "test"), " (a integer)"
+ )
+ with_remove_test_table({
+ res <- dbSendStatement(con, query)
+ dbClearResult(res)
+ #' Attempting to get the rows affected for a result set cleared with
+ #' [dbClearResult()] gives an error.
+ expect_error(dbGetRowsAffected(res))
})
})
},
- #' }
NULL
)
diff --git a/R/spec-meta-get-statement.R b/R/spec-meta-get-statement.R
index 10909ab..3c2a39e 100644
--- a/R/spec-meta-get-statement.R
+++ b/R/spec-meta-get-statement.R
@@ -1,20 +1,60 @@
-#' @template dbispec-sub-wip
+#' spec_meta_get_statement
+#' @usage NULL
#' @format NULL
-#' @section Meta:
-#' \subsection{`dbGetStatement("DBIResult")`}{
+#' @keywords NULL
spec_meta_get_statement <- list(
- #' SQL query can be retrieved from the result.
- get_statement = function(ctx) {
+ get_statement_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbGetStatement)), c("res", "..."))
+ },
+
+ #' @return
+ #' `dbGetStatement()` returns a string, the query used in
+ get_statement_query = function(ctx) {
with_connection({
query <- "SELECT 1 as a"
- res <- dbSendQuery(con, query)
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
- s <- dbGetStatement(res)
- expect_is(s, "character")
- expect_identical(s, query)
+ with_result(
+ #' either [dbSendQuery()]
+ dbSendQuery(con, query),
+ {
+ s <- dbGetStatement(res)
+ expect_is(s, "character")
+ expect_identical(s, query)
+ }
+ )
+ })
+ },
+
+ get_statement_statement = function(ctx) {
+ with_connection({
+ name <- random_table_name()
+
+ with_connection({
+ with_remove_test_table(name = name, {
+ query <- paste0("CREATE TABLE ", name, " (a integer)")
+ with_result(
+ #' or [dbSendStatement()].
+ dbSendQuery(con, query),
+ {
+ s <- dbGetStatement(res)
+ expect_is(s, "character")
+ expect_identical(s, query)
+ }
+ )
+ })
+ })
+ })
+ },
+
+ get_statement_error = function(ctx) {
+ with_connection({
+ res <- dbSendQuery(con, "SELECT 1")
+ dbClearResult(res)
+ #' Attempting to query the statement for a result set cleared with
+ #' [dbClearResult()] gives an error.
+ expect_error(dbGetStatement(res))
})
},
- #' }
NULL
)
diff --git a/R/spec-meta-has-completed.R b/R/spec-meta-has-completed.R
new file mode 100644
index 0000000..a8f2e4c
--- /dev/null
+++ b/R/spec-meta-has-completed.R
@@ -0,0 +1,88 @@
+#' spec_meta_has_completed
+#' @usage NULL
+#' @format NULL
+#' @keywords NULL
+spec_meta_has_completed <- list(
+ has_completed_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbHasCompleted)), c("res", "..."))
+ },
+
+ #' @return
+ #' `dbHasCompleted()` returns a logical scalar.
+ has_completed_query = function(ctx) {
+ with_connection({
+ #' For a query initiated by [dbSendQuery()] with non-empty result set,
+ with_result(
+ dbSendQuery(con, "SELECT 1"),
+ {
+ #' `dbHasCompleted()` returns `FALSE` initially
+ expect_false(expect_visible(dbHasCompleted(res)))
+ #' and `TRUE` after calling [dbFetch()] without limit.
+ check_df(dbFetch(res))
+ expect_true(expect_visible(dbHasCompleted(res)))
+ }
+ )
+ })
+ },
+
+ has_completed_statement = function(ctx) {
+ with_connection({
+ name <- random_table_name()
+
+ with_remove_test_table(name = name, {
+ #' For a query initiated by [dbSendStatement()],
+ with_result(
+ dbSendQuery(con, paste0("CREATE TABLE ", name, " (a integer)")),
+ {
+ #' `dbHasCompleted()` always returns `TRUE`.
+ expect_true(expect_visible(dbHasCompleted(res)))
+ }
+ )
+ })
+ })
+ },
+
+ has_completed_error = function(ctx) {
+ with_connection({
+ res <- dbSendQuery(con, "SELECT 1")
+ dbClearResult(res)
+ #' Attempting to query completion status for a result set cleared with
+ #' [dbClearResult()] gives an error.
+ expect_error(dbHasCompleted(res))
+ })
+ },
+
+ #' @section Specification:
+ has_completed_query_spec = function(ctx) {
+ with_connection({
+ #' The completion status for a query is only guaranteed to be set to
+ #' `FALSE` after attempting to fetch past the end of the entire result.
+ #' Therefore, for a query with an empty result set,
+ with_result(
+ dbSendQuery(con, "SELECT * FROM (SELECT 1 as a) AS x WHERE (1 = 0)"),
+ {
+ #' the initial return value is unspecified,
+ #' but the result value is `TRUE` after trying to fetch only one row.
+ check_df(dbFetch(res, 1))
+ expect_true(expect_visible(dbHasCompleted(res)))
+ }
+ )
+
+ #' Similarly, for a query with a result set of length n,
+ with_result(
+ dbSendQuery(con, "SELECT 1"),
+ {
+ #' the return value is unspecified after fetching n rows,
+ check_df(dbFetch(res, 1))
+ #' but the result value is `TRUE` after trying to fetch only one more
+ #' row.
+ check_df(dbFetch(res, 1))
+ expect_true(expect_visible(dbHasCompleted(res)))
+ }
+ )
+ })
+ },
+
+ NULL
+)
diff --git a/R/spec-meta-is-valid-connection.R b/R/spec-meta-is-valid-connection.R
deleted file mode 100644
index 27d865b..0000000
--- a/R/spec-meta-is-valid-connection.R
+++ /dev/null
@@ -1,16 +0,0 @@
-#' @template dbispec-sub-wip
-#' @format NULL
-#' @section Meta:
-#' \subsection{`dbIsValid("DBIConnection")`}{
-spec_meta_is_valid_connection <- list(
- #' Only an open connection is valid.
- is_valid_connection = function(ctx) {
- con <- connect(ctx)
- expect_true(dbIsValid(con))
- expect_error(dbDisconnect(con), NA)
- expect_false(dbIsValid(con))
- },
-
- #' }
- NULL
-)
diff --git a/R/spec-meta-is-valid-result.R b/R/spec-meta-is-valid-result.R
deleted file mode 100644
index 8b07610..0000000
--- a/R/spec-meta-is-valid-result.R
+++ /dev/null
@@ -1,21 +0,0 @@
-#' @template dbispec-sub-wip
-#' @format NULL
-#' @section Meta:
-#' \subsection{`dbIsValid("DBIResult")`}{
-spec_meta_is_valid_result <- list(
- #' Only an open result set is valid.
- is_valid_result = function(ctx) {
- with_connection({
- query <- "SELECT 1 as a"
- res <- dbSendQuery(con, query)
- expect_true(dbIsValid(res))
- expect_error(dbFetch(res), NA)
- expect_true(dbIsValid(res))
- dbClearResult(res)
- expect_false(dbIsValid(res))
- })
- },
-
- #' }
- NULL
-)
diff --git a/R/spec-meta-is-valid.R b/R/spec-meta-is-valid.R
new file mode 100644
index 0000000..656fd75
--- /dev/null
+++ b/R/spec-meta-is-valid.R
@@ -0,0 +1,61 @@
+#' spec_meta_is_valid
+#' @usage NULL
+#' @format NULL
+#' @keywords NULL
+spec_meta_is_valid <- list(
+ is_valid_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbIsValid)), c("dbObj", "..."))
+ },
+
+ #' @return
+ #' `dbIsValid()` returns a logical scalar,
+ #' `TRUE` if the object specified by `dbObj` is valid,
+ #' `FALSE` otherwise.
+ is_valid_connection = function(ctx) {
+ con <- connect(ctx)
+ #' A [DBIConnection-class] object is initially valid,
+ expect_true(expect_visible(dbIsValid(con)))
+ expect_error(dbDisconnect(con), NA)
+ #' and becomes invalid after disconnecting with [dbDisconnect()].
+ expect_false(expect_visible(dbIsValid(con)))
+ },
+
+ is_valid_result_query = function(ctx) {
+ with_connection({
+ query <- "SELECT 1 as a"
+ res <- dbSendQuery(con, query)
+ #' A [DBIResult-class] object is valid after a call to [dbSendQuery()],
+ expect_true(expect_visible(dbIsValid(res)))
+ expect_error(dbFetch(res), NA)
+ #' and stays valid even after all rows have been fetched;
+ expect_true(expect_visible(dbIsValid(res)))
+ dbClearResult(res)
+ #' only clearing it with [dbClearResult()] invalidates it.
+ expect_false(dbIsValid(res))
+ })
+ },
+
+ is_valid_result_statement = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ query <- paste0("CREATE TABLE test (a ", dbDataType(con, 1L), ")")
+ res <- dbSendStatement(con, query)
+ #' A [DBIResult-class] object is also valid after a call to [dbSendStatement()],
+ expect_true(expect_visible(dbIsValid(res)))
+ #' and stays valid after querying the number of rows affected;
+ expect_error(dbGetRowsAffected(res), NA)
+ expect_true(expect_visible(dbIsValid(res)))
+ dbClearResult(res)
+ #' only clearing it with [dbClearResult()] invalidates it.
+ expect_false(dbIsValid(res))
+ })
+ })
+ },
+
+ #' If the connection to the database system is dropped (e.g., due to
+ #' connectivity problems, server failure, etc.), `dbIsValid()` should return
+ #' `FALSE`. This is not tested automatically.
+
+ NULL
+)
diff --git a/R/spec-meta.R b/R/spec-meta.R
index 1f9a3eb..ff805b4 100644
--- a/R/spec-meta.R
+++ b/R/spec-meta.R
@@ -1,17 +1,14 @@
#' @template dbispec
#' @format NULL
spec_meta <- c(
- spec_meta_is_valid_connection,
- spec_meta_is_valid_result,
+ spec_meta_bind,
+ spec_meta_is_valid,
+ spec_meta_has_completed,
spec_meta_get_statement,
spec_meta_column_info,
spec_meta_get_row_count,
spec_meta_get_rows_affected,
spec_meta_get_info_result,
- spec_meta_bind,
- spec_meta_bind_multi_row,
-
- # dbHasCompleted tested in test_result
# no 64-bit or time input data type yet
diff --git a/R/spec-result-clear-result.R b/R/spec-result-clear-result.R
new file mode 100644
index 0000000..2360e75
--- /dev/null
+++ b/R/spec-result-clear-result.R
@@ -0,0 +1,62 @@
+#' spec_result_clear_result
+#' @usage NULL
+#' @format NULL
+#' @keywords NULL
+spec_result_clear_result <- list(
+ clear_result_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbClearResult)), c("res", "..."))
+ },
+
+ #' @return
+ #' `dbClearResult()` returns `TRUE`, invisibly, for result sets obtained from
+ #' both `dbSendQuery()`
+ clear_result_return_query = function(ctx) {
+ with_connection({
+ res <- dbSendQuery(con, "SELECT 1")
+ expect_invisible_true(dbClearResult(res))
+ })
+ },
+
+ #' and `dbSendStatement()`.
+ clear_result_return_statement = function(ctx) {
+ with_connection({
+ table_name <- random_table_name()
+
+ with_remove_test_table(name = table_name, {
+ res <- dbSendStatement(con, paste0("CREATE TABLE ", table_name , " AS SELECT 1"))
+ expect_invisible_true(dbClearResult(res))
+ })
+ })
+ },
+
+ #' An attempt to close an already closed result set issues a warning
+ cannot_clear_result_twice_query = function(ctx) {
+ with_connection({
+ res <- dbSendQuery(con, "SELECT 1")
+ dbClearResult(res)
+ expect_warning(expect_invisible_true(dbClearResult(res)))
+ })
+ },
+
+ #' in both cases.
+ cannot_clear_result_twice_statement = function(ctx) {
+ table_name <- random_table_name()
+ with_connection({
+ with_remove_test_table(
+ name = table_name,
+ {
+ res <- dbSendStatement(con, paste0("CREATE TABLE ", table_name , " AS SELECT 1"))
+ dbClearResult(res)
+ expect_warning(expect_invisible_true(dbClearResult(res)))
+ })
+ })
+ },
+
+ #' @section Specification:
+ #' `dbClearResult()` frees all resources associated with retrieving
+ #' the result of a query or update operation.
+ #' The DBI backend can expect a call to `dbClearResult()` for each
+ #' [dbSendQuery()] or [dbSendStatement()] call.
+ NULL
+)
diff --git a/R/spec-result-create-table-with-data-type.R b/R/spec-result-create-table-with-data-type.R
index 0b6cce9..50e9ec0 100644
--- a/R/spec-result-create-table-with-data-type.R
+++ b/R/spec-result-create-table-with-data-type.R
@@ -1,36 +1,19 @@
-#' @template dbispec-sub-wip
+#' spec_result_create_table_with_data_type
+#' @usage NULL
#' @format NULL
-#' @section Result:
-#' \subsection{Create table with data type}{
+#' @keywords NULL
spec_result_create_table_with_data_type <- list(
- #' SQL Data types exist for all basic R data types, and the engine can
- #' process them.
- data_type_connection = function(ctx) {
+ #' @section Specification:
+ #' All data types returned by `dbDataType()` are usable in an SQL statement
+ #' of the form
+ data_type_create_table = function(ctx) {
with_connection({
check_connection_data_type <- function(value) {
- eval(bquote({
- expect_is(dbDataType(con, .(value)), "character")
- expect_equal(length(dbDataType(con, .(value))), 1L)
- expect_error({
- as_is_type <- dbDataType(con, I(.(value)))
- expect_identical(dbDataType(con, .(value)), as_is_type)
- }
- , NA)
- expect_error({
- unknown_type <- dbDataType(con, structure(.(value),
- class = "unknown1"))
- expect_identical(dbDataType(con, unclass(.(value))), unknown_type)
- }
- , NA)
- query <- paste0("CREATE TABLE test (a ", dbDataType(con, .(value)),
- ")")
- }))
-
- eval(bquote({
- expect_error(dbExecute(con, .(query)), NA)
- on.exit(expect_error(dbExecute(con, "DROP TABLE test"), NA),
- add = TRUE)
- }))
+ with_remove_test_table({
+ #' `"CREATE TABLE test (a ...)"`.
+ query <- paste0("CREATE TABLE test (a ", dbDataType(con, value), ")")
+ eval(bquote(dbExecute(con, .(query))))
+ })
}
expect_conn_has_data_type <- function(value) {
@@ -45,21 +28,10 @@ spec_result_create_table_with_data_type <- list(
expect_conn_has_data_type(Sys.Date())
expect_conn_has_data_type(Sys.time())
if (!isTRUE(ctx$tweaks$omit_blob_tests)) {
- expect_conn_has_data_type(list(raw(1)))
+ expect_conn_has_data_type(list(as.raw(1:10)))
}
})
},
- #' SQL data type for factor is the same as for character.
- data_type_factor = function(ctx) {
- with_connection({
- expect_identical(dbDataType(con, letters),
- dbDataType(con, factor(letters)))
- expect_identical(dbDataType(con, letters),
- dbDataType(con, ordered(letters)))
- })
- },
-
- #' }
NULL
)
diff --git a/R/spec-result-execute.R b/R/spec-result-execute.R
new file mode 100644
index 0000000..d103385
--- /dev/null
+++ b/R/spec-result-execute.R
@@ -0,0 +1,69 @@
+#' spec_result_execute
+#' @usage NULL
+#' @format NULL
+#' @keywords NULL
+spec_result_execute <- list(
+ execute_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbExecute)), c("conn", "statement", "..."))
+ },
+
+ #' @return
+ #' `dbExecute()` always returns a
+ execute_atomic = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ query <- "CREATE TABLE test AS SELECT 1 AS a"
+
+ ret <- dbExecute(con, query)
+ #' scalar
+ expect_equal(length(ret), 1)
+ #' numeric
+ expect_true(is.numeric(ret))
+ #' that specifies the number of rows affected
+ #' by the statement.
+ })
+ })
+ },
+
+ #' An error is raised when issuing a statement over a closed
+ execute_closed_connection = function(ctx) {
+ with_closed_connection({
+ expect_error(dbExecute(con, "CREATE TABLE test AS SELECT 1 AS a"))
+ })
+ },
+
+ #' or invalid connection,
+ execute_invalid_connection = function(ctx) {
+ with_invalid_connection({
+ expect_error(dbExecute(con, "CREATE TABLE test AS SELECT 1 AS a"))
+ })
+ },
+
+ #' if the syntax of the statement is invalid,
+ execute_syntax_error = function(ctx) {
+ with_connection({
+ expect_error(dbExecute(con, "CREATE"))
+ })
+ },
+
+ #' or if the statement is not a non-`NA` string.
+ execute_non_string = function(ctx) {
+ with_connection({
+ expect_error(dbExecute(con, character()))
+ expect_error(dbExecute(con, letters))
+ expect_error(dbExecute(con, NA_character_))
+ })
+ },
+
+ #' @section Additional arguments:
+ #' The following argument is not part of the `dbExecute()` generic
+ #' (to improve compatibility across backends)
+ #' but is part of the DBI specification:
+ #' - `params` (TBD)
+ #'
+ #' They must be provided as named arguments.
+ #' See the "Specification" section for details on its usage.
+
+ NULL
+)
diff --git a/R/spec-result-fetch.R b/R/spec-result-fetch.R
index 5208979..6fe9ed3 100644
--- a/R/spec-result-fetch.R
+++ b/R/spec-result-fetch.R
@@ -1,162 +1,270 @@
-#' @template dbispec-sub-wip
+#' spec_result_fetch
+#' @usage NULL
#' @format NULL
-#' @section Result:
-#' \subsection{`dbFetch("DBIResult")` and `dbHasCompleted("DBIResult")`}{
+#' @keywords NULL
spec_result_fetch <- list(
- #' Single-value queries can be fetched.
- fetch_single = function(ctx) {
+ fetch_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbFetch)), c("res", "n", "..."))
+ },
+
+ #' @return
+ #' `dbFetch()` always returns a [data.frame]
+ #' with as many rows as records were fetched and as many
+ #' columns as fields in the result set,
+ #' even if the result is a single value
+ fetch_atomic = function(ctx) {
with_connection({
query <- "SELECT 1 as a"
+ with_result(
+ dbSendQuery(con, query),
+ {
+ rows <- check_df(dbFetch(res))
+ expect_identical(rows, data.frame(a = 1L))
+ }
+ )
+ })
+ },
- res <- dbSendQuery(con, query)
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
-
- expect_false(dbHasCompleted(res))
+ #' or has one
+ fetch_one_row = function(ctx) {
+ with_connection({
+ query <- "SELECT 1 as a, 2 as b, 3 as c"
+ with_result(
+ dbSendQuery(con, query),
+ {
+ rows <- check_df(dbFetch(res))
+ expect_identical(rows, data.frame(a = 1L, b = 2L, c = 3L))
+ }
+ )
+ })
+ },
- rows <- dbFetch(res)
- expect_identical(rows, data.frame(a=1L))
- expect_true(dbHasCompleted(res))
+ #' or zero rows.
+ fetch_zero_rows = function(ctx) {
+ with_connection({
+ query <-
+ "SELECT * FROM (SELECT 1 as a, 2 as b, 3 as c) AS x WHERE (1 = 0)"
+ with_result(
+ dbSendQuery(con, query),
+ {
+ rows <- check_df(dbFetch(res))
+ expect_identical(class(rows), "data.frame")
+ }
+ )
})
},
- #' Multi-row single-column queries can be fetched.
- fetch_multi_row_single_column = function(ctx) {
+ #' An attempt to fetch from a closed result set raises an error.
+ fetch_closed = function(ctx) {
with_connection({
- query <- union(
- .ctx = ctx, paste("SELECT", 1:3, "AS a"), .order_by = "a")
+ query <- "SELECT 1"
res <- dbSendQuery(con, query)
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
-
- expect_false(dbHasCompleted(res))
+ dbClearResult(res)
- rows <- dbFetch(res)
- expect_identical(rows, data.frame(a=1L:3L))
- expect_true(dbHasCompleted(res))
+ expect_error(dbFetch(res))
})
},
- #' Multi-row queries can be fetched progressively.
- fetch_progressive = function(ctx) {
+ #' If the `n` argument is not an atomic whole number
+ #' greater or equal to -1 or Inf, an error is raised,
+ fetch_n_bad = function(ctx) {
with_connection({
- query <- union(
- .ctx = ctx, paste("SELECT", 1:25, "AS a"), .order_by = "a")
-
- res <- dbSendQuery(con, query)
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
-
- expect_false(dbHasCompleted(res))
+ query <- "SELECT 1 as a"
+ with_result(
+ dbSendQuery(con, query),
+ {
+ expect_error(dbFetch(res, -2))
+ expect_error(dbFetch(res, 1.5))
+ expect_error(dbFetch(res, integer()))
+ expect_error(dbFetch(res, 1:3))
+ expect_error(dbFetch(res, NA_integer_))
+ }
+ )
+ })
+ },
- rows <- dbFetch(res, 10)
- expect_identical(rows, data.frame(a=1L:10L))
- expect_false(dbHasCompleted(res))
+ #' but a subsequent call to `dbFetch()` with proper `n` argument succeeds.
+ fetch_n_good_after_bad = function(ctx) {
+ with_connection({
+ query <- "SELECT 1 as a"
+ with_result(
+ dbSendQuery(con, query),
+ {
+ expect_error(dbFetch(res, NA_integer_))
+ rows <- check_df(dbFetch(res))
+ expect_identical(rows, data.frame(a = 1L))
+ }
+ )
+ })
+ },
- rows <- dbFetch(res, 10)
- expect_identical(rows, data.frame(a=11L:20L))
- expect_false(dbHasCompleted(res))
+ #' Calling `dbFetch()` on a result set from a data manipulation query
+ #' created by [dbSendStatement()]
+ #' can be fetched and return an empty data frame, with a warning.
+ fetch_no_return_value = function(ctx) {
+ with_connection({
+ query <- "CREATE TABLE test (a integer)"
- rows <- dbFetch(res, 10)
- expect_identical(rows, data.frame(a=21L:25L))
- expect_true(dbHasCompleted(res))
+ with_remove_test_table({
+ with_result(
+ dbSendStatement(con, query),
+ {
+ expect_warning(rows <- check_df(dbFetch(res)))
+ expect_identical(rows, data.frame())
+ }
+ )
+ })
})
},
- #' If more rows than available are fetched, the result is returned in full
- #' but no warning is issued.
- fetch_more_rows = function(ctx) {
+ #' @section Specification:
+ #' Fetching multi-row queries with one
+ fetch_multi_row_single_column = function(ctx) {
with_connection({
query <- union(
.ctx = ctx, paste("SELECT", 1:3, "AS a"), .order_by = "a")
- res <- dbSendQuery(con, query)
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
-
- expect_false(dbHasCompleted(res))
-
- expect_warning(rows <- dbFetch(res, 5L), NA)
- expect_identical(rows, data.frame(a=1L:3L))
- expect_true(dbHasCompleted(res))
+ with_result(
+ dbSendQuery(con, query),
+ {
+ rows <- check_df(dbFetch(res))
+ expect_identical(rows, data.frame(a = 1:3))
+ }
+ )
})
},
- #' If zero rows are fetched, the result is still fully typed.
- fetch_zero_rows = function(ctx) {
+ #' or more columns be default returns the entire result.
+ fetch_multi_row_multi_column = function(ctx) {
with_connection({
query <- union(
- .ctx = ctx, paste("SELECT", 1:3, "AS a"), .order_by = "a")
-
- res <- dbSendQuery(con, query)
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
+ .ctx = ctx, paste("SELECT", 1:5, "AS a,", 4:0, "AS b"), .order_by = "a")
+
+ with_result(
+ dbSendQuery(con, query),
+ {
+ rows <- check_df(dbFetch(res))
+ expect_identical(rows, data.frame(a = 1:5, b = 4:0))
+ }
+ )
+ })
+ },
- expect_warning(rows <- dbFetch(res, 0L), NA)
- expect_identical(rows, data.frame(a=integer()))
+ #' Multi-row queries can also be fetched progressively
+ fetch_n_progressive = function(ctx) {
+ with_connection({
+ query <- union(
+ .ctx = ctx, paste("SELECT", 1:25, "AS a"), .order_by = "a")
- expect_warning(dbClearResult(res), NA)
- on.exit(NULL, add = FALSE)
+ with_result(
+ dbSendQuery(con, query),
+ {
+ #' by passing a whole number ([integer]
+ rows <- check_df(dbFetch(res, 10L))
+ expect_identical(rows, data.frame(a = 1L:10L))
+
+ #' or [numeric])
+ rows <- check_df(dbFetch(res, 10))
+ expect_identical(rows, data.frame(a = 11L:20L))
+
+ #' as the `n` argument.
+ rows <- check_df(dbFetch(res, n = 5))
+ expect_identical(rows, data.frame(a = 21L:25L))
+ }
+ )
})
},
- #' If less rows than available are fetched, the result is returned in full
- #' but no warning is issued.
- fetch_premature_close = function(ctx) {
+ #' A value of [Inf] for the `n` argument is supported
+ #' and also returns the full result.
+ fetch_n_multi_row_inf = function(ctx) {
with_connection({
query <- union(
.ctx = ctx, paste("SELECT", 1:3, "AS a"), .order_by = "a")
- res <- dbSendQuery(con, query)
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
-
- expect_warning(rows <- dbFetch(res, 2L), NA)
- expect_identical(rows, data.frame(a=1L:2L))
-
- expect_warning(dbClearResult(res), NA)
- on.exit(NULL, add = FALSE)
+ with_result(
+ dbSendQuery(con, query),
+ {
+ rows <- check_df(dbFetch(res, n = Inf))
+ expect_identical(rows, data.frame(a = 1:3))
+ }
+ )
})
},
- #' Side-effect-only queries (without return value) can be fetched.
- fetch_no_return_value = function(ctx) {
+ #' If more rows than available are fetched, the result is returned in full
+ #' without warning.
+ fetch_n_more_rows = function(ctx) {
with_connection({
- query <- "CREATE TABLE test (a integer)"
-
- res <- dbSendStatement(con, query)
- on.exit({
- expect_error(dbClearResult(res), NA)
- expect_error(dbClearResult(dbSendStatement(con, "DROP TABLE test")), NA)
- }
- , add = TRUE)
-
- expect_true(dbHasCompleted(res))
-
- rows <- dbFetch(res)
- expect_identical(rows, data.frame())
+ query <- union(
+ .ctx = ctx, paste("SELECT", 1:3, "AS a"), .order_by = "a")
- expect_true(dbHasCompleted(res))
+ with_result(
+ dbSendQuery(con, query),
+ {
+ rows <- check_df(dbFetch(res, 5L))
+ expect_identical(rows, data.frame(a = 1:3))
+ #' If fewer rows than requested are returned, further fetches will
+ #' return a data frame with zero rows.
+ rows <- check_df(dbFetch(res))
+ expect_identical(rows, data.frame(a = integer()))
+ }
+ )
})
},
- #' Fetching from a closed result set raises an error.
- fetch_closed = function(ctx) {
+ #' If zero rows are fetched, the columns of the data frame are still fully
+ #' typed.
+ fetch_n_zero_rows = function(ctx) {
with_connection({
- query <- "SELECT 1"
+ query <- union(
+ .ctx = ctx, paste("SELECT", 1:3, "AS a"), .order_by = "a")
- res <- dbSendQuery(con, query)
- dbClearResult(res)
+ with_result(
+ dbSendQuery(con, query),
+ {
+ rows <- check_df(dbFetch(res, 0L))
+ expect_identical(rows, data.frame(a = integer()))
+ }
+ )
+ })
+ },
- expect_error(dbHasCompleted(res))
+ #' Fetching fewer rows than available is permitted,
+ #' no warning is issued when clearing the result set.
+ fetch_n_premature_close = function(ctx) {
+ with_connection({
+ query <- union(
+ .ctx = ctx, paste("SELECT", 1:3, "AS a"), .order_by = "a")
- expect_error(dbFetch(res))
+ with_result(
+ dbSendQuery(con, query),
+ {
+ rows <- check_df(dbFetch(res, 2L))
+ expect_identical(rows, data.frame(a = 1:2))
+ }
+ )
})
},
- #' Querying a disconnected connection throws error.
- cannot_query_disconnected = function(ctx) {
- # TODO: Rename to fetch_disconnected
- con <- connect(ctx)
- dbDisconnect(con)
- expect_error(dbGetQuery(con, "SELECT 1"))
+ #'
+ #' A column named `row_names` is treated like any other column.
+ fetch_row_names = function(ctx) {
+ with_connection({
+ query <- "SELECT 1 AS row_names"
+
+ with_result(
+ dbSendQuery(con, query),
+ {
+ rows <- check_df(dbFetch(res))
+ expect_identical(rows, data.frame(row_names = 1L))
+ expect_identical(.row_names_info(rows), -1L)
+ }
+ )
+ })
},
- #' }
NULL
)
diff --git a/R/spec-result-get-query.R b/R/spec-result-get-query.R
index 89503ca..33b909c 100644
--- a/R/spec-result-get-query.R
+++ b/R/spec-result-get-query.R
@@ -1,79 +1,196 @@
-# TODO: Decide where to put this, it's a connection method but requires result methods to be implemented
-
-#' @template dbispec-sub-wip
+#' spec_result_get_query
+#' @usage NULL
#' @format NULL
-#' @section Result:
-#' \subsection{`dbGetQuery("DBIConnection", "ANY")`}{
+#' @keywords NULL
spec_result_get_query <- list(
- #' Single-value queries can be read with dbGetQuery
- get_query_single = function(ctx) {
+ get_query_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbGetQuery)), c("conn", "statement", "..."))
+ },
+
+ #' @return
+ #' `dbGetQuery()` always returns a [data.frame]
+ #' with as many rows as records were fetched and as many
+ #' columns as fields in the result set,
+ #' even if the result is a single value
+ get_query_atomic = function(ctx) {
with_connection({
query <- "SELECT 1 as a"
- rows <- dbGetQuery(con, query)
+ rows <- check_df(dbGetQuery(con, query))
expect_identical(rows, data.frame(a=1L))
})
},
- #' Multi-row single-column queries can be read with dbGetQuery.
+ #' or has one
+ get_query_one_row = function(ctx) {
+ with_connection({
+ query <- "SELECT 1 as a, 2 as b, 3 as c"
+
+ rows <- check_df(dbGetQuery(con, query))
+ expect_identical(rows, data.frame(a=1L, b=2L, c=3L))
+ })
+ },
+
+ #' or zero rows.
+ get_query_zero_rows = function(ctx) {
+ with_connection({
+ # Not all SQL dialects seem to support the query used here.
+ query <-
+ "SELECT * FROM (SELECT 1 as a, 2 as b, 3 as c) AS x WHERE (1 = 0)"
+
+ rows <- check_df(dbGetQuery(con, query))
+ expect_identical(names(rows), letters[1:3])
+ expect_identical(dim(rows), c(0L, 3L))
+ })
+ },
+
+
+ #' An error is raised when issuing a query over a closed
+ get_query_closed_connection = function(ctx) {
+ with_closed_connection({
+ expect_error(dbGetQuery(con, "SELECT 1"))
+ })
+ },
+
+ #' or invalid connection,
+ get_query_invalid_connection = function(ctx) {
+ with_invalid_connection({
+ expect_error(dbGetQuery(con, "SELECT 1"))
+ })
+ },
+
+ #' if the syntax of the query is invalid,
+ get_query_syntax_error = function(ctx) {
+ with_connection({
+ expect_error(dbGetQuery(con, "SELECT"))
+ })
+ },
+
+ #' or if the query is not a non-`NA` string.
+ get_query_non_string = function(ctx) {
+ with_connection({
+ expect_error(dbGetQuery(con, character()))
+ expect_error(dbGetQuery(con, letters))
+ expect_error(dbGetQuery(con, NA_character_))
+ })
+ },
+
+ #' If the `n` argument is not an atomic whole number
+ #' greater or equal to -1 or Inf, an error is raised,
+ get_query_n_bad = function(ctx) {
+ with_connection({
+ query <- "SELECT 1 as a"
+ expect_error(dbGetQuery(con, query, -2))
+ expect_error(dbGetQuery(con, query, 1.5))
+ expect_error(dbGetQuery(con, query, integer()))
+ expect_error(dbGetQuery(con, query, 1:3))
+ expect_error(dbGetQuery(con, query, NA_integer_))
+ })
+ },
+
+ #' but a subsequent call to `dbGetQuery()` with proper `n` argument succeeds.
+ get_query_good_after_bad_n = function(ctx) {
+ with_connection({
+ query <- "SELECT 1 as a"
+ expect_error(dbGetQuery(con, query, NA_integer_))
+ rows <- check_df(dbGetQuery(con, query))
+ expect_identical(rows, data.frame(a = 1L))
+ })
+ },
+
+ #' @section Additional arguments:
+ #' The following arguments are not part of the `dbGetQuery()` generic
+ #' (to improve compatibility across backends)
+ #' but are part of the DBI specification:
+ #' - `n` (default: -1)
+ #' - `params` (TBD)
+ #'
+ #' They must be provided as named arguments.
+ #' See the "Specification" and "Value" sections for details on their usage.
+
+ #' @section Specification:
+ #' Fetching multi-row queries with one
get_query_multi_row_single_column = function(ctx) {
with_connection({
query <- union(
.ctx = ctx, paste("SELECT", 1:3, "AS a"), .order_by = "a")
- rows <- dbGetQuery(con, query)
- expect_identical(rows, data.frame(a=1L:3L))
+ rows <- check_df(dbGetQuery(con, query))
+ expect_identical(rows, data.frame(a = 1:3))
})
},
- #' Empty single-column queries can be read with
- #' [DBI::dbGetQuery()]. Not all SQL dialects support the query
- #' used here.
- get_query_empty_single_column = function(ctx) {
+ #' or more columns be default returns the entire result.
+ get_query_multi_row_multi_column = function(ctx) {
with_connection({
- query <- "SELECT * FROM (SELECT 1 as a) AS x WHERE (1 = 0)"
+ query <- union(
+ .ctx = ctx, paste("SELECT", 1:5, "AS a,", 4:0, "AS b"), .order_by = "a")
- rows <- dbGetQuery(con, query)
- expect_identical(names(rows), "a")
- expect_identical(dim(rows), c(0L, 1L))
+ rows <- check_df(dbGetQuery(con, query))
+ expect_identical(rows, data.frame(a = 1:5, b = 4:0))
})
},
- #' Single-row multi-column queries can be read with dbGetQuery.
- get_query_single_row_multi_column = function(ctx) {
+ #' A value of [Inf] for the `n` argument is supported
+ #' and also returns the full result.
+ get_query_n_multi_row_inf = function(ctx) {
with_connection({
- query <- "SELECT 1 as a, 2 as b, 3 as c"
+ query <- union(
+ .ctx = ctx, paste("SELECT", 1:3, "AS a"), .order_by = "a")
- rows <- dbGetQuery(con, query)
- expect_identical(rows, data.frame(a=1L, b=2L, c=3L))
+ rows <- check_df(dbGetQuery(con, query, n = Inf))
+ expect_identical(rows, data.frame(a = 1:3))
})
},
- #' Multi-row multi-column queries can be read with dbGetQuery.
- get_query_multi = function(ctx) {
+ #' If more rows than available are fetched, the result is returned in full
+ #' without warning.
+ get_query_n_more_rows = function(ctx) {
with_connection({
- query <- union(.ctx = ctx, paste("SELECT", 1:2, "AS a,", 2:3, "AS b"),
- .order_by = "a")
+ query <- union(
+ .ctx = ctx, paste("SELECT", 1:3, "AS a"), .order_by = "a")
- rows <- dbGetQuery(con, query)
- expect_identical(rows, data.frame(a=1L:2L, b=2L:3L))
+ rows <- check_df(dbGetQuery(con, query, n = 5L))
+ expect_identical(rows, data.frame(a = 1:3))
})
},
- #' Empty multi-column queries can be read with
- #' [DBI::dbGetQuery()]. Not all SQL dialects support the query
- #' used here.
- get_query_empty_multi_column = function(ctx) {
+ #' If zero rows are fetched, the columns of the data frame are still fully
+ #' typed.
+ get_query_n_zero_rows = function(ctx) {
with_connection({
- query <-
- "SELECT * FROM (SELECT 1 as a, 2 as b, 3 as c) AS x WHERE (1 = 0)"
+ query <- union(
+ .ctx = ctx, paste("SELECT", 1:3, "AS a"), .order_by = "a")
- rows <- dbGetQuery(con, query)
- expect_identical(names(rows), letters[1:3])
- expect_identical(dim(rows), c(0L, 3L))
+ rows <- check_df(dbGetQuery(con, query, n = 0L))
+ expect_identical(rows, data.frame(a=integer()))
+ })
+ },
+
+ #' Fetching fewer rows than available is permitted,
+ #' no warning is issued.
+ get_query_n_incomplete = function(ctx) {
+ with_connection({
+ query <- union(
+ .ctx = ctx, paste("SELECT", 1:3, "AS a"), .order_by = "a")
+
+ rows <- check_df(dbGetQuery(con, query, n = 2L))
+ expect_identical(rows, data.frame(a = 1:2))
+ })
+ },
+
+ #'
+ #' A column named `row_names` is treated like any other column.
+ get_query_row_names = function(ctx) {
+ with_connection({
+ query <- "SELECT 1 AS row_names"
+
+ rows <- check_df(dbGetQuery(con, query))
+ expect_identical(rows, data.frame(row_names = 1L))
+ expect_identical(.row_names_info(rows), -1L)
})
},
- #' }
NULL
)
diff --git a/R/spec-result-roundtrip.R b/R/spec-result-roundtrip.R
index eedb21c..0497cde 100644
--- a/R/spec-result-roundtrip.R
+++ b/R/spec-result-roundtrip.R
@@ -1,191 +1,49 @@
-#' @template dbispec-sub-wip
+#' spec_result_roundtrip
+#' @usage NULL
#' @format NULL
-#' @section Result:
-#' \subsection{Data roundtrip}{
+#' @keywords NULL
spec_result_roundtrip <- list(
- #' Data conversion from SQL to R: integer
+ #' @section Specification:
+ #' The column types of the returned data frame depend on the data returned:
+ #' - [integer] for integer values between -2^31 and 2^31 - 1
data_integer = function(ctx) {
with_connection({
- test_select(.ctx = ctx, con, 1L, -100L)
+ test_select_with_null(.ctx = ctx, con, 1L, -100L)
})
},
- #' Data conversion from SQL to R: integer with typed NULL values.
- data_integer_null_below = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con, 1L, -100L, .add_null = "below")
- })
- },
-
- #' Data conversion from SQL to R: integer with typed NULL values
- #' in the first row.
- data_integer_null_above = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con, 1L, -100L, .add_null = "above")
- })
- },
-
- #' Data conversion from SQL to R: numeric.
+ #' - [numeric] for numbers with a fractional component
data_numeric = function(ctx) {
with_connection({
- test_select(.ctx = ctx, con, 1.5, -100.5)
- })
- },
-
- #' Data conversion from SQL to R: numeric with typed NULL values.
- data_numeric_null_below = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con, 1.5, -100.5, .add_null = "below")
- })
- },
-
- #' Data conversion from SQL to R: numeric with typed NULL values
- #' in the first row.
- data_numeric_null_above = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con, 1.5, -100.5, .add_null = "above")
+ test_select_with_null(.ctx = ctx, con, 1.5, -100.5)
})
},
- #' Data conversion from SQL to R: logical. Optional, conflict with the
- #' `data_logical_int` test.
+ #' - [logical] for Boolean values (some backends may return an integer)
data_logical = function(ctx) {
with_connection({
- test_select(.ctx = ctx, con,
- "CAST(1 AS boolean)" = TRUE, "cast(0 AS boolean)" = FALSE)
- })
- },
+ int_values <- 1:0
+ values <- ctx$tweaks$logical_return(as.logical(int_values))
- #' Data conversion from SQL to R: logical with typed NULL values.
- data_logical_null_below = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con,
- "CAST(1 AS boolean)" = TRUE, "cast(0 AS boolean)" = FALSE,
- .add_null = "below")
- })
- },
-
- #' Data conversion from SQL to R: logical with typed NULL values
- #' in the first row
- data_logical_null_above = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con,
- "CAST(1 AS boolean)" = TRUE, "cast(0 AS boolean)" = FALSE,
- .add_null = "above")
- })
- },
+ sql_names <- paste0("CAST(", int_values, " AS ", dbDataType(con, logical()), ")")
- #' Data conversion from SQL to R: logical (as integers). Optional,
- #' conflict with the `data_logical` test.
- data_logical_int = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con,
- "CAST(1 AS boolean)" = 1L, "cast(0 AS boolean)" = 0L)
- })
- },
-
- #' Data conversion from SQL to R: logical (as integers) with typed NULL
- #' values.
- data_logical_int_null_below = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con,
- "CAST(1 AS boolean)" = 1L, "cast(0 AS boolean)" = 0L,
- .add_null = "below")
+ test_select_with_null(.ctx = ctx, con, .dots = setNames(values, sql_names))
})
},
- #' Data conversion from SQL to R: logical (as integers) with typed NULL
- #' values
- #' in the first row.
- data_logical_int_null_above = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con,
- "CAST(1 AS boolean)" = 1L, "cast(0 AS boolean)" = 0L,
- .add_null = "above")
- })
- },
-
- #' Data conversion from SQL to R: A NULL value is returned as NA.
- data_null = function(ctx) {
- with_connection({
- check_result <- function(rows) {
- expect_true(is.na(rows$a))
- }
-
- test_select(.ctx = ctx, con, "NULL" = is.na)
- })
- },
-
- #' Data conversion from SQL to R: 64-bit integers.
- data_64_bit = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con,
- "10000000000" = 10000000000, "-10000000000" = -10000000000)
- })
- },
-
- #' Data conversion from SQL to R: 64-bit integers with typed NULL values.
- data_64_bit_null_below = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con,
- "10000000000" = 10000000000, "-10000000000" = -10000000000,
- .add_null = "below")
- })
- },
-
- #' Data conversion from SQL to R: 64-bit integers with typed NULL values
- #' in the first row.
- data_64_bit_null_above = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con,
- "10000000000" = 10000000000, "-10000000000" = -10000000000,
- .add_null = "above")
- })
- },
-
- #' Data conversion from SQL to R: character.
+ #' - [character] for text
data_character = function(ctx) {
with_connection({
values <- texts
test_funs <- rep(list(has_utf8_or_ascii_encoding), length(values))
sql_names <- as.character(dbQuoteString(con, texts))
- test_select(.ctx = ctx, con, .dots = setNames(values, sql_names))
- test_select(.ctx = ctx, con, .dots = setNames(test_funs, sql_names))
- })
- },
-
- #' Data conversion from SQL to R: character with typed NULL values.
- data_character_null_below = function(ctx) {
- with_connection({
- values <- texts
- test_funs <- rep(list(has_utf8_or_ascii_encoding), length(values))
- sql_names <- as.character(dbQuoteString(con, texts))
-
- test_select(.ctx = ctx, con, .dots = setNames(values, sql_names),
- .add_null = "below")
- test_select(.ctx = ctx, con, .dots = setNames(test_funs, sql_names),
- .add_null = "below")
+ test_select_with_null(.ctx = ctx, con, .dots = setNames(values, sql_names))
+ test_select_with_null(.ctx = ctx, con, .dots = setNames(test_funs, sql_names))
})
},
- #' Data conversion from SQL to R: character with typed NULL values
- #' in the first row.
- data_character_null_above = function(ctx) {
- with_connection({
- values <- texts
- test_funs <- rep(list(has_utf8_or_ascii_encoding), length(values))
- sql_names <- as.character(dbQuoteString(con, texts))
-
- test_select(.ctx = ctx, con, .dots = setNames(values, sql_names),
- .add_null = "above")
- test_select(.ctx = ctx, con, .dots = setNames(test_funs, sql_names),
- .add_null = "above")
- })
- },
-
- #' Data conversion from SQL to R: raw. Not all SQL dialects support the
- #' syntax of the query used here.
+ #' - lists of [raw] for blobs (with `NULL` entries for SQL NULL values)
data_raw = function(ctx) {
if (isTRUE(ctx$tweaks$omit_blob_tests)) {
skip("tweak: omit_blob_tests")
@@ -195,300 +53,191 @@ spec_result_roundtrip <- list(
values <- list(is_raw_list)
sql_names <- paste0("cast(1 as ", dbDataType(con, list(raw())), ")")
- test_select(.ctx = ctx, con, .dots = setNames(values, sql_names))
+ test_select_with_null(.ctx = ctx, con, .dots = setNames(values, sql_names))
})
},
- #' Data conversion from SQL to R: raw with typed NULL values.
- data_raw_null_below = function(ctx) {
- if (isTRUE(ctx$tweaks$omit_blob_tests)) {
- skip("tweak: omit_blob_tests")
- }
-
+ #' - coercible using [as.Date()] for dates
+ data_date = function(ctx) {
with_connection({
- values <- list(is_raw_list)
- sql_names <- paste0("cast(1 as ", dbDataType(con, list(raw())), ")")
+ char_values <- paste0("2015-01-", sprintf("%.2d", 1:12))
+ values <- as_date_equals_to(as.Date(char_values))
+ sql_names <- ctx$tweaks$date_cast(char_values)
- test_select(.ctx = ctx, con, .dots = setNames(values, sql_names),
- .add_null = "below")
+ test_select_with_null(.ctx = ctx, con, .dots = setNames(values, sql_names))
})
},
- #' Data conversion from SQL to R: raw with typed NULL values
- #' in the first row.
- data_raw_null_above = function(ctx) {
- if (isTRUE(ctx$tweaks$omit_blob_tests)) {
- skip("tweak: omit_blob_tests")
- }
-
+ #' (also applies to the return value of the SQL function `current_date`)
+ data_date_current = function(ctx) {
with_connection({
- values <- list(is_raw_list)
- sql_names <- paste0("cast(1 as ", dbDataType(con, list(raw())), ")")
-
- test_select(.ctx = ctx, con, .dots = setNames(values, sql_names),
- .add_null = "above")
+ test_select_with_null(
+ .ctx = ctx, con,
+ "current_date" ~ is_roughly_current_date)
})
},
- #' Data conversion from SQL to R: date, returned as integer with class.
- data_date = function(ctx) {
+ #' - coercible using [hms::as.hms()] for times
+ data_time = function(ctx) {
with_connection({
- test_select(.ctx = ctx, con,
- "date('2015-01-01')" = as_integer_date("2015-01-01"),
- "date('2015-02-02')" = as_integer_date("2015-02-02"),
- "date('2015-03-03')" = as_integer_date("2015-03-03"),
- "date('2015-04-04')" = as_integer_date("2015-04-04"),
- "date('2015-05-05')" = as_integer_date("2015-05-05"),
- "date('2015-06-06')" = as_integer_date("2015-06-06"),
- "date('2015-07-07')" = as_integer_date("2015-07-07"),
- "date('2015-08-08')" = as_integer_date("2015-08-08"),
- "date('2015-09-09')" = as_integer_date("2015-09-09"),
- "date('2015-10-10')" = as_integer_date("2015-10-10"),
- "date('2015-11-11')" = as_integer_date("2015-11-11"),
- "date('2015-12-12')" = as_integer_date("2015-12-12"),
- "current_date" ~ as_integer_date(Sys.time()))
- })
- },
+ char_values <- c("00:00:00", "12:34:56")
+ time_values <- as_hms_equals_to(hms::as.hms(char_values))
+ sql_names <- ctx$tweaks$time_cast(char_values)
- #' Data conversion from SQL to R: date with typed NULL values.
- data_date_null_below = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con,
- "date('2015-01-01')" = as_integer_date("2015-01-01"),
- "date('2015-02-02')" = as_integer_date("2015-02-02"),
- "date('2015-03-03')" = as_integer_date("2015-03-03"),
- "date('2015-04-04')" = as_integer_date("2015-04-04"),
- "date('2015-05-05')" = as_integer_date("2015-05-05"),
- "date('2015-06-06')" = as_integer_date("2015-06-06"),
- "date('2015-07-07')" = as_integer_date("2015-07-07"),
- "date('2015-08-08')" = as_integer_date("2015-08-08"),
- "date('2015-09-09')" = as_integer_date("2015-09-09"),
- "date('2015-10-10')" = as_integer_date("2015-10-10"),
- "date('2015-11-11')" = as_integer_date("2015-11-11"),
- "date('2015-12-12')" = as_integer_date("2015-12-12"),
- "current_date" ~ as_integer_date(Sys.time()),
- .add_null = "below")
+ test_select_with_null(.ctx = ctx, con, .dots = setNames(time_values, sql_names))
})
},
- #' Data conversion from SQL to R: date with typed NULL values
- #' in the first row.
- data_date_null_above = function(ctx) {
+ #' (also applies to the return value of the SQL function `current_time`)
+ data_time_current = function(ctx) {
with_connection({
- test_select(.ctx = ctx, con,
- "date('2015-01-01')" = as_integer_date("2015-01-01"),
- "date('2015-02-02')" = as_integer_date("2015-02-02"),
- "date('2015-03-03')" = as_integer_date("2015-03-03"),
- "date('2015-04-04')" = as_integer_date("2015-04-04"),
- "date('2015-05-05')" = as_integer_date("2015-05-05"),
- "date('2015-06-06')" = as_integer_date("2015-06-06"),
- "date('2015-07-07')" = as_integer_date("2015-07-07"),
- "date('2015-08-08')" = as_integer_date("2015-08-08"),
- "date('2015-09-09')" = as_integer_date("2015-09-09"),
- "date('2015-10-10')" = as_integer_date("2015-10-10"),
- "date('2015-11-11')" = as_integer_date("2015-11-11"),
- "date('2015-12-12')" = as_integer_date("2015-12-12"),
- "current_date" ~ as_integer_date(Sys.time()),
- .add_null = "above")
+ test_select_with_null(
+ .ctx = ctx, con,
+ "current_time" ~ coercible_to_time)
})
},
- #' Data conversion from SQL to R: time.
- data_time = function(ctx) {
+ #' - coercible using [as.POSIXct()] for timestamps
+ data_timestamp = function(ctx) {
with_connection({
- test_select(.ctx = ctx, con,
- "time '00:00:00'" = "00:00:00",
- "time '12:34:56'" = "12:34:56",
- "current_time" ~ is.character)
- })
- },
+ char_values <- c("2015-10-11 00:00:00", "2015-10-11 12:34:56")
+ time_values <- rep(list(coercible_to_timestamp), 2L)
+ sql_names <- ctx$tweaks$time_cast(char_values)
- #' Data conversion from SQL to R: time with typed NULL values.
- data_time_null_below = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con,
- "time '00:00:00'" = "00:00:00",
- "time '12:34:56'" = "12:34:56",
- "current_time" ~ is.character,
- .add_null = "below")
+ test_select_with_null(.ctx = ctx, con, .dots = setNames(time_values, sql_names))
})
},
- #' Data conversion from SQL to R: time with typed NULL values
- #' in the first row.
- data_time_null_above = function(ctx) {
+ #' (also applies to the return value of the SQL function `current_timestamp`)
+ data_timestamp_current = function(ctx) {
with_connection({
- test_select(.ctx = ctx, con,
- "time '00:00:00'" = "00:00:00",
- "time '12:34:56'" = "12:34:56",
- "current_time" ~ is.character,
- .add_null = "above")
+ test_select_with_null(
+ .ctx = ctx, con,
+ "current_timestamp" ~ is_roughly_current_timestamp)
})
},
- #' Data conversion from SQL to R: time (using alternative syntax with
- #' parentheses for specifying time literals).
- data_time_parens = function(ctx) {
+ #' - [NA] for SQL `NULL` values
+ data_null = function(ctx) {
with_connection({
- test_select(.ctx = ctx, con,
- "time('00:00:00')" = "00:00:00",
- "time('12:34:56')" = "12:34:56",
- "current_time" ~ is.character)
- })
- },
+ check_result <- function(rows) {
+ expect_true(is.na(rows$a))
+ }
- #' Data conversion from SQL to R: time (using alternative syntax with
- #' parentheses for specifying time literals) with typed NULL values.
- data_time_parens_null_below = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con,
- "time('00:00:00')" = "00:00:00",
- "time('12:34:56')" = "12:34:56",
- "current_time" ~ is.character,
- .add_null = "below")
+ test_select(.ctx = ctx, con, "NULL" = is.na)
})
},
- #' Data conversion from SQL to R: time (using alternative syntax with
- #' parentheses for specifying time literals) with typed NULL values
- #' in the first row.
- data_time_parens_null_above = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con,
- "time('00:00:00')" = "00:00:00",
- "time('12:34:56')" = "12:34:56",
- "current_time" ~ is.character,
- .add_null = "above")
- })
- },
+ #'
+ #' If dates and timestamps are supported by the backend, the following R types are
+ #' used:
+ #' - [Date] for dates
+ data_date_typed = function(ctx) {
+ if (!isTRUE(ctx$tweaks$date_typed)) {
+ skip("tweak: !date_typed")
+ }
- #' Data conversion from SQL to R: timestamp.
- data_timestamp = function(ctx) {
with_connection({
- test_select(.ctx = ctx, con,
- "timestamp '2015-10-11 00:00:00'" = is_time,
- "timestamp '2015-10-11 12:34:56'" = is_time,
- "current_timestamp" ~ is_roughly_current_time)
- })
- },
+ char_values <- paste0("2015-01-", sprintf("%.2d", 1:12))
+ values <- lapply(char_values, as_numeric_date)
+ sql_names <- ctx$tweaks$date_cast(char_values)
- #' Data conversion from SQL to R: timestamp with typed NULL values.
- data_timestamp_null_below = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con,
- "timestamp '2015-10-11 00:00:00'" = is_time,
- "timestamp '2015-10-11 12:34:56'" = is_time,
- "current_timestamp" ~ is_roughly_current_time,
- .add_null = "below")
+ test_select_with_null(.ctx = ctx, con, .dots = setNames(values, sql_names))
})
},
- #' Data conversion from SQL to R: timestamp with typed NULL values
- #' in the first row.
- data_timestamp_null_above = function(ctx) {
- with_connection({
- test_select(.ctx = ctx, con,
- "timestamp '2015-10-11 00:00:00'" = is_time,
- "timestamp '2015-10-11 12:34:56'" = is_time,
- "current_timestamp" ~ is_roughly_current_time,
- .add_null = "above")
- })
- },
+ #' (also applies to the return value of the SQL function `current_date`)
+ data_date_current_typed = function(ctx) {
+ if (!isTRUE(ctx$tweaks$date_typed)) {
+ skip("tweak: !date_typed")
+ }
- #' Data conversion from SQL to R: timestamp with time zone.
- data_timestamp_utc = function(ctx) {
with_connection({
- test_select(.ctx = ctx,
- con,
- "timestamp '2015-10-11 00:00:00+02:00'" =
- as.POSIXct("2015-10-11 00:00:00+02:00"),
- "timestamp '2015-10-11 12:34:56-05:00'" =
- as.POSIXct("2015-10-11 12:34:56-05:00"),
- "current_timestamp" ~ is_roughly_current_time)
+ test_select_with_null(
+ .ctx = ctx, con,
+ "current_date" ~ is_roughly_current_date_typed)
})
},
- #' Data conversion from SQL to R: timestamp with time zone with typed NULL
- #' values.
- data_timestamp_utc_null_below = function(ctx) {
+ #' - [POSIXct] for timestamps
+ data_timestamp_typed = function(ctx) {
+ if (!isTRUE(ctx$tweaks$timestamp_typed)) {
+ skip("tweak: !timestamp_typed")
+ }
+
with_connection({
- test_select(.ctx = ctx,
- con,
- "timestamp '2015-10-11 00:00:00+02:00'" =
- as.POSIXct("2015-10-11 00:00:00+02:00"),
- "timestamp '2015-10-11 12:34:56-05:00'" =
- as.POSIXct("2015-10-11 12:34:56-05:00"),
- "current_timestamp" ~ is_roughly_current_time,
- .add_null = "below")
+ char_values <- c("2015-10-11 00:00:00", "2015-10-11 12:34:56")
+ timestamp_values <- rep(list(is_timestamp), 2L)
+ sql_names <- ctx$tweaks$timestamp_cast(char_values)
+
+ test_select_with_null(.ctx = ctx, con, .dots = setNames(timestamp_values, sql_names))
})
},
- #' Data conversion from SQL to R: timestamp with time zone with typed NULL
- #' values
- #' in the first row.
- data_timestamp_utc_null_above = function(ctx) {
+ #' (also applies to the return value of the SQL function `current_timestamp`)
+ data_timestamp_current_typed = function(ctx) {
+ if (!isTRUE(ctx$tweaks$timestamp_typed)) {
+ skip("tweak: !timestamp_typed")
+ }
+
with_connection({
- test_select(.ctx = ctx,
- con,
- "timestamp '2015-10-11 00:00:00+02:00'" =
- as.POSIXct("2015-10-11 00:00:00+02:00"),
- "timestamp '2015-10-11 12:34:56-05:00'" =
- as.POSIXct("2015-10-11 12:34:56-05:00"),
- "current_timestamp" ~ is_roughly_current_time,
- .add_null = "above")
+ test_select_with_null(
+ .ctx = ctx, con,
+ "current_timestamp" ~ is_roughly_current_timestamp_typed)
})
},
- #' Data conversion: timestamp (alternative syntax with parentheses
- #' for specifying timestamp literals).
- data_timestamp_parens = function(ctx) {
+ #'
+ #' R has no built-in type with lossless support for the full range of 64-bit
+ #' or larger integers. If 64-bit integers are returned from a query,
+ #' the following rules apply:
+ #' - Values are returned in a container with support for the full range of
+ #' valid 64-bit values (such as the `integer64` class of the \pkg{bit64}
+ #' package)
+ #' - Coercion to numeric always returns a number that is as close as possible
+ #' to the true value
+ data_64_bit_numeric = function(ctx) {
with_connection({
- test_select(.ctx = ctx,
- con,
- "datetime('2015-10-11 00:00:00')" =
- as.POSIXct("2015-10-11 00:00:00Z"),
- "datetime('2015-10-11 12:34:56')" =
- as.POSIXct("2015-10-11 12:34:56Z"),
- "current_timestamp" ~ is_roughly_current_time)
+ char_values <- c("10000000000", "-10000000000")
+ test_values <- as_numeric_equals_to(as.numeric(char_values))
+
+ test_select_with_null(.ctx = ctx, con, .dots = setNames(test_values, char_values))
})
},
- #' Data conversion: timestamp (alternative syntax with parentheses
- #' for specifying timestamp literals) with typed NULL values.
- data_timestamp_parens_null_below = function(ctx) {
+ #' - Loss of precision when converting to numeric gives a warning
+ data_64_bit_numeric_warning = function(ctx) {
with_connection({
- test_select(.ctx = ctx,
- con,
- "datetime('2015-10-11 00:00:00')" =
- as.POSIXct("2015-10-11 00:00:00Z"),
- "datetime('2015-10-11 12:34:56')" =
- as.POSIXct("2015-10-11 12:34:56Z"),
- "current_timestamp" ~ is_roughly_current_time,
- .add_null = "below")
+ char_values <- c("1234567890123456789", "-1234567890123456789")
+ test_values <- as_numeric_equals_to(as.numeric(char_values))
+
+ expect_warning(
+ test_select_with_null(.ctx = ctx, con, .dots = setNames(test_values, char_values))
+ )
})
},
- #' Data conversion: timestamp (alternative syntax with parentheses
- #' for specifying timestamp literals) with typed NULL values
- #' in the first row.
- data_timestamp_parens_null_above = function(ctx) {
+ #' - Conversion to character always returns a lossless decimal representation
+ #' of the data
+ data_64_bit_lossless = function(ctx) {
with_connection({
- test_select(.ctx = ctx,
- con,
- "datetime('2015-10-11 00:00:00')" =
- as.POSIXct("2015-10-11 00:00:00Z"),
- "datetime('2015-10-11 12:34:56')" =
- as.POSIXct("2015-10-11 12:34:56Z"),
- "current_timestamp" ~ is_roughly_current_time,
- .add_null = "above")
+ char_values <- c("1234567890123456789", "-1234567890123456789")
+ test_values <- as_character_equals_to(char_values)
+
+ test_select_with_null(.ctx = ctx, con, .dots = setNames(test_values, char_values))
})
},
- #' }
NULL
)
+test_select_with_null <- function(...) {
+ test_select(..., .add_null = "none")
+ test_select(..., .add_null = "above")
+ test_select(..., .add_null = "below")
+}
+
# NB: .table = TRUE will not work in bigrquery
test_select <- function(con, ..., .dots = NULL, .add_null = "none",
.table = FALSE, .ctx, .envir = parent.frame()) {
@@ -530,18 +279,19 @@ test_select <- function(con, ..., .dots = NULL, .add_null = "none",
}
if (.table) {
- query <- paste("CREATE TABLE test AS", query)
- expect_warning(dbExecute(con, query), NA)
- on.exit(expect_error(dbExecute(con, "DROP TABLE test"), NA), add = TRUE)
- expect_warning(rows <- dbReadTable(con, "test"), NA)
+ with_remove_test_table({
+ query <- paste("CREATE TABLE test AS", query)
+ dbExecute(con, query)
+ rows <- check_df(dbReadTable(con, "test"))
+ })
} else {
- expect_warning(rows <- dbGetQuery(con, query), NA)
+ rows <- check_df(dbGetQuery(con, query))
}
if (.add_null != "none") {
- rows <- rows[order(rows$id), -(length(sql_names) + 1L)]
+ rows <- rows[order(rows$id), -(length(sql_names) + 1L), drop = FALSE]
if (.add_null == "above") {
- rows <- rows[2:1, ]
+ rows <- rows[2:1, , drop = FALSE]
}
}
@@ -558,7 +308,11 @@ test_select <- function(con, ..., .dots = NULL, .add_null = "none",
if (.add_null != "none") {
expect_equal(nrow(rows), 2L)
- expect_true(all(is.na(unname(unlist(rows[2L, ])))))
+ if (is.list(rows[[1L]])) {
+ expect_true(is.null(rows[2L, 1L][[1L]]))
+ } else {
+ expect_true(is.na(rows[2L, 1L]))
+ }
} else {
expect_equal(nrow(rows), 1L)
}
@@ -586,15 +340,76 @@ is_raw_list <- function(x) {
is.list(x) && is.raw(x[[1L]])
}
-is_time <- function(x) {
+coercible_to_date <- function(x) {
+ x_date <- try_silent(as.Date(x))
+ !is.null(x_date) && all(is.na(x) == is.na(x_date))
+}
+
+as_date_equals_to <- function(x) {
+ lapply(x, function(xx) {
+ function(value) as.Date(value) == xx
+ })
+}
+
+is_roughly_current_date <- function(x) {
+ coercible_to_date(x) && (abs(Sys.Date() - as.Date(x)) <= 1)
+}
+
+coercible_to_time <- function(x) {
+ x_hms <- try_silent(hms::as.hms(x))
+ !is.null(x_hms) && all(is.na(x) == is.na(x_hms))
+}
+
+as_hms_equals_to <- function(x) {
+ lapply(x, function(xx) {
+ function(value) hms::as.hms(value) == xx
+ })
+}
+
+coercible_to_timestamp <- function(x) {
+ x_timestamp <- try_silent(as.POSIXct(x))
+ !is.null(x_timestamp) && all(is.na(x) == is.na(x_timestamp))
+}
+
+as_timestamp_equals_to <- function(x) {
+ lapply(x, function(xx) {
+ function(value) as.POSIXct(value) == xx
+ })
+}
+
+as_numeric_equals_to <- function(x) {
+ lapply(x, function(xx) {
+ function(value) as.numeric(value) == xx
+ })
+}
+
+as_character_equals_to <- function(x) {
+ lapply(x, function(xx) {
+ function(value) as.character(value) == xx
+ })
+}
+
+is_roughly_current_timestamp <- function(x) {
+ coercible_to_timestamp(x) && (Sys.time() - as.POSIXct(x, tz = "UTC") <= hms::hms(2))
+}
+
+is_date <- function(x) {
+ inherits(x, "Date")
+}
+
+is_roughly_current_date_typed <- function(x) {
+ is_date(x) && (abs(Sys.Date() - x) <= 1)
+}
+
+is_timestamp <- function(x) {
inherits(x, "POSIXct")
}
-is_roughly_current_time <- function(x) {
- is_time(x) && (Sys.time() - x <= 2)
+is_roughly_current_timestamp_typed <- function(x) {
+ is_timestamp(x) && (Sys.time() - x <= hms::hms(2))
}
-as_integer_date <- function(d) {
+as_numeric_date <- function(d) {
d <- as.Date(d)
- structure(as.integer(unclass(d)), class = class(d))
+ structure(as.numeric(unclass(d)), class = class(d))
}
diff --git a/R/spec-result-send-query.R b/R/spec-result-send-query.R
index b0f0025..3748d34 100644
--- a/R/spec-result-send-query.R
+++ b/R/spec-result-send-query.R
@@ -1,82 +1,94 @@
-#' @template dbispec-sub-wip
+#' spec_result_send_query
+#' @usage NULL
#' @format NULL
-#' @section Result:
-#' \subsection{Construction: `dbSendQuery("DBIConnection")` and `dbClearResult("DBIResult")`}{
+#' @keywords NULL
spec_result_send_query <- list(
- #' Can issue trivial query, result object inherits from "DBIResult".
- trivial_query = function(ctx) {
+ send_query_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbSendQuery)), c("conn", "statement", "..."))
+ },
+
+ #' @return
+ #' `dbSendQuery()` returns
+ send_query_trivial = function(ctx) {
with_connection({
- res <- dbSendQuery(con, "SELECT 1")
- on.exit(expect_error(dbClearResult(res), NA), add = TRUE)
+ res <- expect_visible(dbSendQuery(con, "SELECT 1"))
+ #' an S4 object that inherits from [DBIResult-class].
expect_s4_class(res, "DBIResult")
+ #' The result set can be used with [dbFetch()] to extract records.
+ expect_equal(check_df(dbFetch(res))[[1]], 1)
+ #' Once you have finished using a result, make sure to clear it
+ #' with [dbClearResult()].
+ dbClearResult(res)
})
},
- #' Return value, currently tests that the return value is always
- #' `TRUE`, and that an attempt to close a closed result set issues a
- #' warning.
- clear_result_return = function(ctx) {
- with_connection({
- res <- dbSendQuery(con, "SELECT 1")
- expect_true(dbClearResult(res))
- expect_warning(expect_true(dbClearResult(res)))
+ #' An error is raised when issuing a query over a closed
+ send_query_closed_connection = function(ctx) {
+ with_closed_connection({
+ expect_error(dbSendQuery(con, "SELECT 1"))
})
},
- #' Leaving a result open when closing a connection gives a warning.
- stale_result_warning = function(ctx) {
- with_connection({
- expect_warning(dbClearResult(dbSendQuery(con, "SELECT 1")), NA)
- expect_warning(dbClearResult(dbSendQuery(con, "SELECT 2")), NA)
+ #' or invalid connection,
+ send_query_invalid_connection = function(ctx) {
+ with_invalid_connection({
+ expect_error(dbSendQuery(con, "SELECT 1"))
})
+ },
- expect_warning(
- with_connection(dbSendQuery(con, "SELECT 1"))
- )
-
+ #' if the syntax of the query is invalid,
+ send_query_syntax_error = function(ctx) {
with_connection({
- expect_warning(res1 <- dbSendQuery(con, "SELECT 1"), NA)
- expect_true(dbIsValid(res1))
- expect_warning(res2 <- dbSendQuery(con, "SELECT 2"))
- expect_true(dbIsValid(res2))
- expect_false(dbIsValid(res1))
- dbClearResult(res2)
+ expect_error(dbSendQuery(con, "SELECT"))
})
},
- #' Can issue a command query that creates a table, inserts a row, and
- #' deletes it; the result sets for these query always have "completed"
- #' status.
- command_query = function(ctx) {
+ #' or if the query is not a non-`NA` string.
+ send_query_non_string = function(ctx) {
with_connection({
- on.exit({
- res <- dbSendStatement(con, "DROP TABLE test")
- expect_true(dbHasCompleted(res))
- expect_error(dbClearResult(res), NA)
- }
- , add = TRUE)
-
- res <- dbSendStatement(con, "CREATE TABLE test (a integer)")
- expect_true(dbHasCompleted(res))
- expect_error(dbClearResult(res), NA)
+ expect_error(dbSendQuery(con, character()))
+ expect_error(dbSendQuery(con, letters))
+ expect_error(dbSendQuery(con, NA_character_))
+ })
+ },
- res <- dbSendStatement(con, "INSERT INTO test SELECT 1")
- expect_true(dbHasCompleted(res))
- expect_error(dbClearResult(res), NA)
+ #' @section Specification:
+ send_query_result_valid = function(ctx) {
+ with_connection({
+ #' No warnings occur under normal conditions.
+ expect_warning(res <- dbSendQuery(con, "SELECT 1"), NA)
+ #' When done, the DBIResult object must be cleared with a call to
+ #' [dbClearResult()].
+ dbClearResult(res)
})
},
- #' Issuing an invalid query throws error (but no warnings, e.g. related to
- #' pending results, are thrown).
- invalid_query = function(ctx) {
+ send_query_stale_warning = function(ctx) {
+ #' Failure to clear the result set leads to a warning
+ #' when the connection is closed.
expect_warning(
with_connection({
- expect_error(dbSendStatement(con, "RAISE"))
- }),
- NA
+ dbSendQuery(con, "SELECT 1")
+ })
)
},
- #' }
+ #'
+ #' If the backend supports only one open result set per connection,
+ send_query_only_one_result_set = function(ctx) {
+ with_connection({
+ res1 <- dbSendQuery(con, "SELECT 1")
+ #' issuing a second query invalidates an already open result set
+ #' and raises a warning.
+ expect_warning(res2 <- dbSendQuery(con, "SELECT 2"))
+ expect_false(dbIsValid(res1))
+ #' The newly opened result set is valid
+ expect_true(dbIsValid(res2))
+ #' and must be cleared with `dbClearResult()`.
+ dbClearResult(res2)
+ })
+ },
+
NULL
)
diff --git a/R/spec-result-send-statement.R b/R/spec-result-send-statement.R
new file mode 100644
index 0000000..af5c7a2
--- /dev/null
+++ b/R/spec-result-send-statement.R
@@ -0,0 +1,102 @@
+#' spec_result_send_statement
+#' @usage NULL
+#' @format NULL
+#' @keywords NULL
+spec_result_send_statement <- list(
+ send_statement_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbSendStatement)), c("conn", "statement", "..."))
+ },
+
+ #' @return
+ #' `dbSendStatement()` returns
+ send_statement_trivial = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ res <- expect_visible(dbSendStatement(con, "CREATE TABLE test AS SELECT 1 AS a"))
+ #' an S4 object that inherits from [DBIResult-class].
+ expect_s4_class(res, "DBIResult")
+ #' The result set can be used with [dbGetRowsAffected()] to
+ #' determine the number of rows affected by the query.
+ expect_error(dbGetRowsAffected(res), NA)
+ #' Once you have finished using a result, make sure to clear it
+ #' with [dbClearResult()].
+ dbClearResult(res)
+ })
+ })
+ },
+
+ #' An error is raised when issuing a statement over a closed
+ send_statement_closed_connection = function(ctx) {
+ with_closed_connection({
+ expect_error(dbSendStatement(con, "CREATE TABLE test AS SELECT 1 AS a"))
+ })
+ },
+
+ #' or invalid connection,
+ send_statement_invalid_connection = function(ctx) {
+ with_invalid_connection({
+ expect_error(dbSendStatement(con, "CREATE TABLE test AS SELECT 1 AS a"))
+ })
+ },
+
+ #' if the syntax of the statement is invalid,
+ send_statement_syntax_error = function(ctx) {
+ with_connection({
+ expect_error(dbSendStatement(con, "CREATE"))
+ })
+ },
+
+ #' or if the statement is not a non-`NA` string.
+ send_statement_non_string = function(ctx) {
+ with_connection({
+ expect_error(dbSendStatement(con, character()))
+ expect_error(dbSendStatement(con, letters))
+ expect_error(dbSendStatement(con, NA_character_))
+ })
+ },
+
+ #' @section Specification:
+ send_statement_result_valid = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ #' No warnings occur under normal conditions.
+ expect_warning(res <- dbSendStatement(con, "CREATE TABLE test AS SELECT 1 AS a"), NA)
+ #' When done, the DBIResult object must be cleared with a call to
+ #' [dbClearResult()].
+ dbClearResult(res)
+ })
+ })
+ },
+
+ send_statement_stale_warning = function(ctx) {
+ #' Failure to clear the result set leads to a warning
+ #' when the connection is closed.
+ expect_warning(
+ with_connection({
+ expect_warning(dbSendStatement(con, "SELECT 1"), NA)
+ })
+ )
+ },
+
+ #' If the backend supports only one open result set per connection,
+ send_statement_only_one_result_set = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ res1 <- dbSendStatement(con, "CREATE TABLE test AS SELECT 1 AS a")
+ with_remove_test_table(name = "test2", {
+ #' issuing a second query invalidates an already open result set
+ #' and raises a warning.
+ expect_warning(res2 <- dbSendStatement(con, "CREATE TABLE test2 AS SELECT 1 AS a"))
+ expect_false(dbIsValid(res1))
+ #' The newly opened result set is valid
+ expect_true(dbIsValid(res2))
+ #' and must be cleared with `dbClearResult()`.
+ dbClearResult(res2)
+ })
+ })
+ })
+ },
+
+ NULL
+)
diff --git a/R/spec-result.R b/R/spec-result.R
index 2805b88..984c3f7 100644
--- a/R/spec-result.R
+++ b/R/spec-result.R
@@ -3,7 +3,10 @@
spec_result <- c(
spec_result_send_query,
spec_result_fetch,
+ spec_result_clear_result,
spec_result_get_query,
+ spec_result_send_statement,
+ spec_result_execute,
spec_result_create_table_with_data_type,
spec_result_roundtrip
)
@@ -12,11 +15,7 @@ spec_result <- c(
# Helpers -----------------------------------------------------------------
union <- function(..., .order_by = NULL, .ctx) {
- if (is.null(.ctx$tweaks$union)) {
- query <- paste(c(...), collapse = " UNION ")
- } else {
- query <- .ctx$tweaks$union(c(...))
- }
+ query <- .ctx$tweaks$union(c(...))
if (!missing(.order_by)) {
query <- paste(query, "ORDER BY", .order_by)
diff --git a/R/spec-sql-exists-table.R b/R/spec-sql-exists-table.R
new file mode 100644
index 0000000..b697d6c
--- /dev/null
+++ b/R/spec-sql-exists-table.R
@@ -0,0 +1,117 @@
+#' spec_sql_exists_table
+#' @usage NULL
+#' @format NULL
+#' @keywords NULL
+spec_sql_exists_table <- list(
+ exists_table_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbExistsTable)), c("conn", "name", "..."))
+ },
+
+ #' @return
+ #' `dbExistsTable()` returns a logical scalar, `TRUE` if the table or view
+ #' specified by the `name` argument exists, `FALSE` otherwise.
+ exists_table = function(ctx) {
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ expect_false(expect_visible(dbExistsTable(con, "iris")))
+ iris <- get_iris(ctx)
+ dbWriteTable(con, "iris", iris)
+
+ expect_true(expect_visible(dbExistsTable(con, "iris")))
+
+ expect_false(expect_visible(dbExistsTable(con, "test")))
+
+ #' This includes temporary tables if supported by the database.
+ if (isTRUE(ctx$tweaks$temporary_tables)) {
+ dbWriteTable(con, "test", data.frame(a = 1L), temporary = TRUE)
+ expect_true(expect_visible(dbExistsTable(con, "test")))
+ }
+ })
+
+ expect_false(expect_visible(dbExistsTable(con, "iris")))
+ })
+ },
+
+ #'
+ #' An error is raised when calling this method for a closed
+ exists_table_closed_connection = function(ctx) {
+ with_closed_connection({
+ expect_error(dbExistsTable(con, "test"))
+ })
+ },
+
+ #' or invalid connection.
+ exists_table_invalid_connection = function(ctx) {
+ with_invalid_connection({
+ expect_error(dbExistsTable(con, "test"))
+ })
+ },
+
+ #' An error is also raised
+ exists_table_error = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ dbWriteTable(con, "test", data.frame(a = 1L))
+ #' if `name` cannot be processed with [dbQuoteIdentifier()]
+ expect_error(dbExistsTable(con, NA))
+ #' or if this results in a non-scalar.
+ expect_error(dbExistsTable(con, c("test", "test")))
+ })
+ })
+ },
+
+ #' @section Additional arguments:
+ #' TBD: `temporary = NA`
+ #'
+ #' This must be provided as named argument.
+ #' See the "Specification" section for details on their usage.
+
+ #' @section Specification:
+ #' The `name` argument is processed as follows,
+ exists_table_name = function(ctx) {
+ with_connection({
+ #' to support databases that allow non-syntactic names for their objects:
+ if (isTRUE(ctx$tweaks$strict_identifier)) {
+ table_names <- "a"
+ } else {
+ table_names <- c("a", "with spaces", "with,comma")
+ }
+
+ for (table_name in table_names) {
+ with_remove_test_table(name = table_name, {
+ expect_false(dbExistsTable(con, table_name))
+
+ test_in <- data.frame(a = 1L)
+ dbWriteTable(con, table_name, test_in)
+
+ #' - If an unquoted table name as string: `dbExistsTable()` will do the
+ #' quoting,
+ expect_true(dbExistsTable(con, table_name))
+ #' perhaps by calling `dbQuoteIdentifier(conn, x = name)`
+ #' - If the result of a call to [dbQuoteIdentifier()]: no more quoting is done
+ expect_true(dbExistsTable(con, dbQuoteIdentifier(con, table_name)))
+ })
+ }
+ })
+ },
+
+ #'
+ #' For all tables listed by [dbListTables()], `dbExistsTable()` returns `TRUE`.
+ exists_table_list = function(ctx) {
+ with_connection({
+ name <- random_table_name()
+ with_remove_test_table(
+ name = name,
+ {
+ dbWriteTable(con, name, data.frame(a = 1))
+ for (table_name in dbListTables(con)) {
+ expect_true(dbExistsTable(con, table_name))
+ }
+ }
+ )
+ })
+ },
+
+ NULL
+)
diff --git a/R/spec-sql-list-fields.R b/R/spec-sql-list-fields.R
index 48d1aa2..e2cd642 100644
--- a/R/spec-sql-list-fields.R
+++ b/R/spec-sql-list-fields.R
@@ -6,14 +6,25 @@ spec_sql_list_fields <- list(
#' Can list the fields for a table in the database.
list_fields = function(ctx) {
with_connection({
- on.exit(expect_error(dbRemoveTable(con, "iris"), NA),
- add = TRUE)
+ with_remove_test_table(name = "iris", {
+ iris <- get_iris(ctx)
+ dbWriteTable(con, "iris", iris)
- iris <- get_iris(ctx)
- dbWriteTable(con, "iris", iris)
+ fields <- dbListFields(con, "iris")
+ expect_identical(fields, names(iris))
+ })
+ })
+ },
- fields <- dbListFields(con, "iris")
- expect_identical(fields, names(iris))
+
+ #'
+ #' A column named `row_names` is treated like any other column.
+ list_fields_row_names = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ dbWriteTable(con, "test", data.frame(a = 1L, row_names = 2L))
+ expect_identical(dbListFields(con, "test"), c("a", "row_names"))
+ })
})
},
diff --git a/R/spec-sql-list-tables.R b/R/spec-sql-list-tables.R
index 197d87c..fe4c3db 100644
--- a/R/spec-sql-list-tables.R
+++ b/R/spec-sql-list-tables.R
@@ -1,41 +1,89 @@
-#' @template dbispec-sub-wip
+#' spec_sql_list_tables
+#' @usage NULL
#' @format NULL
-#' @section SQL:
-#' \subsection{`dbListTables("DBIConnection")`}{
+#' @keywords NULL
spec_sql_list_tables <- list(
- #' Can list the tables in the database, adding and removing tables affects
- #' the list. Can also check existence of a table.
+ list_tables_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbListTables)), c("conn", "..."))
+ },
+
+ #' @return
+ #' `dbListTables()`
list_tables = function(ctx) {
with_connection({
- expect_error(dbGetQuery(con, "SELECT * FROM iris"))
+ with_remove_test_table(name = "iris", {
+ tables <- dbListTables(con)
+ #' returns a character vector
+ expect_is(tables, "character")
+ #' that enumerates all tables
+ expect_false("iris" %in% tables)
- tables <- dbListTables(con)
- expect_is(tables, "character")
- expect_false("iris" %in% tables)
+ #' and views
+ # TODO
+ #' in the database.
- expect_false(dbExistsTable(con, "iris"))
+ #' Tables added with [dbWriteTable()]
+ iris <- get_iris(ctx)
+ dbWriteTable(con, "iris", iris)
- on.exit(expect_error(dbRemoveTable(con, "iris"), NA),
- add = TRUE)
+ #' are part of the list,
+ tables <- dbListTables(con)
+ expect_true("iris" %in% tables)
+ })
- iris <- get_iris(ctx)
- dbWriteTable(con, "iris", iris)
+ with_remove_test_table({
+ #' including temporary tables if supported by the database.
+ if (isTRUE(ctx$tweaks$temporary_tables)) {
+ dbWriteTable(con, "test", data.frame(a = 1L), temporary = TRUE)
+ tables <- dbListTables(con)
+ expect_true("test" %in% tables)
+ }
+ })
+ #' As soon a table is removed from the database,
+ #' it is also removed from the list of database tables.
tables <- dbListTables(con)
- expect_true("iris" %in% tables)
+ expect_false("iris" %in% tables)
- expect_true(dbExistsTable(con, "iris"))
+ #'
+ #' The returned names are suitable for quoting with `dbQuoteIdentifier()`.
+ if (isTRUE(ctx$tweaks$strict_identifier)) {
+ table_names <- "a"
+ } else {
+ table_names <- c("a", "with spaces", "with,comma")
+ }
- dbRemoveTable(con, "iris")
- on.exit(NULL, add = FALSE)
+ for (table_name in table_names) {
+ with_remove_test_table(name = dbQuoteIdentifier(con, table_name), {
+ dbWriteTable(con, dbQuoteIdentifier(con, table_name), data.frame(a = 2L))
+ tables <- dbListTables(con)
+ expect_true(table_name %in% tables)
+ expect_true(dbQuoteIdentifier(con, table_name) %in% dbQuoteIdentifier(con, tables))
+ })
+ }
+ })
+ },
- tables <- dbListTables(con)
- expect_false("iris" %in% tables)
+ #' An error is raised when calling this method for a closed
+ list_tables_closed_connection = function(ctx) {
+ with_closed_connection({
+ expect_error(dbListTables(con))
+ })
+ },
- expect_false(dbExistsTable(con, "iris"))
+ #' or invalid connection.
+ list_tables_invalid_connection = function(ctx) {
+ with_invalid_connection({
+ expect_error(dbListTables(con))
})
},
- #' }
+ #' @section Additional arguments:
+ #' TBD: `temporary = NA`
+ #'
+ #' This must be provided as named argument.
+ #' See the "Specification" section for details on their usage.
+
NULL
)
diff --git a/R/spec-sql-quote-identifier.R b/R/spec-sql-quote-identifier.R
index 0b37362..e939dd7 100644
--- a/R/spec-sql-quote-identifier.R
+++ b/R/spec-sql-quote-identifier.R
@@ -1,67 +1,152 @@
-#' @template dbispec-sub-wip
+#' spec_sql_quote_identifier
+#' @usage NULL
#' @format NULL
-#' @section SQL:
-#' \subsection{`dbQuoteIdentifier("DBIConnection")`}{
+#' @keywords NULL
spec_sql_quote_identifier <- list(
- #' Can quote identifiers that consist of letters only.
+ quote_identifier_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbQuoteIdentifier)), c("conn", "x", "..."))
+ },
+
+ #' @return
+ quote_identifier_return = function(ctx) {
+ with_connection({
+ #' `dbQuoteIdentifier()` returns an object that can be coerced to [character],
+ simple_out <- dbQuoteIdentifier(con, "simple")
+ expect_error(as.character(simple_out), NA)
+ expect_is(as.character(simple_out), "character")
+ })
+ },
+
+ quote_identifier_vectorized = function(ctx) {
+ with_connection({
+ #' of the same length as the input.
+ simple <- "simple"
+ simple_out <- dbQuoteIdentifier(con, simple)
+ expect_equal(length(simple_out), 1L)
+
+ letters_out <- dbQuoteIdentifier(con, letters)
+ expect_equal(length(letters_out), length(letters))
+
+ #' For an empty character vector this function returns a length-0 object.
+ empty <- character()
+ empty_out <- dbQuoteIdentifier(con, empty)
+ expect_equal(length(empty_out), 0L)
+
+ #' An error is raised if the input contains `NA`,
+ expect_error(dbQuoteIdentifier(con, NA))
+ expect_error(dbQuoteIdentifier(con, NA_character_))
+ expect_error(dbQuoteIdentifier(con, c("a", NA_character_)))
+ #' but not for an empty string.
+ expect_error(dbQuoteIdentifier(con, ""), NA)
+
+ #'
+ #' When passing the returned object again to `dbQuoteIdentifier()`
+ #' as `x`
+ #' argument, it is returned unchanged.
+ expect_identical(dbQuoteIdentifier(con, simple_out), simple_out)
+ expect_identical(dbQuoteIdentifier(con, letters_out), letters_out)
+ expect_identical(dbQuoteIdentifier(con, empty_out), empty_out)
+ #' Passing objects of class [SQL] should also return them unchanged.
+ expect_identical(dbQuoteIdentifier(con, SQL(simple)), SQL(simple))
+ expect_identical(dbQuoteIdentifier(con, SQL(letters)), SQL(letters))
+ expect_identical(dbQuoteIdentifier(con, SQL(empty)), SQL(empty))
+
+ #' (For backends it may be most convenient to return [SQL] objects
+ #' to achieve this behavior, but this is not required.)
+ })
+ },
+
+ #' @section Specification:
+ #' Calling [dbGetQuery()] for a query of the format `SELECT 1 AS ...`
+ #' returns a data frame with the identifier, unquoted, as column name.
quote_identifier = function(ctx) {
with_connection({
+ #' Quoted identifiers can be used as table and column names in SQL queries,
simple <- dbQuoteIdentifier(con, "simple")
- query <- paste0("SELECT 1 as", simple)
-
- expect_warning(rows <- dbGetQuery(con, query), NA)
+ #' in particular in queries like `SELECT 1 AS ...`
+ query <- paste0("SELECT 1 AS", simple)
+ rows <- check_df(dbGetQuery(con, query))
expect_identical(names(rows), "simple")
expect_identical(unlist(unname(rows)), 1L)
+
+ #' and `SELECT * FROM (SELECT 1) ...`.
+ query <- paste0("SELECT * FROM (SELECT 1) ", simple)
+ rows <- check_df(dbGetQuery(con, query))
+ expect_identical(unlist(unname(rows)), 1L)
})
},
- #' Can quote identifiers with special characters, and create identifiers
- #' that contain quotes and spaces.
- quote_identifier_special = function(ctx) {
- if (isTRUE(ctx$tweaks$strict_identifier)) {
- skip("tweak: strict_identifier")
- }
+ #' The method must use a quoting mechanism that is unambiguously different
+ #' from the quoting mechanism used for strings, so that a query like
+ quote_identifier_string = function(ctx) {
+ with_connection({
+ #' `SELECT ... FROM (SELECT 1 AS ...)`
+ query <- paste0(
+ "SELECT ", dbQuoteIdentifier(con, "b"), " FROM (",
+ "SELECT 1 AS ", dbQuoteIdentifier(con, "a"), ")"
+ )
+ #' throws an error if the column names do not match.
+ eval(bquote(expect_error(dbGetQuery(con, .(query)))))
+ })
+ },
+
+ quote_identifier_special = function(ctx) {
with_connection({
- simple <- dbQuoteIdentifier(con, "simple")
- with_space <- dbQuoteIdentifier(con, "with space")
- with_dot <- dbQuoteIdentifier(con, "with.dot")
- with_comma <- dbQuoteIdentifier(con, "with,comma")
- quoted_simple <- dbQuoteIdentifier(con, as.character(simple))
+ #'
+ #' The method can quote column names that
+ #' contain special characters such as a space,
+ with_space_in <- "with space"
+ with_space <- dbQuoteIdentifier(con, with_space_in)
+ #' a dot,
+ with_dot_in <- "with.dot"
+ with_dot <- dbQuoteIdentifier(con, with_dot_in)
+ #' a comma,
+ with_comma_in <- "with,comma"
+ with_comma <- dbQuoteIdentifier(con, with_comma_in)
+ #' or quotes used to mark strings
+ with_quote_in <- as.character(dbQuoteString(con, "a"))
+ with_quote <- dbQuoteIdentifier(con, with_quote_in)
+ #' or identifiers,
+ empty_in <- ""
+ empty <- dbQuoteIdentifier(con, empty_in)
+ quoted_empty <- dbQuoteIdentifier(con, as.character(empty))
quoted_with_space <- dbQuoteIdentifier(con, as.character(with_space))
quoted_with_dot <- dbQuoteIdentifier(con, as.character(with_dot))
quoted_with_comma <- dbQuoteIdentifier(con, as.character(with_comma))
+ quoted_with_quote <- dbQuoteIdentifier(con, as.character(with_quote))
+
+ #' if the database supports this.
+ if (isTRUE(ctx$tweaks$strict_identifier)) {
+ skip("tweak: strict_identifier")
+ }
+ #' In any case, checking the validity of the identifier
+ #' should be performed only when executing a query,
+ #' and not by `dbQuoteIdentifier()`.
query <- paste0("SELECT ",
- "1 as", simple, ",",
"2 as", with_space, ",",
"3 as", with_dot, ",",
"4 as", with_comma, ",",
- "5 as", quoted_simple, ",",
- "6 as", quoted_with_space, ",",
- "7 as", quoted_with_dot, ",",
- "8 as", quoted_with_comma)
+ "5 as", with_quote, ",",
+ "6 as", quoted_empty, ",",
+ "7 as", quoted_with_space, ",",
+ "8 as", quoted_with_dot, ",",
+ "9 as", quoted_with_comma, ",",
+ "10 as", quoted_with_quote)
- expect_warning(rows <- dbGetQuery(con, query), NA)
+ rows <- check_df(dbGetQuery(con, query))
expect_identical(names(rows),
- c("simple", "with space", "with.dot", "with,comma",
- as.character(simple), as.character(with_space),
- as.character(with_dot), as.character(with_comma)))
- expect_identical(unlist(unname(rows)), 1:8)
- })
- },
-
- #' Character vectors are treated as a single qualified identifier.
- quote_identifier_not_vectorized = function(ctx) {
- with_connection({
- simple_out <- dbQuoteIdentifier(con, "simple")
- expect_equal(length(simple_out), 1L)
- letters_out <- dbQuoteIdentifier(con, letters[1:3])
- expect_equal(length(letters_out), 1L)
+ c(with_space_in, with_dot_in, with_comma_in,
+ with_quote_in,
+ as.character(empty), as.character(with_space),
+ as.character(with_dot), as.character(with_comma),
+ as.character(with_quote)))
+ expect_identical(unlist(unname(rows)), 2:10)
})
},
- #' }
NULL
)
diff --git a/R/spec-sql-quote-string.R b/R/spec-sql-quote-string.R
index 2395916..e37fa41 100644
--- a/R/spec-sql-quote-string.R
+++ b/R/spec-sql-quote-string.R
@@ -1,52 +1,142 @@
-#' @template dbispec-sub-wip
+#' spec_sql_quote_string
+#' @usage NULL
#' @format NULL
-#' @section SQL:
-#' \subsection{`dbQuoteString("DBIConnection")`}{
+#' @keywords NULL
spec_sql_quote_string <- list(
- #' Can quote strings, and create strings that contain quotes and spaces.
- quote_string = function(ctx) {
+ quote_string_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbQuoteString)), c("conn", "x", "..."))
+ },
+
+ #' @return
+ quote_string_return = function(ctx) {
+ with_connection({
+ #' `dbQuoteString()` returns an object that can be coerced to [character],
+ simple <- "simple"
+ simple_out <- dbQuoteString(con, simple)
+ expect_error(as.character(simple_out), NA)
+ expect_is(as.character(simple_out), "character")
+ expect_equal(length(simple_out), 1L)
+ })
+ },
+
+ quote_string_vectorized = function(ctx) {
+ with_connection({
+ #' of the same length as the input.
+ letters_out <- dbQuoteString(con, letters)
+ expect_equal(length(letters_out), length(letters))
+
+ #' For an empty character vector this function returns a length-0 object.
+ empty_out <- dbQuoteString(con, character())
+ expect_equal(length(empty_out), 0L)
+ })
+ },
+
+ quote_string_double = function(ctx) {
+ with_connection({
+ simple <- "simple"
+ simple_out <- dbQuoteString(con, simple)
+
+ letters_out <- dbQuoteString(con, letters)
+
+ empty <- character()
+ empty_out <- dbQuoteString(con, character())
+
+ #'
+ #' When passing the returned object again to `dbQuoteString()`
+ #' as `x`
+ #' argument, it is returned unchanged.
+ expect_identical(dbQuoteString(con, simple_out), simple_out)
+ expect_identical(dbQuoteString(con, letters_out), letters_out)
+ expect_identical(dbQuoteString(con, empty_out), empty_out)
+ #' Passing objects of class [SQL] should also return them unchanged.
+ expect_identical(dbQuoteString(con, SQL(simple)), SQL(simple))
+ expect_identical(dbQuoteString(con, SQL(letters)), SQL(letters))
+ expect_identical(dbQuoteString(con, SQL(empty)), SQL(empty))
+
+ #' (For backends it may be most convenient to return [SQL] objects
+ #' to achieve this behavior, but this is not required.)
+ })
+ },
+
+ #' @section Specification:
+ quote_string_roundtrip = function(ctx) {
+ with_connection({
+ do_test_string <- function(x) {
+ #' The returned expression can be used in a `SELECT ...` query,
+ query <- paste0("SELECT ", paste(dbQuoteString(con, x), collapse = ", "))
+ #' and for any scalar character `x` the value of
+ #' \code{dbGetQuery(paste0("SELECT ", dbQuoteString(x)))[[1]]}
+ #' must be identical to `x`,
+ x_out <- check_df(dbGetQuery(con, query))
+ expect_equal(nrow(x_out), 1L)
+ expect_identical(unlist(unname(x_out)), x)
+ }
+
+ test_chars <- c(
+ #' even if `x` contains
+ "",
+ #' spaces,
+ " ",
+ #' tabs,
+ "\t",
+ #' quotes (single
+ "'",
+ #' or double),
+ '"',
+ #' backticks,
+ "`",
+ #' or newlines
+ "\n"
+ )
+ #' (in any combination)
+ # length(test_chars) ** 3
+ test_strings_0 <- expand_char(test_chars, "a", test_chars, "b", test_chars)
+
+ #' or is itself the result of a `dbQuoteString()` call coerced back to
+ #' character (even repeatedly).
+ test_strings_1 <- as.character(dbQuoteString(con, test_strings_0))
+ test_strings_2 <- as.character(dbQuoteString(con, test_strings_1))
+
+ test_strings <- c(test_strings_0, test_strings_1, test_strings_2)
+ do_test_string(test_strings)
+ })
+ },
+
+ quote_string_na = function(ctx) {
with_connection({
- simple <- dbQuoteString(con, "simple")
- with_spaces <- dbQuoteString(con, "with spaces")
- quoted_simple <- dbQuoteString(con, as.character(simple))
- quoted_with_spaces <- dbQuoteString(con, as.character(with_spaces))
null <- dbQuoteString(con, NA_character_)
quoted_null <- dbQuoteString(con, as.character(null))
na <- dbQuoteString(con, "NA")
quoted_na <- dbQuoteString(con, as.character(na))
- query <- paste0("SELECT",
- simple, "as simple,",
- with_spaces, "as with_spaces,",
+ query <- paste0("SELECT ",
null, " as null_return,",
na, "as na_return,",
- quoted_simple, "as quoted_simple,",
- quoted_with_spaces, "as quoted_with_spaces,",
quoted_null, "as quoted_null,",
quoted_na, "as quoted_na")
- expect_warning(rows <- dbGetQuery(con, query), NA)
- expect_identical(rows$simple, "simple")
- expect_identical(rows$with_spaces, "with spaces")
+ #' If `x` is `NA`, the result must merely satisfy [is.na()].
+ rows <- check_df(dbGetQuery(con, query))
expect_true(is.na(rows$null_return))
+ #' The strings `"NA"` or `"NULL"` are not treated specially.
expect_identical(rows$na_return, "NA")
- expect_identical(rows$quoted_simple, as.character(simple))
- expect_identical(rows$quoted_with_spaces, as.character(with_spaces))
expect_identical(rows$quoted_null, as.character(null))
expect_identical(rows$quoted_na, as.character(na))
})
},
- #' Can quote more than one string at once by passing a character vector.
- quote_string_vectorized = function(ctx) {
+ quote_string_na_is_null = function(ctx) {
with_connection({
- simple_out <- dbQuoteString(con, "simple")
- expect_equal(length(simple_out), 1L)
- letters_out <- dbQuoteString(con, letters)
- expect_equal(length(letters_out), length(letters))
+ #'
+ #' `NA` should be translated to an unquoted SQL `NULL`,
+ null <- dbQuoteString(con, NA_character_)
+ #' so that the query `SELECT * FROM (SELECT 1) a WHERE ... IS NULL`
+ rows <- check_df(dbGetQuery(con, paste0("SELECT * FROM (SELECT 1) a WHERE ", null, " IS NULL")))
+ #' returns one row.
+ expect_equal(nrow(rows), 1L)
})
},
- #' }
NULL
)
diff --git a/R/spec-sql-read-table.R b/R/spec-sql-read-table.R
new file mode 100644
index 0000000..b54ee24
--- /dev/null
+++ b/R/spec-sql-read-table.R
@@ -0,0 +1,303 @@
+#' spec_sql_read_table
+#' @usage NULL
+#' @format NULL
+#' @keywords NULL
+spec_sql_read_table <- list(
+ read_table_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbReadTable)), c("conn", "name", "..."))
+ },
+
+ #' @return
+ #' `dbReadTable()` returns a data frame that contains the complete data
+ #' from the remote table, effectively the result of calling [dbGetQuery()]
+ #' with `SELECT * FROM <name>`.
+ read_table = function(ctx) {
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ iris_in <- get_iris(ctx)
+ dbWriteTable(con, "iris", iris_in)
+ iris_out <- check_df(dbReadTable(con, "iris"))
+
+ expect_equal_df(iris_out, iris_in)
+ })
+ })
+ },
+
+ #' An error is raised if the table does not exist.
+ read_table_missing = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ expect_error(dbReadTable(con, "test"))
+ })
+ })
+ },
+
+ #' An empty table is returned as a data frame with zero rows.
+ read_table_empty = function(ctx) {
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ iris_in <- get_iris(ctx)[integer(), ]
+ dbWriteTable(con, "iris", iris_in)
+ iris_out <- check_df(dbReadTable(con, "iris"))
+
+ expect_equal(nrow(iris_out), 0L)
+ expect_equal_df(iris_out, iris_in)
+ })
+ })
+ },
+
+ #'
+ #' The presence of [rownames] depends on the `row.names` argument,
+ #' see [sqlColumnToRownames()] for details:
+ read_table_row_names_false = function(ctx) {
+ #' - If `FALSE` or `NULL`, the returned data frame doesn't have row names.
+ for (row.names in list(FALSE, NULL)) {
+ with_connection({
+ with_remove_test_table(name = "mtcars", {
+ mtcars_in <- datasets::mtcars
+ dbWriteTable(con, "mtcars", mtcars_in, row.names = TRUE)
+ mtcars_out <- check_df(dbReadTable(con, "mtcars", row.names = row.names))
+
+ expect_true("row_names" %in% names(mtcars_out))
+ expect_true(all(mtcars_out$row_names %in% rownames(mtcars_in)))
+ expect_true(all(rownames(mtcars_in) %in% mtcars_out$row_names))
+ expect_equal_df(mtcars_out[names(mtcars_out) != "row_names"], unrowname(mtcars_in))
+ })
+ })
+ }
+ },
+
+ read_table_row_names_true_exists = function(ctx) {
+ #' - If `TRUE`, a column named "row_names" is converted to row names,
+ row.names <- TRUE
+
+ with_connection({
+ with_remove_test_table(name = "mtcars", {
+ mtcars_in <- datasets::mtcars
+ dbWriteTable(con, "mtcars", mtcars_in, row.names = NA)
+ mtcars_out <- check_df(dbReadTable(con, "mtcars", row.names = row.names))
+
+ expect_equal_df(mtcars_out, mtcars_in)
+ })
+ })
+ },
+
+ read_table_row_names_true_missing = function(ctx) {
+ #' an error is raised if no such column exists.
+ row.names <- TRUE
+
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ iris_in <- get_iris(ctx)
+ dbWriteTable(con, "iris", iris_in, row.names = NA)
+ expect_error(dbReadTable(con, "iris", row.names = row.names))
+ })
+ })
+ },
+
+ read_table_row_names_na_exists = function(ctx) {
+ #' - If `NA`, a column named "row_names" is converted to row names if it exists,
+ row.names <- NA
+
+ with_connection({
+ with_remove_test_table(name = "mtcars", {
+ mtcars_in <- datasets::mtcars
+ dbWriteTable(con, "mtcars", mtcars_in, row.names = TRUE)
+ mtcars_out <- check_df(dbReadTable(con, "mtcars", row.names = row.names))
+
+ expect_equal_df(mtcars_out, mtcars_in)
+ })
+ })
+ },
+
+ read_table_row_names_na_missing = function(ctx) {
+ #' otherwise no translation occurs.
+ row.names <- NA
+
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ iris_in <- get_iris(ctx)
+ dbWriteTable(con, "iris", iris_in, row.names = FALSE)
+ iris_out <- check_df(dbReadTable(con, "iris", row.names = row.names))
+
+ expect_equal_df(iris_out, iris_in)
+ })
+ })
+ },
+
+ read_table_row_names_string_exists = function(ctx) {
+ #' - If a string, this specifies the name of the column in the remote table
+ #' that contains the row names,
+ row.names <- "make_model"
+
+ with_connection({
+ with_remove_test_table(name = "mtcars", {
+ mtcars_in <- datasets::mtcars
+ mtcars_in$make_model <- rownames(mtcars_in)
+ mtcars_in <- unrowname(mtcars_in)
+
+ dbWriteTable(con, "mtcars", mtcars_in, row.names = FALSE)
+ mtcars_out <- check_df(dbReadTable(con, "mtcars", row.names = row.names))
+
+ expect_false("make_model" %in% names(mtcars_out))
+ expect_true(all(mtcars_in$make_model %in% rownames(mtcars_out)))
+ expect_true(all(rownames(mtcars_out) %in% mtcars_in$make_model))
+ expect_equal_df(unrowname(mtcars_out), mtcars_in[names(mtcars_in) != "make_model"])
+ })
+ })
+ },
+
+ read_table_row_names_string_missing = function(ctx) {
+ #' an error is raised if no such column exists.
+ row.names <- "missing"
+
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ iris_in <- get_iris(ctx)
+ dbWriteTable(con, "iris", iris_in, row.names = FALSE)
+ expect_error(dbReadTable(con, "iris", row.names = row.names))
+ })
+ })
+ },
+ #'
+
+ read_table_row_names_default = function(ctx) {
+ #'
+ #' The default is `row.names = FALSE`.
+ #'
+ with_connection({
+ with_remove_test_table(name = "mtcars", {
+ mtcars_in <- datasets::mtcars
+ dbWriteTable(con, "mtcars", mtcars_in, row.names = TRUE)
+ mtcars_out <- check_df(dbReadTable(con, "mtcars"))
+
+ expect_true("row_names" %in% names(mtcars_out))
+ expect_true(all(mtcars_out$row_names %in% rownames(mtcars_in)))
+ expect_true(all(rownames(mtcars_in) %in% mtcars_out$row_names))
+ expect_equal_df(mtcars_out[names(mtcars_out) != "row_names"], unrowname(mtcars_in))
+ })
+ })
+ },
+
+ read_table_check_names = function(ctx) {
+ with_connection({
+ #' If the database supports identifiers with special characters,
+ if (isTRUE(ctx$tweaks$strict_identifier)) {
+ skip("tweak: strict_identifier")
+ }
+
+ #' the columns in the returned data frame are converted to valid R
+ #' identifiers
+ with_remove_test_table({
+ test_in <- data.frame(a = 1:3, b = 4:6)
+ names(test_in) <- c("with spaces", "with,comma")
+ dbWriteTable(con, "test", test_in)
+ #' if the `check.names` argument is `TRUE`,
+ test_out <- check_df(dbReadTable(con, "test", check.names = TRUE))
+
+ expect_identical(names(test_out), make.names(names(test_out), unique = TRUE))
+ expect_equal_df(test_out, setNames(test_in, names(test_out)))
+ })
+
+ #' otherwise non-syntactic column names can be returned unquoted.
+ with_remove_test_table({
+ test_in <- data.frame(a = 1:3, b = 4:6)
+ names(test_in) <- c("with spaces", "with,comma")
+ dbWriteTable(con, "test", test_in)
+ test_out <- check_df(dbReadTable(con, "test", check.names = FALSE))
+
+ expect_equal_df(test_out, test_in)
+ })
+ })
+ },
+
+ #'
+ #' An error is raised when calling this method for a closed
+ read_table_closed_connection = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ dbWriteTable(con, "test", data.frame(a = 1))
+ with_closed_connection(con = "con2", {
+ expect_error(dbReadTable(con2, "test"))
+ })
+ })
+ })
+ },
+
+ #' or invalid connection.
+ read_table_invalid_connection = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ dbWriteTable(con, "test", data.frame(a = 1))
+ with_invalid_connection(con = "con2", {
+ expect_error(dbReadTable(con2, "test"))
+ })
+ })
+ })
+ },
+
+ #' An error is raised
+ read_table_error = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ dbWriteTable(con, "test", data.frame(a = 1L))
+ #' if `name` cannot be processed with [dbQuoteIdentifier()]
+ expect_error(dbReadTable(con, NA))
+ #' or if this results in a non-scalar.
+ expect_error(dbReadTable(con, c("test", "test")))
+
+ #' Unsupported values for `row.names` and `check.names`
+ #' (non-scalars,
+ expect_error(dbReadTable(con, "test", row.names = letters))
+ #' unsupported data types,
+ expect_error(dbReadTable(con, "test", row.names = list(1L)))
+ expect_error(dbReadTable(con, "test", check.names = 1L))
+ #' `NA` for `check.names`)
+ expect_error(dbReadTable(con, "test", check.names = NA))
+ #' also raise an error.
+ })
+ })
+ },
+
+ #' @section Additional arguments:
+ #' The following arguments are not part of the `dbReadTable()` generic
+ #' (to improve compatibility across backends)
+ #' but are part of the DBI specification:
+ #' - `row.names`
+ #' - `check.names`
+ #'
+ #' They must be provided as named arguments.
+ #' See the "Value" section for details on their usage.
+
+ #' @section Specification:
+ #' The `name` argument is processed as follows,
+ read_table_name = function(ctx) {
+ with_connection({
+ #' to support databases that allow non-syntactic names for their objects:
+ if (isTRUE(ctx$tweaks$strict_identifier)) {
+ table_names <- "a"
+ } else {
+ table_names <- c("a", "with spaces", "with,comma")
+ }
+
+ for (table_name in table_names) {
+ with_remove_test_table(name = dbQuoteIdentifier(con, table_name), {
+ test_in <- data.frame(a = 1L)
+ dbWriteTable(con, table_name, test_in)
+
+ #' - If an unquoted table name as string: `dbReadTable()` will do the
+ #' quoting,
+ test_out <- check_df(dbReadTable(con, table_name))
+ expect_equal_df(test_out, test_in)
+ #' perhaps by calling `dbQuoteIdentifier(conn, x = name)`
+ #' - If the result of a call to [dbQuoteIdentifier()]: no more quoting is done
+ test_out <- check_df(dbReadTable(con, dbQuoteIdentifier(con, table_name)))
+ expect_equal_df(test_out, test_in)
+ })
+ }
+ })
+ },
+
+ NULL
+)
diff --git a/R/spec-sql-read-write-roundtrip.R b/R/spec-sql-read-write-roundtrip.R
deleted file mode 100644
index b462ced..0000000
--- a/R/spec-sql-read-write-roundtrip.R
+++ /dev/null
@@ -1,241 +0,0 @@
-#' @template dbispec-sub-wip
-#' @format NULL
-#' @section SQL:
-#' \subsection{Roundtrip tests}{
-spec_sql_read_write_roundtrip <- list(
- #' Can create tables with keywords as table and column names.
- roundtrip_keywords = function(ctx) {
- with_connection({
- tbl_in <- data.frame(SELECT = "UNIQUE", FROM = "JOIN", WHERE = "ORDER",
- stringsAsFactors = FALSE)
-
- on.exit(expect_error(dbRemoveTable(con, "EXISTS"), NA), add = TRUE)
- dbWriteTable(con, "EXISTS", tbl_in)
-
- tbl_out <- dbReadTable(con, "EXISTS")
- expect_identical(tbl_in, tbl_out)
- })
- },
-
- #' Can create tables with quotes, commas, and spaces in column names and
- #' data.
- roundtrip_quotes = function(ctx) {
- with_connection({
- tbl_in <- data.frame(a = as.character(dbQuoteString(con, "")),
- b = as.character(dbQuoteIdentifier(con, "")),
- c = "with space",
- d = ",",
- stringsAsFactors = FALSE)
-
- if (!isTRUE(ctx$tweaks$strict_identifier)) {
- names(tbl_in) <- c(
- as.character(dbQuoteIdentifier(con, "")),
- as.character(dbQuoteString(con, "")),
- "with space",
- ",")
- }
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA), add = TRUE)
- dbWriteTable(con, "test", tbl_in)
-
- tbl_out <- dbReadTable(con, "test")
- expect_identical(tbl_in, tbl_out)
- })
- },
-
- #' Can create tables with integer columns.
- roundtrip_integer = function(ctx) {
- with_connection({
- tbl_in <- data.frame(a = c(1:5, NA), id = 1:6)
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA), add = TRUE)
- dbWriteTable(con, "test", tbl_in)
-
- tbl_out <- dbReadTable(con, "test")
- expect_identical(tbl_in, tbl_out[order(tbl_out$id), ])
- })
- },
-
- #' Can create tables with numeric columns.
- roundtrip_numeric = function(ctx) {
- with_connection({
- tbl_in <- data.frame(a = c(seq(1, 3, by = 0.5), NA), id = 1:6)
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA), add = TRUE)
- dbWriteTable(con, "test", tbl_in)
-
- tbl_out <- dbReadTable(con, "test")
- expect_identical(tbl_in, tbl_out[order(tbl_out$id), ])
- })
- },
-
- #' Can create tables with numeric columns that contain special values such
- #' as `Inf` and `NaN`.
- roundtrip_numeric_special = function(ctx) {
- with_connection({
- tbl_in <- data.frame(a = c(seq(1, 3, by = 0.5), NA, -Inf, Inf, NaN),
- id = 1:9)
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA), add = TRUE)
- dbWriteTable(con, "test", tbl_in)
-
- tbl_out <- dbReadTable(con, "test")
- expect_equal(tbl_in$a, tbl_out$a[order(tbl_out$id)])
- })
- },
-
- #' Can create tables with logical columns.
- roundtrip_logical = function(ctx) {
- with_connection({
- tbl_in <- data.frame(a = c(TRUE, FALSE, NA), id = 1:3)
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA), add = TRUE)
- dbWriteTable(con, "test", tbl_in)
-
- tbl_out <- dbReadTable(con, "test")
- expect_identical(tbl_in, tbl_out[order(tbl_out$id), ])
- })
- },
-
- #' Can create tables with logical columns, returned as integer.
- roundtrip_logical_int = function(ctx) {
- with_connection({
- tbl_in <- data.frame(a = c(TRUE, FALSE, NA), id = 1:3)
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA), add = TRUE)
- dbWriteTable(con, "test", tbl_in)
-
- tbl_out <- dbReadTable(con, "test")
- expect_identical(as.integer(tbl_in$a), tbl_out$a[order(tbl_out$id)])
- })
- },
-
- #' Can create tables with NULL values.
- roundtrip_null = function(ctx) {
- with_connection({
- tbl_in <- data.frame(a = NA)
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA), add = TRUE)
- dbWriteTable(con, "test", tbl_in)
-
- tbl_out <- dbReadTable(con, "test")
- expect_true(is.na(tbl_out$a))
- })
- },
-
- #' Can create tables with 64-bit columns.
- roundtrip_64_bit = function(ctx) {
- with_connection({
- tbl_in <- data.frame(a = c(-1e14, 1e15, 0.25, NA), id = 1:4)
- tbl_in_trunc <- data.frame(a = trunc(tbl_in$a))
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA), add = TRUE)
- dbWriteTable(con, "test", tbl_in, field.types = "bigint")
-
- tbl_out <- dbReadTable(con, "test")
- expect_identical(tbl_in_trunc, tbl_out[order(tbl_out$id), ])
- })
- },
-
- #' Can create tables with character columns.
- roundtrip_character = function(ctx) {
- with_connection({
- tbl_in <- data.frame(a = c(text_cyrillic, text_latin,
- text_chinese, text_ascii, NA),
- id = 1:5, stringsAsFactors = FALSE)
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA), add = TRUE)
- dbWriteTable(con, "test", tbl_in)
-
- tbl_out <- dbReadTable(con, "test")
- expect_identical(tbl_in, tbl_out[order(tbl_out$id), ])
-
- expect_true(all_have_utf8_or_ascii_encoding(tbl_out$a))
- })
- },
-
- #' Can create tables with factor columns.
- roundtrip_factor = function(ctx) {
- with_connection({
- tbl_in <- data.frame(a = factor(c(text_cyrillic, text_latin,
- text_chinese, text_ascii, NA)),
- id = 1:5, stringsAsFactors = FALSE)
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA), add = TRUE)
- dbWriteTable(con, "test", tbl_in)
-
- tbl_out <- dbReadTable(con, "test")
- expect_identical(as.character(tbl_in$a), tbl_out$a[order(tbl_out$id)])
-
- expect_true(all_have_utf8_or_ascii_encoding(tbl_out$a))
- })
- },
-
- #' Can create tables with raw columns.
- roundtrip_raw = function(ctx) {
- if (isTRUE(ctx$tweaks$omit_blob_tests)) {
- skip("tweak: omit_blob_tests")
- }
-
- with_connection({
- tbl_in <- list(a = list(as.raw(1:10), NA), id = 1:2)
- tbl_in <- structure(tbl_in, class = "data.frame",
- row.names = c(NA, -2L))
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA), add = TRUE)
- dbWriteTable(con, "test", tbl_in)
-
- tbl_out <- dbReadTable(con, "test")
- expect_identical(tbl_in, tbl_out[order(tbl_out$id), ])
- })
- },
-
- #' Can create tables with date columns.
- roundtrip_date = function(ctx) {
- with_connection({
- tbl_in <- data.frame(id = 1:6)
- tbl_in$a <- c(Sys.Date() + 1:5, NA)
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA), add = TRUE)
- dbWriteTable(con, "test", tbl_in)
-
- tbl_out <- dbReadTable(con, "test")
- expect_equal(tbl_in, tbl_out[order(tbl_out$id), ])
- expect_is(unclass(tbl_out$a), "integer")
- })
- },
-
- #' Can create tables with timestamp columns.
- roundtrip_timestamp = function(ctx) {
- with_connection({
- tbl_in <- data.frame(id = 1:5)
- tbl_in$a <- round(Sys.time()) + c(1, 60, 3600, 86400, NA)
- tbl_in$b <- as.POSIXlt(tbl_in$a, tz = "GMT")
- tbl_in$c <- as.POSIXlt(tbl_in$a, tz = "PST")
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA), add = TRUE)
- dbWriteTable(con, "test", tbl_in)
-
- tbl_out <- dbReadTable(con, "test")
- expect_identical(tbl_in, tbl_out[order(tbl_out$id), ])
- })
- },
-
- #' Can create tables with row names.
- roundtrip_rownames = function(ctx) {
- with_connection({
- tbl_in <- data.frame(a = c(1:5, NA),
- row.names = paste0(LETTERS[1:6], 1:6),
- id = 1:6)
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA), add = TRUE)
- dbWriteTable(con, "test", tbl_in)
-
- tbl_out <- dbReadTable(con, "test")
- expect_identical(rownames(tbl_in), rownames(tbl_out)[order(tbl_out$id)])
- })
- },
-
- #' }
- NULL
-)
diff --git a/R/spec-sql-read-write-table.R b/R/spec-sql-read-write-table.R
deleted file mode 100644
index 17a5a55..0000000
--- a/R/spec-sql-read-write-table.R
+++ /dev/null
@@ -1,137 +0,0 @@
-#' @template dbispec-sub-wip
-#' @format NULL
-#' @section SQL:
-#' \subsection{`dbReadTable("DBIConnection")` and `dbWriteTable("DBIConnection")`}{
-spec_sql_read_write_table <- list(
- #' Can write the [datasets::iris] data as a table to the
- #' database, but won't overwrite by default.
- write_table = function(ctx) {
- with_connection({
- expect_error(dbGetQuery(con, "SELECT * FROM iris"))
- on.exit(expect_error(dbRemoveTable(con, "iris"), NA),
- add = TRUE)
-
- iris <- get_iris(ctx)
- dbWriteTable(con, "iris", iris)
- expect_error(dbWriteTable(con, "iris", iris))
-
- with_connection({
- expect_error(dbGetQuery(con2, "SELECT * FROM iris"), NA)
- }
- , con = "con2")
- })
-
- with_connection({
- expect_error(dbGetQuery(con, "SELECT * FROM iris"))
- })
- },
-
- #' Can read the [datasets::iris] data from a database table.
- read_table = function(ctx) {
- with_connection({
- expect_error(dbGetQuery(con, "SELECT * FROM iris"))
- on.exit(expect_error(dbRemoveTable(con, "iris"), NA),
- add = TRUE)
-
- iris_in <- get_iris(ctx)
- iris_in$Species <- as.character(iris_in$Species)
- order_in <- do.call(order, iris_in)
-
- dbWriteTable(con, "iris", iris_in)
- iris_out <- dbReadTable(con, "iris")
- order_out <- do.call(order, iris_out)
-
- expect_identical(unrowname(iris_in[order_in, ]), unrowname(iris_out[order_out, ]))
- })
- },
-
- #' Can write the [datasets::iris] data as a table to the
- #' database, will overwrite if asked.
- overwrite_table = function(ctx) {
- with_connection({
- expect_error(dbGetQuery(con, "SELECT * FROM iris"))
- on.exit(expect_error(dbRemoveTable(con, "iris"), NA),
- add = TRUE)
-
- iris <- get_iris(ctx)
- dbWriteTable(con, "iris", iris)
- expect_error(dbWriteTable(con, "iris", iris[1:10,], overwrite = TRUE),
- NA)
- iris_out <- dbReadTable(con, "iris")
- expect_identical(nrow(iris_out), 10L)
- })
- },
-
- #' Can write the [datasets::iris] data as a table to the
- #' database, will append if asked.
- append_table = function(ctx) {
- with_connection({
- expect_error(dbGetQuery(con, "SELECT * FROM iris"))
- on.exit(expect_error(dbRemoveTable(con, "iris"), NA),
- add = TRUE)
-
- iris <- get_iris(ctx)
- dbWriteTable(con, "iris", iris)
- expect_error(dbWriteTable(con, "iris", iris[1:10,], append = TRUE), NA)
- iris_out <- dbReadTable(con, "iris")
- expect_identical(nrow(iris_out), nrow(iris) + 10L)
- })
- },
-
- #' Cannot append to nonexisting table.
- append_table_error = function(ctx) {
- with_connection({
- expect_error(dbGetQuery(con, "SELECT * FROM iris"))
- on.exit(expect_error(dbRemoveTable(con, "iris")))
-
- iris <- get_iris(ctx)
- expect_error(dbWriteTable(con, "iris", iris[1:20,], append = TRUE))
- })
- },
-
- #' Can write the [datasets::iris] data as a temporary table to
- #' the database, the table is not available in a second connection and is
- #' gone after reconnecting.
- temporary_table = function(ctx) {
- with_connection({
- expect_error(dbGetQuery(con, "SELECT * FROM iris"))
-
- iris <- get_iris(ctx)
- dbWriteTable(con, "iris", iris[1:30, ], temporary = TRUE)
- iris_out <- dbReadTable(con, "iris")
- expect_identical(nrow(iris_out), 30L)
-
- with_connection({
- expect_error(dbGetQuery(con2, "SELECT * FROM iris"))
- }
- , con = "con2")
- })
-
- with_connection({
- expect_error(dbGetQuery(con, "SELECT * FROM iris"))
- try(dbRemoveTable(con, "iris"), silent = TRUE)
- })
- },
-
- #' A new table is visible in a second connection.
- table_visible_in_other_connection = function(ctx) {
- with_connection({
- expect_error(dbGetQuery(con, "SELECT * from test"))
-
- on.exit(expect_error(dbRemoveTable(con, "test"), NA),
- add = TRUE)
-
- data <- data.frame(a = 1L)
- dbWriteTable(con, "test", data)
-
- with_connection({
- expect_error(rows <- dbGetQuery(con2, "SELECT * FROM test"), NA)
- expect_identical(rows, data)
- }
- , con = "con2")
- })
- },
-
- #' }
- NULL
-)
diff --git a/R/spec-sql-remove-table.R b/R/spec-sql-remove-table.R
new file mode 100644
index 0000000..3e593f5
--- /dev/null
+++ b/R/spec-sql-remove-table.R
@@ -0,0 +1,161 @@
+#' spec_sql_remove_table
+#' @usage NULL
+#' @format NULL
+#' @keywords NULL
+spec_sql_remove_table <- list(
+ remove_table_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbRemoveTable)), c("conn", "name", "..."))
+ },
+
+ #' @return
+ #' `dbRemoveTable()` returns `TRUE`, invisibly.
+ remove_table_return = function(ctx) {
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ iris <- get_iris(ctx)
+ dbWriteTable(con, "iris", iris)
+
+ expect_invisible_true(dbRemoveTable(con, "iris"))
+ })
+ })
+ },
+
+ #' If the table does not exist, an error is raised.
+ remove_table_missing = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ expect_error(dbRemoveTable("test"))
+ })
+ })
+ },
+
+ #' An attempt to remove a view with this function may result in an error.
+ #'
+ #'
+ #' An error is raised when calling this method for a closed
+ remove_table_closed_connection = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ dbWriteTable(con, "test", data.frame(a = 1))
+ with_closed_connection(con = "con2", {
+ expect_error(dbRemoveTable(con2, "test"))
+ })
+ })
+ })
+ },
+
+ #' or invalid connection.
+ remove_table_invalid_connection = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ dbWriteTable(con, "test", data.frame(a = 1))
+ with_invalid_connection(con = "con2", {
+ expect_error(dbRemoveTable(con2, "test"))
+ })
+ })
+ })
+ },
+
+ #' An error is also raised
+ remove_table_error = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ dbWriteTable(con, "test", data.frame(a = 1L))
+ #' if `name` cannot be processed with [dbQuoteIdentifier()]
+ expect_error(dbRemoveTable(con, NA))
+ #' or if this results in a non-scalar.
+ expect_error(dbRemoveTable(con, c("test", "test")))
+ })
+ })
+ },
+
+ #' @section Specification:
+ #' A table removed by `dbRemoveTable()` doesn't appear in the list of tables
+ #' returned by [dbListTables()],
+ #' and [dbExistsTable()] returns `FALSE`.
+ remove_table_list = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ dbWriteTable(con, "test", data.frame(a = 1L))
+ expect_true("test" %in% dbListTables(con))
+ expect_true(dbExistsTable(con, "test"))
+
+ dbRemoveTable(con, "test")
+ expect_false("test" %in% dbListTables(con))
+ expect_false(dbExistsTable(con, "test"))
+ })
+ })
+ },
+
+ #' The removal propagates immediately to other connections to the same database.
+ remove_table_other_con = function(ctx) {
+ with_connection({
+ with_connection(con = "con2", {
+ with_remove_test_table({
+ dbWriteTable(con, "test", data.frame(a = 1L))
+ expect_true("test" %in% dbListTables(con2))
+ expect_true(dbExistsTable(con2, "test"))
+
+ dbRemoveTable(con, "test")
+ expect_false("test" %in% dbListTables(con2))
+ expect_false(dbExistsTable(con2, "test"))
+ })
+ })
+ })
+ },
+
+ #' This function can also be used to remove a temporary table.
+ remove_table_temporary = function(ctx) {
+ if (!isTRUE(ctx$tweaks$temporary_tables)) {
+ skip("tweak: temporary_tables")
+ }
+
+ with_connection({
+ with_remove_test_table({
+ dbWriteTable(con, "test", data.frame(a = 1L), temporary = TRUE)
+ expect_true("test" %in% dbListTables(con))
+ expect_true(dbExistsTable(con, "test"))
+
+ dbRemoveTable(con, "test")
+ expect_false("test" %in% dbListTables(con))
+ expect_false(dbExistsTable(con, "test"))
+ })
+ })
+ },
+
+ #'
+ #' The `name` argument is processed as follows,
+ remove_table_name = function(ctx) {
+ with_connection({
+ #' to support databases that allow non-syntactic names for their objects:
+ if (isTRUE(ctx$tweaks$strict_identifier)) {
+ table_names <- "a"
+ } else {
+ table_names <- c("a", "with spaces", "with,comma")
+ }
+
+ test_in <- data.frame(a = 1L)
+
+ for (table_name in table_names) {
+ with_remove_test_table(name = dbQuoteIdentifier(con, table_name), {
+ #' - If an unquoted table name as string: `dbRemoveTable()` will do the
+ #' quoting,
+ dbWriteTable(con, table_name, test_in)
+ expect_true(dbRemoveTable(con, table_name))
+ #' perhaps by calling `dbQuoteIdentifier(conn, x = name)`
+ })
+ }
+
+ for (table_name in table_names) {
+ with_remove_test_table(name = dbQuoteIdentifier(con, table_name), {
+ #' - If the result of a call to [dbQuoteIdentifier()]: no more quoting is done
+ dbWriteTable(con, table_name, test_in)
+ expect_true(dbRemoveTable(con, dbQuoteIdentifier(con, table_name)))
+ })
+ }
+ })
+ },
+
+ NULL
+)
diff --git a/R/spec-sql-write-table.R b/R/spec-sql-write-table.R
new file mode 100644
index 0000000..1a7ace4
--- /dev/null
+++ b/R/spec-sql-write-table.R
@@ -0,0 +1,755 @@
+#' spec_sql_write_table
+#' @usage NULL
+#' @format NULL
+#' @keywords NULL
+spec_sql_write_table <- list(
+ write_table_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbWriteTable)), c("conn", "name", "value", "..."))
+ },
+
+ #' @return
+ #' `dbWriteTable()` returns `TRUE`, invisibly.
+ write_table_return = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ expect_invisible_true(dbWriteTable(con, "test", data.frame(a = 1L)))
+ })
+ })
+ },
+
+ #' If the table exists, and both `append` and `overwrite` arguments are unset,
+ write_table_overwrite = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ test_in <- data.frame(a = 1L)
+ dbWriteTable(con, "test", test_in)
+ expect_error(dbWriteTable(con, "test", data.frame(a = 2L)))
+
+ test_out <- check_df(dbReadTable(con, "test"))
+ expect_equal_df(test_out, test_in)
+ })
+ })
+ },
+
+ #' or `append = TRUE` and the data frame with the new data has different
+ #' column names,
+ #' an error is raised; the remote table remains unchanged.
+ write_table_append_incompatible = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ test_in <- data.frame(a = 1L)
+ dbWriteTable(con, "test", test_in)
+ expect_error(dbWriteTable(con, "test", data.frame(b = 2L), append = TRUE))
+
+ test_out <- check_df(dbReadTable(con, "test"))
+ expect_equal_df(test_out, test_in)
+ })
+ })
+ },
+
+ #'
+ #' An error is raised when calling this method for a closed
+ write_table_closed_connection = function(ctx) {
+ with_closed_connection({
+ expect_error(dbWriteTable(con, "test", data.frame(a = 1)))
+ })
+ },
+
+ #' or invalid connection.
+ write_table_invalid_connection = function(ctx) {
+ with_invalid_connection({
+ expect_error(dbListTables(con, "test", data.frame(a = 1)))
+ })
+ },
+
+ #' An error is also raised
+ write_table_error = function(ctx) {
+ with_connection({
+ test_in <- data.frame(a = 1L)
+ with_remove_test_table({
+ #' if `name` cannot be processed with [dbQuoteIdentifier()]
+ expect_error(dbWriteTable(con, NA, test_in))
+ #' or if this results in a non-scalar.
+ expect_error(dbWriteTable(con, c("test", "test"), test_in))
+
+ #' Invalid values for the additional arguments `row.names`,
+ #' `overwrite`, `append`, `field.types`, and `temporary`
+ #' (non-scalars,
+ expect_error(dbWriteTable(con, "test", test_in, row.names = letters))
+ expect_error(dbWriteTable(con, "test", test_in, overwrite = c(TRUE, FALSE)))
+ expect_error(dbWriteTable(con, "test", test_in, append = c(TRUE, FALSE)))
+ expect_error(dbWriteTable(con, "test", test_in, temporary = c(TRUE, FALSE)))
+ #' unsupported data types,
+ expect_error(dbWriteTable(con, "test", test_in, row.names = list(1L)))
+ expect_error(dbWriteTable(con, "test", test_in, overwrite = 1L))
+ expect_error(dbWriteTable(con, "test", test_in, append = 1L))
+ expect_error(dbWriteTable(con, "test", test_in, field.types = 1L))
+ expect_error(dbWriteTable(con, "test", test_in, temporary = 1L))
+ #' `NA`,
+ expect_error(dbWriteTable(con, "test", test_in, overwrite = NA))
+ expect_error(dbWriteTable(con, "test", test_in, append = NA))
+ expect_error(dbWriteTable(con, "test", test_in, field.types = NA))
+ expect_error(dbWriteTable(con, "test", test_in, temporary = NA))
+ #' incompatible values,
+ expect_error(dbWriteTable(con, "test", test_in, field.types = letters))
+ expect_error(dbWriteTable(con, "test", test_in, field.types = c(b = "INTEGER")))
+ expect_error(dbWriteTable(con, "test", test_in, overwrite = TRUE, append = TRUE))
+ expect_error(dbWriteTable(con, "test", test_in, append = TRUE, field.types = c(a = "INTEGER")))
+ #' duplicate
+ expect_error(dbWriteTable(con, "test", test_in, field.types = c(a = "INTEGER", a = "INTEGER")))
+ #' or missing names,
+ expect_error(dbWriteTable(con, "test", test_in, field.types = c("INTEGER")))
+ })
+
+ with_remove_test_table({
+ dbWriteTable(con, "test", test_in)
+ #' incompatible columns)
+ expect_error(dbWriteTable(con, "test", data.frame(b = 2L, c = 3L), append = TRUE))
+ })
+ #' also raise an error.
+ })
+ },
+
+ #' @section Additional arguments:
+ #' The following arguments are not part of the `dbWriteTable()` generic
+ #' (to improve compatibility across backends)
+ #' but are part of the DBI specification:
+ #' - `row.names` (default: `NA`)
+ #' - `overwrite` (default: `FALSE`)
+ #' - `append` (default: `FALSE`)
+ #' - `field.types` (default: `NULL`)
+ #' - `temporary` (default: `FALSE`)
+ #'
+ #' They must be provided as named arguments.
+ #' See the "Specification" and "Value" sections for details on their usage.
+
+ #' @section Specification:
+ #' The `name` argument is processed as follows,
+ write_table_name = function(ctx) {
+ with_connection({
+ #' to support databases that allow non-syntactic names for their objects:
+ if (isTRUE(ctx$tweaks$strict_identifier)) {
+ table_names <- "a"
+ } else {
+ table_names <- c("a", "with spaces", "with,comma")
+ }
+
+ for (table_name in table_names) {
+ test_in <- data.frame(a = 1)
+ with_remove_test_table(name = dbQuoteIdentifier(con, table_name), {
+ #' - If an unquoted table name as string: `dbWriteTable()` will do the quoting,
+ dbWriteTable(con, table_name, test_in)
+ test_out <- check_df(dbReadTable(con, dbQuoteIdentifier(con, table_name)))
+ expect_equal_df(test_out, test_in)
+ #' perhaps by calling `dbQuoteIdentifier(conn, x = name)`
+ })
+
+ with_remove_test_table(name = dbQuoteIdentifier(con, table_name), {
+ #' - If the result of a call to [dbQuoteIdentifier()]: no more quoting is done
+ dbWriteTable(con, dbQuoteIdentifier(con, table_name), test_in)
+ test_out <- check_df(dbReadTable(con, table_name))
+ expect_equal_df(test_out, test_in)
+ })
+ }
+ })
+ },
+
+ #'
+ #' If the `overwrite` argument is `TRUE`, an existing table of the same name
+ #' will be overwritten.
+ overwrite_table = function(ctx) {
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ iris <- get_iris(ctx)
+ dbWriteTable(con, "iris", iris)
+ expect_error(dbWriteTable(con, "iris", iris[1:10,], overwrite = TRUE),
+ NA)
+ iris_out <- check_df(dbReadTable(con, "iris"))
+ expect_equal_df(iris_out, iris[1:10, ])
+ })
+ })
+ },
+
+ #' This argument doesn't change behavior if the table does not exist yet.
+ overwrite_table_missing = function(ctx) {
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ iris_in <- get_iris(ctx)
+ expect_error(dbWriteTable(con, "iris", iris[1:10,], overwrite = TRUE),
+ NA)
+ iris_out <- check_df(dbReadTable(con, "iris"))
+ expect_equal_df(iris_out, iris_in[1:10, ])
+ })
+ })
+ },
+
+ #'
+ #' If the `append` argument is `TRUE`, the rows in an existing table are
+ #' preserved, and the new data are appended.
+ append_table = function(ctx) {
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ iris <- get_iris(ctx)
+ dbWriteTable(con, "iris", iris)
+ expect_error(dbWriteTable(con, "iris", iris[1:10,], append = TRUE), NA)
+ iris_out <- check_df(dbReadTable(con, "iris"))
+ expect_equal_df(iris_out, rbind(iris, iris[1:10,]))
+ })
+ })
+ },
+
+ #' If the table doesn't exist yet, it is created.
+ append_table_new = function(ctx) {
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ iris <- get_iris(ctx)
+ expect_error(dbWriteTable(con, "iris", iris[1:10,], append = TRUE), NA)
+ iris_out <- check_df(dbReadTable(con, "iris"))
+ expect_equal_df(iris_out, iris[1:10,])
+ })
+ })
+ },
+
+ #'
+ #' If the `temporary` argument is `TRUE`, the table is not available in a
+ #' second connection and is gone after reconnecting.
+ temporary_table = function(ctx) {
+ #' Not all backends support this argument.
+ if (!isTRUE(ctx$tweaks$temporary_tables)) {
+ skip("tweak: temporary_tables")
+ }
+
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ iris <- get_iris(ctx)[1:30, ]
+ dbWriteTable(con, "iris", iris, temporary = TRUE)
+ iris_out <- check_df(dbReadTable(con, "iris"))
+ expect_equal_df(iris_out, iris)
+
+ with_connection(
+ expect_error(dbReadTable(con2, "iris")),
+ con = "con2")
+ })
+ })
+
+ with_connection({
+ expect_error(dbReadTable(con, "iris"))
+ })
+ },
+
+ #' A regular, non-temporary table is visible in a second connection
+ table_visible_in_other_connection = function(ctx) {
+ iris <- get_iris(ctx)[1:30,]
+
+ with_connection({
+ dbWriteTable(con, "iris", iris)
+ iris_out <- check_df(dbReadTable(con, "iris"))
+ expect_equal_df(iris_out, iris)
+
+ with_connection(
+ expect_equal_df(dbReadTable(con2, "iris"), iris),
+ con = "con2")
+ })
+
+ #' and after reconnecting to the database.
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ expect_equal_df(check_df(dbReadTable(con, "iris")), iris)
+ })
+ })
+ },
+
+ #'
+ #' SQL keywords can be used freely in table names, column names, and data.
+ roundtrip_keywords = function(ctx) {
+ with_connection({
+ tbl_in <- data.frame(
+ SELECT = "UNIQUE", FROM = "JOIN", WHERE = "ORDER",
+ stringsAsFactors = FALSE
+ )
+ test_table_roundtrip(con, tbl_in, name = "EXISTS")
+ })
+ },
+
+ #' Quotes, commas, and spaces can also be used in the data,
+ #' and, if the database supports non-syntactic identifiers,
+ #' also for table names and column names.
+ roundtrip_quotes = function(ctx) {
+ with_connection({
+ if (!isTRUE(ctx$tweaks$strict_identifier)) {
+ table_names <- c(
+ as.character(dbQuoteIdentifier(con, "")),
+ as.character(dbQuoteString(con, "")),
+ "with space",
+ ",")
+ } else {
+ table_names <- "a"
+ }
+
+ for (table_name in table_names) {
+ tbl_in <- data.frame(
+ a = as.character(dbQuoteString(con, "")),
+ b = as.character(dbQuoteIdentifier(con, "")),
+ c = "with space",
+ d = ",",
+ stringsAsFactors = FALSE
+ )
+
+ if (!isTRUE(ctx$tweaks$strict_identifier)) {
+ names(tbl_in) <- c(
+ as.character(dbQuoteIdentifier(con, "")),
+ as.character(dbQuoteString(con, "")),
+ "with space",
+ ",")
+ }
+
+ test_table_roundtrip(con, tbl_in)
+ }
+ })
+ },
+
+ #'
+ #' The following data types must be supported at least,
+ #' and be read identically with [dbReadTable()]:
+ #' - integer
+ roundtrip_integer = function(ctx) {
+ with_connection({
+ tbl_in <- data.frame(a = c(1:5))
+ test_table_roundtrip(con, tbl_in)
+ })
+ },
+
+ #' - numeric
+ roundtrip_numeric = function(ctx) {
+ with_connection({
+ tbl_in <- data.frame(a = c(seq(1, 3, by = 0.5)))
+ test_table_roundtrip(con, tbl_in)
+ })
+ },
+
+ #' (also with `Inf` and `NaN` values,
+ roundtrip_numeric_special = function(ctx) {
+ with_connection({
+ tbl_in <- data.frame(a = c(seq(1, 3, by = 0.5), -Inf, Inf, NaN))
+ tbl_exp <- tbl_in
+ #' the latter are translated to `NA`)
+ tbl_exp$a[is.nan(tbl_exp$a)] <- NA_real_
+ test_table_roundtrip(con, tbl_in, tbl_exp)
+ })
+ },
+
+ #' - logical
+ roundtrip_logical = function(ctx) {
+ with_connection({
+ tbl_in <- data.frame(a = c(TRUE, FALSE, NA))
+ tbl_exp <- tbl_in
+ tbl_exp$a <- ctx$tweaks$logical_return(tbl_exp$a)
+ test_table_roundtrip(con, tbl_in, tbl_exp)
+ })
+ },
+
+ #' - `NA` as NULL
+ roundtrip_null = function(ctx) {
+ with_connection({
+ tbl_in <- data.frame(a = NA)
+ test_table_roundtrip(
+ con, tbl_in,
+ transform = function(tbl_out) {
+ tbl_out$a <- as.logical(tbl_out$a) # Plain NA is of type logical
+ tbl_out
+ }
+ )
+ })
+ },
+
+ #' - 64-bit values (using `"bigint"` as field type);
+ roundtrip_64_bit_numeric = function(ctx) {
+ with_connection({
+ tbl_in <- data.frame(a = c(-1e14, 1e15))
+ test_table_roundtrip(
+ con, tbl_in,
+ transform = function(tbl_out) {
+ #' the result can be converted to a numeric, which may lose precision,
+ tbl_out$a <- as.numeric(tbl_out$a)
+ tbl_out
+ },
+ field.types = c(a = "BIGINT")
+ )
+ })
+ },
+
+ roundtrip_64_bit_character = function(ctx) {
+ with_connection({
+ tbl_in <- data.frame(a = c(-1e14, 1e15))
+ tbl_exp <- tbl_in
+ tbl_exp$a <- format(tbl_exp$a, scientific = FALSE)
+ test_table_roundtrip(
+ con, tbl_in, tbl_exp,
+ transform = function(tbl_out) {
+ # ' or to character, which gives the full decimal representation as a
+ # ' character vector
+ tbl_out$a <- as.character(tbl_out$a)
+ tbl_out
+ },
+ field.types = c(a = "BIGINT")
+ )
+ })
+ },
+
+ #' - character (in both UTF-8
+ roundtrip_character = function(ctx) {
+ with_connection({
+ tbl_in <- data.frame(
+ a = c(texts),
+ stringsAsFactors = FALSE
+ )
+ test_table_roundtrip(con, tbl_in)
+ })
+ },
+
+ #' and native encodings),
+ roundtrip_character_native = function(ctx) {
+ with_connection({
+ tbl_in <- data.frame(
+ a = c(enc2native(texts)),
+ stringsAsFactors = FALSE
+ )
+ test_table_roundtrip(con, tbl_in)
+ })
+ },
+
+ #' supporting empty strings
+ roundtrip_character_empty = function(ctx) {
+ with_connection({
+ tbl_in <- data.frame(
+ a = c("", "a"),
+ stringsAsFactors = FALSE
+ )
+ test_table_roundtrip(con, tbl_in)
+ })
+
+ with_connection({
+ tbl_in <- data.frame(
+ a = c("a", ""),
+ stringsAsFactors = FALSE
+ )
+ test_table_roundtrip(con, tbl_in)
+ })
+ },
+
+ #' - factor (returned as character)
+ roundtrip_factor = function(ctx) {
+ with_connection({
+ tbl_in <- data.frame(
+ a = factor(c(texts))
+ )
+ tbl_exp <- tbl_in
+ tbl_exp$a <- as.character(tbl_exp$a)
+ test_table_roundtrip(con, tbl_in, tbl_exp)
+ })
+ },
+
+ #' - list of raw
+ roundtrip_raw = function(ctx) {
+ #' (if supported by the database)
+ if (isTRUE(ctx$tweaks$omit_blob_tests)) {
+ skip("tweak: omit_blob_tests")
+ }
+
+ with_connection({
+ tbl_in <- data.frame(id = 1L, a = I(list(as.raw(1:10))))
+ tbl_exp <- tbl_in
+ tbl_exp$a <- blob::as.blob(unclass(tbl_in$a))
+ test_table_roundtrip(
+ con, tbl_in, tbl_exp,
+ transform = function(tbl_out) {
+ tbl_out$a <- blob::as.blob(tbl_out$a)
+ tbl_out
+ }
+ )
+ })
+ },
+
+ #' - objects of type [blob::blob]
+ roundtrip_blob = function(ctx) {
+ #' (if supported by the database)
+ if (isTRUE(ctx$tweaks$omit_blob_tests)) {
+ skip("tweak: omit_blob_tests")
+ }
+
+ with_connection({
+ tbl_in <- data.frame(id = 1L, a = blob::blob(as.raw(1:10)))
+ test_table_roundtrip(
+ con, tbl_in,
+ transform = function(tbl_out) {
+ tbl_out$a <- blob::as.blob(tbl_out$a)
+ tbl_out
+ }
+ )
+ })
+ },
+
+ #' - date
+ roundtrip_date = function(ctx) {
+ #' (if supported by the database;
+ if (!isTRUE(ctx$tweaks$date_typed)) {
+ skip("tweak: !date_typed")
+ }
+
+ with_connection({
+ #' returned as `Date`)
+ tbl_in <- data.frame(a = as_numeric_date(c(Sys.Date() + 1:5)))
+ test_table_roundtrip(
+ con, tbl_in,
+ transform = function(tbl_out) {
+ expect_is(unclass(tbl_out$a), "numeric")
+ tbl_out
+ }
+ )
+ })
+ },
+
+ #' - time
+ roundtrip_time = function(ctx) {
+ #' (if supported by the database;
+ if (!isTRUE(ctx$tweaks$time_typed)) {
+ skip("tweak: !time_typed")
+ }
+
+
+ with_connection({
+ now <- Sys.time()
+ tbl_in <- data.frame(a = c(now + 1:5) - now)
+
+ tbl_exp <- tbl_in
+ tbl_exp$a <- hms::as.hms(tbl_exp$a)
+
+ test_table_roundtrip(
+ con, tbl_in, tbl_exp,
+ transform = function(tbl_out) {
+ #' returned as objects that inherit from `difftime`)
+ expect_is(tbl_out$a, "difftime")
+ tbl_out$a <- hms::as.hms(tbl_out$a)
+ tbl_out
+ }
+ )
+ })
+ },
+
+ #' - timestamp
+ roundtrip_timestamp = function(ctx) {
+ #' (if supported by the database;
+ if (!isTRUE(ctx$tweaks$timestamp_typed)) {
+ skip("tweak: !timestamp_typed")
+ }
+
+ with_connection({
+ #' returned as `POSIXct`
+ #' with time zone support)
+ tbl_in <- data.frame(id = 1:5)
+ tbl_in$a <- round(Sys.time()) + c(1, 60, 3600, 86400, NA)
+ tbl_in$b <- as.POSIXct(tbl_in$a, tz = "GMT")
+ tbl_in$c <- as.POSIXct(tbl_in$a, tz = "PST8PDT")
+ tbl_in$d <- as.POSIXct(tbl_in$a, tz = "UTC")
+
+ test_table_roundtrip(con, tbl_in)
+ })
+ },
+
+ #'
+ #' Mixing column types in the same table is supported.
+ roundtrip_mixed = function(ctx) {
+ with_connection({
+ data <- list("a", 1L, 1.5)
+ data <- lapply(data, c, NA)
+ expanded <- expand.grid(a = data, b = data, c = data)
+ tbl_in_list <- lapply(
+ seq_len(nrow(expanded)),
+ function(i) {
+ as.data.frame(lapply(expanded[i, ], unlist, recursive = FALSE))
+ }
+ )
+
+ lapply(tbl_in_list, test_table_roundtrip, con = con)
+ })
+ },
+
+ #'
+ #' The `field.types` argument must be a named character vector with at most
+ #' one entry for each column.
+ #' It indicates the SQL data type to be used for a new column.
+ roundtrip_field_types = function(ctx) {
+ with_connection({
+ tbl_in <- data.frame(a = numeric())
+ tbl_exp <- data.frame(a = integer())
+ test_table_roundtrip(
+ con, tbl_in, tbl_exp,
+ field.types = c(a = "INTEGER")
+ )
+ })
+ },
+
+ #'
+ #' The interpretation of [rownames] depends on the `row.names` argument,
+ #' see [sqlRownamesToColumn()] for details:
+ write_table_row_names_false = function(ctx) {
+ #' - If `FALSE` or `NULL`, row names are ignored.
+ for (row.names in list(FALSE, NULL)) {
+ with_connection({
+ with_remove_test_table(name = "mtcars", {
+ mtcars_in <- datasets::mtcars
+ dbWriteTable(con, "mtcars", mtcars_in, row.names = row.names)
+ mtcars_out <- check_df(dbReadTable(con, "mtcars", row.names = FALSE))
+
+ expect_false("row_names" %in% names(mtcars_out))
+ expect_equal_df(mtcars_out, unrowname(mtcars_in))
+ })
+ })
+ }
+ },
+
+ write_table_row_names_true_exists = function(ctx) {
+ #' - If `TRUE`, row names are converted to a column named "row_names",
+ row.names <- TRUE
+
+ with_connection({
+ with_remove_test_table(name = "mtcars", {
+ mtcars_in <- datasets::mtcars
+ dbWriteTable(con, "mtcars", mtcars_in, row.names = row.names)
+ mtcars_out <- check_df(dbReadTable(con, "mtcars", row.names = FALSE))
+
+ expect_true("row_names" %in% names(mtcars_out))
+ expect_true(all(rownames(mtcars_in) %in% mtcars_out$row_names))
+ expect_true(all(mtcars_out$row_names %in% rownames(mtcars_in)))
+ expect_equal_df(mtcars_out[names(mtcars_out) != "row_names"], unrowname(mtcars_in))
+ })
+ })
+ },
+
+ write_table_row_names_true_missing = function(ctx) {
+ #' even if the input data frame only has natural row names from 1 to `nrow(...)`.
+ row.names <- TRUE
+
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ iris_in <- get_iris(ctx)
+ dbWriteTable(con, "iris", iris_in, row.names = row.names)
+ iris_out <- check_df(dbReadTable(con, "iris", row.names = FALSE))
+
+ expect_true("row_names" %in% names(iris_out))
+ expect_true(all(rownames(iris_in) %in% iris_out$row_names))
+ expect_true(all(iris_out$row_names %in% rownames(iris_in)))
+ expect_equal_df(iris_out[names(iris_out) != "row_names"], iris_in)
+ })
+ })
+ },
+
+ write_table_row_names_na_exists = function(ctx) {
+ #' - If `NA`, a column named "row_names" is created if the data has custom row names,
+ row.names <- NA
+
+ with_connection({
+ with_remove_test_table(name = "mtcars", {
+ mtcars_in <- datasets::mtcars
+ dbWriteTable(con, "mtcars", mtcars_in, row.names = row.names)
+ mtcars_out <- check_df(dbReadTable(con, "mtcars", row.names = FALSE))
+
+ expect_true("row_names" %in% names(mtcars_out))
+ expect_true(all(rownames(mtcars_in) %in% mtcars_out$row_names))
+ expect_true(all(mtcars_out$row_names %in% rownames(mtcars_in)))
+ expect_equal_df(mtcars_out[names(mtcars_out) != "row_names"], unrowname(mtcars_in))
+ })
+ })
+ },
+
+ write_table_row_names_na_missing = function(ctx) {
+ #' no extra column is created in the case of natural row names.
+ row.names <- NA
+
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ iris_in <- get_iris(ctx)
+ dbWriteTable(con, "iris", iris_in, row.names = row.names)
+ iris_out <- check_df(dbReadTable(con, "iris", row.names = FALSE))
+
+ expect_equal_df(iris_out, iris_in)
+ })
+ })
+ },
+
+ write_table_row_names_string_exists = function(ctx) {
+ row.names <- "make_model"
+ #' - If a string, this specifies the name of the column in the remote table
+ #' that contains the row names,
+
+ with_connection({
+ with_remove_test_table(name = "mtcars", {
+ mtcars_in <- datasets::mtcars
+
+ dbWriteTable(con, "mtcars", mtcars_in, row.names = row.names)
+ mtcars_out <- check_df(dbReadTable(con, "mtcars", row.names = FALSE))
+
+ expect_true("make_model" %in% names(mtcars_out))
+ expect_true(all(mtcars_out$make_model %in% rownames(mtcars_in)))
+ expect_true(all(rownames(mtcars_in) %in% mtcars_out$make_model))
+ expect_equal_df(mtcars_out[names(mtcars_out) != "make_model"], unrowname(mtcars_in))
+ })
+ })
+ },
+
+ write_table_row_names_string_missing = function(ctx) {
+ row.names <- "seq"
+ #' even if the input data frame only has natural row names.
+
+ with_connection({
+ with_remove_test_table(name = "iris", {
+ iris_in <- get_iris(ctx)
+ dbWriteTable(con, "iris", iris_in, row.names = row.names)
+ iris_out <- check_df(dbReadTable(con, "iris", row.names = FALSE))
+
+ expect_true("seq" %in% names(iris_out))
+ expect_true(all(iris_out$seq %in% rownames(iris_in)))
+ expect_true(all(rownames(iris_in) %in% iris_out$seq))
+ expect_equal_df(iris_out[names(iris_out) != "seq"], iris_in)
+ })
+ })
+ },
+
+ NULL
+)
+
+test_table_roundtrip <- function(...) {
+ test_table_roundtrip_one(..., .add_na = "none")
+ test_table_roundtrip_one(..., .add_na = "above")
+ test_table_roundtrip_one(..., .add_na = "below")
+}
+
+test_table_roundtrip_one <- function(con, tbl_in, tbl_expected = tbl_in, transform = identity, name = "test", field.types = NULL, .add_na = "none") {
+ force(tbl_expected)
+ if (.add_na == "above") {
+ tbl_in <- add_na_above(tbl_in)
+ tbl_expected <- add_na_above(tbl_expected)
+ } else if (.add_na == "below") {
+ tbl_in <- add_na_below(tbl_in)
+ tbl_expected <- add_na_below(tbl_expected)
+ }
+
+ with_remove_test_table(name = dbQuoteIdentifier(con, name), {
+ dbWriteTable(con, name, tbl_in, field.types = field.types)
+
+ tbl_out <- check_df(dbReadTable(con, name, check.names = FALSE))
+ tbl_out <- transform(tbl_out)
+ expect_equal_df(tbl_out, tbl_expected)
+ })
+}
+
+add_na_above <- function(tbl) {
+ tbl <- rbind(tbl, tbl[nrow(tbl) + 1L, , drop = FALSE])
+ unrowname(tbl)
+}
+
+add_na_below <- function(tbl) {
+ tbl <- rbind(tbl[nrow(tbl) + 1L, , drop = FALSE], tbl)
+ unrowname(tbl)
+}
diff --git a/R/spec-sql.R b/R/spec-sql.R
index 0280728..e805706 100644
--- a/R/spec-sql.R
+++ b/R/spec-sql.R
@@ -3,8 +3,10 @@
spec_sql <- c(
spec_sql_quote_string,
spec_sql_quote_identifier,
- spec_sql_read_write_table,
- spec_sql_read_write_roundtrip,
+ spec_sql_read_table,
+ spec_sql_write_table,
spec_sql_list_tables,
+ spec_sql_exists_table,
+ spec_sql_remove_table,
spec_sql_list_fields
)
diff --git a/R/spec-stress-connection.R b/R/spec-stress-connection.R
index 00294b5..3ab2a48 100644
--- a/R/spec-stress-connection.R
+++ b/R/spec-stress-connection.R
@@ -1,12 +1,13 @@
#' @template dbispec-sub-wip
#' @format NULL
+#' @importFrom withr with_output_sink
#' @section Connection:
#' \subsection{Stress tests}{
spec_stress_connection <- list(
#' Open 50 simultaneous connections
simultaneous_connections = function(ctx) {
cons <- list()
- on.exit(expect_error(lapply(cons, dbDisconnect), NA), add = TRUE)
+ on.exit(try_silent(lapply(cons, dbDisconnect)), add = TRUE)
for (i in seq_len(50L)) {
cons <- c(cons, connect(ctx))
}
@@ -25,44 +26,6 @@ spec_stress_connection <- list(
}
},
- #' Repeated load, instantiation, connection, disconnection, and unload of
- #' package in a new R session.
- stress_load_connect_unload = function(ctx) {
- skip_on_travis()
- skip_on_appveyor()
- skip_if_not(getRversion() != "3.3.0")
-
- pkg <- get_pkg(ctx)
-
- script_file <- tempfile("DBItest", fileext = ".R")
- local({
- sink(script_file)
- on.exit(sink(), add = TRUE)
- cat(
- "devtools::RCMD('INSTALL', ", shQuote(pkg$path), ")\n",
- "library(DBI, quietly = TRUE)\n",
- "connect_args <- ",
- sep = ""
- )
- dput(ctx$connect_args)
- cat(
- "for (i in 1:50) {\n",
- " drv <- ", pkg$package, "::", deparse(ctx$drv_call), "\n",
- " con <- do.call(dbConnect, c(drv, connect_args))\n",
- " dbDisconnect(con)\n",
- " unloadNamespace(getNamespace(\"", pkg$package, "\"))\n",
- "}\n",
- sep = ""
- )
- })
-
- with_temp_libpaths({
- expect_equal(system(paste0("R -q --vanilla -f ", shQuote(script_file)),
- ignore.stdout = TRUE, ignore.stderr = TRUE),
- 0L)
- })
- },
-
#' }
NULL
)
diff --git a/R/spec-stress-driver.R b/R/spec-stress-driver.R
deleted file mode 100644
index 07b13cc..0000000
--- a/R/spec-stress-driver.R
+++ /dev/null
@@ -1,34 +0,0 @@
-#' @template dbispec-sub-wip
-#' @format NULL
-#' @section Driver:
-#' \subsection{Repeated loading, instantiation, and unloading}{
-spec_stress_driver <- list(
- #' Repeated load, instantiation, and unload of package in a new R session.
- stress_load_unload = function(ctx) {
- skip_on_travis()
- skip_on_appveyor()
- skip_if_not(getRversion() != "3.3.0")
-
- pkg <- get_pkg(ctx)
-
- script_file <- tempfile("DBItest", fileext = ".R")
- cat(
- "devtools::RCMD('INSTALL', ", shQuote(pkg$path), ")\n",
- "for (i in 1:50) {\n",
- " ", pkg$package, "::", deparse(ctx$drv_call), "\n",
- " unloadNamespace(getNamespace(\"", pkg$package, "\"))\n",
- "}\n",
- sep = "",
- file = script_file
- )
-
- with_temp_libpaths({
- expect_equal(system(paste0("R -q --vanilla -f ", shQuote(script_file)),
- ignore.stdout = TRUE, ignore.stderr = TRUE),
- 0L)
- })
- },
-
- #' }
- NULL
-)
diff --git a/R/spec-stress.R b/R/spec-stress.R
index f8ca853..126d45c 100644
--- a/R/spec-stress.R
+++ b/R/spec-stress.R
@@ -1,6 +1,5 @@
#' @template dbispec
#' @format NULL
spec_stress <- c(
- spec_stress_driver,
spec_stress_connection
)
diff --git a/R/spec-transaction-begin-commit-rollback.R b/R/spec-transaction-begin-commit-rollback.R
new file mode 100644
index 0000000..0cb3a6b
--- /dev/null
+++ b/R/spec-transaction-begin-commit-rollback.R
@@ -0,0 +1,192 @@
+#' spec_transaction_begin_commit_rollback
+#' @usage NULL
+#' @format NULL
+#' @keywords NULL
+spec_transaction_begin_commit_rollback <- list(
+ begin_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbBegin)), c("conn", "..."))
+ },
+
+ commit_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbCommit)), c("conn", "..."))
+ },
+
+ rollback_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbRollback)), c("conn", "..."))
+ },
+
+ #' @return
+ #' `dbBegin()`, `dbCommit()` and `dbRollback()` return `TRUE`, invisibly.
+ begin_commit_return_value = function(ctx) {
+ with_connection({
+ expect_invisible_true(dbBegin(con))
+ with_rollback_on_error({
+ expect_invisible_true(dbCommit(con))
+ })
+ })
+ },
+
+ begin_rollback_return_value = function(ctx) {
+ with_connection({
+ expect_invisible_true(dbBegin(con))
+ expect_invisible_true(dbRollback(con))
+ })
+ },
+
+ #' The implementations are expected to raise an error in case of failure,
+ #' but this is not tested.
+ begin_commit_closed = function(ctx) {
+ with_closed_connection({
+ #' In any way, all generics throw an error with a closed
+ expect_error(dbBegin(con))
+ expect_error(dbCommit(con))
+ expect_error(dbRollback(con))
+ })
+ },
+
+ begin_commit_invalid = function(ctx) {
+ with_invalid_connection({
+ #' or invalid connection.
+ expect_error(dbBegin(con))
+ expect_error(dbCommit(con))
+ expect_error(dbRollback(con))
+ })
+ },
+
+ commit_without_begin = function(ctx) {
+ #' In addition, a call to `dbCommit()`
+ with_connection({
+ expect_error(dbCommit(con))
+ })
+ },
+
+ rollback_without_begin = function(ctx) {
+ #' or `dbRollback()`
+ with_connection({
+ #' without a prior call to `dbBegin()` raises an error.
+ expect_error(dbRollback(con))
+ })
+ },
+
+ begin_begin = function(ctx) {
+ #' Nested transactions are not supported by DBI,
+ with_connection({
+ #' an attempt to call `dbBegin()` twice
+ dbBegin(con)
+ with_rollback_on_error({
+ #' yields an error.
+ expect_error(dbBegin(con))
+ dbCommit(con)
+ })
+ })
+ },
+
+ #' @section Specification:
+ #' Actual support for transactions may vary between backends.
+ begin_commit = function(ctx) {
+ with_connection({
+ #' A transaction is initiated by a call to `dbBegin()`
+ dbBegin(con)
+ #' and committed by a call to `dbCommit()`.
+ success <- FALSE
+ expect_error({dbCommit(con); success <- TRUE}, NA)
+ if (!success) dbRollback(con)
+ })
+ },
+
+ #' Data written in a transaction must persist after the transaction is committed.
+ begin_write_commit = function(ctx) {
+ with_connection({
+ #' For example, a table that is missing when the transaction is started
+ expect_false(dbExistsTable(con, "test"))
+
+ dbBegin(con)
+ with_rollback_on_error({
+ #' but is created
+ dbExecute(con, paste0("CREATE TABLE test (a ", dbDataType(con, 0L), ")"))
+
+ #' and populated during the transaction
+ dbExecute(con, paste0("INSERT INTO test (a) VALUES (1)"))
+
+ #' must exist and contain the data added there
+ expect_equal(check_df(dbReadTable(con, "test")), data.frame(a = 1))
+
+ #' both during
+ dbCommit(con)
+ })
+
+ #' and after the transaction,
+ expect_equal(check_df(dbReadTable(con, "test")), data.frame(a = 1))
+ })
+
+ with_connection({
+ with_remove_test_table({
+ #' and also in a new connection.
+ expect_true(dbExistsTable(con, "test"))
+ expect_equal(check_df(dbReadTable(con, "test")), data.frame(a = 1))
+ })
+ })
+ },
+
+ begin_rollback = function(ctx) {
+ with_connection({
+ #'
+ #' A transaction
+ dbBegin(con)
+ #' can also be aborted with `dbRollback()`.
+ expect_error(dbRollback(con), NA)
+ })
+ },
+
+ #' All data written in such a transaction must be removed after the
+ #' transaction is rolled back.
+ begin_write_rollback = function(ctx) {
+ with_connection({
+ #' For example, a table that is missing when the transaction is started
+ with_remove_test_table({
+ dbBegin(con)
+
+ #' but is created during the transaction
+ expect_error(
+ dbExecute(con, paste0("CREATE TABLE test (a ", dbDataType(con, 0L), ")")),
+ NA
+ )
+
+ #' must not exist anymore after the rollback.
+ dbRollback(con)
+ expect_false(dbExistsTable(con, "test"))
+ })
+ })
+ },
+
+ begin_write_disconnect = function(ctx) {
+ #'
+ #' Disconnection from a connection with an open transaction
+ with_connection({
+ dbBegin(con)
+
+ dbExecute(con, paste0("CREATE TABLE test (a ", dbDataType(con, 0L), ")"))
+ })
+
+ with_connection({
+ #' effectively rolls back the transaction.
+ #' All data written in such a transaction must be removed after the
+ #' transaction is rolled back.
+ with_remove_test_table({
+ expect_false(dbExistsTable(con, "test"))
+ })
+ })
+ },
+
+ #'
+ #' The behavior is not specified if other arguments are passed to these
+ #' functions. In particular, \pkg{RSQLite} issues named transactions
+ #' with support for nesting
+ #' if the `name` argument is set.
+ #'
+ #' The transaction isolation level is not specified by DBI.
+ NULL
+)
diff --git a/R/spec-transaction-begin-commit.R b/R/spec-transaction-begin-commit.R
deleted file mode 100644
index 839589b..0000000
--- a/R/spec-transaction-begin-commit.R
+++ /dev/null
@@ -1,98 +0,0 @@
-#' @template dbispec-sub
-#' @format NULL
-#' @section Transactions:
-#' \subsection{`dbBegin("DBIConnection")` and `dbCommit("DBIConnection")`}{
-spec_transaction_begin_commit <- list(
- #' Transactions are available in DBI, but actual support may vary between backends.
- begin_commit = function(ctx) {
- with_connection({
- #' A transaction is initiated by a call to [DBI::dbBegin()]
- dbBegin(con)
- on.exit(dbRollback(con), add = FALSE)
- #' and committed by a call to [DBI::dbCommit()].
- expect_error({dbCommit(con); on.exit(NULL, add = FALSE)}, NA)
- })
- },
-
- begin_commit_return_value = function(ctx) {
- with_connection({
- #' Both generics expect an object of class \code{\linkS4class{DBIConnection}}
- #' and return `TRUE` (invisibly) upon success.
- expect_invisible_true(dbBegin(con))
- on.exit(dbRollback(con), add = FALSE)
- expect_invisible_true(dbCommit(con))
- on.exit(NULL, add = FALSE)
- })
- },
-
- #'
- #' The implementations are expected to raise an error in case of failure,
- #' but this is difficult to test in an automated way.
- begin_commit_closed = function(ctx) {
- con <- connect(ctx)
- dbDisconnect(con)
-
- #' In any way, both generics should throw an error with a closed connection.
- expect_error(dbBegin(con))
- expect_error(dbCommit(con))
- },
-
- commit_without_begin = function(ctx) {
- #' In addition, a call to [DBI::dbCommit()] without
- #' a call to [DBI::dbBegin()] should raise an error.
- with_connection({
- expect_error(dbCommit(con))
- })
- },
-
- begin_begin = function(ctx) {
- #' Nested transactions are not supported by DBI,
- with_connection({
- #' an attempt to call [DBI::dbBegin()] twice
- dbBegin(con)
- on.exit(dbRollback(con), add = FALSE)
- #' should yield an error.
- expect_error(dbBegin(con))
- dbCommit(con)
- on.exit(NULL, add = FALSE)
- })
- },
-
- #'
- #' Data written in a transaction must persist after the transaction is committed.
- begin_write_commit = function(ctx) {
- with_connection({
- #' For example, a table that is missing when the transaction is started
- expect_false(dbExistsTable(con, "test"))
-
- dbBegin(con)
- on.exit(dbRollback(con), add = FALSE)
-
- #' but is created
- dbExecute(con, paste0("CREATE TABLE test (a ", dbDataType(con, 0L), ")"))
-
- #' and populated during the transaction
- dbExecute(con, paste0("INSERT INTO test (a) VALUES (1)"))
-
- #' must exist and contain the data added there
- expect_equal(dbReadTable(con, "test"), data.frame(a = 1))
-
- #' both during
- dbCommit(con)
- on.exit(dbRemoveTable(con, "test"), add = FALSE)
-
- #' and after the transaction.
- expect_equal(dbReadTable(con, "test"), data.frame(a = 1))
- })
- },
-
- #'
- #' The behavior is not specified if other arguments are passed to these
- #' functions. In particular, \pkg{RSQLite} issues named transactions
- #' if the `name` argument is set.
- #'
- #' The transaction isolation level is not specified by DBI.
- #'
- #' }
- NULL
-)
diff --git a/R/spec-transaction-begin-rollback.R b/R/spec-transaction-begin-rollback.R
deleted file mode 100644
index 3e1134f..0000000
--- a/R/spec-transaction-begin-rollback.R
+++ /dev/null
@@ -1,10 +0,0 @@
-#' @template dbispec-sub-wip
-#' @format NULL
-#' @section Transactions:
-#' \subsection{`dbBegin("DBIConnection")` and `dbRollback("DBIConnection")`}{
-spec_transaction_begin_rollback <- list(
- #' Filler
-
- #' }
- NULL
-)
diff --git a/R/spec-transaction-with-transaction.R b/R/spec-transaction-with-transaction.R
index 21b3bdf..af8a04d 100644
--- a/R/spec-transaction-with-transaction.R
+++ b/R/spec-transaction-with-transaction.R
@@ -1,10 +1,133 @@
-#' @template dbispec-sub-wip
+#' spec_transaction_with_transaction
+#' @usage NULL
#' @format NULL
-#' @section Transactions:
-#' \subsection{`dbWithTransaction("DBIConnection")` and `dbBreak()`}{
+#' @keywords NULL
spec_transaction_with_transaction <- list(
- #' Filler
+ with_transaction_formals = function(ctx) {
+ # <establish formals of described functions>
+ expect_equal(names(formals(dbWithTransaction)), c("conn", "code", "..."))
+ },
+
+ #' @return
+ #' `dbWithTransaction()` returns the value of the executed code.
+ with_transaction_return_value = function(ctx) {
+ name <- random_table_name()
+ with_connection({
+ expect_identical(dbWithTransaction(con, name), name)
+ })
+ },
+
+ #' Failure to initiate the transaction
+ #' (e.g., if the connection is closed
+ with_transaction_error_closed = function(ctx) {
+ with_closed_connection({
+ expect_error(dbWithTransaction(con, NULL))
+ })
+ },
+
+ #' or invalid
+ with_transaction_error_invalid = function(ctx) {
+ with_invalid_connection({
+ expect_error(dbWithTransaction(con, NULL))
+ })
+ },
+
+ #' of if [dbBegin()] has been called already)
+ with_transaction_error_nested = function(ctx) {
+ with_connection({
+ dbBegin(con)
+ #' gives an error.
+ expect_error(dbWithTransaction(con, NULL))
+ dbRollback(con)
+ })
+ },
+
+ #' @section Specification:
+ #' `dbWithTransaction()` initiates a transaction with `dbBegin()`, executes
+ #' the code given in the `code` argument, and commits the transaction with
+ #' [dbCommit()].
+ with_transaction_success = function(ctx) {
+ with_connection({
+ with_remove_test_table({
+ expect_false(dbExistsTable(con, "test"))
+
+ dbWithTransaction(
+ con,
+ {
+ dbExecute(con, paste0("CREATE TABLE test (a ", dbDataType(con, 0L), ")"))
+ dbExecute(con, paste0("INSERT INTO test (a) VALUES (1)"))
+ expect_equal(check_df(dbReadTable(con, "test")), data.frame(a = 1))
+ }
+ )
+
+ expect_equal(check_df(dbReadTable(con, "test")), data.frame(a = 1))
+ })
+ })
+ },
+
+ #' If the code raises an error, the transaction is instead aborted with
+ #' [dbRollback()], and the error is propagated.
+ with_transaction_failure = function(ctx) {
+ name <- random_table_name()
+
+ with_connection({
+ with_remove_test_table({
+ expect_false(dbExistsTable(con, "test"))
+
+ expect_error(
+ dbWithTransaction(
+ con,
+ {
+ dbExecute(con, paste0("CREATE TABLE test (a ", dbDataType(con, 0L), ")"))
+ dbExecute(con, paste0("INSERT INTO test (a) VALUES (1)"))
+ stop(name)
+ }
+ ),
+ name,
+ fixed = TRUE
+ )
+
+ expect_false(dbExistsTable(con, "test"))
+ })
+ })
+ },
+
+ #' If the code calls `dbBreak()`, execution of the code stops and the
+ #' transaction is silently aborted.
+ with_transaction_break = function(ctx) {
+ name <- random_table_name()
+
+ with_connection({
+ with_remove_test_table({
+ expect_false(dbExistsTable(con, "test"))
+
+ expect_error(
+ dbWithTransaction(
+ con,
+ {
+ dbExecute(con, paste0("CREATE TABLE test (a ", dbDataType(con, 0L), ")"))
+ dbExecute(con, paste0("INSERT INTO test (a) VALUES (1)"))
+ dbBreak()
+ }
+ ),
+ NA
+ )
+
+ expect_false(dbExistsTable(con, "test"))
+ })
+ })
+ },
+
+ #' All side effects caused by the code
+ with_transaction_side_effects = function(ctx) {
+ with_connection({
+ expect_false(exists("a", inherits = FALSE))
+ #' (such as the creation of new variables)
+ dbWithTransaction(con, a <- 42)
+ #' propagate to the calling environment.
+ expect_identical(get0("a", inherits = FALSE), 42)
+ })
+ },
- #' }
NULL
)
diff --git a/R/spec-transaction.R b/R/spec-transaction.R
index 793eee2..da65e31 100644
--- a/R/spec-transaction.R
+++ b/R/spec-transaction.R
@@ -1,8 +1,7 @@
#' @template dbispec
#' @format NULL
spec_transaction <- c(
- spec_transaction_begin_commit,
- spec_transaction_begin_rollback,
+ spec_transaction_begin_commit_rollback,
spec_transaction_with_transaction,
NULL
diff --git a/R/spec.R b/R/spec.R
index 9fc75cd..3aa4fc3 100644
--- a/R/spec.R
+++ b/R/spec.R
@@ -1,20 +1,6 @@
#' DBI specification
#'
-#' @description
-#' The \pkg{DBI} package defines the generic DataBase Interface for R.
-#' The connection to individual DBMS is made by packages that import \pkg{DBI}
-#' (so-called \emph{DBI backends}).
-#' This document formalizes the behavior expected by the functions declared in
-#' \pkg{DBI} and implemented by the individal backends.
-#'
-#' To ensure maximum portability and exchangeability, and to reduce the effort
-#' for implementing a new DBI backend, the \pkg{DBItest} package defines
-#' a comprehensive set of test cases that test conformance to the DBI
-#' specification.
-#' In fact, this document is derived from comments in the test definitions of
-#' the \pkg{DBItest} package.
-#' This ensures that an extension or update to the tests will be reflected in
-#' this document.
+#' Placeholder page.
#'
#' @format NULL
#' @usage NULL
diff --git a/R/test-all.R b/R/test-all.R
index 2fd0aaa..4ef6724 100644
--- a/R/test-all.R
+++ b/R/test-all.R
@@ -1,6 +1,6 @@
#' Run all tests
#'
-#' This function calls all tests defined in this package (see the section
+#' `test_all()` calls all tests defined in this package (see the section
#' "Tests" below).
#'
#' @section Tests:
@@ -23,3 +23,13 @@ test_all <- function(skip = NULL, ctx = get_default_context()) {
test_compliance(skip = skip, ctx = ctx)
# stress tests are not tested by default (#92)
}
+
+#' @rdname test_all
+#' @description `test_some()` allows testing one or more tests, it works by
+#' constructing the `skip` argument using negative lookaheads.
+#' @param test `[character]`\cr A character vector of regular expressions
+#' describing the tests to run.
+#' @export
+test_some <- function(test, ctx = get_default_context()) {
+ test_all(skip = paste0("(?!", paste(test, collapse = "|"), ").*$"), ctx = ctx)
+}
diff --git a/R/tweaks.R b/R/tweaks.R
index 84bbfad..70340c8 100644
--- a/R/tweaks.R
+++ b/R/tweaks.R
@@ -4,47 +4,84 @@
#' @name tweaks
#' @aliases NULL
{ # nolint
- tweak_names <- c(
+ tweak_names <- alist(
#' @param ... `[any]`\cr
#' Unknown tweaks are accepted, with a warning. The ellipsis
#' also asserts that all arguments are named.
- "...",
+ "..." = ,
#' @param constructor_name `[character(1)]`\cr
#' Name of the function that constructs the `Driver` object.
- "constructor_name",
+ "constructor_name" = NULL,
#' @param constructor_relax_args `[logical(1)]`\cr
#' If `TRUE`, allow a driver constructor with default values for all
#' arguments; otherwise, require a constructor with empty argument list
#' (default).
- "constructor_relax_args",
+ "constructor_relax_args" = FALSE,
#' @param strict_identifier `[logical(1)]`\cr
#' Set to `TRUE` if the DBMS does not support arbitrarily-named
#' identifiers even when quoting is used.
- "strict_identifier",
+ "strict_identifier" = FALSE,
#' @param omit_blob_tests `[logical(1)]`\cr
#' Set to `TRUE` if the DBMS does not support a `BLOB` data
#' type.
- "omit_blob_tests",
+ "omit_blob_tests" = FALSE,
#' @param current_needs_parens `[logical(1)]`\cr
#' Set to `TRUE` if the SQL functions `current_date`,
#' `current_time`, and `current_timestamp` require parentheses.
- "current_needs_parens",
+ "current_needs_parens" = FALSE,
#' @param union `[function(character)]`\cr
#' Function that combines several subqueries into one so that the
#' resulting query returns the concatenated results of the subqueries
- "union",
+ "union" = function(x) paste(x, collapse = " UNION "),
#' @param placeholder_pattern `[character]`\cr
- #' A pattern for placeholders used in [DBI::dbBind()], e.g.,
+ #' A pattern for placeholders used in [dbBind()], e.g.,
#' `"?"`, `"$1"`, or `":name"`. See
#' [make_placeholder_fun()] for details.
- "placeholder_pattern",
+ "placeholder_pattern" = NULL,
+
+ #' @param logical_return `[function(logical)]`\cr
+ #' A vectorized function that converts logical values to the data type
+ #' returned by the DBI backend.
+ "logical_return" = identity,
+
+ #' @param date_cast `[function(character)]`\cr
+ #' A vectorized function that creates an SQL expression for coercing a
+ #' string to a date value.
+ "date_cast" = function(x) paste0("date('", x, "')"),
+
+ #' @param time_cast `[function(character)]`\cr
+ #' A vectorized function that creates an SQL expression for coercing a
+ #' string to a time value.
+ "time_cast" = function(x) paste0("time('", x, "')"),
+
+ #' @param timestamp_cast `[function(character)]`\cr
+ #' A vectorized function that creates an SQL expression for coercing a
+ #' string to a timestamp value.
+ "timestamp_cast" = function(x) paste0("timestamp('", x, "')"),
+
+ #' @param date_typed `[logical(1L)]`\cr
+ #' Set to `FALSE` if the DBMS doesn't support a dedicated type for dates.
+ "date_typed" = TRUE,
+
+ #' @param time_typed `[logical(1L)]`\cr
+ #' Set to `FALSE` if the DBMS doesn't support a dedicated type for times.
+ "time_typed" = TRUE,
+
+ #' @param timestamp_typed `[logical(1L)]`\cr
+ #' Set to `FALSE` if the DBMS doesn't support a dedicated type for
+ #' timestamps.
+ "timestamp_typed" = TRUE,
+
+ #' @param temporary_tables `[logical(1L)]`\cr
+ #' Set to `FALSE` if the DBMS doesn't support temporary tables.
+ "temporary_tables" = TRUE,
# Dummy argument
NULL
@@ -53,11 +90,9 @@
# A helper function that constructs the tweaks() function in a DRY fashion.
make_tweaks <- function(envir = parent.frame()) {
- fmls <- vector(mode = "list", length(tweak_names))
- names(fmls) <- tweak_names
- fmls["..."] <- alist(`...` = )
+ fmls <- tweak_names[-length(tweak_names)]
- tweak_quoted <- lapply(setNames(nm = tweak_names), as.name)
+ tweak_quoted <- lapply(setNames(nm = names(fmls)), as.name)
tweak_quoted <- c(tweak_quoted)
list_call <- as.call(c(quote(list), tweak_quoted))
@@ -105,3 +140,11 @@ format.DBItest_tweaks <- function(x, ...) {
print.DBItest_tweaks <- function(x, ...) {
cat(format(x), sep = "\n")
}
+
+#' @export
+`$.DBItest_tweaks` <- function(x, tweak) {
+ if (!(tweak %in% names(tweak_names))) {
+ stop("Tweak not found: ", tweak, call. = FALSE)
+ }
+ NextMethod()
+}
diff --git a/R/utils.R b/R/utils.R
index 1793e3d..d53b283 100644
--- a/R/utils.R
+++ b/R/utils.R
@@ -1,16 +1,11 @@
`%||%` <- function(a, b) if (is.null(a)) b else a
-get_pkg <- function(ctx) {
- if (!requireNamespace("devtools", quietly = TRUE)) {
- skip("devtools not installed")
- }
-
+get_pkg_path <- function(ctx) {
pkg_name <- package_name(ctx)
expect_is(pkg_name, "character")
pkg_path <- find.package(pkg_name)
-
- devtools::as.package(pkg_path)
+ pkg_path
}
utils::globalVariables("con")
@@ -27,12 +22,107 @@ with_connection <- function(code, con = "con", env = parent.frame()) {
eval(bquote({
.(con) <- connect(ctx)
- on.exit(expect_error(dbDisconnect(.(con)), NA), add = TRUE)
+ on.exit(try_silent(dbDisconnect(.(con))), add = TRUE)
local(.(code_sub))
}
), envir = env)
}
+# Expects a variable "ctx" in the environment env,
+# evaluates the code inside local() after defining a variable "con"
+# (can be overridden by specifying con argument)
+# that points to a newly opened and then closed connection. Disconnects on exit.
+with_closed_connection <- function(code, con = "con", env = parent.frame()) {
+ code_sub <- substitute(code)
+
+ con <- as.name(con)
+
+ eval(bquote({
+ .(con) <- connect(ctx)
+ dbDisconnect(.(con))
+ local(.(code_sub))
+ }
+ ), envir = env)
+}
+
+# Expects a variable "ctx" in the environment env,
+# evaluates the code inside local() after defining a variable "con"
+# (can be overridden by specifying con argument)
+# that points to a newly opened but invalidated connection. Disconnects on exit.
+with_invalid_connection <- function(code, con = "con", env = parent.frame()) {
+ code_sub <- substitute(code)
+
+ stopifnot(con != "..con")
+ con <- as.name(con)
+
+ eval(bquote({
+ ..con <- connect(ctx)
+ on.exit(dbDisconnect(..con), add = TRUE)
+ .(con) <- unserialize(serialize(..con, NULL))
+ local(.(code_sub))
+ }
+ ), envir = env)
+}
+
+# Evaluates the code inside local() after defining a variable "res"
+# (can be overridden by specifying con argument)
+# that points to a result set created by query. Clears on exit.
+with_result <- function(query, code, res = "res", env = parent.frame()) {
+ code_sub <- substitute(code)
+ query_sub <- substitute(query)
+
+ res <- as.name(res)
+
+ eval(bquote({
+ .(res) <- .(query_sub)
+ on.exit(dbClearResult(.(res)), add = TRUE)
+ local(.(code_sub))
+ }
+ ), envir = env)
+}
+
+# Evaluates the code inside local() after defining a variable "con"
+# (can be overridden by specifying con argument)
+# that points to a connection. Removes the table specified by name on exit,
+# if it exists.
+with_remove_test_table <- function(code, name = "test", con = "con", env = parent.frame()) {
+ code_sub <- substitute(code)
+
+ con <- as.name(con)
+
+ eval(bquote({
+ on.exit(
+ try_silent(
+ dbExecute(.(con), paste0("DROP TABLE ", dbQuoteIdentifier(.(con), .(name))))
+ ),
+ add = TRUE
+ )
+ local(.(code_sub))
+ }
+ ), envir = env)
+}
+
+# Evaluates the code inside local() after defining a variable "con"
+# (can be overridden by specifying con argument)
+# that points to a result set created by query. Clears on exit.
+with_rollback_on_error <- function(code, con = "con", env = parent.frame()) {
+ code_sub <- substitute(code)
+
+ con <- as.name(con)
+
+ eval(bquote({
+ on.exit(
+ try_silent(
+ dbRollback(.(con))
+ ),
+ add = TRUE
+ )
+ local(.(code_sub))
+ on.exit(NULL, add = FALSE)
+ }
+ ), envir = env)
+}
+
get_iris <- function(ctx) {
datasets_iris <- datasets::iris
if (isTRUE(ctx$tweaks$strict_identifier)) {
@@ -49,3 +139,33 @@ unrowname <- function(x) {
random_table_name <- function(n = 10) {
paste0(sample(letters, n, replace = TRUE), collapse = "")
}
+
+compact <- function(x) {
+ x[!vapply(x, is.null, logical(1L))]
+}
+
+expand_char <- function(...) {
+ df <- expand.grid(..., stringsAsFactors = FALSE)
+ do.call(paste0, df)
+}
+
+try_silent <- function(code) {
+ tryCatch(
+ code,
+ error = function(e) NULL)
+}
+
+check_df <- function(df) {
+ expect_is(df, "data.frame")
+ if (length(df) >= 1L) {
+ lengths <- vapply(df, length, integer(1L), USE.NAMES = FALSE)
+ expect_equal(diff(lengths), rep(0L, length(lengths) - 1L))
+ expect_equal(nrow(df), lengths[[1]])
+ }
+
+ df_names <- names(df)
+ expect_true(all(df_names != ""))
+ expect_false(anyNA(df_names))
+
+ df
+}
diff --git a/build/vignette.rds b/build/vignette.rds
index 8830b39..356a26a 100644
Binary files a/build/vignette.rds and b/build/vignette.rds differ
diff --git a/inst/doc/test.html b/inst/doc/test.html
index 4da7606..aae7430 100644
--- a/inst/doc/test.html
+++ b/inst/doc/test.html
@@ -4,7 +4,7 @@
<head>
-<meta charset="utf-8">
+<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="pandoc" />
@@ -12,7 +12,7 @@
<meta name="author" content="Kirill Müller" />
-<meta name="date" content="2016-12-03" />
+<meta name="date" content="2017-06-18" />
<title>Testing DBI backends</title>
@@ -70,7 +70,7 @@ code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Inf
<h1 class="title toc-ignore">Testing DBI backends</h1>
<h4 class="author"><em>Kirill Müller</em></h4>
-<h4 class="date"><em>2016-12-03</em></h4>
+<h4 class="date"><em>2017-06-18</em></h4>
@@ -119,7 +119,7 @@ DBItest::<span class="kw">test_all</span>()</code></pre></div>
(function () {
var script = document.createElement("script");
script.type = "text/javascript";
- script.src = "https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML";
+ script.src = "https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML";
document.getElementsByTagName("head")[0].appendChild(script);
})();
</script>
diff --git a/man/DBIspec-wip.Rd b/man/DBIspec-wip.Rd
index 8df0931..a1056c0 100644
--- a/man/DBIspec-wip.Rd
+++ b/man/DBIspec-wip.Rd
@@ -1,19 +1,7 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/spec.R, R/spec-driver-get-info.R,
-% R/spec-connection-connect.R, R/spec-connection-data-type.R,
-% R/spec-connection-get-info.R, R/spec-result-send-query.R,
-% R/spec-result-fetch.R, R/spec-result-get-query.R,
-% R/spec-result-create-table-with-data-type.R, R/spec-result-roundtrip.R,
-% R/spec-sql-quote-string.R, R/spec-sql-quote-identifier.R,
-% R/spec-sql-read-write-table.R, R/spec-sql-read-write-roundtrip.R,
-% R/spec-sql-list-tables.R, R/spec-sql-list-fields.R,
-% R/spec-meta-is-valid-connection.R, R/spec-meta-is-valid-result.R,
-% R/spec-meta-get-statement.R, R/spec-meta-column-info.R,
-% R/spec-meta-get-row-count.R, R/spec-meta-get-rows-affected.R,
-% R/spec-meta-get-info-result.R, R/spec-meta-bind.R,
-% R/spec-meta-bind-multi-row.R, R/spec-transaction-begin-rollback.R,
-% R/spec-transaction-with-transaction.R, R/spec-compliance-methods.R,
-% R/spec-compliance-read-only.R, R/spec-stress-driver.R,
+% R/spec-connection-get-info.R, R/spec-sql-list-fields.R,
+% R/spec-meta-column-info.R, R/spec-meta-get-info-result.R,
% R/spec-stress-connection.R
\docType{data}
\name{DBIspec-wip}
@@ -27,28 +15,10 @@ Placeholder page.
\subsection{\code{dbGetInfo("DBIDriver")} (deprecated)}{
Return value of dbGetInfo has necessary elements.
}
-
-
-\subsection{Repeated loading, instantiation, and unloading}{
-Repeated load, instantiation, and unload of package in a new R session.
-}
}
\section{Connection}{
-\subsection{Construction: \code{dbConnect("DBIDriver")} and \code{dbDisconnect("DBIConnection", "ANY")}}{
-Can connect and disconnect, connection object inherits from
-"DBIConnection".
-Repeated disconnect throws warning.
-}
-
-
-\subsection{\code{dbDataType("DBIConnection", "ANY")}}{
-SQL Data types exist for all basic R data types. dbDataType() does not
-throw an error and returns a nonempty atomic character
-}
-
-
\subsection{\code{dbGetInfo("DBIConnection")} (deprecated)}{
Return value of dbGetInfo has necessary elements
}
@@ -57,297 +27,29 @@ Return value of dbGetInfo has necessary elements
\subsection{Stress tests}{
Open 50 simultaneous connections
Open and close 50 connections
-Repeated load, instantiation, connection, disconnection, and unload of
-package in a new R session.
-}
-}
-
-\section{Result}{
-
-\subsection{Construction: \code{dbSendQuery("DBIConnection")} and \code{dbClearResult("DBIResult")}}{
-Can issue trivial query, result object inherits from "DBIResult".
-Return value, currently tests that the return value is always
-\code{TRUE}, and that an attempt to close a closed result set issues a
-warning.
-Leaving a result open when closing a connection gives a warning.
-Can issue a command query that creates a table, inserts a row, and
-deletes it; the result sets for these query always have "completed"
-status.
-Issuing an invalid query throws error (but no warnings, e.g. related to
-pending results, are thrown).
-}
-
-
-\subsection{\code{dbFetch("DBIResult")} and \code{dbHasCompleted("DBIResult")}}{
-Single-value queries can be fetched.
-Multi-row single-column queries can be fetched.
-Multi-row queries can be fetched progressively.
-If more rows than available are fetched, the result is returned in full
-but no warning is issued.
-If zero rows are fetched, the result is still fully typed.
-If less rows than available are fetched, the result is returned in full
-but no warning is issued.
-Side-effect-only queries (without return value) can be fetched.
-Fetching from a closed result set raises an error.
-Querying a disconnected connection throws error.
-}
-
-
-\subsection{\code{dbGetQuery("DBIConnection", "ANY")}}{
-Single-value queries can be read with dbGetQuery
-Multi-row single-column queries can be read with dbGetQuery.
-Empty single-column queries can be read with
-\code{\link[DBI:dbGetQuery]{DBI::dbGetQuery()}}. Not all SQL dialects support the query
-used here.
-Single-row multi-column queries can be read with dbGetQuery.
-Multi-row multi-column queries can be read with dbGetQuery.
-Empty multi-column queries can be read with
-\code{\link[DBI:dbGetQuery]{DBI::dbGetQuery()}}. Not all SQL dialects support the query
-used here.
-}
-
-
-\subsection{Create table with data type}{
-SQL Data types exist for all basic R data types, and the engine can
-process them.
-SQL data type for factor is the same as for character.
-}
-
-
-\subsection{Data roundtrip}{
-Data conversion from SQL to R: integer
-Data conversion from SQL to R: integer with typed NULL values.
-Data conversion from SQL to R: integer with typed NULL values
-in the first row.
-Data conversion from SQL to R: numeric.
-Data conversion from SQL to R: numeric with typed NULL values.
-Data conversion from SQL to R: numeric with typed NULL values
-in the first row.
-Data conversion from SQL to R: logical. Optional, conflict with the
-\code{data_logical_int} test.
-Data conversion from SQL to R: logical with typed NULL values.
-Data conversion from SQL to R: logical with typed NULL values
-in the first row
-Data conversion from SQL to R: logical (as integers). Optional,
-conflict with the \code{data_logical} test.
-Data conversion from SQL to R: logical (as integers) with typed NULL
-values.
-Data conversion from SQL to R: logical (as integers) with typed NULL
-values
-in the first row.
-Data conversion from SQL to R: A NULL value is returned as NA.
-Data conversion from SQL to R: 64-bit integers.
-Data conversion from SQL to R: 64-bit integers with typed NULL values.
-Data conversion from SQL to R: 64-bit integers with typed NULL values
-in the first row.
-Data conversion from SQL to R: character.
-Data conversion from SQL to R: character with typed NULL values.
-Data conversion from SQL to R: character with typed NULL values
-in the first row.
-Data conversion from SQL to R: raw. Not all SQL dialects support the
-syntax of the query used here.
-Data conversion from SQL to R: raw with typed NULL values.
-Data conversion from SQL to R: raw with typed NULL values
-in the first row.
-Data conversion from SQL to R: date, returned as integer with class.
-Data conversion from SQL to R: date with typed NULL values.
-Data conversion from SQL to R: date with typed NULL values
-in the first row.
-Data conversion from SQL to R: time.
-Data conversion from SQL to R: time with typed NULL values.
-Data conversion from SQL to R: time with typed NULL values
-in the first row.
-Data conversion from SQL to R: time (using alternative syntax with
-parentheses for specifying time literals).
-Data conversion from SQL to R: time (using alternative syntax with
-parentheses for specifying time literals) with typed NULL values.
-Data conversion from SQL to R: time (using alternative syntax with
-parentheses for specifying time literals) with typed NULL values
-in the first row.
-Data conversion from SQL to R: timestamp.
-Data conversion from SQL to R: timestamp with typed NULL values.
-Data conversion from SQL to R: timestamp with typed NULL values
-in the first row.
-Data conversion from SQL to R: timestamp with time zone.
-Data conversion from SQL to R: timestamp with time zone with typed NULL
-values.
-Data conversion from SQL to R: timestamp with time zone with typed NULL
-values
-in the first row.
-Data conversion: timestamp (alternative syntax with parentheses
-for specifying timestamp literals).
-Data conversion: timestamp (alternative syntax with parentheses
-for specifying timestamp literals) with typed NULL values.
-Data conversion: timestamp (alternative syntax with parentheses
-for specifying timestamp literals) with typed NULL values
-in the first row.
}
}
\section{SQL}{
-\subsection{\code{dbQuoteString("DBIConnection")}}{
-Can quote strings, and create strings that contain quotes and spaces.
-Can quote more than one string at once by passing a character vector.
-}
-
-
-\subsection{\code{dbQuoteIdentifier("DBIConnection")}}{
-Can quote identifiers that consist of letters only.
-Can quote identifiers with special characters, and create identifiers
-that contain quotes and spaces.
-Character vectors are treated as a single qualified identifier.
-}
-
-
-\subsection{\code{dbReadTable("DBIConnection")} and \code{dbWriteTable("DBIConnection")}}{
-Can write the \link[datasets:iris]{datasets::iris} data as a table to the
-database, but won't overwrite by default.
-Can read the \link[datasets:iris]{datasets::iris} data from a database table.
-Can write the \link[datasets:iris]{datasets::iris} data as a table to the
-database, will overwrite if asked.
-Can write the \link[datasets:iris]{datasets::iris} data as a table to the
-database, will append if asked.
-Cannot append to nonexisting table.
-Can write the \link[datasets:iris]{datasets::iris} data as a temporary table to
-the database, the table is not available in a second connection and is
-gone after reconnecting.
-A new table is visible in a second connection.
-}
-
-
-\subsection{Roundtrip tests}{
-Can create tables with keywords as table and column names.
-Can create tables with quotes, commas, and spaces in column names and
-data.
-Can create tables with integer columns.
-Can create tables with numeric columns.
-Can create tables with numeric columns that contain special values such
-as \code{Inf} and \code{NaN}.
-Can create tables with logical columns.
-Can create tables with logical columns, returned as integer.
-Can create tables with NULL values.
-Can create tables with 64-bit columns.
-Can create tables with character columns.
-Can create tables with factor columns.
-Can create tables with raw columns.
-Can create tables with date columns.
-Can create tables with timestamp columns.
-Can create tables with row names.
-}
-
-
-\subsection{\code{dbListTables("DBIConnection")}}{
-Can list the tables in the database, adding and removing tables affects
-the list. Can also check existence of a table.
-}
-
-
\subsection{\code{dbListFields("DBIConnection")}}{
Can list the fields for a table in the database.
-}
-}
-
-\section{Meta}{
-\subsection{\code{dbIsValid("DBIConnection")}}{
-Only an open connection is valid.
+A column named \code{row_names} is treated like any other column.
}
-
-
-\subsection{\code{dbIsValid("DBIResult")}}{
-Only an open result set is valid.
-}
-
-
-\subsection{\code{dbGetStatement("DBIResult")}}{
-SQL query can be retrieved from the result.
}
+\section{Meta}{
\subsection{\code{dbColumnInfo("DBIResult")}}{
Column information is correct.
}
-\subsection{\code{dbGetRowCount("DBIResult")}}{
-Row count information is correct.
-}
-
-
-\subsection{\code{dbGetRowsAffected("DBIResult")}}{
-Information on affected rows is correct.
-}
-
-
\subsection{\code{dbGetInfo("DBIResult")} (deprecated)}{
Return value of dbGetInfo has necessary elements
}
}
-\section{Parametrised queries and statements}{
-
-\subsection{\code{dbBind("DBIResult")}}{
-Empty binding with check of
-return value.
-Binding of integer values raises an
-error if connection is closed.
-Binding of integer values with check of
-return value.
-Binding of integer values with too many
-values.
-Binding of integer values with too few
-values.
-Binding of integer values, repeated.
-Binding of integer values with wrong names.
-Binding of integer values.
-Binding of numeric values.
-Binding of logical values.
-Binding of logical values (coerced to integer).
-Binding of \code{NULL} values.
-Binding of character values.
-Binding of date values.
-Binding of \link{POSIXct} timestamp values.
-Binding of \link{POSIXlt} timestamp values.
-Binding of raw values.
-Binding of statements.
-Repeated binding of statements.
-}
-
-
-\subsection{\code{dbBind("DBIResult")}}{
-Binding of multi-row integer values.
-Binding of multi-row integer values with zero rows.
-Binding of multi-row integer values with unequal length.
-Binding of multi-row statements.
-}
-}
-
-\section{Transactions}{
-
-\subsection{\code{dbBegin("DBIConnection")} and \code{dbRollback("DBIConnection")}}{
-Filler
-}
-
-
-\subsection{\code{dbWithTransaction("DBIConnection")} and \code{dbBreak()}}{
-Filler
-}
-}
-
-\section{Full compliance}{
-
-\subsection{All of DBI}{
-The package defines three classes that implement the required methods.
-All methods have an ellipsis \code{...} in their formals.
-}
-
-
-\subsection{Read-only access}{
-Writing to the database fails. (You might need to set up a separate
-test context just for this test.)
-}
-}
\keyword{datasets}
\keyword{internal}
-
diff --git a/man/DBIspec.Rd b/man/DBIspec.Rd
index e7fe425..fb2ba56 100644
--- a/man/DBIspec.Rd
+++ b/man/DBIspec.Rd
@@ -1,194 +1,51 @@
% Generated by roxygen2: do not edit by hand
% Please edit documentation in R/spec.R, R/spec-getting-started.R,
-% R/spec-driver-class.R, R/spec-driver-constructor.R,
-% R/spec-driver-data-type.R, R/spec-driver.R, R/spec-connection.R,
-% R/spec-result.R, R/spec-sql.R, R/spec-meta-bind.R, R/spec-meta.R,
-% R/spec-transaction-begin-commit.R, R/spec-transaction.R,
-% R/spec-compliance.R, R/spec-stress.R
+% R/spec-compliance-methods.R, R/spec-driver-constructor.R, R/spec-driver.R,
+% R/spec-connection.R, R/spec-result.R, R/spec-sql.R, R/spec-meta.R,
+% R/spec-transaction.R, R/spec-compliance.R, R/spec-stress.R
\docType{data}
\name{DBIspec}
\alias{DBIspec}
\title{DBI specification}
\description{
-The \pkg{DBI} package defines the generic DataBase Interface for R.
-The connection to individual DBMS is made by packages that import \pkg{DBI}
-(so-called \emph{DBI backends}).
-This document formalizes the behavior expected by the functions declared in
-\pkg{DBI} and implemented by the individal backends.
-
-To ensure maximum portability and exchangeability, and to reduce the effort
-for implementing a new DBI backend, the \pkg{DBItest} package defines
-a comprehensive set of test cases that test conformance to the DBI
-specification.
-In fact, this document is derived from comments in the test definitions of
-the \pkg{DBItest} package.
-This ensures that an extension or update to the tests will be reflected in
-this document.
+Placeholder page.
}
-\section{Getting started}{
+\section{Definition}{
-A DBI backend is an R package,
-which should import the \pkg{DBI}
+A DBI backend is an R package
+which imports the \pkg{DBI}
and \pkg{methods}
packages.
For better or worse, the names of many existing backends start with
\sQuote{R}, e.g., \pkg{RSQLite}, \pkg{RMySQL}, \pkg{RSQLServer}; it is up
-to the package author to adopt this convention or not.
+to the backend author to adopt this convention or not.
}
-\section{Driver}{
+\section{DBI classes and methods}{
-Each DBI backend implements a \dfn{driver class},
-which must be an S4 class and inherit from the \code{DBIDriver} class.
-This section describes the construction of, and the methods defined for,
-this driver class.
+A backend defines three classes,
+which are subclasses of
+\linkS4class{DBIDriver},
+\linkS4class{DBIConnection},
+and \linkS4class{DBIResult}.
+The backend provides implementation for all methods
+of these base classes
+that are defined but not implemented by DBI.
+All methods have an ellipsis \code{...} in their formals.
+}
+\section{Construction of the DBIDriver object}{
-\subsection{Construction}{
-The backend must support creation of an instance of this driver class
+The backend must support creation of an instance of its \linkS4class{DBIDriver}
+subclass
with a \dfn{constructor function}.
By default, its name is the package name without the leading \sQuote{R}
(if it exists), e.g., \code{SQLite} for the \pkg{RSQLite} package.
-For the automated tests, the constructor name can be tweaked using the
-\code{constructor_name} tweak.
-
+However, backend authors may choose a different name.
The constructor must be exported, and
it must be a function
that is callable without arguments.
-For the automated tests, unless the
-\code{constructor_relax_args} tweak is set to \code{TRUE},
-an empty argument list is expected.
-Otherwise, an argument list where all arguments have default values
-is also accepted.
-
-}
-
-
-\subsection{\code{dbDataType("DBIDriver", "ANY")}}{
-The backend can override the \code{\link[DBI:dbDataType]{DBI::dbDataType()}} generic
-for its driver class.
-This generic expects an arbitrary object as second argument
-and returns a corresponding SQL type
-as atomic
-character value
-with at least one character.
-As-is objects (i.e., wrapped by \code{\link[base:I]{base::I()}}) must be
-supported and return the same results as their unwrapped counterparts.
-
-To query the values returned by the default implementation,
-run \code{example(dbDataType, package = "DBI")}.
-If the backend needs to override this generic,
-it must accept all basic R data types as its second argument, namely
-\code{\link[base:logical]{base::logical()}},
-\code{\link[base:integer]{base::integer()}},
-\code{\link[base:numeric]{base::numeric()}},
-\code{\link[base:character]{base::character()}},
-dates (see \code{\link[base:Dates]{base::Dates()}}),
-date-time (see \code{\link[base:DateTimeClasses]{base::DateTimeClasses()}}),
-and \code{\link[base:difftime]{base::difftime()}}.
-It also must accept lists of \code{raw} vectors
-and map them to the BLOB (binary large object) data type.
-The behavior for other object types is not specified.
-}
-}
-
-\section{Parametrized queries and statements}{
-
-\pkg{DBI} supports parametrized (or prepared) queries and statements
-via the \code{\link[DBI:dbBind]{DBI::dbBind()}} generic.
-Parametrized queries are different from normal queries
-in that they allow an arbitrary number of placeholders,
-which are later substituted by actual values.
-Parametrized queries (and statements) serve two purposes:
-\itemize{
-\item The same query can be executed more than once with different values.
-The DBMS may cache intermediate information for the query,
-such as the execution plan,
-and execute it faster.
-\item Separation of query syntax and parameters protects against SQL injection.
-}
-
-The placeholder format is currently not specified by \pkg{DBI};
-in the future, a uniform placeholder syntax may be supported.
-Consult the backend documentation for the supported formats.
-For automated testing, backend authors specify the placeholder syntax with
-the \code{placeholder_pattern} tweak.
-Known examples are:
-\itemize{
-\item \code{?} (positional matching in order of appearance) in \pkg{RMySQL} and \pkg{RSQLite}
-\item \code{$1} (positional matching by index) in \pkg{RPostgres} and \pkg{RSQLite}
-\item \code{:name} and \code{$name} (named matching) in \pkg{RSQLite}
+DBI recommends to define a constructor with an empty argument list.
}
-\pkg{DBI} clients execute parametrized statements as follows:
-\enumerate{
-\item Call \code{\link[DBI:dbSendQuery]{DBI::dbSendQuery()}} or \code{\link[DBI:dbSendStatement]{DBI::dbSendStatement()}} with a query or statement
-that contains placeholders,
-store the returned \code{\linkS4class{DBIResult}} object in a variable.
-Mixing placeholders (in particular, named and unnamed ones) is not
-recommended.
-It is good practice to register a call to \code{\link[DBI:dbClearResult]{DBI::dbClearResult()}} via
-\code{\link[=on.exit]{on.exit()}} right after calling \code{dbSendQuery()}, see the last
-enumeration item.
-\item Construct a list with parameters
-that specify actual values for the placeholders.
-The list must be named or unnamed,
-depending on the kind of placeholders used.
-Named values are matched to named parameters, unnamed values
-are matched by position.
-All elements in this list must have the same lengths and contain values
-supported by the backend; a \code{\link[=data.frame]{data.frame()}} is internally stored as such
-a list.
-The parameter list is passed a call to \code{\link[=dbBind]{dbBind()}} on the \code{DBIResult}
-object.
-\item Retrieve the data or the number of affected rows from the \code{DBIResult} object.
-\itemize{
-\item For queries issued by \code{dbSendQuery()},
-call \code{\link[DBI:dbFetch]{DBI::dbFetch()}}.
-\item For statements issued by \code{dbSendStatements()},
-call \code{\link[DBI:dbGetRowsAffected]{DBI::dbGetRowsAffected()}}.
-(Execution begins immediately after the \code{dbBind()} call,
-the statement is processed entirely before the function returns.
-Calls to \code{dbFetch()} are ignored.)
-}
-\item Repeat 2. and 3. as necessary.
-\item Close the result set via \code{\link[DBI:dbClearResult]{DBI::dbClearResult()}}.
-}
-}
-
-\section{Transactions}{
-
-\subsection{\code{dbBegin("DBIConnection")} and \code{dbCommit("DBIConnection")}}{
-Transactions are available in DBI, but actual support may vary between backends.
-A transaction is initiated by a call to \code{\link[DBI:dbBegin]{DBI::dbBegin()}}
-and committed by a call to \code{\link[DBI:dbCommit]{DBI::dbCommit()}}.
-Both generics expect an object of class \code{\linkS4class{DBIConnection}}
-and return \code{TRUE} (invisibly) upon success.
-
-The implementations are expected to raise an error in case of failure,
-but this is difficult to test in an automated way.
-In any way, both generics should throw an error with a closed connection.
-In addition, a call to \code{\link[DBI:dbCommit]{DBI::dbCommit()}} without
-a call to \code{\link[DBI:dbBegin]{DBI::dbBegin()}} should raise an error.
-Nested transactions are not supported by DBI,
-an attempt to call \code{\link[DBI:dbBegin]{DBI::dbBegin()}} twice
-should yield an error.
-
-Data written in a transaction must persist after the transaction is committed.
-For example, a table that is missing when the transaction is started
-but is created
-and populated during the transaction
-must exist and contain the data added there
-both during
-and after the transaction.
-
-The behavior is not specified if other arguments are passed to these
-functions. In particular, \pkg{RSQLite} issues named transactions
-if the \code{name} argument is set.
-
-The transaction isolation level is not specified by DBI.
-
-}
-}
\keyword{datasets}
-
diff --git a/man/DBItest-package.Rd b/man/DBItest-package.Rd
index f7eb08b..f4afc6e 100644
--- a/man/DBItest-package.Rd
+++ b/man/DBItest-package.Rd
@@ -7,7 +7,7 @@
\title{DBItest: Testing 'DBI' Back Ends}
\description{
A helper that tests 'DBI' back ends for conformity
-to the interface, currently work in progress.
+to the interface.
}
\details{
The two most important functions are \code{\link[=make_context]{make_context()}} and
@@ -27,4 +27,3 @@ Useful links:
\author{
Kirill Müller
}
-
diff --git a/man/context.Rd b/man/context.Rd
index 60b13f3..7b21b8d 100644
--- a/man/context.Rd
+++ b/man/context.Rd
@@ -38,4 +38,3 @@ be used in test messages.}
\description{
Create a test context, set and query the default context.
}
-
diff --git a/man/make_placeholder_fun.Rd b/man/make_placeholder_fun.Rd
index a584085..2e89ef5 100644
--- a/man/make_placeholder_fun.Rd
+++ b/man/make_placeholder_fun.Rd
@@ -18,4 +18,3 @@ Examples: \code{?, ?, ?, ...}, \code{$1, $2, $3, ...}, \code{:a, :b, :c}
For internal use by the \code{placeholder_format} tweak.
}
\keyword{internal}
-
diff --git a/man/spec_connection_disconnect.Rd b/man/spec_connection_disconnect.Rd
new file mode 100644
index 0000000..95f8206
--- /dev/null
+++ b/man/spec_connection_disconnect.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-connection-disconnect.R
+\docType{data}
+\name{spec_connection_disconnect}
+\alias{spec_connection_disconnect}
+\title{spec_connection_disconnect}
+\value{
+\code{dbDisconnect()} returns \code{TRUE}, invisibly.
+}
+\description{
+spec_connection_disconnect
+}
+\section{Specification}{
+
+A warning is issued on garbage collection when a connection has been
+released without calling \code{dbDisconnect()}.
+A warning is issued immediately when calling \code{dbDisconnect()} on an
+already disconnected
+or invalid connection.
+}
+
diff --git a/man/spec_driver_connect.Rd b/man/spec_driver_connect.Rd
new file mode 100644
index 0000000..cb4564e
--- /dev/null
+++ b/man/spec_driver_connect.Rd
@@ -0,0 +1,31 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-driver-connect.R
+\docType{data}
+\name{spec_driver_connect}
+\alias{spec_driver_connect}
+\title{spec_driver_connect}
+\value{
+\code{dbConnect()} returns an S4 object that inherits from \linkS4class{DBIConnection}.
+This object is used to communicate with the database engine.
+}
+\description{
+spec_driver_connect
+}
+\section{Specification}{
+
+DBI recommends using the following argument names for authentication
+parameters, with \code{NULL} default:
+\itemize{
+\item \code{user} for the user name (default: current user)
+\item \code{password} for the password
+\item \code{host} for the host name (default: local connection)
+\item \code{port} for the port number (default: local connection)
+\item \code{dbname} for the name of the database on the host, or the database file
+name
+}
+
+The defaults should provide reasonable behavior, in particular a
+local connection for \code{host = NULL}. For some DBMS (e.g., PostgreSQL),
+this is different to a TCP/IP connection to \code{localhost}.
+}
+
diff --git a/man/spec_driver_data_type.Rd b/man/spec_driver_data_type.Rd
new file mode 100644
index 0000000..a8550e9
--- /dev/null
+++ b/man/spec_driver_data_type.Rd
@@ -0,0 +1,45 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-driver-data-type.R
+\docType{data}
+\name{spec_driver_data_type}
+\alias{spec_driver_data_type}
+\title{spec_driver_data_type}
+\value{
+\code{dbDataType()} returns the SQL type that corresponds to the \code{obj} argument
+as a non-empty
+character string.
+For data frames, a character vector with one element per column
+is returned.
+An error is raised for invalid values for the \code{obj} argument such as a
+\code{NULL} value.
+}
+\description{
+spec_driver_data_type
+}
+\section{Specification}{
+
+The backend can override the \code{\link[=dbDataType]{dbDataType()}} generic
+for its driver class.
+
+This generic expects an arbitrary object as second argument.
+To query the values returned by the default implementation,
+run \code{example(dbDataType, package = "DBI")}.
+If the backend needs to override this generic,
+it must accept all basic R data types as its second argument, namely
+\link{logical},
+\link{integer},
+\link{numeric},
+\link{character},
+dates (see \link{Dates}),
+date-time (see \link{DateTimeClasses}),
+and \link{difftime}.
+If the database supports blobs,
+this method also must accept lists of \link{raw} vectors,
+and \link[blob:blob]{blob::blob} objects.
+As-is objects (i.e., wrapped by \code{\link[=I]{I()}}) must be
+supported and return the same results as their unwrapped counterparts.
+The SQL data type for \link{factor}
+and \link{ordered} is the same as for character.
+The behavior for other object types is not specified.
+}
+
diff --git a/man/spec_meta_bind.Rd b/man/spec_meta_bind.Rd
new file mode 100644
index 0000000..b407ef2
--- /dev/null
+++ b/man/spec_meta_bind.Rd
@@ -0,0 +1,115 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-meta-bind-runner.R, R/spec-meta-bind.R
+\docType{data}
+\name{spec_meta_bind}
+\alias{spec_meta_bind}
+\alias{spec_meta_bind}
+\title{spec_meta_bind}
+\value{
+\code{dbBind()} returns the result set,
+invisibly,
+for queries issued by \code{\link[=dbSendQuery]{dbSendQuery()}}
+and also for data manipulation statements issued by
+\code{\link[=dbSendStatement]{dbSendStatement()}}.
+Calling \code{dbBind()} for a query without parameters
+raises an error.
+Binding too many
+or not enough values,
+or parameters with wrong names
+or unequal length,
+also raises an error.
+If the placeholders in the query are named,
+all parameter values must have names
+(which must not be empty
+or \code{NA}),
+and vice versa,
+otherwise an error is raised.
+The behavior for mixing placeholders of different types
+(in particular mixing positional and named placeholders)
+is not specified.
+
+Calling \code{dbBind()} on a result set already cleared by \code{\link[=dbClearResult]{dbClearResult()}}
+also raises an error.
+}
+\description{
+spec_meta_bind
+
+spec_meta_bind
+}
+\section{Specification}{
+
+\pkg{DBI} clients execute parametrized statements as follows:
+\enumerate{
+\item Call \code{\link[=dbSendQuery]{dbSendQuery()}} or \code{\link[=dbSendStatement]{dbSendStatement()}} with a query or statement
+that contains placeholders,
+store the returned \linkS4class{DBIResult} object in a variable.
+Mixing placeholders (in particular, named and unnamed ones) is not
+recommended.
+It is good practice to register a call to \code{\link[=dbClearResult]{dbClearResult()}} via
+\code{\link[=on.exit]{on.exit()}} right after calling \code{dbSendQuery()} or \code{dbSendStatement()}
+(see the last enumeration item).
+Until \code{dbBind()} has been called, the returned result set object has the
+following behavior:
+\itemize{
+\item \code{\link[=dbFetch]{dbFetch()}} raises an error (for \code{dbSendQuery()})
+\item \code{\link[=dbGetRowCount]{dbGetRowCount()}} returns zero (for \code{dbSendQuery()})
+\item \code{\link[=dbGetRowsAffected]{dbGetRowsAffected()}} returns an integer \code{NA} (for \code{dbSendStatement()})
+\item \code{\link[=dbIsValid]{dbIsValid()}} returns \code{TRUE}
+\item \code{\link[=dbHasCompleted]{dbHasCompleted()}} returns \code{FALSE}
+}
+\item Construct a list with parameters
+that specify actual values for the placeholders.
+The list must be named or unnamed,
+depending on the kind of placeholders used.
+Named values are matched to named parameters, unnamed values
+are matched by position in the list of parameters.
+All elements in this list must have the same lengths and contain values
+supported by the backend; a \link{data.frame} is internally stored as such
+a list.
+The parameter list is passed to a call to \code{dbBind()} on the \code{DBIResult}
+object.
+\item Retrieve the data or the number of affected rows from the \code{DBIResult} object.
+\itemize{
+\item For queries issued by \code{dbSendQuery()},
+call \code{\link[=dbFetch]{dbFetch()}}.
+\item For statements issued by \code{dbSendStatements()},
+call \code{\link[=dbGetRowsAffected]{dbGetRowsAffected()}}.
+(Execution begins immediately after the \code{dbBind()} call,
+the statement is processed entirely before the function returns.)
+}
+\item Repeat 2. and 3. as necessary.
+\item Close the result set via \code{\link[=dbClearResult]{dbClearResult()}}.
+}
+
+
+The elements of the \code{params} argument do not need to be scalars,
+vectors of arbitrary length
+(including length 0)
+are supported.
+For queries, calling \code{dbFetch()} binding such parameters returns
+concatenated results, equivalent to binding and fetching for each set
+of values and connecting via \code{\link[=rbind]{rbind()}}.
+For data manipulation statements, \code{dbGetRowsAffected()} returns the
+total number of rows affected if binding non-scalar parameters.
+\code{dbBind()} also accepts repeated calls on the same result set
+for both queries
+and data manipulation statements,
+even if no results are fetched between calls to \code{dbBind()}.
+
+At least the following data types are accepted:
+\itemize{
+\item \link{integer}
+\item \link{numeric}
+\item \link{logical} for Boolean values (some backends may return an integer)
+\item \link{NA}
+\item \link{character}
+\item \link{factor} (bound as character,
+with warning)
+\item \link{Date}
+\item \link{POSIXct} timestamps
+\item \link{POSIXlt} timestamps
+\item lists of \link{raw} for blobs (with \code{NULL} entries for SQL NULL values)
+\item objects of type \link[blob:blob]{blob::blob}
+}
+}
+
diff --git a/man/spec_meta_get_row_count.Rd b/man/spec_meta_get_row_count.Rd
new file mode 100644
index 0000000..eb3d9a3
--- /dev/null
+++ b/man/spec_meta_get_row_count.Rd
@@ -0,0 +1,29 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-meta-get-row-count.R
+\docType{data}
+\name{spec_meta_get_row_count}
+\alias{spec_meta_get_row_count}
+\title{spec_meta_get_row_count}
+\value{
+\code{dbGetRowCount()} returns a scalar number (integer or numeric),
+the number of rows fetched so far.
+After calling \code{\link[=dbSendQuery]{dbSendQuery()}},
+the row count is initially zero.
+After a call to \code{\link[=dbFetch]{dbFetch()}} without limit,
+the row count matches the total number of rows returned.
+Fetching a limited number of rows
+increases the number of rows by the number of rows returned,
+even if fetching past the end of the result set.
+For queries with an empty result set,
+zero is returned
+even after fetching.
+For data manipulation statements issued with
+\code{\link[=dbSendStatement]{dbSendStatement()}},
+zero is returned before
+and after calling \code{dbFetch()}.
+Attempting to get the row count for a result set cleared with
+\code{\link[=dbClearResult]{dbClearResult()}} gives an error.
+}
+\description{
+spec_meta_get_row_count
+}
diff --git a/man/spec_meta_get_rows_affected.Rd b/man/spec_meta_get_rows_affected.Rd
new file mode 100644
index 0000000..88eafe6
--- /dev/null
+++ b/man/spec_meta_get_rows_affected.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-meta-get-rows-affected.R
+\docType{data}
+\name{spec_meta_get_rows_affected}
+\alias{spec_meta_get_rows_affected}
+\title{spec_meta_get_rows_affected}
+\value{
+\code{dbGetRowsAffected()} returns a scalar number (integer or numeric),
+the number of rows affected by a data manipulation statement
+issued with \code{\link[=dbSendStatement]{dbSendStatement()}}.
+The value is available directly after the call
+and does not change after calling \code{\link[=dbFetch]{dbFetch()}}.
+For queries issued with \code{\link[=dbSendQuery]{dbSendQuery()}},
+zero is returned before
+and after the call to \code{dbFetch()}.
+Attempting to get the rows affected for a result set cleared with
+\code{\link[=dbClearResult]{dbClearResult()}} gives an error.
+}
+\description{
+spec_meta_get_rows_affected
+}
diff --git a/man/spec_meta_get_statement.Rd b/man/spec_meta_get_statement.Rd
new file mode 100644
index 0000000..3a2c703
--- /dev/null
+++ b/man/spec_meta_get_statement.Rd
@@ -0,0 +1,16 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-meta-get-statement.R
+\docType{data}
+\name{spec_meta_get_statement}
+\alias{spec_meta_get_statement}
+\title{spec_meta_get_statement}
+\value{
+\code{dbGetStatement()} returns a string, the query used in
+either \code{\link[=dbSendQuery]{dbSendQuery()}}
+or \code{\link[=dbSendStatement]{dbSendStatement()}}.
+Attempting to query the statement for a result set cleared with
+\code{\link[=dbClearResult]{dbClearResult()}} gives an error.
+}
+\description{
+spec_meta_get_statement
+}
diff --git a/man/spec_meta_has_completed.Rd b/man/spec_meta_has_completed.Rd
new file mode 100644
index 0000000..f337b3c
--- /dev/null
+++ b/man/spec_meta_has_completed.Rd
@@ -0,0 +1,32 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-meta-has-completed.R
+\docType{data}
+\name{spec_meta_has_completed}
+\alias{spec_meta_has_completed}
+\title{spec_meta_has_completed}
+\value{
+\code{dbHasCompleted()} returns a logical scalar.
+For a query initiated by \code{\link[=dbSendQuery]{dbSendQuery()}} with non-empty result set,
+\code{dbHasCompleted()} returns \code{FALSE} initially
+and \code{TRUE} after calling \code{\link[=dbFetch]{dbFetch()}} without limit.
+For a query initiated by \code{\link[=dbSendStatement]{dbSendStatement()}},
+\code{dbHasCompleted()} always returns \code{TRUE}.
+Attempting to query completion status for a result set cleared with
+\code{\link[=dbClearResult]{dbClearResult()}} gives an error.
+}
+\description{
+spec_meta_has_completed
+}
+\section{Specification}{
+
+The completion status for a query is only guaranteed to be set to
+\code{FALSE} after attempting to fetch past the end of the entire result.
+Therefore, for a query with an empty result set,
+the initial return value is unspecified,
+but the result value is \code{TRUE} after trying to fetch only one row.
+Similarly, for a query with a result set of length n,
+the return value is unspecified after fetching n rows,
+but the result value is \code{TRUE} after trying to fetch only one more
+row.
+}
+
diff --git a/man/spec_meta_is_valid.Rd b/man/spec_meta_is_valid.Rd
new file mode 100644
index 0000000..a388474
--- /dev/null
+++ b/man/spec_meta_is_valid.Rd
@@ -0,0 +1,25 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-meta-is-valid.R
+\docType{data}
+\name{spec_meta_is_valid}
+\alias{spec_meta_is_valid}
+\title{spec_meta_is_valid}
+\value{
+\code{dbIsValid()} returns a logical scalar,
+\code{TRUE} if the object specified by \code{dbObj} is valid,
+\code{FALSE} otherwise.
+A \linkS4class{DBIConnection} object is initially valid,
+and becomes invalid after disconnecting with \code{\link[=dbDisconnect]{dbDisconnect()}}.
+A \linkS4class{DBIResult} object is valid after a call to \code{\link[=dbSendQuery]{dbSendQuery()}},
+and stays valid even after all rows have been fetched;
+only clearing it with \code{\link[=dbClearResult]{dbClearResult()}} invalidates it.
+A \linkS4class{DBIResult} object is also valid after a call to \code{\link[=dbSendStatement]{dbSendStatement()}},
+and stays valid after querying the number of rows affected;
+only clearing it with \code{\link[=dbClearResult]{dbClearResult()}} invalidates it.
+If the connection to the database system is dropped (e.g., due to
+connectivity problems, server failure, etc.), \code{dbIsValid()} should return
+\code{FALSE}. This is not tested automatically.
+}
+\description{
+spec_meta_is_valid
+}
diff --git a/man/spec_result_clear_result.Rd b/man/spec_result_clear_result.Rd
new file mode 100644
index 0000000..32b44ce
--- /dev/null
+++ b/man/spec_result_clear_result.Rd
@@ -0,0 +1,24 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-result-clear-result.R
+\docType{data}
+\name{spec_result_clear_result}
+\alias{spec_result_clear_result}
+\title{spec_result_clear_result}
+\value{
+\code{dbClearResult()} returns \code{TRUE}, invisibly, for result sets obtained from
+both \code{dbSendQuery()}
+and \code{dbSendStatement()}.
+An attempt to close an already closed result set issues a warning
+in both cases.
+}
+\description{
+spec_result_clear_result
+}
+\section{Specification}{
+
+\code{dbClearResult()} frees all resources associated with retrieving
+the result of a query or update operation.
+The DBI backend can expect a call to \code{dbClearResult()} for each
+\code{\link[=dbSendQuery]{dbSendQuery()}} or \code{\link[=dbSendStatement]{dbSendStatement()}} call.
+}
+
diff --git a/man/spec_result_create_table_with_data_type.Rd b/man/spec_result_create_table_with_data_type.Rd
new file mode 100644
index 0000000..e92f362
--- /dev/null
+++ b/man/spec_result_create_table_with_data_type.Rd
@@ -0,0 +1,16 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-result-create-table-with-data-type.R
+\docType{data}
+\name{spec_result_create_table_with_data_type}
+\alias{spec_result_create_table_with_data_type}
+\title{spec_result_create_table_with_data_type}
+\description{
+spec_result_create_table_with_data_type
+}
+\section{Specification}{
+
+All data types returned by \code{dbDataType()} are usable in an SQL statement
+of the form
+\code{"CREATE TABLE test (a ...)"}.
+}
+
diff --git a/man/spec_result_execute.Rd b/man/spec_result_execute.Rd
new file mode 100644
index 0000000..eceb68f
--- /dev/null
+++ b/man/spec_result_execute.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-result-execute.R
+\docType{data}
+\name{spec_result_execute}
+\alias{spec_result_execute}
+\title{spec_result_execute}
+\value{
+\code{dbExecute()} always returns a
+scalar
+numeric
+that specifies the number of rows affected
+by the statement.
+An error is raised when issuing a statement over a closed
+or invalid connection,
+if the syntax of the statement is invalid,
+or if the statement is not a non-\code{NA} string.
+}
+\description{
+spec_result_execute
+}
+\section{Additional arguments}{
+
+The following argument is not part of the \code{dbExecute()} generic
+(to improve compatibility across backends)
+but is part of the DBI specification:
+\itemize{
+\item \code{params} (TBD)
+}
+
+They must be provided as named arguments.
+See the "Specification" section for details on its usage.
+}
+
diff --git a/man/spec_result_fetch.Rd b/man/spec_result_fetch.Rd
new file mode 100644
index 0000000..9c98b46
--- /dev/null
+++ b/man/spec_result_fetch.Rd
@@ -0,0 +1,46 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-result-fetch.R
+\docType{data}
+\name{spec_result_fetch}
+\alias{spec_result_fetch}
+\title{spec_result_fetch}
+\value{
+\code{dbFetch()} always returns a \link{data.frame}
+with as many rows as records were fetched and as many
+columns as fields in the result set,
+even if the result is a single value
+or has one
+or zero rows.
+An attempt to fetch from a closed result set raises an error.
+If the \code{n} argument is not an atomic whole number
+greater or equal to -1 or Inf, an error is raised,
+but a subsequent call to \code{dbFetch()} with proper \code{n} argument succeeds.
+Calling \code{dbFetch()} on a result set from a data manipulation query
+created by \code{\link[=dbSendStatement]{dbSendStatement()}}
+can be fetched and return an empty data frame, with a warning.
+}
+\description{
+spec_result_fetch
+}
+\section{Specification}{
+
+Fetching multi-row queries with one
+or more columns be default returns the entire result.
+Multi-row queries can also be fetched progressively
+by passing a whole number (\link{integer}
+or \link{numeric})
+as the \code{n} argument.
+A value of \link{Inf} for the \code{n} argument is supported
+and also returns the full result.
+If more rows than available are fetched, the result is returned in full
+without warning.
+If fewer rows than requested are returned, further fetches will
+return a data frame with zero rows.
+If zero rows are fetched, the columns of the data frame are still fully
+typed.
+Fetching fewer rows than available is permitted,
+no warning is issued when clearing the result set.
+
+A column named \code{row_names} is treated like any other column.
+}
+
diff --git a/man/spec_result_get_query.Rd b/man/spec_result_get_query.Rd
new file mode 100644
index 0000000..6361c74
--- /dev/null
+++ b/man/spec_result_get_query.Rd
@@ -0,0 +1,54 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-result-get-query.R
+\docType{data}
+\name{spec_result_get_query}
+\alias{spec_result_get_query}
+\title{spec_result_get_query}
+\value{
+\code{dbGetQuery()} always returns a \link{data.frame}
+with as many rows as records were fetched and as many
+columns as fields in the result set,
+even if the result is a single value
+or has one
+or zero rows.
+An error is raised when issuing a query over a closed
+or invalid connection,
+if the syntax of the query is invalid,
+or if the query is not a non-\code{NA} string.
+If the \code{n} argument is not an atomic whole number
+greater or equal to -1 or Inf, an error is raised,
+but a subsequent call to \code{dbGetQuery()} with proper \code{n} argument succeeds.
+}
+\description{
+spec_result_get_query
+}
+\section{Additional arguments}{
+
+The following arguments are not part of the \code{dbGetQuery()} generic
+(to improve compatibility across backends)
+but are part of the DBI specification:
+\itemize{
+\item \code{n} (default: -1)
+\item \code{params} (TBD)
+}
+
+They must be provided as named arguments.
+See the "Specification" and "Value" sections for details on their usage.
+}
+
+\section{Specification}{
+
+Fetching multi-row queries with one
+or more columns be default returns the entire result.
+A value of \link{Inf} for the \code{n} argument is supported
+and also returns the full result.
+If more rows than available are fetched, the result is returned in full
+without warning.
+If zero rows are fetched, the columns of the data frame are still fully
+typed.
+Fetching fewer rows than available is permitted,
+no warning is issued.
+
+A column named \code{row_names} is treated like any other column.
+}
+
diff --git a/man/spec_result_roundtrip.Rd b/man/spec_result_roundtrip.Rd
new file mode 100644
index 0000000..d66d9d5
--- /dev/null
+++ b/man/spec_result_roundtrip.Rd
@@ -0,0 +1,51 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-result-roundtrip.R
+\docType{data}
+\name{spec_result_roundtrip}
+\alias{spec_result_roundtrip}
+\title{spec_result_roundtrip}
+\description{
+spec_result_roundtrip
+}
+\section{Specification}{
+
+The column types of the returned data frame depend on the data returned:
+\itemize{
+\item \link{integer} for integer values between -2^31 and 2^31 - 1
+\item \link{numeric} for numbers with a fractional component
+\item \link{logical} for Boolean values (some backends may return an integer)
+\item \link{character} for text
+\item lists of \link{raw} for blobs (with \code{NULL} entries for SQL NULL values)
+\item coercible using \code{\link[=as.Date]{as.Date()}} for dates
+(also applies to the return value of the SQL function \code{current_date})
+\item coercible using \code{\link[hms:as.hms]{hms::as.hms()}} for times
+(also applies to the return value of the SQL function \code{current_time})
+\item coercible using \code{\link[=as.POSIXct]{as.POSIXct()}} for timestamps
+(also applies to the return value of the SQL function \code{current_timestamp})
+\item \link{NA} for SQL \code{NULL} values
+}
+
+If dates and timestamps are supported by the backend, the following R types are
+used:
+\itemize{
+\item \link{Date} for dates
+(also applies to the return value of the SQL function \code{current_date})
+\item \link{POSIXct} for timestamps
+(also applies to the return value of the SQL function \code{current_timestamp})
+}
+
+R has no built-in type with lossless support for the full range of 64-bit
+or larger integers. If 64-bit integers are returned from a query,
+the following rules apply:
+\itemize{
+\item Values are returned in a container with support for the full range of
+valid 64-bit values (such as the \code{integer64} class of the \pkg{bit64}
+package)
+\item Coercion to numeric always returns a number that is as close as possible
+to the true value
+\item Loss of precision when converting to numeric gives a warning
+\item Conversion to character always returns a lossless decimal representation
+of the data
+}
+}
+
diff --git a/man/spec_result_send_query.Rd b/man/spec_result_send_query.Rd
new file mode 100644
index 0000000..c4fa488
--- /dev/null
+++ b/man/spec_result_send_query.Rd
@@ -0,0 +1,35 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-result-send-query.R
+\docType{data}
+\name{spec_result_send_query}
+\alias{spec_result_send_query}
+\title{spec_result_send_query}
+\value{
+\code{dbSendQuery()} returns
+an S4 object that inherits from \linkS4class{DBIResult}.
+The result set can be used with \code{\link[=dbFetch]{dbFetch()}} to extract records.
+Once you have finished using a result, make sure to clear it
+with \code{\link[=dbClearResult]{dbClearResult()}}.
+An error is raised when issuing a query over a closed
+or invalid connection,
+if the syntax of the query is invalid,
+or if the query is not a non-\code{NA} string.
+}
+\description{
+spec_result_send_query
+}
+\section{Specification}{
+
+No warnings occur under normal conditions.
+When done, the DBIResult object must be cleared with a call to
+\code{\link[=dbClearResult]{dbClearResult()}}.
+Failure to clear the result set leads to a warning
+when the connection is closed.
+
+If the backend supports only one open result set per connection,
+issuing a second query invalidates an already open result set
+and raises a warning.
+The newly opened result set is valid
+and must be cleared with \code{dbClearResult()}.
+}
+
diff --git a/man/spec_result_send_statement.Rd b/man/spec_result_send_statement.Rd
new file mode 100644
index 0000000..26b12fb
--- /dev/null
+++ b/man/spec_result_send_statement.Rd
@@ -0,0 +1,35 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-result-send-statement.R
+\docType{data}
+\name{spec_result_send_statement}
+\alias{spec_result_send_statement}
+\title{spec_result_send_statement}
+\value{
+\code{dbSendStatement()} returns
+an S4 object that inherits from \linkS4class{DBIResult}.
+The result set can be used with \code{\link[=dbGetRowsAffected]{dbGetRowsAffected()}} to
+determine the number of rows affected by the query.
+Once you have finished using a result, make sure to clear it
+with \code{\link[=dbClearResult]{dbClearResult()}}.
+An error is raised when issuing a statement over a closed
+or invalid connection,
+if the syntax of the statement is invalid,
+or if the statement is not a non-\code{NA} string.
+}
+\description{
+spec_result_send_statement
+}
+\section{Specification}{
+
+No warnings occur under normal conditions.
+When done, the DBIResult object must be cleared with a call to
+\code{\link[=dbClearResult]{dbClearResult()}}.
+Failure to clear the result set leads to a warning
+when the connection is closed.
+If the backend supports only one open result set per connection,
+issuing a second query invalidates an already open result set
+and raises a warning.
+The newly opened result set is valid
+and must be cleared with \code{dbClearResult()}.
+}
+
diff --git a/man/spec_sql_exists_table.Rd b/man/spec_sql_exists_table.Rd
new file mode 100644
index 0000000..d1eeaaf
--- /dev/null
+++ b/man/spec_sql_exists_table.Rd
@@ -0,0 +1,42 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-sql-exists-table.R
+\docType{data}
+\name{spec_sql_exists_table}
+\alias{spec_sql_exists_table}
+\title{spec_sql_exists_table}
+\value{
+\code{dbExistsTable()} returns a logical scalar, \code{TRUE} if the table or view
+specified by the \code{name} argument exists, \code{FALSE} otherwise.
+This includes temporary tables if supported by the database.
+
+An error is raised when calling this method for a closed
+or invalid connection.
+An error is also raised
+if \code{name} cannot be processed with \code{\link[=dbQuoteIdentifier]{dbQuoteIdentifier()}}
+or if this results in a non-scalar.
+}
+\description{
+spec_sql_exists_table
+}
+\section{Additional arguments}{
+
+TBD: \code{temporary = NA}
+
+This must be provided as named argument.
+See the "Specification" section for details on their usage.
+}
+
+\section{Specification}{
+
+The \code{name} argument is processed as follows,
+to support databases that allow non-syntactic names for their objects:
+\itemize{
+\item If an unquoted table name as string: \code{dbExistsTable()} will do the
+quoting,
+perhaps by calling \code{dbQuoteIdentifier(conn, x = name)}
+\item If the result of a call to \code{\link[=dbQuoteIdentifier]{dbQuoteIdentifier()}}: no more quoting is done
+}
+
+For all tables listed by \code{\link[=dbListTables]{dbListTables()}}, \code{dbExistsTable()} returns \code{TRUE}.
+}
+
diff --git a/man/spec_sql_list_tables.Rd b/man/spec_sql_list_tables.Rd
new file mode 100644
index 0000000..09878c0
--- /dev/null
+++ b/man/spec_sql_list_tables.Rd
@@ -0,0 +1,33 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-sql-list-tables.R
+\docType{data}
+\name{spec_sql_list_tables}
+\alias{spec_sql_list_tables}
+\title{spec_sql_list_tables}
+\value{
+\code{dbListTables()}
+returns a character vector
+that enumerates all tables
+and views
+in the database.
+Tables added with \code{\link[=dbWriteTable]{dbWriteTable()}}
+are part of the list,
+including temporary tables if supported by the database.
+As soon a table is removed from the database,
+it is also removed from the list of database tables.
+
+The returned names are suitable for quoting with \code{dbQuoteIdentifier()}.
+An error is raised when calling this method for a closed
+or invalid connection.
+}
+\description{
+spec_sql_list_tables
+}
+\section{Additional arguments}{
+
+TBD: \code{temporary = NA}
+
+This must be provided as named argument.
+See the "Specification" section for details on their usage.
+}
+
diff --git a/man/spec_sql_quote_identifier.Rd b/man/spec_sql_quote_identifier.Rd
new file mode 100644
index 0000000..01121d3
--- /dev/null
+++ b/man/spec_sql_quote_identifier.Rd
@@ -0,0 +1,47 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-sql-quote-identifier.R
+\docType{data}
+\name{spec_sql_quote_identifier}
+\alias{spec_sql_quote_identifier}
+\title{spec_sql_quote_identifier}
+\value{
+\code{dbQuoteIdentifier()} returns an object that can be coerced to \link{character},
+of the same length as the input.
+For an empty character vector this function returns a length-0 object.
+An error is raised if the input contains \code{NA},
+but not for an empty string.
+
+When passing the returned object again to \code{dbQuoteIdentifier()}
+as \code{x}
+argument, it is returned unchanged.
+Passing objects of class \link{SQL} should also return them unchanged.
+(For backends it may be most convenient to return \link{SQL} objects
+to achieve this behavior, but this is not required.)
+}
+\description{
+spec_sql_quote_identifier
+}
+\section{Specification}{
+
+Calling \code{\link[=dbGetQuery]{dbGetQuery()}} for a query of the format \code{SELECT 1 AS ...}
+returns a data frame with the identifier, unquoted, as column name.
+Quoted identifiers can be used as table and column names in SQL queries,
+in particular in queries like \code{SELECT 1 AS ...}
+and \code{SELECT * FROM (SELECT 1) ...}.
+The method must use a quoting mechanism that is unambiguously different
+from the quoting mechanism used for strings, so that a query like
+\code{SELECT ... FROM (SELECT 1 AS ...)}
+throws an error if the column names do not match.
+
+The method can quote column names that
+contain special characters such as a space,
+a dot,
+a comma,
+or quotes used to mark strings
+or identifiers,
+if the database supports this.
+In any case, checking the validity of the identifier
+should be performed only when executing a query,
+and not by \code{dbQuoteIdentifier()}.
+}
+
diff --git a/man/spec_sql_quote_string.Rd b/man/spec_sql_quote_string.Rd
new file mode 100644
index 0000000..3dcd235
--- /dev/null
+++ b/man/spec_sql_quote_string.Rd
@@ -0,0 +1,45 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-sql-quote-string.R
+\docType{data}
+\name{spec_sql_quote_string}
+\alias{spec_sql_quote_string}
+\title{spec_sql_quote_string}
+\value{
+\code{dbQuoteString()} returns an object that can be coerced to \link{character},
+of the same length as the input.
+For an empty character vector this function returns a length-0 object.
+
+When passing the returned object again to \code{dbQuoteString()}
+as \code{x}
+argument, it is returned unchanged.
+Passing objects of class \link{SQL} should also return them unchanged.
+(For backends it may be most convenient to return \link{SQL} objects
+to achieve this behavior, but this is not required.)
+}
+\description{
+spec_sql_quote_string
+}
+\section{Specification}{
+
+The returned expression can be used in a \code{SELECT ...} query,
+and for any scalar character \code{x} the value of
+\code{dbGetQuery(paste0("SELECT ", dbQuoteString(x)))[[1]]}
+must be identical to \code{x},
+even if \code{x} contains
+spaces,
+tabs,
+quotes (single
+or double),
+backticks,
+or newlines
+(in any combination)
+or is itself the result of a \code{dbQuoteString()} call coerced back to
+character (even repeatedly).
+If \code{x} is \code{NA}, the result must merely satisfy \code{\link[=is.na]{is.na()}}.
+The strings \code{"NA"} or \code{"NULL"} are not treated specially.
+
+\code{NA} should be translated to an unquoted SQL \code{NULL},
+so that the query \code{SELECT * FROM (SELECT 1) a WHERE ... IS NULL}
+returns one row.
+}
+
diff --git a/man/spec_sql_read_table.Rd b/man/spec_sql_read_table.Rd
new file mode 100644
index 0000000..fe7cc16
--- /dev/null
+++ b/man/spec_sql_read_table.Rd
@@ -0,0 +1,74 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-sql-read-table.R
+\docType{data}
+\name{spec_sql_read_table}
+\alias{spec_sql_read_table}
+\title{spec_sql_read_table}
+\value{
+\code{dbReadTable()} returns a data frame that contains the complete data
+from the remote table, effectively the result of calling \code{\link[=dbGetQuery]{dbGetQuery()}}
+with \code{SELECT * FROM <name>}.
+An error is raised if the table does not exist.
+An empty table is returned as a data frame with zero rows.
+
+The presence of \link{rownames} depends on the \code{row.names} argument,
+see \code{\link[=sqlColumnToRownames]{sqlColumnToRownames()}} for details:
+\itemize{
+\item If \code{FALSE} or \code{NULL}, the returned data frame doesn't have row names.
+\item If \code{TRUE}, a column named "row_names" is converted to row names,
+an error is raised if no such column exists.
+\item If \code{NA}, a column named "row_names" is converted to row names if it exists,
+otherwise no translation occurs.
+\item If a string, this specifies the name of the column in the remote table
+that contains the row names,
+an error is raised if no such column exists.
+}
+
+The default is \code{row.names = FALSE}.
+
+If the database supports identifiers with special characters,
+the columns in the returned data frame are converted to valid R
+identifiers
+if the \code{check.names} argument is \code{TRUE},
+otherwise non-syntactic column names can be returned unquoted.
+
+An error is raised when calling this method for a closed
+or invalid connection.
+An error is raised
+if \code{name} cannot be processed with \code{\link[=dbQuoteIdentifier]{dbQuoteIdentifier()}}
+or if this results in a non-scalar.
+Unsupported values for \code{row.names} and \code{check.names}
+(non-scalars,
+unsupported data types,
+\code{NA} for \code{check.names})
+also raise an error.
+}
+\description{
+spec_sql_read_table
+}
+\section{Additional arguments}{
+
+The following arguments are not part of the \code{dbReadTable()} generic
+(to improve compatibility across backends)
+but are part of the DBI specification:
+\itemize{
+\item \code{row.names}
+\item \code{check.names}
+}
+
+They must be provided as named arguments.
+See the "Value" section for details on their usage.
+}
+
+\section{Specification}{
+
+The \code{name} argument is processed as follows,
+to support databases that allow non-syntactic names for their objects:
+\itemize{
+\item If an unquoted table name as string: \code{dbReadTable()} will do the
+quoting,
+perhaps by calling \code{dbQuoteIdentifier(conn, x = name)}
+\item If the result of a call to \code{\link[=dbQuoteIdentifier]{dbQuoteIdentifier()}}: no more quoting is done
+}
+}
+
diff --git a/man/spec_sql_remove_table.Rd b/man/spec_sql_remove_table.Rd
new file mode 100644
index 0000000..2d41630
--- /dev/null
+++ b/man/spec_sql_remove_table.Rd
@@ -0,0 +1,38 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-sql-remove-table.R
+\docType{data}
+\name{spec_sql_remove_table}
+\alias{spec_sql_remove_table}
+\title{spec_sql_remove_table}
+\value{
+\code{dbRemoveTable()} returns \code{TRUE}, invisibly.
+If the table does not exist, an error is raised.
+An attempt to remove a view with this function may result in an error.
+
+An error is raised when calling this method for a closed
+or invalid connection.
+An error is also raised
+if \code{name} cannot be processed with \code{\link[=dbQuoteIdentifier]{dbQuoteIdentifier()}}
+or if this results in a non-scalar.
+}
+\description{
+spec_sql_remove_table
+}
+\section{Specification}{
+
+A table removed by \code{dbRemoveTable()} doesn't appear in the list of tables
+returned by \code{\link[=dbListTables]{dbListTables()}},
+and \code{\link[=dbExistsTable]{dbExistsTable()}} returns \code{FALSE}.
+The removal propagates immediately to other connections to the same database.
+This function can also be used to remove a temporary table.
+
+The \code{name} argument is processed as follows,
+to support databases that allow non-syntactic names for their objects:
+\itemize{
+\item If an unquoted table name as string: \code{dbRemoveTable()} will do the
+quoting,
+perhaps by calling \code{dbQuoteIdentifier(conn, x = name)}
+\item If the result of a call to \code{\link[=dbQuoteIdentifier]{dbQuoteIdentifier()}}: no more quoting is done
+}
+}
+
diff --git a/man/spec_sql_write_table.Rd b/man/spec_sql_write_table.Rd
new file mode 100644
index 0000000..22a3574
--- /dev/null
+++ b/man/spec_sql_write_table.Rd
@@ -0,0 +1,129 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-sql-write-table.R
+\docType{data}
+\name{spec_sql_write_table}
+\alias{spec_sql_write_table}
+\title{spec_sql_write_table}
+\value{
+\code{dbWriteTable()} returns \code{TRUE}, invisibly.
+If the table exists, and both \code{append} and \code{overwrite} arguments are unset,
+or \code{append = TRUE} and the data frame with the new data has different
+column names,
+an error is raised; the remote table remains unchanged.
+
+An error is raised when calling this method for a closed
+or invalid connection.
+An error is also raised
+if \code{name} cannot be processed with \code{\link[=dbQuoteIdentifier]{dbQuoteIdentifier()}}
+or if this results in a non-scalar.
+Invalid values for the additional arguments \code{row.names},
+\code{overwrite}, \code{append}, \code{field.types}, and \code{temporary}
+(non-scalars,
+unsupported data types,
+\code{NA},
+incompatible values,
+duplicate
+or missing names,
+incompatible columns)
+also raise an error.
+}
+\description{
+spec_sql_write_table
+}
+\section{Additional arguments}{
+
+The following arguments are not part of the \code{dbWriteTable()} generic
+(to improve compatibility across backends)
+but are part of the DBI specification:
+\itemize{
+\item \code{row.names} (default: \code{NA})
+\item \code{overwrite} (default: \code{FALSE})
+\item \code{append} (default: \code{FALSE})
+\item \code{field.types} (default: \code{NULL})
+\item \code{temporary} (default: \code{FALSE})
+}
+
+They must be provided as named arguments.
+See the "Specification" and "Value" sections for details on their usage.
+}
+
+\section{Specification}{
+
+The \code{name} argument is processed as follows,
+to support databases that allow non-syntactic names for their objects:
+\itemize{
+\item If an unquoted table name as string: \code{dbWriteTable()} will do the quoting,
+perhaps by calling \code{dbQuoteIdentifier(conn, x = name)}
+\item If the result of a call to \code{\link[=dbQuoteIdentifier]{dbQuoteIdentifier()}}: no more quoting is done
+}
+
+If the \code{overwrite} argument is \code{TRUE}, an existing table of the same name
+will be overwritten.
+This argument doesn't change behavior if the table does not exist yet.
+
+If the \code{append} argument is \code{TRUE}, the rows in an existing table are
+preserved, and the new data are appended.
+If the table doesn't exist yet, it is created.
+
+If the \code{temporary} argument is \code{TRUE}, the table is not available in a
+second connection and is gone after reconnecting.
+Not all backends support this argument.
+A regular, non-temporary table is visible in a second connection
+and after reconnecting to the database.
+
+SQL keywords can be used freely in table names, column names, and data.
+Quotes, commas, and spaces can also be used in the data,
+and, if the database supports non-syntactic identifiers,
+also for table names and column names.
+
+The following data types must be supported at least,
+and be read identically with \code{\link[=dbReadTable]{dbReadTable()}}:
+\itemize{
+\item integer
+\item numeric
+(also with \code{Inf} and \code{NaN} values,
+the latter are translated to \code{NA})
+\item logical
+\item \code{NA} as NULL
+\item 64-bit values (using \code{"bigint"} as field type);
+the result can be converted to a numeric, which may lose precision,
+\item character (in both UTF-8
+and native encodings),
+supporting empty strings
+\item factor (returned as character)
+\item list of raw
+(if supported by the database)
+\item objects of type \link[blob:blob]{blob::blob}
+(if supported by the database)
+\item date
+(if supported by the database;
+returned as \code{Date})
+\item time
+(if supported by the database;
+returned as objects that inherit from \code{difftime})
+\item timestamp
+(if supported by the database;
+returned as \code{POSIXct}
+with time zone support)
+}
+
+Mixing column types in the same table is supported.
+
+The \code{field.types} argument must be a named character vector with at most
+one entry for each column.
+It indicates the SQL data type to be used for a new column.
+
+The interpretation of \link{rownames} depends on the \code{row.names} argument,
+see \code{\link[=sqlRownamesToColumn]{sqlRownamesToColumn()}} for details:
+\itemize{
+\item If \code{FALSE} or \code{NULL}, row names are ignored.
+\item If \code{TRUE}, row names are converted to a column named "row_names",
+even if the input data frame only has natural row names from 1 to \code{nrow(...)}.
+\item If \code{NA}, a column named "row_names" is created if the data has custom row names,
+no extra column is created in the case of natural row names.
+\item If a string, this specifies the name of the column in the remote table
+that contains the row names,
+even if the input data frame only has natural row names.
+}
+}
+
diff --git a/man/spec_transaction_begin_commit_rollback.Rd b/man/spec_transaction_begin_commit_rollback.Rd
new file mode 100644
index 0000000..404f4d4
--- /dev/null
+++ b/man/spec_transaction_begin_commit_rollback.Rd
@@ -0,0 +1,57 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-transaction-begin-commit-rollback.R
+\docType{data}
+\name{spec_transaction_begin_commit_rollback}
+\alias{spec_transaction_begin_commit_rollback}
+\title{spec_transaction_begin_commit_rollback}
+\value{
+\code{dbBegin()}, \code{dbCommit()} and \code{dbRollback()} return \code{TRUE}, invisibly.
+The implementations are expected to raise an error in case of failure,
+but this is not tested.
+In any way, all generics throw an error with a closed
+or invalid connection.
+In addition, a call to \code{dbCommit()}
+or \code{dbRollback()}
+without a prior call to \code{dbBegin()} raises an error.
+Nested transactions are not supported by DBI,
+an attempt to call \code{dbBegin()} twice
+yields an error.
+}
+\description{
+spec_transaction_begin_commit_rollback
+}
+\section{Specification}{
+
+Actual support for transactions may vary between backends.
+A transaction is initiated by a call to \code{dbBegin()}
+and committed by a call to \code{dbCommit()}.
+Data written in a transaction must persist after the transaction is committed.
+For example, a table that is missing when the transaction is started
+but is created
+and populated during the transaction
+must exist and contain the data added there
+both during
+and after the transaction,
+and also in a new connection.
+
+A transaction
+can also be aborted with \code{dbRollback()}.
+All data written in such a transaction must be removed after the
+transaction is rolled back.
+For example, a table that is missing when the transaction is started
+but is created during the transaction
+must not exist anymore after the rollback.
+
+Disconnection from a connection with an open transaction
+effectively rolls back the transaction.
+All data written in such a transaction must be removed after the
+transaction is rolled back.
+
+The behavior is not specified if other arguments are passed to these
+functions. In particular, \pkg{RSQLite} issues named transactions
+with support for nesting
+if the \code{name} argument is set.
+
+The transaction isolation level is not specified by DBI.
+}
+
diff --git a/man/spec_transaction_with_transaction.Rd b/man/spec_transaction_with_transaction.Rd
new file mode 100644
index 0000000..ad007e5
--- /dev/null
+++ b/man/spec_transaction_with_transaction.Rd
@@ -0,0 +1,31 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-transaction-with-transaction.R
+\docType{data}
+\name{spec_transaction_with_transaction}
+\alias{spec_transaction_with_transaction}
+\title{spec_transaction_with_transaction}
+\value{
+\code{dbWithTransaction()} returns the value of the executed code.
+Failure to initiate the transaction
+(e.g., if the connection is closed
+or invalid
+of if \code{\link[=dbBegin]{dbBegin()}} has been called already)
+gives an error.
+}
+\description{
+spec_transaction_with_transaction
+}
+\section{Specification}{
+
+\code{dbWithTransaction()} initiates a transaction with \code{dbBegin()}, executes
+the code given in the \code{code} argument, and commits the transaction with
+\code{\link[=dbCommit]{dbCommit()}}.
+If the code raises an error, the transaction is instead aborted with
+\code{\link[=dbRollback]{dbRollback()}}, and the error is propagated.
+If the code calls \code{dbBreak()}, execution of the code stops and the
+transaction is silently aborted.
+All side effects caused by the code
+(such as the creation of new variables)
+propagate to the calling environment.
+}
+
diff --git a/man/test_all.Rd b/man/test_all.Rd
index 8e1e19b..53f5341 100644
--- a/man/test_all.Rd
+++ b/man/test_all.Rd
@@ -4,9 +4,12 @@
% R/test-meta.R, R/test-transaction.R, R/test-compliance.R, R/test-stress.R
\name{test_all}
\alias{test_all}
+\alias{test_some}
\title{Run all tests}
\usage{
test_all(skip = NULL, ctx = get_default_context())
+
+test_some(test, ctx = get_default_context())
}
\arguments{
\item{skip}{\code{[character()]}\cr A vector of regular expressions to match
@@ -14,10 +17,16 @@ against test names; skip test if matching any.}
\item{ctx}{\code{[DBItest_context]}\cr A test context as created by
\code{\link[=make_context]{make_context()}}.}
+
+\item{test}{\code{[character]}\cr A character vector of regular expressions
+describing the tests to run.}
}
\description{
-This function calls all tests defined in this package (see the section
+\code{test_all()} calls all tests defined in this package (see the section
"Tests" below).
+
+\code{test_some()} allows testing one or more tests, it works by
+constructing the \code{skip} argument using negative lookaheads.
}
\section{Tests}{
diff --git a/man/test_compliance.Rd b/man/test_compliance.Rd
index ea0f957..0b187d9 100644
--- a/man/test_compliance.Rd
+++ b/man/test_compliance.Rd
@@ -24,4 +24,3 @@ Other tests: \code{\link{test_connection}},
\code{\link{test_sql}}, \code{\link{test_stress}},
\code{\link{test_transaction}}
}
-
diff --git a/man/test_connection.Rd b/man/test_connection.Rd
index 8a580bb..5459ba3 100644
--- a/man/test_connection.Rd
+++ b/man/test_connection.Rd
@@ -24,4 +24,3 @@ Other tests: \code{\link{test_compliance}},
\code{\link{test_sql}}, \code{\link{test_stress}},
\code{\link{test_transaction}}
}
-
diff --git a/man/test_data_type.Rd b/man/test_data_type.Rd
new file mode 100644
index 0000000..a6214e8
--- /dev/null
+++ b/man/test_data_type.Rd
@@ -0,0 +1,50 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/spec-driver-data-type.R
+\name{test_data_type}
+\alias{test_data_type}
+\title{test_data_type}
+\usage{
+test_data_type(ctx, dbObj)
+}
+\arguments{
+\item{ctx, dbObj}{Arguments to internal test function}
+}
+\value{
+\code{dbDataType()} returns the SQL type that corresponds to the \code{obj} argument
+as a non-empty
+character string.
+For data frames, a character vector with one element per column
+is returned.
+An error is raised for invalid values for the \code{obj} argument such as a
+\code{NULL} value.
+}
+\description{
+test_data_type
+}
+\section{Specification}{
+
+The backend can override the \code{\link[=dbDataType]{dbDataType()}} generic
+for its driver class.
+
+This generic expects an arbitrary object as second argument.
+To query the values returned by the default implementation,
+run \code{example(dbDataType, package = "DBI")}.
+If the backend needs to override this generic,
+it must accept all basic R data types as its second argument, namely
+\link{logical},
+\link{integer},
+\link{numeric},
+\link{character},
+dates (see \link{Dates}),
+date-time (see \link{DateTimeClasses}),
+and \link{difftime}.
+If the database supports blobs,
+this method also must accept lists of \link{raw} vectors,
+and \link[blob:blob]{blob::blob} objects.
+As-is objects (i.e., wrapped by \code{\link[=I]{I()}}) must be
+supported and return the same results as their unwrapped counterparts.
+The SQL data type for \link{factor}
+and \link{ordered} is the same as for character.
+The behavior for other object types is not specified.
+}
+
diff --git a/man/test_driver.Rd b/man/test_driver.Rd
index e6973d0..23d884d 100644
--- a/man/test_driver.Rd
+++ b/man/test_driver.Rd
@@ -24,4 +24,3 @@ Other tests: \code{\link{test_compliance}},
\code{\link{test_sql}}, \code{\link{test_stress}},
\code{\link{test_transaction}}
}
-
diff --git a/man/test_getting_started.Rd b/man/test_getting_started.Rd
index 5775876..8e9e7ae 100644
--- a/man/test_getting_started.Rd
+++ b/man/test_getting_started.Rd
@@ -24,4 +24,3 @@ Other tests: \code{\link{test_compliance}},
\code{\link{test_sql}}, \code{\link{test_stress}},
\code{\link{test_transaction}}
}
-
diff --git a/man/test_meta.Rd b/man/test_meta.Rd
index 6d0e799..4e1e37e 100644
--- a/man/test_meta.Rd
+++ b/man/test_meta.Rd
@@ -23,4 +23,3 @@ Other tests: \code{\link{test_compliance}},
\code{\link{test_result}}, \code{\link{test_sql}},
\code{\link{test_stress}}, \code{\link{test_transaction}}
}
-
diff --git a/man/test_result.Rd b/man/test_result.Rd
index 56ebfec..5b83408 100644
--- a/man/test_result.Rd
+++ b/man/test_result.Rd
@@ -23,4 +23,3 @@ Other tests: \code{\link{test_compliance}},
\code{\link{test_meta}}, \code{\link{test_sql}},
\code{\link{test_stress}}, \code{\link{test_transaction}}
}
-
diff --git a/man/test_sql.Rd b/man/test_sql.Rd
index d21eb4a..4eaaea3 100644
--- a/man/test_sql.Rd
+++ b/man/test_sql.Rd
@@ -23,4 +23,3 @@ Other tests: \code{\link{test_compliance}},
\code{\link{test_meta}}, \code{\link{test_result}},
\code{\link{test_stress}}, \code{\link{test_transaction}}
}
-
diff --git a/man/test_stress.Rd b/man/test_stress.Rd
index 983a252..1c7be3e 100644
--- a/man/test_stress.Rd
+++ b/man/test_stress.Rd
@@ -23,4 +23,3 @@ Other tests: \code{\link{test_compliance}},
\code{\link{test_meta}}, \code{\link{test_result}},
\code{\link{test_sql}}, \code{\link{test_transaction}}
}
-
diff --git a/man/test_transaction.Rd b/man/test_transaction.Rd
index 2c50054..402fa4d 100644
--- a/man/test_transaction.Rd
+++ b/man/test_transaction.Rd
@@ -23,4 +23,3 @@ Other tests: \code{\link{test_compliance}},
\code{\link{test_meta}}, \code{\link{test_result}},
\code{\link{test_sql}}, \code{\link{test_stress}}
}
-
diff --git a/man/tweaks.Rd b/man/tweaks.Rd
index 52a2799..bb78e9f 100644
--- a/man/tweaks.Rd
+++ b/man/tweaks.Rd
@@ -4,9 +4,14 @@
\alias{tweaks}
\title{Tweaks for DBI tests}
\usage{
-tweaks(..., constructor_name = NULL, constructor_relax_args = NULL,
- strict_identifier = NULL, omit_blob_tests = NULL,
- current_needs_parens = NULL, union = NULL, placeholder_pattern = NULL)
+tweaks(..., constructor_name = NULL, constructor_relax_args = FALSE,
+ strict_identifier = FALSE, omit_blob_tests = FALSE,
+ current_needs_parens = FALSE, union = function(x) paste(x, collapse =
+ " UNION "), placeholder_pattern = NULL, logical_return = identity,
+ date_cast = function(x) paste0("date('", x, "')"), time_cast = function(x)
+ paste0("time('", x, "')"), timestamp_cast = function(x)
+ paste0("timestamp('", x, "')"), date_typed = TRUE, time_typed = TRUE,
+ timestamp_typed = TRUE, temporary_tables = TRUE)
}
\arguments{
\item{...}{\code{[any]}\cr
@@ -38,11 +43,39 @@ Function that combines several subqueries into one so that the
resulting query returns the concatenated results of the subqueries}
\item{placeholder_pattern}{\code{[character]}\cr
-A pattern for placeholders used in \code{\link[DBI:dbBind]{DBI::dbBind()}}, e.g.,
+A pattern for placeholders used in \code{\link[=dbBind]{dbBind()}}, e.g.,
\code{"?"}, \code{"$1"}, or \code{":name"}. See
\code{\link[=make_placeholder_fun]{make_placeholder_fun()}} for details.}
+
+\item{logical_return}{\code{[function(logical)]}\cr
+A vectorized function that converts logical values to the data type
+returned by the DBI backend.}
+
+\item{date_cast}{\code{[function(character)]}\cr
+A vectorized function that creates an SQL expression for coercing a
+string to a date value.}
+
+\item{time_cast}{\code{[function(character)]}\cr
+A vectorized function that creates an SQL expression for coercing a
+string to a time value.}
+
+\item{timestamp_cast}{\code{[function(character)]}\cr
+A vectorized function that creates an SQL expression for coercing a
+string to a timestamp value.}
+
+\item{date_typed}{\code{[logical(1L)]}\cr
+Set to \code{FALSE} if the DBMS doesn't support a dedicated type for dates.}
+
+\item{time_typed}{\code{[logical(1L)]}\cr
+Set to \code{FALSE} if the DBMS doesn't support a dedicated type for times.}
+
+\item{timestamp_typed}{\code{[logical(1L)]}\cr
+Set to \code{FALSE} if the DBMS doesn't support a dedicated type for
+timestamps.}
+
+\item{temporary_tables}{\code{[logical(1L)]}\cr
+Set to \code{FALSE} if the DBMS doesn't support temporary tables.}
}
\description{
TBD.
}
-
diff --git a/tests/testthat/test-consistency.R b/tests/testthat/test-consistency.R
new file mode 100644
index 0000000..05a4ad1
--- /dev/null
+++ b/tests/testthat/test-consistency.R
@@ -0,0 +1,28 @@
+context("consistency")
+
+test_that("no unnamed specs", {
+ tests <- spec_all[!vapply(spec_all, is.null, logical(1L))]
+ vicinity <- NULL
+ if (any(names(tests) == "")) {
+ vicinity <- sort(unique(unlist(
+ lapply(which(names(tests) == ""), "+", -1:1)
+ )))
+ vicinity <- vicinity[names(tests)[vicinity] != ""]
+ }
+ expect_null(vicinity)
+})
+
+test_that("no duplicate spec names", {
+ all_names <- names(spec_all)
+ dupe_names <- unique(all_names[duplicated(all_names)])
+ expect_equal(dupe_names, rep("", length(dupe_names)))
+})
+
+test_that("all specs used", {
+ env <- asNamespace("DBItest")
+ defined_spec_names <- ls(env, pattern = "^spec_")
+ defined_specs <- mget(defined_spec_names, env)
+ defined_spec_names <- unlist(sapply(defined_specs, names), use.names = FALSE)
+ new_names <- setdiff(defined_spec_names, names(spec_all))
+ expect_equal(new_names, rep("", length(new_names)))
+})
diff --git a/tests/testthat/test-tweaks.R b/tests/testthat/test-tweaks.R
index 302dc89..813c8b7 100644
--- a/tests/testthat/test-tweaks.R
+++ b/tests/testthat/test-tweaks.R
@@ -1,6 +1,6 @@
context("tweaks")
-test_that("multiplication works", {
+test_that("tweaks work as expected", {
expect_true(names(formals(tweaks))[[1]] == "...")
expect_warning(tweaks(`_oooops` = 42, `_darn` = -1), "_oooops, _darn")
expect_warning(tweaks(), NA)
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/r-cran-dbitest.git
More information about the debian-med-commit
mailing list