Version 2026.8.6
Licensing
- The copyright holder is now Folkehelseinstituttet. It read “Core Surveillance”, which names the package family rather than a legal entity.
-
DESCRIPTIONAuthors@Rnow declares that holder withrole = "cph". It declared no copyright holder at all, and neither did any other package in the fleet. Nothing inR CMD checkreports that. - The copyright year is now 2026. It read 2023.
-
CLAUDE.mdnow carries a Licensing section, so the year gets checked rather than silently ageing.
Documentation
Repository prose now follows ASD-STE100 (Simplified Technical English). The sweep covered the roxygen2 blocks in
R/, both vignettes, andREADME.md. It changed no claim and no executable code.index.mdneeded no change.No roxygen sentence runs over 25 words. Counted per authored unit, which is one
@description,@param,@return,@seealso, paragraph or Rd\item, the count fell from 6 to 0. The longest sentence fell from 36 words to 24.Roxygen fields and
\itemizeitems now end in a full stop. Without one, a sentence splitter runs straight through the field boundary and reports a merge as one sentence. That is where the 74-word to 97-word readings came from, in blocks whose longest authored sentence was under 25 words.vignettes/csdb.Rmd,vignettes/backends.RmdandREADME.mdare also at zero sentences over 25 words. The counts before were 3, 1 and 2.vignettes/csdb.Rmdandvignettes/csdb.Rmd.origcarry identical prose edits, so the generated file and its source stay in sync.vignettes/_PRECOMPILER.Rwas not re-run, and no chunk output changed.The v3 statement in the introduction vignette keeps its size.
csdbCAN store acsfmt_rts_data_v3, with the_blankpair or with a validator of your own. What is missing is a validator that knows the v3 shape.Ornamental adjectives are gone from the reference pages: “robust” from
DBConnection_v9, “sophisticated” and “comprehensive” fromDBTable_v9, and “comprehensive” fromget_table_names_and_info(). TheDBTable_v9title is now “R6 Class representing a database table”, parallel withDBConnection_v9.The introduction vignette opens with prose instead of with code output. pkgdown promotes
vignettes/csdb.Rmdto “Get started”, and the first thing on that page was thedata.tableattach message:Attaching package: 'data.table'and the%notin%masking line. A new “What csdb is for” section now comes first. It says what the package does, splitsDBConnection_v9(the connection) fromDBTable_v9(one table), tabulates the three backends, names the missing v3 validator, and says wherecsdbsits in the stack. Its two chunks run without a database server.The
library(data.table)andlibrary(magrittr)chunk is nowmessage = FALSE. No chunk in the vignette uses%>%or baredata.tablesyntax, so the two attach messages announced masking that nothing below them relied on. Thelibrary()calls themselves are unchanged.The overview states a current limitation plainly.
csdbexports field-type and field-contents validators forcsfmt_rts_data_v1andcsfmt_rts_data_v2and none forcsfmt_rts_data_v3;grep("v3", getNamespaceExports("csdb"))returnscharacter(0). That matters now rather than later, becausecstidymarks v1 and v2 deprecated in favour ofset_csfmt_rts_data_v3(), andcsalert’s pipeline ends inens_collapse(heal = TRUE), which returns acsfmt_rts_data_v3. A v3 result can still be written, with the blank validators or with a function of the user’s own, but nothing then checks its columns.The overview shows
is_connected()returningFALSEimmediately afterDBConnection_v9$new()and again afterDBTable_v9$new(). The second call uses a PostgreSQL configuration naming a server that is not running, which is the strongest form of the claim: neither constructor opens a connection.vignettes/csdb.Rmdwas regenerated fromvignettes/csdb.Rmd.origbyvignettes/_PRECOMPILER.R. Apart from the new section and the two suppressed attach messages, the only change in it is thetempfile()path, which differs on every run.
Bug Fixes
-
DBTable_v9$connect()was documented as “Connect from the database”. It connects to the database, which is whatDBConnection_v9$connect()already said. -
DBTable_v9$drop_indexes()was documented as “Drops all indees from the database table”. The word is “indexes”. -
DBTable_v9$insert_data()carried a stray prose line after@param verbose, so roxygen2 rendered theverboseargument as “Boolean. Inserts data into the database table”. That sentence is now the method’s@description, andverbosereads “Boolean.”
Version 2026.8.5
New Features
SQLite is a third backend.
driver = "SQLite"withdbset to a file path connects throughRSQLite, which is now inImports. The driver string is matched case-insensitively, sosqlite,SQLiteandSQLITEall select it; the two ODBC driver strings keep exact matching, because they must equal anodbcinst.inientry.DBConnection_v9creates the parent directory ofdbif it does not exist, then opens the file withextended_types = TRUE. That argument is required, not cosmetic: without it aDATEcolumn reads back as the integer18262rather than aDate, andvalidator_field_contents_csfmt_rts_data_v1()rejects it. NoUSE <db>;is issued, because the file is already the database.DBConnection_v9$print()shows the driver and the file path for SQLite, and omits server, port, user, password, SSL mode and trusted connection, none of which SQLite reads.DBTable_v9identifiers under SQLite are the bare table name:DBI::Id(table = <table_name>)and the plain string.schemais ignored entirely, because SQLite has no schemas.create_table()on SQLite inlines the primary key in theCREATE TABLEstatement and marks every key columnNOT NULL.add_constraint()is therefore a no-op there. SQLite has noALTER TABLE ... ADD CONSTRAINT ... PRIMARY KEY; the statement the other backends use is a syntax error.The SQLite field-type map is closed:
TEXT,INTEGER,DOUBLE,BOOLEAN,DATEandDATETIMEare accepted and anything else is an error naming the column and the type. SQLite accepts any declared type name, soVARCHAR(100),TEXT(100)or a misspelling would otherwise create a table with an unintended affinity and no warning.insert_data()on SQLite writes throughDBI::dbAppendTable(). There is no staging CSV and no external client binary: SQLite is a file, anddbAppendTable()writes 100,000 rows in about 0.02 seconds. Thefileargument is accepted and ignored.insert_data()on SQLite copies its argument before writing, so the caller’sdata.tableis not modified by reference. The three other backends reachwrite_data_infile(), which has always modified it in place.upsert_data()on SQLite stages the rows in a temporary table and then issuesINSERT ... ON CONFLICT (<keys>) DO UPDATE SET, falling back toDO NOTHINGwhen every field is a key. SQLite has neitherMERGEnorON DUPLICATE KEY UPDATE. Three preconditions are checked before any SQL is emitted, because each fails late and obscurely otherwise:keysmust be non-empty, or the statement isON CONFLICT (); every key must be one of the fields; andfieldsmust be exactly the table’s live columns, becauseCREATE TABLE ... AS SELECTdiscards defaults and a partial field list would insert NULL into every omitted column.drop_all_rows()is now an S7 generic. SQL Server and PostgreSQL keep theTRUNCATE TABLEstatement they always received, unchanged; SQLite getsDELETE FROM <table>, becauseTRUNCATE TABLEis a syntax error there.DELETEleaves the primary key and every index intact, which matters because the SQLiteadd_constraint()cannot put a dropped primary key back.keep_rows_where()on SQLite emitsDELETE FROM <table> WHERE (<condition>) IS NOT TRUE, notNOT (<condition>). The two are not the same statement:DELETEremoves only rows whose predicate evaluates to TRUE, and the negation of NULL is NULL, so a plain negation silently retains every row on which the condition is NULL, althoughSELECT ... WHERE <condition>would not have kept it.IS NOT TRUEfolds NULL into FALSE and gives the exact complement. It is also aDELETErather than the drop-and-rename the other two backends use, for the same primary-key reason asdrop_all_rows().drop_rows_where()on SQLite emitsDELETE FROM <table> WHERE <condition>.add_indexes()on SQLite emitsCREATE INDEX IF NOT EXISTS <index> ON <table> (<keys>), with the table name unqualified. SQLite lets the index name carry a schema but never the table:CREATE INDEX ind ON main.tab (a)isnear ".": syntax error.drop_indexes()emitsDROP INDEX IF EXISTS <index>, which names the index alone, because a SQLite index belongs to the schema rather than to the table.confirm_indexes()on SQLite now executes no DDL when the indexes already match.get_indexes()excludes SQLite’s own index names and orders byrowid. Both are required: aPRIMARY KEYauto-createssqlite_autoindex_<table>_1, which is a row insqlite_masterexactly like a user index, androwidorder is creation order, which is the orderadd_indexes()works in.confirm_indexes()compares withidentical(), so an extra name or a different order would drop and re-add every index on every call. The return value is a plain character vector for the same reason.get_table_names_and_info()has a method forSQLiteConnection.nrowisCOUNT(*), which is exact, unlike thereltuplesestimate PostgreSQL reports and thesp_spaceusedfigure SQL Server reports. All three size columns areNA_real_: thedbstatvirtual table is not compiled into the SQLite thatRSQLiteships, so there is no per-table size to report, andpragma page_countdescribes the whole file. An empty database returns a zero-row table that still has all five columns.Both SQLite catalogue filters write the exclusion as
name NOT LIKE 'sqlite\_%' ESCAPE '\', escaping the underscore._is a single-character wildcard in SQLLIKE, so the unescaped'sqlite_%'hides every name beginning “sqlite” followed by any character at all, not only SQLite’s own objects. A user index namedsqliteIdxwould never be found byget_indexes(), andconfirm_indexes()would drop and re-add it on every call; a user table namedsqliteFoowould be missing fromget_table_names_and_info(), soDBTable_v9$nrow(use_count = FALSE)andDBTable_v9$info()would report nothing for it.
Known limitations
-
confirm_indexes()compares index names only. An index with the right name and the wrong columns passes. This is the existing behaviour of all three backends and SQLite matches it.
Documentation
- The introduction vignette now runs on SQLite, in a file created by
tempfile(). It is precompiled fromvignettes/csdb.Rmd.orig, and that precompilation used to need a live PostgreSQL database.knitr::knit()defaults toerror = TRUE, so on a machine without one it did not fail: it exited 0 and wrote seven#> Errortranscripts into the committedvignettes/csdb.Rmd, including aCould not connect to database server ''. Anyone can now rebuild the vignette and get the same output. - Added
vignettes/backends.Rmd, which puts a PostgreSQLdbconfigand a SQLitedbconfigside by side, runs oneDBTable_v9$new()definition against each, and tabulates what a user must know:schemais ignored, the primary key is inlined atCREATE TABLEand cannot be added later, an unrecognised field type is rejected rather than passed through,get_table_names_and_info()reports an exactCOUNT(*)andNAsizes, and no external client binary is needed. No chunk in it executes. -
README.md’s quick start is now the SQLite one, so it runs on a bare machine, and it links to both vignettes. The$keep_rows_where()caution is qualified: the copy, drop and rename it describes is the ODBC path, not the SQLite one. -
index.mdand the_pkgdown.ymlhero lede both name SQLite alongside PostgreSQL and SQL Server.
Development
- Added
tests/testthat/test-sqlite-connection.R, the first tests in the package that open a database connection. SQLite is a file, so they need no server. - Added
tests/testthat/test-sqlite-indexes.R. The block that provesconfirm_indexes()emits no DDL readsPRAGMA schema_versionbefore and after, not the index names: the names are identical whether the call did nothing or dropped and recreated every index, andschema_versionincrements on every schema change. A separate block creates an index namedsqliteBarand a table namedsqliteFooand asserts both are visible, which is what pins theESCAPEclause on the two catalogue filters. - Added
tests/testthat/test-sqlite-data.R, covering the five write and delete paths: type round-trip, the non-finite scrub, the caller’s data.table being left alone, upsert update-not-duplicate, the three upsert preconditions, the NULL-condition row,drop_all_rows()leaving the indexes, and identifiers that need quoting. - The
Inf/NaNtoNAloop moved out ofwrite_data_infile()into an internalscrub_non_finite(), called from there and from the SQLite write path.InfsurvivesDBI::dbAppendTable()and reads back asInf, so without it SQLite would silently disagree with the two backends that writeNA. ThePOSIXtto character conversion is not shared: SQLite needs aPOSIXctto stay one, so thatextended_types = TRUEround-trips it through aDATETIMEcolumn. -
dbplyris inImports. It always was a hard requirement and was never declared:DBTable_v9$tbl()callsdplyr::tbl()on a DBI connection, which dispatches todplyr:::tbl.DBIConnection()and stops incheck_dbplyr()when dbplyr is absent. Three documented methods go through it,tbl(),print_dplyr_select()andnrow(use_count = TRUE), andtbl()is the only read path the package offers, so a csdb without dbplyr is write-only. The gap never surfaced because nothing in csdb calledtbl()until the SQLite tests did; on a library without dbplyr those seven blocks error and the other 108 assertions pass.Suggestswas rejected on measurement: dbplyr adds three packages to anImportsclosure of 42, and the alternative is to make the package’s only read path optional. No csdb code names dbplyr, sofix_dbplyr()inR/xxx_small_import_fix.Rholds adbplyr::reference for the same reasonfix_r6()andfix_s7()hold theirs: without itR CMD checkreports “All declared Imports should be used”. -
RSQLiteis inImportsand has no S3 fallback inget_db_classes(), which stops with a message naming RSQLite if the real S4SQLiteConnectionclass is absent. AS7::new_S3_class()fallback would be worse than useless: with the real S4DBIConnectiondefault present, methods registered against the fallback lose dispatch silently and run the MySQL-flavoureddb_defaultSQL, and registering the real class later does not retarget them. - Documentation is generated by roxygen2 8.0.0.
DESCRIPTIONnow declaresConfig/roxygen2/versionin place ofRoxygenNote, and every.Rdfile was regenerated by that version.NAMESPACEis unchanged.
Version 2026.8.4
Documentation
-
README.mdnow carries what the package is, installation, one quick start, and a table that routes a task to the function that does it. It also states two things the API does not do:create_table()drops and rebuilds a table whose columns differ fromnames(field_types), and no method opens a transaction. - All 11 exported functions gained a
@seealsothat says whether the introduction vignette demonstrates them. Four appear in a vignette code chunk (DBConnection_v9,DBTable_v9,validator_field_types_blank,validator_field_contents_blank); the other seven appear nowhere in the vignette, and their@seealsosays so. - Added three
@familygroups: auth hook functions (both address thecsdb.auth_hookoption, one writing it and one reading it), field type validators (onedb_field_typesargument, checked once insideDBTable_v9$new()), and field contents validators (onedataargument, called frominsert_data()andupsert_data()).DBConnection_v9andDBTable_v9are grouped as database classes:DBTable_v9$new()takes adbconfiglist of exactly the 10 argumentsDBConnection_v9$new()accepts, and builds one.
Bug Fixes
-
get_table_names_and_info(): the documented PostgreSQL example connected throughRPostgres::Postgres(). Those connections are of classPqConnection, and the generic has methods forPostgreSQLandMicrosoft SQL Serveronly, so that example cannot dispatch; it errors with “no applicable method”. It now connects through thePostgreSQL UnicodeODBC driver, which is the class the methods are written for.RPostgreswas also absent fromImportsandSuggests. -
get_table_names_and_info(): thenrowcolumn was documented as the number of rows. It isreltuplesfrompg_classon PostgreSQL, which is an estimate, and therowscolumn ofsp_spaceusedon Microsoft SQL Server. Documented as reported, not as exact. -
DBConnection_v9: the documented PostgreSQL example useddriver = "PostgreSQL". Only"PostgreSQL Unicode"selects a PostgreSQL branch in the connection code, so"PostgreSQL"falls through to the generic branch, which does not passdatabase, and is then followed byUSE <db>;. Changed to"PostgreSQL Unicode". -
validator_field_types_csfmt_rts_data_v2(): the example vector labelled “Valid field types” returnedFALSE, because it omittedisoquarterandisoyearquarter, which the v2 schema holds at positions 11 and 12. The example now returnsTRUE, and a second call shows the v1 layout returningFALSE. -
DBTable_v9: the documented example called$add_indexes(c("name", "date_created")), but that method takes no arguments and readsself$indexes. Indexes are now declared in the constructor. The same example passeddata.frames to$insert_data()and$upsert_data(), both of which reachdata.tablesyntax ([ , (col) := ],with = FALSE) and require adata.table. Changed todata.table::data.table().
Development
-
csdb_set_auth_hook(),DBConnection_v9andDBTable_v9gained runnable examples for the parts that need no database server: setting and restoring the hook, and creating an object without connecting. Their\dontrun{}blocks keep the parts that need a server. - Added
^Rplots\.pdf$to.Rbuildignore.
Version 2026.5.13
CRAN release: 2026-05-13
Bug Fixes
-
DBTable_v9$nrow(use_count = TRUE)now callsdplyr::n()instead of a baren(). This is hygiene only: the bare call sits inside the list passed toR6::R6Class(), whichcodetoolsnever walks, so it produced noR CMD checkcomplaint, anddbplyrrenders both spellings to identical SQL. - PostgreSQL methods (
create_table,keep_rows_where,drop_table) now quoterole_create_tableviaDBI::dbQuoteIdentifier()when emittingSET ROLE. Previously the role name was interpolated raw, which broke on identifiers containing hyphens, mixed case, or reserved words (e.g.SET ROLE token-user-> syntax error), and was a SQL-injection vector if the value came from an env var.
Version 2026.2.2
CRAN release: 2026-03-31
New Features
- Added authentication hook system (
csdb_set_auth_hook(),csdb_get_auth_hook()) to allow automatic credential refresh (e.g., Kerberos tickets) when connection fails
Version 2025.7.19
Version 2025.7.17
- Updated package for CRAN submission with comprehensive improvements
- Added comprehensive documentation with examples for all exported functions
- Fixed critical CRAN compliance issues including system tool availability checks
- Added proper R6 class documentation with detailed usage examples
- Improved all validator function documentation with clear examples
- Added missing dependencies and fixed import declarations
- Updated .Rbuildignore to exclude system files and build artifacts
- Added CLAUDE.md for future development guidance
- Fixed vignette title and improved documentation quality
- All functions now pass R CMD check with only acceptable NOTEs
Version 2024.10.25
-
role_create_tableis now included for dbconnection_v9/dbtable_v9, so that the role can be changed when creating tables in PostgreSQL.
Version 2024.3.11
- Including use_count as an argument in nrow in DBTable_v9, which is slower but more accurate.
Version 2024.3.7
- Including confirm_insert_via_nrow in DBTable_v9. Checks nrow() before insert and after insert. If nrow() has not increased sufficiently, then attempt an upsert.
Version 2023.12.28
- Including validator_field_types_csfmt_rts_data_v2 and validator_field_contents_csfmt_rts_data_v2.
Version 2023.4.12
-
get_table_names_and_nrowis now changed toget_table_names_and_infoand also includes size_total_gb, size_data_gb, size_index_gb. -
infois now included as a method forDBTable_v9
Version 2023.4.4
-
confirm_indexesis now added toDBTable_v9, which confirms that the names and number of indexes in the database are the same as in the R code. It does not confirm the contents of the indexes! -
nrowis now added toDBTable_v9, which is an application of the newget_table_names_and_nrowfunction. -
get_table_names_and_nrowadded as an exported function, that will get all the table names and the nrows from a dbconnection.
