healthdb (development version)
collapse_episode() now removes records with a missing (
NA) start or end date (with a warning reporting the count) before deriving episodes, consistent with restrict_date(). Previously a single missing end date silently merged every later record of that client into one episode on the data.frame backend (anNApropagated throughcummax()), and the data.frame and database backends disagreed on the result. Both backends now drop such records and agree.collapse_episode() on a data.frame now errors when a column would clash with the names it derives (
epi_id,epi_no,epi_seq,epi_start_dt,epi_stop_dt, and internal helpers), matching the database method. Previously the data.frame method silently renamed the clashing column with an_ogsuffix.collapse_episode()’s
gap_overwritedefault is now99999on every backend (the data.frame method previously declaredInf, which was dead via S3 dispatch and would have producedNAon the database method). No change for typical calls through the generic.collapse_episode() on a zero-row data.frame no longer emits spurious “no non-missing arguments to min/max” warnings; it returns the empty result with the episode columns added.
New vignette “The logic behind if_date() and restrict_date()” (
vignette("if_date_logic")) explaining the within rolling window, the shrinking-window search for the apart condition, how the search loop is unrolled into SQL window functions with metaprogramming, and how these relate to known techniques.New vignette “Data wrangling helpers” (
vignette("wrangling")) introducing report_n(), compute_duration(), lookup(), compute_comorbidity(), collapse_episode(), cut_period(), and if_date(), with guidance on keeping the work on the database for as long as possible.The package options (healthdb.verbose, healthdb.force_proceed, and the new healthdb.check_con) are now documented in one place on the package help page; see ?healthdb. Also fixed a broken cross-reference and polished wording throughout the documentation.
Documented case sensitivity of matching: identify_row() “like”/“start” matching is case-sensitive for data.frames but follows the database for remote tables (e.g., case-insensitive ASCII LIKE on SQLite, case-sensitive on PostgreSQL, collation-dependent on SQL Server); compute_comorbidity() expects upper-case codes.
identify_row() now errors informatively when
valsis empty (e.g.,character(0)or NULL) instead of failing with an obscure message or silently returning no rows.identify_row() is now also exported as identify_rows() (consistent with it returning multiple rows); both names work and are documented on the same page.
identify_row() with
match = "like"or"start"on remote tables now generates a singleWHERE ... LIKE ... OR ... LIKE ...clause instead of one sub-query per pattern combined withUNION. The new query is simpler and typically faster. Note a subtle behavior change:UNIONremoved duplicate rows, while the new query (like all other match types) keeps them; results differ only if the source table contains fully duplicated rows.The database connection check (a
SELECT 1round trip) that runs at the start of every database-method function can now be turned off withoptions(healthdb.check_con = FALSE), which saves one round trip to the server per step in a long pipeline.Fixed if_date() and restrict_date() rejecting the boundary case where
apart * (n - 1)equalswithinas impossible. It is satisfiable: n dates with all gaps exactlyapartspan exactlyapart * (n - 1)days, e.g., two dates exactly 30 days apart do satisfy apart = 30 with within = 30. Onlyapart * (n - 1)strictly greater thanwithinis impossible now, and the database method raises the same error as the local method instead of silently returning no match.Corrected the documentation of restrict_date()’s
force_collectargument, which still described a pre-release version: theapartcondition runs on the database (it does not require a local data frame), andforce_collect = TRUEis only an opt-in fallback for the apart-plus-within case when the database does not permit temporary tables or the overlap join.Fixed report_n() (and the
report_onargument of exclude()) erroring with “Can’t coerce from aobject to an integer” on database backends whose counts come back as 64-bit integers (e.g., PostgreSQL). Fixed restrict_date() with both
apartandwithinfailing on PostgreSQL whenwithinis passed as a plain number (e.g.,within = 365, which is a double in R): the generated SQL added a numeric to a date (“date + 365.0”), which PostgreSQL rejects.withinis now coerced to integer in this branch like in the others.Fixed restrict_date() with
aparton PostgreSQL: the generated SQL summed and coalesced boolean columns, which PostgreSQL rejects (“function sum(boolean) does not exist”); the indicators are now explicit integers. Also fixed thecheck_missing = TRUEcount, which could report a garbled number of removed records (e.g., “2.47e-323”) or miss the warning entirely on some backends.The test suite can now run against PostgreSQL (
HEALTHDB_TEST_BACKEND=postgresplus the standardPG*connection variables) and SQL Server (HEALTHDB_TEST_BACKEND=sqlserverplus an ODBC connection string inHEALTHDB_TEST_ODBC), and does so on GitHub Actions via service containers, in addition to the default local SQLite.Behavior change: compute_comorbidity() now also matches ICD-9 codes by prefix (consistent with the ICD-10 change below): codes in
dataare compared with each listed code at the code’s own length, as the codes in Quan et al. (2005) cover all their subdivisions, e.g., “428” (Congestive Heart Failure, 428.x) now captures “4280” and “42800”, which were previously missed by exact matching. This affects both “ICD-9-CM-5digits” and the full-code part of “ICD-9-CM-3digits”. ICD-9 scores may be higher than in previous versions. The prefix matching reproduces the reference SAS implementation, which compares every code with the SASIN:(starts-with) operator; the package code lists were verified code-for-code against the MCHP SAS macros (see ?elix_codes for links).New exported dataset
elix_codes: the ICD codes defining the 31 Elixhauser comorbidity categories used by compute_comorbidity(), with the category labels, full names, and matching rules (prefix/exact) for all three supported ICD versions. compute_comorbidity() is now driven by this dataset internally; its interface and results are unchanged (verified against the previous implementation over every code in the lists).The confirmation prompts in execute_def(), bind_source(), and report_n() now abort with an informative error in non-interactive sessions (e.g., Rscript, knitr) instead of silently proceeding (readline() returns “” when not interactive, which was treated as consent). Interactively, only an explicit “y”/“yes” answer proceeds now; any other answer cancels.
restrict_date() (data.frame method) with mode = “filter” no longer warns (“no non-missing arguments to max”) when no record is left from the previous steps.
Removed leftover commented-out code and browser() calls across the package; no functional change.
Behavior change: define_case_with_age() now applies the age restriction (
age_range) before restrict_n() and restrict_date() instead of after, so thatn_per_clnt,apart, andwithinare evaluated only on age-eligible records (matching definitions such as “two or more visits while aged 18-65”). Results may differ from previous versions for calls that combineage_rangewithn_per_clnt > 1,apart, orwithin: clients who met those count/time-span criteria only by counting out-of-range records are no longer included.Fixed define_case_with_age() with
birth_dateerroring on database backends that lack a SQL translation fordifftime(), most notably SQLite (the generated query contained an invaliddifftime(..., 'days' AS units)and failed at collection). Age is now computed with backend-appropriate date arithmetic (DATEDIFF()on SQL Server, date subtraction on SQLite/PostgreSQL), so thebirth_datepath works across all supported backends. Theagecolumn path was unaffected.define_case_with_age() age-calculation fixes: the age is now signed, so a record dated before its
birth_date(a data error) yields a negative age and is excluded byage_rangerather than being flipped to a positive age that could wrongly pass the filter; the internal temporary age column no longer overwrites an existing column of the same name in the input; andage_rangenow errors when its lower bound exceeds the upper bound instead of silently returning no records.Fixed a bug in define_case() and define_case_with_age(): with
keep = "last", the first occurrence of “min” anywhere in the internally generated code was string-replaced with “max”, which corrupted column names containing “min” (e.g., aclnt_idnamed “admin_id” became “admax_id” and caused an error).Internal refactoring: restrict_date() (database method), define_case(), and define_case_with_age() no longer build queries by string-replacing names in deparsed code; column names are now injected as symbols with ‘rlang’. The generated SQL is unchanged, but column names that contain substrings such as “clnt_id”, “date_var”, “_i”, or “_x” no longer risk corrupting the query.
Behavior change: compute_comorbidity() now matches ICD-10 codes by prefix instead of exactly. Codes in
dataare truncated to their first 3 and 4 characters and compared with the 3-character (e.g., I50) and 4-character (e.g., E115) codes in Quan et al. (2005), respectively, which cover all their subdivisions. Previously, exact matching missed subdivisions of the listed codes, e.g., “I500” was not counted as Congestive Heart Failure (“I50” on the list), and “E1152” (ICD-10-CA) was not counted as Diabetes Complicated (“E115” on the list). ICD-10 scores may therefore be higher than in previous versions. Records with codes that belong to multiple categories in Quan et al. (e.g., I11.0 in both Congestive Heart Failure and Hypertension Complicated) are now counted in all matching categories.Fixed a bug in the data.frame method of identify_row(): when the input contained columns named
ridorincl, the function renamed those columns in the user’s original data frame by reference (e.g.,ridbecamerid.og) and silently overwrote then dropped them from the output. Internal working columns are now given names guaranteed not to clash with the input, and the input is never modified.
healthdb 0.5.1
CRAN release: 2026-05-26
Updated identify_row() to accommodate changes in upcoming ‘dbplyr’ release - Thanks the ‘dbplyr’ devs for the fix!
fixed an error caused by changes in ‘data.table’
healthdb 0.5.0
CRAN release: 2026-01-08
healthdb 0.3.1
CRAN release: 2024-05-23
Bug fixes
Fixed a critical bug introduced by the new code that cleans the window order remaining in database output. The problem is related to this issue Inability to remove window_order() leads to weird, unpredictable results.
bind_source() will bind correctly if every variable name argument is a single string (i.e., selecting variables with common names only).
Fixed a bug in pool_case() when every source had all the flags, the code that fills missing flag == 1 would trigger an error.
healthdb 0.3.0
CRAN release: 2024-05-15
Improvements
fetch_var() will accept database input, but it will not prevent one-to-many joins because the checking could be slow on remote database.
All functions that accept database input now will NOT sort the output to avoid generating the ‘ORDER BY’ clause which may trigger a warning if the output was chained with subsequent operations.
restrict_date() will not check missing date unless the new
check_missingargument was set to TRUE. The checking time may be not negligible for large database; the user should opt in for it.
healthdb 0.2.0
CRAN release: 2024-05-05
Improvements
The
apartargument in restrict_date() is now functional for both data.frame and database input.restrict_date() will give warning if there are missing entries in
date_var. The description ofdup.rmhas been re-written to clarify it is applicable only whenapartis absent.pool_case() now outputs both the number of raw (raw_in_src) and valid (valid_in_src) entries in each source.
List element of execute_def() output is named by definition and source labels.
Most of the verbose message, except data summaries, will be sent as message instead of text output, so that they can be suppressed via
message = FALSEin Rmarkdown.
