Version 26.10.26
- On a connection that the PostgreSQL server ended, a keyed run sends no advisory unlock and no
RESET ROLE, and stops with an R error. Before, the unlock crashed R with a segfault in odbc, exit code 139, as in a CNPG primary failover, and R skipped every cleanup. cs9 needs csdb 2026.10.23.
Version 26.10.25
- On PostgreSQL a keyed run empties a staging table with
TRUNCATEwhen no row of it must stay. It does so also before it addscs9_stg_idto an old staging table. Otherwise the fence deletes the rows of dead runs in transactions of at most 200,000 rows. Before, a dead run of about 25 million rows meant two transactions of that size at the next run.
Version 26.10.24
- On PostgreSQL a keyed run publishes in transactions of at most 200,000 staged rows, in the order of the new staging column
cs9_stg_id. On 2026-10-16 PostgreSQL went down twice during a publish of about 25 million rows in one transaction. A recorded delete runs in one transaction per partition, anddrop_all_rows()becomes aTRUNCATE. A reader can see a partly published run. After a publish that stopped part-way, a retry with the samerun_keychecks the plan list, runs no plan and publishes the rest. A run of the task with a newrun_keyfirst publishes the rest of that run. A staging table from before this version getscs9_stg_idin place and keeps its rows. SQLite publishes in one transaction, as before. cs9 needs csdb 2026.10.18.
Version 26.10.23
- The “Publishing run” message carries the clock time, and a new message “Published in N mins” follows the publish. On Airflow the time stamp of a log line is when the log reader got it. On 2026-10-16 the reader passed “Publishing” on about 5 minutes after R printed it, so the publish looked shorter and a gap looked like idle time.
Version 26.10.22
- cs9 no longer reads
CS9_DBCONFIG_PG_LOAD, and it gives csdb nopg_load. csdb 2026.10.17, which cs9 now needs, sends every PostgreSQL load as JSON over the open connection.
Version 26.10.21
-
CS9_DBCONFIG_PG_LOADsetspg_loadin every database configuration and every csdb connection that cs9 builds. Unset or empty ispsql. Withjson, csdb sends the rows of a load over the open connection, and a load makes nopsqllogin. This needs csdb 2026.10.16. - Any other value stops
library(cs9)andreload_db_config()with an error that names the variable and the allowed values.check_environment_setup()shows the value. Other drivers ignore it.
Version 26.10.20
- In a parallel run,
Task$run()forks its workers once:coresworkers, or one per middle plan when there are fewer. Each worker runs middle plans until none is left. Before, each middle plan had its own fork. - The workers take the plans from a queue file that filelock guards, so cs9 imports filelock.
- A worker keeps its database connections from one plan to the next, and closes them when it ends. A plan whose worker dies counts as a failed plan. Before, the run ignored it.
- The run connection of a keyed run stays open in the main process across the fork, because it holds the lock of the task. A worker never uses it and never closes it.
- cs9 writes its config tables with one
INSERTstatement over the connection of the table, on PostgreSQL and SQLite. Before, each write on PostgreSQL ran psql, which is one more login. - An upsert into a config table is
INSERT ... ON CONFLICTon its keys. It needs the primary key that csdb creates with the table. - In a worker, the data hash bookkeeping keeps the connection of
config_data_hash_for_each_planopen from one plan to the next. Outside a worker each call closes it, as before. -
Task$run()sets the csdb context to<task>|parent, and each plan sets<task>|<plan>. PostgreSQL shows it as theapplication_nameof each connection. An open connection gets the name by the rule of csdb: the charactersA-Z a-z 0-9 _ | . : -, at most 63. This needs csdb 2026.10.15. - On PostgreSQL 18, a keyed run with 40 plans on 4 cores made 57 logins, 40 of them by psql. Before, the same run made 205 logins, 82 of them by psql.
Version 26.10.19
- A run with a
run_keykeeps the date of its first attempt also when its task has no tables, or only config tables. This holds on PostgreSQL and SQLite.run_today()andargset$todaygive that date on a retry with the key. Before, a retry on a later day got the date of the retry. - Such a run keeps its date in
cs9_runin the config database. It takes no lock and stages nothing, and each attempt runs every plan. - With another driver for the config database, such a run stores no date, and
run_today()reads the clock, as before. - The config tables and the task’s tables can share one database and schema. A staged run of the same task name with another key then removes that date. A retry of the key without tables gets the date of the retry.
Version 26.10.18
- In a run with a
run_key, the publish sends oneDELETEper live table for each 500 operations. It joins their conditions byOR. Before, it sent oneDELETEper operation and live table. - An operation that empties the table publishes as one
DELETEwithout a condition. - A condition of
drop_rows_where()orkeep_rows_where()MUST test only the row itself, with no subquery on the same table. In a keyed run every condition reads the live table as it stood before the publish. - The staging table has an index on
cs9_taskandcs9_run_key, then the partition column of a partitioned table. A keyed run adds it to a staging table that exists when the run begins. The index is verified on SQLite and PostgreSQL, not on SQL Server. - On 2026-10-14 a run of
norsyss_data_import_consultationsrecorded 705 operations on a table of 106 partitions. Its publish ran for over 45 minutes.
Version 26.10.17
- A config table is one of cs9’s own config tables, the objects in
config$tables. Before, cs9 counted every table in the database and schema of the config access as a config table. - In production the config and anon accesses share the database and the schema. There a run with a
run_keystaged nothing and wrote its tables directly. - A run with a
run_keywhose task has tables but stages none of them stops with an error. A task whose every table is a config table, and a task without tables, still run unstaged.
Version 26.10.16
-
run_task_detached(),run_task_sequentially_as_callr_bg_using_load_all(),run_task_sequentially_as_rstudio_job_using_load_all()andTaskJob$new()takerun_key = NULL. They pass it torun_task()in the child process, so a developer can start a staged run. - The key reaches the child as an argument: a literal in the child’s call to
run_task(), or a command-line argument of the detachedRscript.run-task.shtakes--key. -
run-task.shtakes--rscript.run_task_detached()passes theRscriptof the calling R by its full path, so the child runs the same R, also underR CMD check. - A runner checks the key before it starts the child.
Version 26.10.15
- A retry with a
run_keyruns the last plan again, like plan 1. A check in the last plan therefore runs again after a publish that failed. -
add_table(),add_partitionedtable()and the cs9 table classes refuse a table name that ends in_cs9stg. Such names are reserved for staging. -
run_today()is exported. It returns the date of the first attempt of the run with arun_keyin progress, or today’s date outside such a run.
Version 26.10.14
- A run started with a
run_key, which a scheduler can pass, stages its writes to the task’s own tables. It publishes them in one transaction after the last plan. A failed run leaves the task’s tables unchanged. - An interactive run writes directly, as before.
run_task()andTask$run()takerun_key = NULLby default, and a run without a key touches no staging table. - A second attempt with the same
run_keyskips each plan that the first attempt finished, except plan 1. -
Task$run()with arun_keybuilds the plans again on every attempt, asrun_task()does. - During a run with a
run_key,tbl()andnrow()read the published table.staged_tbl()reads the rows that the run staged. - Such a run stops when two plans write one row, or when a plan deletes rows that another plan wrote.
- A retry with a
run_keystops before any plan runs when its plans differ from the first attempt. The order of the plans counts. - A later attempt of a published
run_keydoes nothing, also after a run with another key. - A run with a
run_keythat cannot stage its writes stops with an error, before any write. Two tables with one name are such a case, and so is a table that cs9 cannot stage. A table of the config access is never staged. - A plan analysis that calls
staged_tbl()stops the run, because plans MAY read published tables only. - In a run with a
run_key,argset$todayis the date of the first attempt. -
config_tables_last_updatedgets one row per changed table, once per run with arun_key, at the publish. - On PostgreSQL a second run of a task with a
run_keystops while the first is alive. Each such run holds one more connection, for its lock and its publish. - A staged table gets a staging table,
<table>_cs9stg. The tablescs9_run,cs9_run_plan_list,cs9_run_plan_doneandcs9_run_operationhold the state of each run. -
vignette("rebuilding-a-table-safely")shows what a reader sees during a run, a failed run and its retry.
Version 26.10.13
- The
insert_data()method of a partitioned table loads every partition with one call tocsdb::insert_data_many(). On PostgreSQL an insert needs onepsqllogin for the load, plus one for the last-updated bookkeeping, not one for each partition. With SQLite or SQL Server, csdb still loads one partition at a time. - On PostgreSQL, by default (
confirm_insert_via_nrow = FALSE), an insert that fails stores no row in any partition. The exception is an error that says the connection broke, or that a row count differed afterpsqlfinished. The rows may then be stored, and the error says to count them. Withconfirm_insert_via_nrow = TRUE, a duplicate key makes csdb insert partition by partition, so a failure can leave some partitions stored. - When the last-updated bookkeeping fails after the rows are stored,
insert_data()gives a warning that names the partitions and the error, and returns normally. - A partitioned
insert_data()that fails writes no row of its own toconfig_tables_last_updated. Before, it recorded the partitions that it loaded before the failure. Withconfirm_insert_via_nrow = TRUE, or with SQLite or SQL Server, each partition that loaded before the failure still records itself. -
vignette("how-a-task-runs")says what a failed insert into a partitioned table stores, and when to count the rows. - cs9 needs csdb 2026.10.13.
Version 26.10.12
-
Task$run()runs each parallel plan once. Before, a failing plan ran up to 5 times, and only the error of the last try reached the log. The scheduler, for example Airflow, retries the whole task. -
Task$run()setsconfig$plan_attempt_indexto 1 for every plan. It stays for callers that read it. - When one parallel plan fails, the other parallel plans still run. Before, a failure skipped the plans that
pbmcapply::pbmclapply()had scheduled on the same core.Task$run()then stops with one error that counts the failed plans and holds the message of each, in plan order. Before, it held the message of the first failed plan only.
Version 26.10.8
-
check_environment_setup()no longer requiresCS9_DBCONFIG_PASSWORDforPostgreSQL Unicode, so CS9 loads when a hook fromcsdb::csdb_set_password_hook()supplies the password. An unset password is still an empty string. -
vignette("backends")andvignette("installation")say whenCS9_DBCONFIG_PASSWORDis optional.
Version 26.10.7
-
add_task()acceptscoresas a function with no arguments.Task$run()calls it each time the task runs, so a cap that an environment variable sets after the package loads takes effect. A number works as before. -
TaskJobandrun_task_sequentially_as_rstudio_job_using_load_all()still run a task on 1 core when itscoresis a function. -
run_task()builds the plans of afor_each_plantask again on every call, soargset$todayandargset$yesterdayare the date of the run. Before, they were the date when the package loaded. -
get_task()and theshortcut_get_*()methods still show the plans of the last build, with the date of that build. -
vignette("how-a-task-runs")shows a task that reads its cores from an environment variable that changes afteradd_task(). - The section
#rule-plans-per-runofvignette("how-a-task-runs")states when a task builds its plans, and how to run plans that you edit by hand. -
vignette("task-shapes")holds all seven task shapes. The sections of the removedvignette("more-task-shapes")keep their heading ids there. -
vignette("task-shapes")has a diagram for each shape, andvignette("how-a-task-runs")has a diagram of one run. Each diagram comes from a run that the vignette records and checks when it builds. ggplot2 is in Suggests for these diagrams.
Version 26.10.6
- The partitioned
insert_data(),upsert_data(),drop_all_rows_and_then_insert_data()anddrop_all_rows_and_then_upsert_data()write theirconfig_tables_last_updatedrows in one upsert. The rows are the same: one per completed partition. - After an error in one partition, that upsert includes the partitions that completed before the error. The error still reaches the caller.
- Each
DBTableExtended_v9method writes at most oneconfig_tables_last_updatedrow. Before,drop_all_rows_and_then_upsert_data()wrote up to 3 rows for one table, and aninsert_data()that fell back to an upsert wrote 2. - The
get_config_log()help page says thatstart_dateandend_datefilter thedatecolumn. It named atimestampfield that does not exist.
Version 26.10.5
-
get_config_log()filters byssand bytask. Before, both filters stopped withfirst argument has length > 1. -
update_config_tasks_stats()stores the start and stop datetimes with milliseconds.update_config_data_hash_for_each_plan()stores its datetime with milliseconds. So two runs in one second keep two rows. The table schema does not change. - Two rows in the same millisecond still collide. SQL Server
DATETIMErounds to about 3 ms. - The partitioned
drop_all_rows(),drop_rows_where()andkeep_rows_where()write theirconfig_tables_last_updatedrows in one upsert. After an error, that upsert includes the partitions that completed before the error. - The partitioned insert and upsert methods still write one
config_tables_last_updatedrow per partition, in separate writes. - The lint gate passes. The allowlist exempts 4 files from
cyclocomp_linter:R/2_onLoad.R,R/r6_DBPartitionedTableExtended_v9.R,R/r6_Task.RandR/r6_TaskJob.R. The complexity check skips all code in those files, which includes the whole R6 classesDBPartitionedTableExtended_v9,TaskandTaskJob. - Still open from 26.10.3: the local time repeats at the autumn daylight-saving change, and two R processes can collide in the same millisecond.
Version 26.10.4
-
Task$run()reads and stores the data hash of the last plan under its ownindex_plan. With 4 or more plans,cores > 1and a non-interactive session, it usedindex_plan1, the index of the first plan. - The four load methods of
DBTableExtended_v9acceptload_timeoutand pass it to csdb. csdb 2026.10.4 callsinsert_data()andupsert_data()with this argument, which stopped withunused argument. cs9 now needs csdb >= 2026.10.4. -
vignette("task-shapes")shows two task shapes that publish a run only when the whole run succeeds. Both shapes run against SQLite when the vignette builds.
Version 26.10.3
-
update_config_log()writesdatetimein milliseconds, at least 4 ms after its previous row in the R process. Two runs of one task in the same second failed on SQLite withUNIQUE constraint failed. The table schema does not change. Two separate R processes can still collide in the same millisecond, and the local time repeats at the autumn daylight-saving change, as it did before. Verified on PostgreSQL 16 and SQL Server 2022, wheredatetimerounds to 1/300 s and the 4 ms step keeps rows distinct. -
run_task()builds the plans of a task with a plan analysis on every call. A secondrun_task()in one session reused the plans of the first. Theshortcut_get_*()methods still reuse the plans of the last build.
Version 26.8.23
-
vignettes/creating-a-task.Rmd.origno longer carries an emptyif (requireNamespace("magrittr", ...)) { }block. Droppinglibrary(magrittr)left the guard behind with nothing in it. The.origfile ships in the tarball and_PRECOMPILER.Rregenerates the vignette from it.
Version 26.8.22
The package drops
magrittr. Every%>%is now the base pipe|>, andmagrittris gone fromDESCRIPTION.The rewrite is a relocation, not an edit. Each
%>%call was transformed the way R’s parser transforms|>, and the resulting tree was required to match the tree parsed from the rewritten file. A file whose trees disagreed was left untouched and converted by hand instead.%>%is no longer exported.R/x_rexport.Rexisted only to re-export it and is deleted, and the “Imported Functions” section of_pkgdown.ymlgoes with it. Code that relied onlibrary(cs9)for the pipe must use|>or attachmagrittritself.vignettes/creating-a-task.Rmd.origandvignettes/file-layout.Rmd.origare converted too._PRECOMPILER.Rregenerates the shipped.Rmdfrom them, so converting only the.Rmdwould have been undone on the next precompile.The version runs one day ahead of the calendar on purpose. r-universe already published today’s number from a different commit, and one version must never name two trees.
Version 26.8.21
-
R/zzz_imports.Rnamescallr,csutil,later,pbmcapplyandprogressin one plain function, which clears the declared-but-unused note.R CMD checkdoes not walk thepublic = list(...)of anR6Class, so apkg::fn()call inside an R6 method is invisible to its dependency scan. - All five were real.
progresslooked dead, because it is never written asprogress::anywhere in this package. It is required at load time all the same:.onLoadcallsprogressr::handler_progress(), and that function’s body callsprogress::progress_bar. Removing it made the package fail to install. - The version goes to 26.8.21 rather than reusing 26.8.20, which r-universe has already published from a different tree. One version number must not name two.
Version 26.8.20
New Features
-
run_task_detached()andinst/scripts/run-task.shrun one task in a process that outlives the R session which started it. This is the non-interactive counterpart torun_task_sequentially_as_callr_bg_using_load_all(). Use that one in Positron, where a person watches a live console, and this one when nobody is watching: a script, a cron entry, or an agent driving a machine over SSH. The two are not interchangeable.TaskJobpassessupervise = TRUE, socallrkills the task when the parent R process exits, and it streams output throughlater::later(), which needs an event loop thatRscriptdoes not drive. - The script writes the task’s exit code to a status file, and writes it only when the task ends. Wait on that file, never on the log. A log cannot separate “finished cleanly” from “died at line 4”, so a caller that reads the tail of a log is guessing. The absence of the file means the task is still running, never that it is done. If the process is killed hard, no status is written at all, and the lock file still holds the pid, so
kill -0separates “running” from “died without reporting”. - The script refuses to start a task that is already running from the same package, and exits 3. Two concurrent runs of one import write the same rows twice.
- The log opens with the task name, the implementation package and its version, the git branch and commit of the package directory when it is a repository, and whether that tree was clean. A log that reports a duration alone cannot answer which code produced it, and an implementation package copied out of its checkout has no commit to read afterwards.
- The script takes the package directory and the surveillance system expression as options, so it serves any cs9 implementation package. It defaults to the working directory and to
global$ss. - The runner counts error lines in the log and writes the count beside the status file, because exit code 0 does not mean the work succeeded. Measured on 2026-08-17:
norsyss_data_import_consultationsexited 0 with 212 rejectedCOPYstatements, one per partition for each of two weeks whose rows already existed.load_data_infile.db_postgresrunspsqlthroughsystem2()and never reads its exit status, so a rejectedCOPYdoes not reach R at all. A status of 0 beside a non-zero error count is the shape that failure takes. The count is a grep and therefore a heuristic: a count above zero is worth reading, and a count of zero is not proof that a task did what it should.
Verified on a NorSySS prod workspace pod on 2026-08-17, all six paths: no arguments exits 2; a directory holding no DESCRIPTION exits 2 and names the reason; a task that does not exist writes exit code 1; norsyss_data_delete writes exit code 0 after 106 plans in 1.4 minutes; a second call while the first ran exited 3 and surfaced through run_task_detached() as an R error; and no lock file remained afterwards.
Version 26.8.19
Bug Fixes
-
Taskcallscsutil::unnest_dfs_within_list_of_fully_named_lists(). It calledsplutil::, andsplutilwas in neitherImportsnorSuggests. The two packages publish the same utilities under two names, and the NorSySS pods carrycsutilalone. Soupsert_at_end_of_each_plan = TRUEandinsert_at_end_of_each_plan = TRUEboth raisedthere is no package called 'splutil'on every pod, and nothing installed the package that would have fixed it. - There are four call sites, and they are in TWO methods.
run_sequential()holdsR/r6_Task.R:295and:301.run_parallel_plans()holds:351and:357. The parallel pair fails worse: the worker wraps its body in atryCatchwith five attempts and a five second sleep between them, so a missing package costs 25 seconds before it surfaces asError in index N. -
DESCRIPTIONdeclarescsutil,pbmcapplyandutils.pbmcapply::pbmclapply()atR/r6_Task.R:322is how every parallel task runs, so a machine without it could not run one.utilsships with R, so it is always installed, but the dependency was still undeclared. -
tests/testthat/test-plan-end-write.Rcovers all four sites, one test each. Neither feature had a test, which is why thesplutilcalls survived. The two parallel tests mockpbmcapply::pbmclapplyto run the worker in process, so nothing forks and the write is observable. Revertingcsutiltosplutilfails all four. The cs9 suite holds 387 passes, up from 368.
Development
-
Task$run()no longer callsfuture::plan(future::sequential)orforeach::registerDoSEQ(), andDESCRIPTIONno longer declaresfutureorforeach. cs9 never creates a future plan and never registers a parallelforeachbackend, so both lines reset a state that cs9 does not set. The two%dopar%blocks that would have needed them are commented out, atR/r6_Task.R:425and:523.data.table::setDTthreads()on the next line is a real reset and stays. -
26.8.18declaredfutureandforeachrather than removing the calls. That release is superseded on this point. -
Suggestsdeclaresdevtools.devtools::load_all('.')atR/r6_SurveillanceSystem.R:414andR/r6_TaskJob.R:72is generated text, inside aglue::glue()string that cs9 writes to a temporary.Rfile. cs9 then runs that file in a child R process, so the feature needsdevtoolsat run time. Process separation moves the dependency and does not remove it.Suggestsis the right category, because one optional feature needs it and the package does not. - No undeclared namespace call remains. See
CLAUDE.mdfor the sweep, and for the three matches it reports that are text rather than calls.
Version 26.8.18
Bug Fixes
-
DESCRIPTIONdeclaresforeachandfuture.Task$run()callsfuture::plan(future::sequential)andforeach::registerDoSEQ()atR/r6_Task.R:262and:263, on the path every successful run takes. Neither package was inImports, inSuggestsor inNAMESPACE. A machine without them installed therefore raisedthere is no package called 'future'at the end of every task. - The defect is as old as those two lines, and no test reached them.
26.8.17addedtests/testthat/test-fork-ordering.R, which drives the publicTask$run()to completion, so CI ran the line for the first time and went red. The development pods carry both packages, so every local suite stayed green.
Version 26.8.17
Development
-
tests/testthat/test-fork-ordering.Rpins the order thatTask$run()uses: it closes every database connection before it forks. Nothing asserted that order before. A refactor that moves the sweep reintroduces silent data corruption. - The test mocks
pbmcapply::pbmclapplyand drives the publicTask$run(). The mock reads every connection at the fork boundary and returns without forking. A test on the privaterun_sequential()proves that the helper disconnects. It does not prove that the public path calls the helper before it forks. - Two mutations of
R/r6_Task.Rgo red. The first deletes the disconnect sweep and fails 2 assertions. The second movesrun_parallel_plans()in front of the sequential setup and fails 3. -
test-fork-ordering.Rasserts the length of both connection vectors before it asserts that both are open.all(logical(0))is TRUE, so the open check would otherwise pass on an empty list. - No executable
cs9code changed in this release. The version moves because26.8.16is already published from a different tree.
Documentation
-
DBPartitionedTableExtended_v9documents its one shared connection. The object owns it, every child borrows it, and only the parent’sdisconnect()closes it. -
$nrow()and$info()document thepartitioncolumn that26.8.16added. It is character, it matchesnames(self$tables), and it is the last column, sotable_namestays at position 1 andnrowat position 2.
Version 26.8.16
Bug Fixes
-
DBPartitionedTableExtended_v9$nrow(collapse = FALSE)now returns apartitioncolumn. The column holds the partition tag of the table on that row.$info()returns the same column. A caller no longer has to recover the tag from the table name. - The tag was already known, and both methods discarded it. Each one loops over
self$partitions_randomizedand marks the matching row withkeep := TRUE. The loop variable IS the tag at that moment. Both methods now write it into the row. -
partitionischaracter, so it matchesnames(self$tables)and indexesself$tables[[...]]directly. - The change is additive, and
partitionis the LAST column.nrow(collapse = FALSE)returnstable_name,nrow,partition, in that order.info(collapse = FALSE)appendspartitionafter the columns that csdb returns, and moves nothing else. An existing caller that reads column 1 or column 2 by position therefore reads what it read before.table_nameandnrowalso keep their names, their types and their values.nrow(collapse = TRUE)still returns one number.info(collapse = TRUE)aggregates over every partition, so it has no single tag and returns nopartitioncolumn. - This removes a defect in every caller that parses a table name for the tag. The separator is not one string.
DBPartitionedTableExtended_v9$initialize()writesxxpxxfor PostgreSQL and for SQLite, andPARTITIONfor the MSSQL fallback.norsyss.cs9split the name on_PARTITION_to find the partition with the fewest rows. Production is PostgreSQL, so the split returnedNA,self$tables[[NA]]returnedNULL, and the consultations import died onattempt to apply non-function. That task therefore never ran against PostgreSQL. Measured on a NorSySS pod on 2026-08-14. - Both methods declare
partitionbefore the loop, besidekeep, so the column ischaracterfrom the start. The declaration is not what creates the column. A:=whoseimatches no row still creates it, and gives it the type of the assigned value. Measured with data.table 1.18.4 on 2026-08-14.
Development
-
tests/testthat/test-partition-safety.Rpins the new column. It asserts set equality againstnames(pt$tables), becausepartitions_randomizedshuffles the row order. It also asserts thatpt$tables[[row$partition]]$table_nameequalsrow$table_namefor every row. That second assertion is the one that catches the original defect. No test names a separator, because the point of the change is that a caller does not know one. - Three further tests in the same file pin the compatibility contract. One asserts that
table_nameis column 1 ofnrow(collapse = FALSE)and thatnrowis column 2, by position and by type. One asserts thatinfo(collapse = TRUE)returns its four aggregate columns and nopartition. One asserts thatinfo(collapse = FALSE)appendspartitionlast, against the base columns read from csdb itself.
Version 26.8.15
Bug Fixes
-
DESCRIPTIONrequirescsdb (>= 2026.8.15). That is the release whereDBConnection_v9refuses a connection another process opened.Task$run_parallel_plans()forks withpbmcapply::pbmclapplyand passes the table objects into the workers, and every partition of a partitioned table now shares one connection. A forked child that used the parent’s connection received wrong results and no error. -
csdb 2026.8.14carries the shared connection without that guard, so this floor names2026.8.15rather than the release it would otherwise pair with.
Development
- No
cs9source file changed in this release. The version moves so the raised floor reaches a distinct tree, because26.8.14is already published. -
Task$run()closes every connection inrun_sequential()before it forks, so production never reached the corrupt state. Nothing asserts that ordering.csdb 2026.8.15removes the dependency on it.
Version 26.8.14
Bug Fixes
-
DBPartitionedTableExtended_v9now opens one database connection for the whole table, not one per partition. It builds a singlecsdb::DBConnection_v9before the partition loop and passes it to everyDBTableExtended_v9. A table with 106 partitions needed 106 simultaneous connections before this change. That exceeded the NorSySS PostgreSQL limit of 100 connections on 2026-08-13. The import died withFATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute. The failure did not depend on the run size, because the partition count exceeds the budget before the first row is inserted. -
DBPartitionedTableExtended_v9$disconnect()now closes the shared connection once, at the parent. It looped over the partitions and closed each child before. Each child borrows the connection, so a child’sdisconnect()is a no-op incsdb2026.8.14. -
DBTableExtended_v9$initialize()gainsdbconnectionas its eighth and last argument. It forwards that argument tocsdb::DBTable_v9in the eighth position. The argument defaults toNULL, which keeps the previous behaviour: the object builds and owns its own connection.SurveillanceSystem_v9$add_table()names every argument it passes and passes nodbconnection, so it is unaffected. -
DESCRIPTIONrequirescsdb (>= 2026.8.14). That is the version whereDBTable_v9$initialize()acceptsdbconnection. Without this version floor, the incompatibility would fail at run time rather than at install time. -
DBPartitionedTableExtended_v9$drop_all_rows_and_then_upsert_data()now sends each row ofnewdatato the partition that its partition column names. It readself[[self$column_name_partition]]before.selfis the R6 object and holds no field with that name, so[[returnedNULL.NULL == "a"returnslogical(0), and a data.table indexed bylogical(0)holds zero rows. The method therefore randrop_all_rows()on every partition and then upserted nothing. The whole table lost every row, with no error and no warning.drop_all_rows_and_then_insert_data()readsnewdataand is unchanged. -
DBPartitionedTableExtended_v9now rejects anewdatathat it cannot route, before any method destroys a row. This is the third defect of this shape in the class in one week, and the routing fix above does not cover it.private$check_for_correct_partitions_in_data()readnewdata[[self$column_name_partition]], and[[returnsNULLfor a column thatnewdatadoes not carry.unique(NULL)isNULL,!NULL %in% xislogical(0), andsum(logical(0))is 0, so validation passed. Bothdrop_all_rows_and_then_*methods then computedNULL == i, which islogical(0). Each took zero rows for every partition, dropped all of them and wrote nothing back. The whole table lost every row, with no error and no warning. The check now rejects four things, in this order.- A
newdatathat is not adata.frame. It names the class it received. - A
newdatawith no partition column. It names the column it expected. - An
NAin the partition column. - A value that no partition covers. It names those values.
- A
- A zero-row
newdatathat carries the partition column is still accepted. Clearing every partition is a legitimate operation, andtests/testthat/test-partition-safety.Rpins it. Only aNULL, or a table without the column, is rejected. -
DBPartitionedTableExtended_v9now writes a generated partition column with a valid:=call, and it writes into a copy. Four methods heldnewdata[, .(self$column_name_partition) := part]. That form raisesLHS of := must be a symbol, or an atomic vector. Every table with a non-NULLvalue_generator_partitiontherefore failed on its first write. No test built one, which is why the defect survived. The form is nownewdata[, (self$column_name_partition) := part].:=also writes by reference. The old form therefore added a column to the caller’s own data.table, as a side effect of a write.data.table::copy()now runs first. The copy is inside thevalue_generator_partitionbranch only, and not on the common path. A NorSySS production import is memory-bound against about 20 GiB of usable pod memory, so a copy of everynewdatawould double the peak. -
DBPartitionedTableExtended_v9now handles a numeric partition value, which two separate defects broke. First,sample(x, length(x))reads a length-1 numericxas the range1:x. A single partition numbered 9 therefore became a draw from 1 to 9. Both call sites, ininsert_data()and in thepartitions_randomizedactive binding, now index by a permutation:x[sample.int(length(x))]. That form has no such special case and is a no-op below length 2. Second,[[on a list takes a numeric index as a position, soself$tables[[5L]]returned the fifth child rather than the child named “5”. In the constructor it also grew the list to five elements, three of themNULL. All 16self$tables[[...]]sites inR/r6_DBPartitionedTableExtended_v9.Rnow index byas.character(). A character partition value was never affected by either defect. -
DBPartitionedTableExtended_v9now declaresindexesas a public field, andinitialize()assigns it. The class tookindexesas a constructor argument and gave it to every child, but it held no field of that name.self$indexeswas thereforeNULL, and thedrop_indexes = names(self$indexes)default inupsert_data()and indrop_all_rows_and_then_upsert_data()evaluated toNULL. Each child received an explicitNULL, which overrode the child’s ownnames(child$indexes)default. Both defaults are now a plainNULL, so the runtime behaviour is identical and is now explicit. Nothing drops an index by default. Whether an index rebuild beats row-by-row index maintenance depends on four things: rows changed per child, index count and width, table size, and concurrency. ADROP INDEXalso briefly blocks reads and writes. That needs measurement against real batch sizes. A caller who wants the drop now passesdrop_indexes = names(pt$indexes). -
DBPartitionedTableExtended_v9$upsert_data()computedpartitions_in_usetwice from the same expression. One of the two lines is removed. Nothing observable changes. The method still writes to the partitions in the ordernewdatanames them. It is not randomised, because no transaction spans the partition loop, so each child releases its locks before the next runs. -
DBPartitionedTableExtended_v9now computes one route vector, and all four write methods route with it.private$check_for_correct_partitions_in_data()returnsas.character()of the partition column. Each write method captures that vector and indexes withroute == as.character(i), besideself$tables[[as.character(i)]]. One coercion now names the child and selects its rows. Validation usedsetdiff()on the raw values before, while the write methods used==. Those were two independent answers to one question, and they agreed. An adversarial review said a factor column made them diverge and wiped every partition. Measured against the real class on 2026-08-14, it does not.setdiff()converts a factor to character, and==against a factor compares its labels, so both sides already read the same thing. The class is refactored so that the question has one answer, not because the two answers differed. -
DBPartitionedTableExtended_v9now rejects a partition column that is not atomic. A list column passed the old check, becausesetdiff()also converts a list to character.setdiff(list("a"), c("a", "b", "c"))is therefore empty.drop_all_rows_and_then_upsert_data()then dropped every partition and died inside csdb onis.infinite(get(i)), which has no list method. A three-partition table holding six rows lost all six, and nothing failed until after the last drop. That is the fourth silent-data-loss defect of this shape in the class in one week.is.atomic()now rejects the column before the first drop. A factor is atomic, so a factor column still routes. -
DBPartitionedTableExtended_v9$initialize()now rejects a partition set that the class cannot use. It ran no check ontable_name_partitionsat all before. The check runs before the class opens its connection, so a rejected construction leaves no database connection open. It rejects four things.- A zero-length set. The table then carried no children, and every write was a silent no-op.
- An
NA. - An empty string after
as.character().self$tables[[""]] <- childmatches no name, so it APPENDS. A set ofc("a", "")built three children for two partitions. - A duplicate after
as.character().self$tables[[name]]takes the first match, so a duplicated partition made one child unreachable whilelength(self$partitions)still counted it.
- The guard now compares the character form of a partition value, not the raw value.
names(self$tables)and all 16self$tables[[...]]sites already usedas.character(), so the character form was already the identity of a partition. One accepted input changes as a result. Anewdatavalue of1.0000000000000002against a partition of1was rejected before, becausesetdiff()compares two doubles exactly. It now routes to the child named “1”. Both values print as1, and the partition column is stored asTEXT. A caller MUST NOT give one table two partition values thatas.character()renders identically.initialize()now rejects that set.
Known limitation
- A child that a caller keeps after
DBPartitionedTableExtended_v9$disconnect()can reopen the shared connection.csdb::DBConnection_v9$autoconnectioncallsconnect()on every access, so any later use of that child opens a new connection. The child cannot close that connection again, because it does not own it. Only the parent closes it. Callers MUST NOT use a child after they disconnect the parent. A caller that does MUST disconnect the parent again.r6_Task.Rcallsdisconnect()on the objects in the task’s table list. A partitioned table is registered there as the parent, so the production teardown path is unaffected.
Licensing
-
DESCRIPTIONno longer carries explicitAuthorandMaintainerfields. R derives both fromAuthors@R, which is the single source. The explicit fields named “Core Surveillance” as the copyright holder, whileAuthors@Rnames Folkehelseinstituttet. The 2026-08-06 sweep correctedAuthors@Rand left the free text behind, so the two disagreed on the legal entity.
Development
-
tests/testthat/test-shared-connection.Ris new. It builds a three-partition table against a temporary SQLite file, and it asserts five things.- The three children hold one connection object, counted by
data.table::address(). - A write reaches the partition that its
partcolumn names. -
disconnect()on the parent closes the shared connection exactly once. The test counts the closes. - A
disconnect()loop over every child leaves the partitioned table usable. - A child kept past the parent’s
disconnect()reopens the shared connection, and only the parent closes it again.
- The three children hold one connection object, counted by
-
tests/testthat/test-partition-routing.Ris new. It builds a three-partition table against a temporary SQLite file, and it asserts three things. The file holds 35 assertions, and 29 of them fail when the routing defect is present.-
drop_all_rows_and_then_upsert_data()sends every row to the partition that itspartcolumn names. The three partitions receive 2, 1 and 3 rows, so a row that reaches the wrong partition changes a count. -
drop_all_rows_and_then_upsert_data()anddrop_all_rows_and_then_insert_data()write the same rows to the same partitions. -
drop_all_rows_and_then_upsert_data()empties a partition thatnewdatadoes not name.
-
-
tests/testthat/test-partition-safety.Ris new. It holds 21 tests and 178 assertions against a temporary SQLite file. The duplicatedpartitions_in_useline has no test, because its removal changes nothing that a test can observe. The whole cs9 suite now reports 298 passing assertions, up from 256.- The first test is the one that matters. It writes two sentinel rows into each of three partitions, calls
drop_all_rows_and_then_upsert_data()with anewdatathat lacks the partition column, and then re-reads every partition. Before the fix it reported no error and zero rows in all three partitions. An error alone would not prove that the rows survived. - A zero-row
newdatathat carries the partition column still clears every partition. - A table with a
value_generator_partitionroutes every row, and all four write methods leave the caller’s data.table unchanged. - A single numeric partition numbered 9 gives
partitions_randomized == 9. Under seed 5 the oldsample()returned 2. - A table with partitions
c(5L, 7L)holds exactly two children, named “5” and “7”, and a row routes to the child its value names. - A
newdatathat names exactly one numeric partition reaches that partition. This covers the secondsample()call site, which is insideinsert_data(). -
pt$indexesholds the list the constructor received. - A factor partition column against numeric partitions routes every row, and every row survives. This is the case the adversarial review named. The test pins the measured behaviour, so a later change cannot regress it in silence.
- A list partition column is rejected, and all six sentinel rows survive.
- The constructor rejects each of the four unusable partition sets. Each error message names the problem.
-
check_for_correct_partitions_in_data()returns a character vector whose length isnrow(newdata). The test reaches private throughpt$.__enclos_env__$private, and it is the only test that does.
- The first test is the one that matters. It writes two sentinel rows into each of three partitions, calls
Version 26.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 2025.
-
CLAUDE.mdnow carries a Licensing section, so the year gets checked rather than silently ageing.
Documentation
- Repository prose rewritten to ASD-STE100 (Simplified Technical English). Every sentence in the roxygen blocks, the five vignettes,
README.md,index.mdandNEWS.mdis now at most 25 words. 35 sentences were over that limit before, counted over the whole repository. They divide into 2 in roxygen, 15 in the vignettes, 16 inNEWS.mdand 2 inindex.md. The vignette count of 15 covers both the built.Rmdfiles and their.Rmd.origsources. No claim, number, condition or attribution changed. The NorSySS figures, the 106-diseases and 378-locations counts, and the White & Valcarcel Salamanca attribution are unaltered. - Long sentences that buried a sequence are split into one idea each. Three places stated three conditions in a single sentence and now state one per sentence. They are the
SET ROLE ""trap invignette("backends"), the same trap invignette("installation"), and thecsdbversion-floor entry inNEWS.md. - RFC-2119 keywords are capitalised where the vignettes and roxygen state an obligation.
CS9_PATHMUST NOT be empty.CS9_DBCONFIG_ACCESSMUST includeconfig. The function named bydata_selector_fn_nameMUST return a named list. - Roxygen field, parameter and return descriptions now end in a full stop. The
TaskJobmethod summary is a list. It was an indented block, which Rd collapsed into one paragraph. - Vignette prose edits were applied identically to each precompiled
.Rmdand its.Rmd.origsource.vignettes/_PRECOMPILER.Rtherefore still reproduces the.Rmdfrom the.orig.
Version 26.8.5
New Features
-
CS9_DBCONFIG_DRIVER=SQLiteis accepted, matched case-insensitively. SQLite is a file rather than a server.CS9_DBCONFIG_SERVERandCS9_DBCONFIG_PORTtherefore moved out of the always-required tier ofcheck_environment_setup(). Only the server-based drivers now require them. A SQLite environment needsCS9_AUTO,CS9_PATH,CS9_DBCONFIG_ACCESS,CS9_DBCONFIG_DRIVERand oneCS9_DBCONFIG_DB_<ACCESS>file path per access. It needs noCS9_DBCONFIG_USER, noCS9_DBCONFIG_PASSWORDand noCS9_DBCONFIG_SCHEMA_*. -
check_environment_setup()now rejects aCS9_DBCONFIG_ACCESSlist that omitsconfig. The four configuration tables are built from that access unconditionally, so its absence used to fail later and obscurely. -
reload_db_config()— new export, no arguments. Re-reads everyCS9_DBCONFIG_*variable and rebuilds the configuration tables. A package that sets its own values in.onLoad()needs this.cs9is a dependency, loads first, and reads the environment before that package runs. The reload is state-safe. It disconnects every table it replaces, so a repeated reload does not leak connections. It also emptiesconfig$tablesbefore it rebuilds them. A reload against an invalid environment therefore leaves an empty table list, not tables describing the previous configuration.
Bug Fixes
-
DESCRIPTIONrequirescsdb (>= 2026.8.5)and carriesRemotes: niphr/csdb. The barecsdbit had before let a resolver satisfy the dependency with any version. CRAN and RSPM serve version 2026.5.13, which predates csdb’s SQLite backend. Installed against that,CS9_DBCONFIG_DRIVER=SQLitematched no branch in csdb and fell through to the generic ODBC arm.$connect()then failed withCan't open lib 'SQLite' : file not found, which names neither csdb nor a version. csdb 2026.8.5 is on GitHub and not on CRAN, so the floor alone would leave the dependency unsatisfiable. TheRemotesfield is what makes it obtainable.cs9is not submitted to CRAN, so the field costs nothing. - Two blocks in
tests/testthat/test-sqlite-config.Rnow callskip_if_not_installed("csdb", "2026.8.5"). This is not redundant with the version floor. Nothing enforces anImportsversion after installation.cs9reachescsdbthroughcsdb::alone, soNAMESPACEholds no import directive. R therefore runs no version check at load time. Measured on 2026-08-05 against csdb 2026.5.13,R CMD INSTALLexits 0 andlibrary(cs9)succeeds. The guard is keyed on the version and on nothing else, so it cannot hide a failure that is not a version mismatch. -
setup_database_tables()now builds all four tables into a local list and assignsconfig$tablesonce, at the end. Assigning each table directly meant a failure in the third constructor left a partially-populatedconfig$tablesbehind, indistinguishable from a complete one. - Under SQLite, a dbconfig’s
idis the database file path rather than[db].[schema]. A partitioned table’s per-partition name uses thexxpxxseparator that PostgreSQL uses.PARTITIONis a keyword in SQLite’s window-function grammar.
Documentation
- The installation vignette starts on SQLite. A reader now installs
cs9, writes six settings into.Renviron, validates them and then opens the database, all before the vignette mentions a server. The last step is deliberate.check_environment_setup()only checks the variables. It takes a$connect()on a configuration table to create the SQLite file and theconfig_logtable in it. The PostgreSQL-in-Docker guide follows below. - The installation vignette no longer says CS9 requires PostgreSQL for full functionality, which stopped being true when the SQLite backend landed. It says PostgreSQL is the production backend and points at the SQLite section for the alternative.
-
vignette("backends")— new. It puts theCS9_DBCONFIG_*environments for PostgreSQL and SQLite side by side. It has a table of what each backend does with every variable. It gives the tierscheck_environment_setup()validates in, and the.onLoad()pattern that sets the variables from another package and then callsreload_db_config(). It carries no R-level detail:vignette("backends", package = "csdb")is the companion for that. - Both vignettes now warn about two traps that cost nothing to avoid and are hard to diagnose. An empty
CS9_PATHcounts as a missing variable, soCS9_PATH=fails validation. And an unsetCS9_DBCONFIG_ROLE_CREATE_TABLEreachescsdbas""rather thanNULL.""is not the"x"no-role sentinel, so the PostgreSQLcreate_tablecan emitSET ROLE "". Both PostgreSQL blocks now set the variable explicitly. - The PostgreSQL
.Renvironblock in the installation vignette carried both of those traps, and had done so since before the SQLite work. It wroteCS9_PATH=with no value. The variable table below it calledCS9_PATH“usually empty”.CS9_DBCONFIG_ROLE_CREATE_TABLEwas absent. A reader who copied the block gotMissing required environment variables: CS9_PATH. All three are fixed. -
README.mdnames both backends and links to the two vignettes. -
_pkgdown.ymlindexes all five vignettes.cs9andbackendswere missing from thearticles:list;cs9had been missing independently of this work, although the navbar links to it.
Version 26.8.4
Documentation
- Updated introduction vignette with single-instance design principle, NorSySS case study, and comparative analysis table
- Updated task creation vignette with single-instance framing
- Added Apache Airflow integration guidance to installation vignette
- Updated CLAUDE.md with surveillance-domain examples from academic paper (White & Valcarcel Salamanca, NIPH)
- Introduction vignette now states when CS9 is not the right choice. It names the hard requirements (PostgreSQL, containers, systems administration capability) and the workflow reorganisation adoption costs. It names
plnras the simpler option for a pilot or a resource-constrained setting. - Introduction vignette now states that CS9 analyses each stratum independently by default. It also states that borrowing strength across strata is a decision about the statistical method, not a property of the framework. Adjusting exceedance probabilities for multiple comparisons is the same kind of decision. With 106 diseases across 378 locations the number of simultaneous tests is large. Nothing in CS9 controls the family-wise error rate or the false discovery rate for you.
- Introduction vignette now lists what the framework handles: per-analysis structured logging, schema validation and time-period partitioning, framework-level parallelism, and validation workflows
Version 26.5.13
New Features
-
TaskJobR6 class andrun_task_sequentially_as_callr_bg_using_load_all()wrapper. A drop-in alternative torun_task_sequentially_as_rstudio_job_using_load_all()that works in editors without RStudio’s job API (notably Positron, which does not implementrunScriptJob). Spawns the task in a freshcallr::r_bg()process, so the current environment is not polluted. Captures output via a pipe. Streams the output back to the calling R console (prefixed with the task name) vialater::later()polling. Includes$start(),$wait(),$is_alive(),$status(),$tail(),$kill().
Version 25.7.31
New Features
- Enhanced environment variable validation with detailed diagnostic function
check_environment_setup() - Improved graceful degradation for CRAN compatibility - package loads with limited functionality when database infrastructure is not available
- Context-aware environment variable validation with clear user guidance
- Robust error handling in package loading process
Improvements
- Updated system environment configuration handling
- Comprehensive startup messages guide users through configuration issues
- Enhanced database connection error handling
- Improved package loading sequence with better error isolation
Documentation
- Comprehensive installation vignette explaining infrastructure requirements
- Enhanced function documentation with CRAN-compatible examples
- Clear guidance on functionality available in different deployment scenarios
- Added comprehensive package-level documentation (
?cs9) - Enhanced
check_environment_setup()documentation with detailed examples - Updated vignettes with CRAN vs. full setup guidance
CRAN Preparation
- Removed fhiplot dependency (replaced with standard R functions)
- Fixed non-portable file names in vignettes directory
- Cleaned up build artifacts and hidden files
- Updated LICENSE file copyright year to 2025
- Created vignette precompiler system for maintainable documentation
- Verified graceful degradation in minimal environments
Version 2025.2.24
New Features
- Added
get_config_log()function to retrieve configuration log entries from theconfig_logtable.- Supports optional filtering by surveillance system (
ss), task name (task), and date range (start_date,end_date). - Returns a
data.tablewith the filtered entries.
- Supports optional filtering by surveillance system (
Improvements
- Updated
update_config_log()to also route custom messages (...) to themessage()function for clearer console output.
Version 2025.2.21
-
Added
update_config_logfunction. Logs configuration updates including surveillance system (ss), task name (task), and a custommessage.
Version 2024.6.17
- When running in parallel, a seed is set according to the index of the first analysis in each plan.
Version 2024.3.7
- Including confirm_insert_via_nrow in DBtables. Checks nrow() before insert and after insert. If nrow() has not increased sufficiently, then attempt an upsert.
Version 2023.5.3
- In R 4.3.0
as.character(lubridate::now())adds microseconds, which breaks the SQL upload. This is now replaced bycstime::now_c().
Version 2023.4.13
-
DBPartitionedTableExtended_v9$info()bug fixed with argumentcollapse=TRUE. - Inclusion of
confirm_indexesinDBPartitionedTableExtended_v9.
Version 2023.4.12
-
DBPartitionedTableExtended_v9$nrow()now has a new argumentcollapse=FALSEthat provides partion-specific results -
DBPartitionedTableExtended_v9$info()now includes sizes in MB
Version 2023.4.2
- Extension of
DBPartitionedTableExtended_v9so that it is easier to use multiple partitioning variables.
Version 2023.4.1
- Inclusion of
partitions_randomizedinDBPartitionedTableExtended_v9so that when running in parallel, the database tables don’t get locked. - Inclusion of
remove_tableinDBPartitionedTableExtended_v9 - Fixed an error in RAM calculation in parallel for
get_config_tasks_stats
Version 2023.3.31
-
DBTableExtended_v9now automatically includes a column for all tables, calledauto_last_updated_datetime, which is automatically calculated each time that row is changed. - Creation of
DBPartitionedTableExtended_v9, which allows for one dataset to be partitioned amongst multiple SQL tables automatically.
Version 2023.3.8
-
SurveillanceSystem_v9constructor now takes in an argument calledimplementation_version, which can be used to identify what version of analytics code is currently being run. -
update_config_last_updatedhas now been replaced byupdate_config_tables_last_updated(which contains when the tables were last updated) andconfig_tasks_stats(which contains all the runtimes of the tasks). -
SurveillanceSystem_v9now uses an internal R6 classDBTableExtended_v9(which extendscsdb::DBTable_v9) instead of usingcsdb::DBTable_v9directly.DBTableExtended_v9callsupdate_config_last_updatedafter altering a database table.
Version 8.0.2
- Allows for multiple databases to be used for different access levels.
-
copy_into_new_table_wherenow also copies indexes. - V8 schemas now have a nice print function.
- V8 redirects now have a nice print function.
-
copy_into_new_table_whereuses tablock. -
upsert_at_end_of_each_planandinsert_at_end_of_each_plancan now take named lists as the return value from theaction_fn. - Custom progressr handler.
Version 8.0.1
- When using
sc8::add_task_from_config_v8the schema list is checked to make sure they are actually schemas. This will solve the issue where people incorrectly add non-existent schemas to the task. -
insert_data,upsert_data,drop_all_rows_and_then_insert_dataare now the recommended ways of inserting data addin_load_production- schemas now use
load_folder_fn, which should dynamically check if a user has permission to write to a folder, solving permissions errors - Including
tm_get_schema_names - Both
granularity_timeANDgranularity_geoare now included in db censors - Requires R >= 4.1.0
-
sc8::config$plan_attempt_indexnow exists. When running plans in parallel, if a plan fails it is retried five times. This lets a user track which attempt they are on. This is mostly useful so that emails and smses are only sent whensc8::config$plan_attempt_index==1 - (Disabled) TABLOCK is disabled right now due to issues where data would not be uploaded.
- (Disabled) Data is sorted before sending it to bcp to speed up in/upserts.
Version 8.0.0
- Release of schema redirects that allow for restricted and anonymous datasets to be seamlessly used by people with different access rights
- Consistent naming of
task_from_config_v8andadd_schema_v8
Version 7.1.4
-
db_insert_data,db_upsert_data,db_drop_all_rows_and_then_upsert_dataare now the recommended ways of inserting data
Version 7.1.3
-
update_config_datetimeandget_config_datetimenow automatically record database table updates as well
Version 7.1.0
-
task_from_config_v3sets a new direction for creation of tasks and management of tasks -
describe_tasksanddescribe_schemashelp with automatic documentation
Version 7.0.8
-
task_inline_v1allows for easy inline task creation - Corresponding RStudio addin for inline tasks that copy from one db table to another
Version 7.0.7
-
copy_into_new_table_whereallows for the creation of a new table from an old table - Including
task_from_config_v2 - First RStudio addin
