[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