Changelog
Source:NEWS.md
dqcheckr 0.3.0
The drift report now surfaces per-column numeric spread changes: a new “Numeric spread changes (min / max / sd)” table shows each stat’s previous and current value with its shift, and
compare_snapshots()returns them asspread_changes. (The values were already computed and stored incolumn_snapshotsevery run; they were never read back.) Three new comparison rules —max_numeric_sd_shift_pct,max_numeric_min_shift_pct,max_numeric_max_shift_pct— breach-flag a shift, settable at rules level or per column like their mean-shift sibling. They have no default: unset, the shifts are reported but never judged, so existing configs and reports gain information without gaining new failure modes. Generated global configs list them commented out with an example value. Internally, the drift side’s per-column threshold resolution was generalised from a mean-shift-only helper to one rule-keyed resolver (.column_comparison_overrides()) covering all four per-column comparison rules.validate_config()’s unknown-key warning now distinguishes a likely typo from a possibly-newer vocabulary key: a near-miss still gets “Did you mean…”, while an unknown key resembling nothing known states the installed dqcheckr version and that a newer release may define it — so a config written against a newer dqcheckr explains itself on an older one instead of reading as a mistake.A
snapshot_dbpointing at a directory (or any path SQLite cannot open) now raises the typeddqcheckr_db_errorinstead of leaking a raw RSQLite error.file.exists()isTRUEfor a directory, so such a path slipped past the missing-file guard and failed opaquely at connect; connect-time failures are now typed at the single connection helper, covering every database path (read and write), with a directory-specific message.compare_snapshots()now checks that the database has acolumn_snapshotstable alongside the existingsnapshots-table check, raising the same typeddqcheckr_schema_error. A database carrying onlysnapshots(a partial restore or export of run history) passed every guard and then failed deep in the drift computation with a raw untyped “no such table” error.The database initialiser now refuses a foreign
column_snapshotstable with the same typeddqcheckr_schema_errorit already raised for a foreignsnapshotstable. Asnapshot_dbpointing at a database that held another application’scolumn_snapshotsused to be adopted silently: dqcheckr’ssnapshotstable was planted inside the foreign file and every subsequent run then failed to record its snapshot (“Snapshot not recorded”), so the dataset could never accumulate history and a database dqcheckr did not create had been modified. The refusal rolls back inside the initialiser’s transaction, leaving the foreign file exactly as it was.col_threshold()no longer aborts with an untypedsubscript out of boundserror when acolumn_rulesentry is a bare value where a rule map belongs (e.g.column_rules: {mycol: 5}). The same guard now also covers malformed threshold values inside a valid rule map — a quoted number ("0.5"), a vector, or a word crashed the exported check functions with untyped base-R errors (threshold * 100on a character;ifon a length-2 condition) while the drift side silently ignored the identical override. A value that is not a single finite numeric now simply does not resolve at its level and the descent continues to the rules-level threshold, then the default — so the run and drift surfaces agree. The run path already rejected such configs viavalidate_config(); this guards the exported check functions when they are called directly. The table-level twintable_threshold()(min_row_count,max_row_count,max_file_size_mb) now applies the same single-finite-numeric discipline: a malformed value falls through to the default instead of crashingcheck_min_row_count()untyped — where the pre-crash status comparison was lexicographic (50 < "100"is FALSE), so the crash was the only thing preventing a silently wrong verdict.sniff_dataset()no longer reports a single-quotequote_charas “detected” on the strength of the file merely containing no apostrophes. On an unquoted file with stray double-quotes (inch marks:5" nail), the default-quote pass fails by line merge and the'candidate used to win vacuously — writing a config that asserts quoting the file does not use, which a later delivery with an unclosed leading apostrophe would repay by row-merging the rest of the file into one field. A non-default quote is now accepted only when some sampled field actually starts and ends with it; otherwise detection re-runs under the default quote with readr’s tokenizer (field-start-only quote semantics, the same as the runtime reader).sniff_dataset()now detects the delimiter of an all-fields-quoted CSV whose quoted free-text fields carry embedded newlines (so a logical record spans several physical lines) – e.g. a ServiceNow export. The per-physical-line field counter cannot see across a multi-line quoted record and used to reject every delimiter, mis-classifying the file as single-column; when the strict pass finds nothing, areadr-based fallback re-detects the delimiter, guarded so a lone stray delimiter in one row cannot fake a multi-column file.generate_dataset_config()can no longer emit a config its ownvalidate_config()cannot parse: a mapping key derived from a column name is emitted in explicit-key block form (? key/: value) once it would exceed libyaml’s 1024-character inline-key limit, and.y_quote()now escapes embedded newlines/tabs plus every other C0 control character,DEL, and the C1 range as\xNN(YAML’s printable set excludes them and libyaml refuses a document carrying one raw — a stray vertical tab from an Excel cell break was enough), so even a pathological (or mis-sniffed) column name yields parseable YAML that round-trips to the delivery’s exact spelling. As a structural backstop, both generators now parse the emitted YAML back before renaming it into place: any future emitter gap aborts typed (dqcheckr_generate_error) with no file written, instead of leaving a created-but-unreadable config that the create-only guard would then defend from regeneration. A failure of the write itself is likewise typed (dqcheckr_write_error, the class already used when the rename into place fails) rather than escaping as a rawwriteLineserror.infer_col_type()now recognises ISO-style date-time strings ("%Y-%m-%d %H:%M:%S"and"%Y-%m-%dT%H:%M:%S") as thedatetype, so a timestamp column (e.g. a ServiceNowopened_atexport) classifies asdaterather than falling through tocharacter. The new shapes are fully anchored, so they match only a genuine<date><sep>HH:MM:SSvalue and never overlap the date-only shapes. The GUI’s wizard-preview type inference is kept in sync.New vignette “The dqcheckr workflow: generate, edit, validate, run” — a fully executed walk-through of the whole loop on a bundled example: bootstrap a deployment with the two generators, read the generated self-documenting YAML (including the duplicate-header rename and the positional-list warning), edit a rule, validate (including the did-you-mean typo path), run, list the history, compare two deliveries, and the packed-FWF
TODOand never-overwrite special cases. Because it executes,R CMD checkruns the entire chain as a living test.-
New
validate_config(dataset_name, config_dir): validation of the global and dataset YAML against the config vocabulary, reporting every finding in one pass (severities error/warning/note) instead of aborting on the first. The severity rule: config mistakes are errors — wrong types/ranges, misplaced rule keys (rules-level vs per-column),fwf_col_names/fwf_widthslength mismatches, duplicate output column names, unresolvedTODOwidth placeholders, a missing file source — while delivery-facing findings are warnings, because they can equally mean the supplier changed the delivery, and drift must be recorded, not crash the run. Unknown keys get a did-you-mean suggestion and warn, so hand-kept extra keys round-trip. The validator never crashes on malformed values (any internal error becomes an error-severity finding). An unreadable dataset config aborts with distinct classes —dqcheckr_missing_file,dqcheckr_empty_config,dqcheckr_config_parse_error,dqcheckr_invalid_config— while an empty or comments-only global config is tolerated as all-defaults with a warning (matching how the package has always run).When the delivery file the config points at is resolvable, validation additionally cross-checks the config against the file’s header only (never the body — cheap even for multi-GB files on a network share):
col_nameslength vs the physical column count,key_columns/expected_columns/column_types/column_rulesnaming columns that exist, andfwf_widthssumming to the record length — all warnings, per the severity rule. The CSV header is parsed withreadr(honouring the configured delimiter, quote, and encoding), so a legal quoted field carrying an embedded newline is counted as one column rather than mis-split by a line-based read into a spurious column-count warning. When no delivery is resolvable the header tier is skipped with the reason stated in the result and byprint()— a verdict always says which tier it reached.run_dq_check()validates first, before any other work: error findings abort with adqcheckr_validation_errorwhose message lists every finding, and no snapshot row is written — an unrunnable config can no longer leave apendingorphan in the database. Warning findings are printed and persisted into the run’s results asVC-01(“Config validation”) WARN checks — they land in the report, the warn counts, and the overall status, so delivery drift the runtime checks cannot see (e.g. a surplus column readr silently auto-names) still leaves a recorded trace. Note-severity findings (e.g. bothcurrent_fileandfolderset, wherecurrent_filewins) are now surfaced in the run log too, rather than being silently dropped on the run path. A corrupt dataset config aborts the run with the typed classes above instead of the YAML parser’s raw error. Thedescriptionkey the GUI wizard writes is part of the config vocabulary (never read by the checks). The two change-thresholds (max_numeric_mean_shift_pct,max_row_count_change_pct) accept any value >= 0 — they bound an unbounded relative change, and deployed configs legally carry values above 1 — with a warning when a value looks like a raw percentage. CP-04 (numeric mean shift) now honours a per-column
max_numeric_mean_shift_pctset incolumn_rules, which the GUI has always written but the check silently ignored — the read goes through the same column > rules > default resolution as every other per-column threshold. The drift report applies the same per-column values, so the run report andcompare_snapshots()cannot contradict each other on one snapshot pair. A malformed per-column value is now rejected up front by validation — bothrun_dq_check()andcompare_snapshots()validate the config before doing any work (see below) — rather than silently tolerated; the internal drift resolver still warns and falls back to the rules level as a last-resort safety net if ever reached with a raw value.compare_snapshots()now validates the dataset configuration before reading any thresholds — the same gaterun_dq_check()applies — so a malformed threshold (or any error-severity config finding) is rejected with adqcheckr_validation_erroron the drift path too, not tolerated there while the run path aborts. Run and drift can no longer disagree on what counts as a runnable config. A snapshot-only comparison may still omitcurrent_file/folder: a drift diffs snapshots already in the database and reads no delivery file, so that run-only requirement is exempted and does not block it. Relatedly, a corrupt config now surfaces the same typeddqcheckr_config_parse_erroron thecompare_snapshots()andlist_runs()paths (andload_config()generally) asrun_dq_check()already did, rather than leaking the YAML parser’s raw error.Two more config mistakes now fail with a clear, typed message instead of a confusing downstream error. An explicit
expected_columns: []is an error-severity validation finding that names the fix — an empty list is never a way to disable the schema-contract check; omit the key instead — rather than being read as “expect zero columns” and failing every column. And a directory given where acurrent_file/previous_fileis expected aborts withdqcheckr_invalid_configpointing atfolder:mode, instead of failing deep in the CSV reader.New
list_runs(dataset_name, config_dir, n): name-based run history. Resolves the snapshot database from the dataset’s merged configuration exactly asrun_dq_check()does and returns the recent-runs data frame fromread_recent_snapshots(), so the workflow can be driven end to end by dataset names — the returnedidcolumn is whatcompare_snapshots()takes to compare a specific pair of runs.Internal: a config vocabulary (
R/vocabulary.R) now records every YAML key the package consumes — scope, type, default, positional flag, description — as the single source of truth the upcoming validator checks against and the config generators emit from. In support, the per-site default literals were consolidated into shared constants (.default_read,.default_qc_rules, joining the existing.default_paths/.default_comparison_rules):max_missing_rate’s 0.05 previously lived independently in two files. No behaviour change.New
sniff_dataset(path): pure structure inference over a delivery file — format (CSV vs fixed-width), encoding (the same full-file streamed UTF-8 scan and single-byte fallback asread_dataset()), delimiter/quote, header presence, column names (duplicate header names renamed positionally with the originals recorded), per-column types viainfer_col_type()(one implementation — the sniff cannot disagree with run-time classification), and key-column candidates. Fixed-width boundaries come fromreadr::fwf_empty(), with the widths anchored at position 0 so a leading blank margin (or a right-aligned first column) folds into column 1 instead of shifting the whole grid; a packed file (no blank gutters) is reported with an explicitfwf_packedmarker rather than a wrong confident guess. The same explicit marker is used for any fixed-width file that is not single-byte-per-character — non-UTF-8 multibyte, UTF-16/32, and also valid multibyte UTF-8 (accented text):readr::fwf_empty()returns byte offsets while the sample is sliced by characters, andreadr::read_fwf()itself slices by bytes at run time, so a confident guess for such a file could silently truncate its last column (and no single width vector fits a file whose accented and plain rows differ in byte length). Widths are left as aTODOfor review instead of being sniffed wrongly. The fixed-width gate itself tests byte lengths as well as character lengths, so a genuine byte-fixed multibyte file (uniform bytes, varying characters — the on-disk norm, sincereadr::read_fwf()slices by bytes) reaches that explicitTODOpath instead of falling through to a confidently wrong single-column CSV whose “header” is the first data record; the minimum packed-record length is likewise measured in bytes. No side effects; each headline field’s origin is recorded as detected/default/generated. This is the detection half of the config generator.New
generate_dataset_config(path, config_dir, dataset_name): sniffs a delivery and writes a fully-optioned, self-documenting YAML config. Every config key appears exactly once — detected values live, optional settings commented out with their defaults — and each key carries the same description the validator’s vocabulary uses. Duplicate and empty header names (a trailing delimiter) are renamed positionally with# was "..."annotations andcsv_skip: 1, with rename suffixes bumped past any name already in the header; positional lists are always emitted complete under a do-not-comment warning; fixed-width files get a character-position ruler comment, and a packed file gets explicitTODOwidths thatvalidate_config()flags as an error, sorun_dq_check()refuses to run until they are filled in. Emitted values are YAML-safe (backslashes escaped, map keys quoted, paths in forward-slash form with relative paths kept relative); columns whose sampled type is unknown stay unpinned. Create-only: an existing config aborts withdqcheckr_config_existsand is never touched (the never-overwrite rule). Both generators write crash-safely — the YAML is emitted to a temp file and renamed into place only once complete, so an interrupted generation can never leave a truncated config that the create-only guard would then treat as “already generated” (the accepted residual concurrent-generation window is documented in the specification vignette, Accepted design decisions).New
generate_global_config(config_dir): writes a fully-optioneddqcheckr.yml— the shared infrastructure paths set live to their built-in defaults, and a commenteddefault_rulesblock listing every rule key with its default value and description (rules with no default show an example and a note that unset keeps the check off). Change a threshold by uncommenting one line. Create-only, like the dataset generator. A brand-new deployment now bootstraps with exactly two generator calls and onerun_dq_check().
dqcheckr 0.2.5
CRAN release: 2026-07-18
Internal
The per-column check loops (QC-07/08/09/10/11/13/15, SC-01/02, and the comparison checks CP-04/05/06/07) no longer grow their result list with
c(results, list(...))inside aforloop, which reallocated the whole list on every column and made a run O(columns^2). They now build results withlapply()and compact once, so wide deliveries (100+ columns) build their results in linear time. Check output is unchanged.The missing/empty predicate (
is.na(x) | x == "") and the four comparison-check default thresholds are each now defined once and shared by the QC checks, the comparison checks, the drift report, and the snapshot writer, instead of being reimplemented at each call site. This removes the risk of, for example, the QC report and the drift report applying different default thresholds to the same rule after only one copy was edited.The shared Quarto render pipeline (render into a temp dir, verify an output file was produced, abort with
dqcheckr_render_errorif not, then move the result into place) is now a single internal helper (.quarto_render_to_file()) called by both the main report writer and the drift report writer, instead of being duplicated near-verbatim in each. A future change to the render pipeline now lands in one place rather than needing to be mirrored by hand.
Testing
- Added coverage for two previously-untested error paths: an FWF config that omits
fwf_widths(must abort withdqcheckr_invalid_config), andrender_report()when Quarto returns without writing an output file (must abort withdqcheckr_render_errorrather than name a report that does not exist).
Bug fixes
QC-07 (
check_numeric_stats()) no longer reportsInf/NaNsummary statistics.as.numeric("Inf")isInf, notNA, so a literal infinity in a delivery classified the column as numeric and survived the check’sis.na()filter — min/max reported±Inf, and with both signs present the mean came backNaN. The aggregates are now taken over the finite subset, and the number of excluded values is appended toobserved, so an infinity in a delivery is still visible rather than silently dropped. (Snapshot statistics were never affected:compute_col_stats()already filtered non-finite inputs and serialised its aggregates through.finite_or_na().)Duplicate per-column statistics no longer multiply a column’s rows in the drift report.
column_snapshotscarries no uniqueness constraint (a constraint would turn a duplicate into a lost snapshot row through the non-fatal write path), and custom checks may emit several results for one column under onecheck_id— so two rows for the same (column, statistic) are reachable on a clean run. Both consumers join on the column name, and a join row-multiplies: the column appeared twice in every per-column drift table (four times if duplicated in both snapshots), breach flags included.compare_snapshots()now collapses duplicates to the first value and warns, naming what was collapsed. See Accepted design decisions (AD-03) in the specification vignette.A custom-check result carrying a non-character field (e.g. a plain list built without
dq_result(), withobserved = 42) no longer costs the run its entire snapshot row. Such a result passedrun_custom_checks()’s field-presence validation but failed the snapshot writer’s typed extraction much later, dropping the whole history row and all column statistics under a misleading “SQLite write failed” warning — while the report still rendered, so the loss was easy to miss.run_custom_checks()now normalises every string field at the boundary (length-1 atomic values are coerced withas.character(), matching whatdq_result()produces; anything else aborts withdqcheckr_invalid_custom_checks, naming the element and field), anddq_result()itself now coercescheck_id,check_name, andcolumnthe same way it already coercedobservedandmessage.The CP-04 numeric-mean-shift comparison no longer aborts the whole run when a numeric column contains a literal
Inf/-Infvalue.as.numeric("Inf")isInf(notNA), so such a column still classifies as numeric and reached the mean comparison; a non-finite mean slipped past the guard and made the shiftNaN, which crashed the pipeline atif (shift_pct > threshold). The check now filters to finite values first (ascheck_outliers()andcompute_col_stats()already do), so anInf-bearing column is handled as a warning rather than a crash. R’s ownwrite.csv()emits the literalInf, so this is a realistic corrupted-delivery input.compare_snapshots()now rejects aNULLor emptydataset_namewith a cleardqcheckr_invalid_argumenterror instead of aborting with an empty message. ANULLbound as SQLNULL(matching no rows) and the “need 2 snapshots” abort then collapsed to a zero-length, blank-text condition.read_recent_snapshots()now clamps a negativento0rather than returning the entire history. SQLite readsLIMIT -1as “no limit”, so a negativensilently returned every row instead of capping the result as documented.compare_snapshots()now returns the rendered drift report’s path as areport_pathelement on its result (NULLwhen no report was written), so a caller can link to the report directly instead of reconstructing the filename from a slug pattern – the reconstruction approach is fragile and broke when the drift filename gained its snapshot ids.A snapshot’s
render_statusnow carries a"pending"state while its report is still rendering. The row is written as"pending"and only flipped to"success"(withreport_file) once the report is confirmed on disk, or to"failed"if the render is skipped or errors. Previously the row was written as"success"with a NULLreport_filebefore the render finished, which a concurrent reader could not tell apart from a completed pre-0.2.3 row and would turn into a broken report link. Consumers linking to a report should treat a"pending"row as not-yet-available rather than reconstructing a filename.The drift report no longer silently reports a report that was never written. When Quarto returned without error but produced no output file,
compare_snapshots()still announced (and, withopen_report = TRUE, tried to open) the intended path. The drift writer now raises the same typed error the main report already did, andcompare_snapshots()downgrades it to a warning with no report link – the computed drift is still returned.Drift report filenames now include both compared snapshot ids (
drift_dataset_20260718_010203_1_2.html), so two comparisons of one dataset started in the same wall-clock second (a loop over id pairs, two GUI users) no longer collide on a single filename and silently overwrite each other – the same protectionreport_filename()already gives the main report.Column statistics can no longer store a non-finite value even when the aggregate itself overflows. An earlier fix excluded non-finite inputs (
Inf/-Inffrom a corrupted delivery), butsd()of finite-but-very-large values still overflows the double range internally and returnedInf, which was stored as the literal string"Inf"and poisoned drift arithmetic. Every numeric aggregate is now serialised through a finiteness guard (non-finite ->NA), and the drift reader maps any non-finite value parsed from an older database toNAas well, so a snapshot written before this fix cannot re-poison a comparison.Report filenames now include the snapshot id (
dataset_20260718_010203_47.html), so two runs of one dataset that start in the same wall-clock second no longer collide on a single filename. Previously the second run silently overwrote the first run’s report while the first run’s snapshot kept pointing at it. The filename is now written to the snapshot’sreport_filecolumn by an update after the report is confirmed rendered, rather than optimistically at insert time, so the column can never name a report that was not written.Configuration lookups no longer partial-match. R’s
$operator falls back to a prefix match when the exact element is absent, so a config with nocolumn_rules:key but a parked or mistyped one (column_rules_disabled,column_rules_old, …) had that section silently drive per-column thresholds – producing wrong PASS/FAIL verdicts from a section the config did not contain. Every config-key access now uses exact[[ ]]indexing, so a parked or renamed section is correctly treated as absent.QC-16 no longer reports a spurious clean pass for a delivery whose encoding it did not actually verify. A declared multi-byte or unknown encoding (
UTF-16LE,UTF-32,Shift-JIS,GB18030, …) used to be reported as “a single-byte encoding; every byte is valid by construction” – which is false, and which also silently disabled the crash guard that scanning gives UTF-8 deliveries. Such an encoding is now read as declared but reported as a WARN stating it was not validity-checked. Genuine single-byte encodings (ISO-8859-x, Windows-125x) still PASS.The UTF-8 validity scan now streams the file in bounded chunks instead of reading it into one in-memory vector, so an arbitrarily large delivery is verified in flat memory rather than exhausting it on the network-share hosts dqcheckr deploys to.
Non-finite values in a numeric column (
Inf/-Inf, e.g. from a corrupted upstream delivery whose CSV contains the literal text “Inf”) are no longer mishandled.compute_col_stats()excludes them from the mean, standard deviation, minimum, and maximum, so the snapshot database can no longer store the literal string “NaN”/“Inf” and poison later drift comparisons; the finite mean shift a contaminated delivery causes is still reported as drift.check_outliers()(QC-15), which previously aborted with an uninformative “missing value where TRUE/FALSE needed” on such a column, now runs cleanly.A zero-column delivery (for example an empty file) is now recorded. Previously
compute_col_stats()returnedNULL, and the snapshot write then failed and lost the entire run; the run is now snapshotted with a column count of zero.-
Failures that were previously swallowed into silent, benign-looking results are now surfaced.
-
read_recent_snapshots()andlist_snapshots()no longer report a corrupt, locked, or unreadable database as an empty history. They emit a warning naming the cause before returning the empty frame, so “the read failed” is distinguishable from “no runs yet”. - When the snapshot write fails,
run_dq_check()now appends “[snapshot NOT recorded]” to its result line instead of printing an unqualified success while the history row was lost. - QC-16 no longer reports a confident PASS when the UTF-8 validity scan itself fails (for example, out of memory on a very large delivery). It reports a WARN stating the encoding could not be verified, rather than a PASS whose rationale wrongly called UTF-8 “a single-byte encoding, valid by construction”.
-
Concurrent runs sharing one snapshot database no longer lose a snapshot or die during migration. Connections now set
PRAGMA busy_timeout, so a run that meets a database another run is writing waits for the lock instead of failing instantly (its snapshot was previously swallowed into a warning). Schema creation and the auto-migration of older databases now run inside a singleBEGIN IMMEDIATEtransaction, so two first-runs after an upgrade can no longer both add the same column (the loser used to die with “duplicate column name”). WAL journalling is intentionally not used, as it is unsafe on the network file systems dqcheckr is deployed on.init_snapshot_db()no longer modifies asnapshotstable it did not create. If the target database already contains an unrelated table of that name, it now aborts with a typeddqcheckr_schema_errorinstead of altering the user’s table. Column-existence checks during migration are now case-insensitive, matching SQLite, so a column stored as e.g.Report_Fileis recognised rather than re-added.The drift report now flags a missing-rate or non-numeric-rate change in the same direction as the corresponding comparison check. Both
.compute_drift()columns usedabs(), so a column that improved between deliveries (fewer missing values, or less non-numeric junk) was flagged as breaching, while the CP-03 and CP-07 checks – reading the samemax_missing_rate_change_pp/max_non_numeric_rate_change_ppthresholds – passed it. Drift now breaches only on an increase, matching the checks. (The numeric-mean-shift drift keeps its two-directional test, matching the two-directional CP-04 check.)A run whose HTML report is not produced no longer records a successful-looking snapshot. When the Quarto CLI is absent the report is skipped with a warning, but the snapshot row previously kept
render_status = "success"and areport_filenaming a report that was never written, so history readers (and the GUI’s “Open report” link) pointed at a file that 404s. The snapshot is now reconciled against what actually happened: if no report file exists the row is markedrender_status = "failed"and itsreport_fileis cleared. Rendering that returns without producing a file now raises instead of reporting success, andreport_fileis guaranteed to name a report that exists.Column type inference no longer misclassifies a value whose prefix happens to be a date.
as.Date()matches a prefix and silently ignores trailing characters, so"2024-01-15x"and the 9-digit id"202401159"(whose first eight digits parse under%Y%m%d) were both classified as"date"— which made corrupt dates pass QC-06 and stripped the numeric checks (QC-07/08/11) from id columns of eight or more digits. Each date format is now gated on an anchored shape before calendar validation. The documented caveat is unchanged: a genuine eight-digit value that is also a valid%Y%m%ddate still classifies as a date; a nine-digit id now correctly classifies as numeric.read_recent_snapshots()now returns the full set of columns even for a snapshot database created before 0.2.3. Previously it ranSELECT *without migrating, so an older database returned rows with the newer columns (such asreport_file) absent rather thanNA, and callers relying on those columns errored instead of degrading. The missing columns are now filled in after the read with the same defaults a migration would apply; the database file itself is not modified, so reads remain safe on read-only or network-shared databases.
Packaging
- The bundled
starwars_csvdemonstration data (inst/demonstrations/data/) is now shipped. An unanchored.gitignorerule had excluded it, so a fresh clone was missing the file that 27 example blocks and two tests depend on, andR CMD checkfailed on a clean checkout. - The fixed-width demonstration (
starwars_fwf) now has its data file. A newinst/demonstrations/makedata.Rderivesstarwars.fwffrom the CSV, so the FWF half ofdemo.Rruns end-to-end instead of aborting on a missing file.
dqcheckr 0.2.4
Bug fixes
- A declared
encodingof ASCII (or a formal alias such asUS-ASCII) is now read as UTF-8. ASCII is a strict subset of UTF-8, so this is lossless — and it removes a hard R session crash (“Invalid multibyte sequence” inside vroom/iconv, not a catchable R error) when a delivery declared ASCII contains a byte above 127 beyond whatever sample an encoding sniffer originally looked at.
New features
- New QC-16 “File encoding” check. When the effective encoding is UTF-8,
read_dataset()now validity-scans the entire file before parsing. A delivery that is not valid UTF-8 no longer risks crashing or silently producing mojibake: it is read with a single-byte fallback encoding, the run completes, and QC-16 reports a FAIL naming the detector’s best guess at the actual encoding (suppliers can change export encodings between deliveries, so this is checked per delivery). Valid files and declared single-byte encodings (which have no invalid byte sequences) report PASS. New dependency:stringi.
dqcheckr 0.2.3
Bug fixes
- A delivery with zero data rows (e.g. a header-only CSV) no longer aborts
run_dq_check()with an untyped “missing value where TRUE/FALSE needed” error. Missing rates are defined as 0 for empty inputs, and QC-14 gains an unconditional “Empty file” FAIL sub-check so an empty delivery always fails the run — with a snapshot and report — instead of crashing it. - CP-04 (numeric mean shift) no longer errors when a numeric column has no parseable values in the current delivery; it now emits a WARN result saying the mean shift cannot be computed.
-
detect_files()no longer considers subdirectories offolderwhen picking the current/previous file by modification time. -
dq_result(threshold = NULL)now yields anNAthreshold instead of failing with “argument is of length zero”; invalid vectorstatusvalues produce a clear typed error. - An invalid regex in a
column_rulespatternnow produces a QC-13 FAIL result naming the pattern instead of aborting the run; an invalidcolumn_order_severityis rejected atload_config()time with a typeddqcheckr_invalid_configerror instead of aborting mid-check. -
run_custom_checks()now validates each returned element (required fields and a valid status) and aborts withdqcheckr_invalid_custom_checksnaming the offending element, instead of failing later with misleading errors. - Snapshot writes are now wrapped in a single transaction, so a failure can no longer leave a
snapshotsrow without itscolumn_snapshotsstats; column-level custom results are batch-inserted. - The snapshot
run_timestampand the report filename are now derived from one timestamp taken at the start of the run. Previously they came from two separate clock reads, so links that reconstruct the report filename from the snapshot timestamp (as the GUI does) could intermittently point at a file with a name one second off. -
compare_snapshots()no longer announces (and offers to open) a drift report path when Quarto is unavailable and no report was written. - Explicitly supplied snapshot IDs are now validated to belong to the requested dataset; previously IDs from another dataset silently produced a cross-dataset “drift” comparison.
- Rendered reports are moved from the render directory with a copy+delete fallback when
file.rename()fails across filesystems (e.g. a network-share report directory); previously the move failed silently. - QC-10 (numeric bounds) now counts violating rows rather than distinct values — a million rows of the same out-of-range value no longer reads as “1 out-of-range value(s)”.
- QC-09 (allowed values) compares numerically when the YAML rule supplies numbers, so a file value of
"2.10"matches an allowed value of2.1. -
read_recent_snapshots()’s empty-database fallback now has the same columns as the live query (it was missingcomparison_mode,render_status, andtype_changed_cols_vs_previous).
New features
- New
report_filecolumn in thesnapshotstable stores the rendered report’s filename outright (auto-migrated into existing databases), so consumers no longer need to reconstruct it from the run timestamp.read_recent_snapshots()returns it;NAfor pre-0.2.3 rows.
Documentation
-
run_dq_check()andcompare_snapshots()now document that relativesnapshot_db/report_output_dirpaths resolve against the working directory, notconfig_dir. -
infer_col_type()documents its date-format precedence, the all-must-parse rule, and the all-8-digit-identifier caveat, with a pointer tocolumn_typesoverrides.
Performance
- Column types are now inferred once per data frame and shared across all type-dependent checks.
run_qc_checks()and the individual check functions gain an optionaltypesargument; previously each of QC-06/07/08/11/15, the column statistics, and five comparison checks re-ran full-column type inference (five date parses plus a numeric parse) independently — the dominant cost on large files. -
infer_col_type()rejects non-matching date formats from a 100-value head sample before scanning the full column (results are identical; only the rejection path gets cheaper). -
compute_col_stats()builds one data frame per column instead of one per statistic row, cutting allocations on wide files.
dqcheckr 0.2.2
CRAN release: 2026-06-18
New features
- New optional CSV config key
csv_skip(parallel tofwf_skip):read_dataset()now forwardsskip = config$csv_skip %||% 0Ltoreadr::read_delim(). This lets a config supply an explicitcol_nameslist and drop the file’s original header row — required for delivery files whose header repeats column names (e.g. a header that repeatsName/Amountper bundled record), where the real header is unusable and must be replaced positionally. Defaults to0L, so existing configs are byte-for-byte unaffected.
Internal
-
?dqcheckrpackage help now resolves (the"_PACKAGE"doc block no longer carries@noRd). - Added a test for an out-of-range
csv_skipvalue (skip exceeding the row count yields a zero-row frame). - All negative tests now assert on a typed error class rather than using a bare
expect_error().
dqcheckr 0.2.1
CRAN release: 2026-06-08
Bug fixes
- Report and drift filename slugs now use UTC consistently, matching the UTC timestamps stored in the snapshot DB.
- Removed redundant
@importFromdeclarations for template-only packages. - Removed dead
read_pass_rate_trend()function fromdrift.R. -
CP-07type-change comparison now uses a single guard so it only runs the non-numeric rate comparison when both snapshots classify the column as numeric — eliminates spuriousWARNs on type-changed columns. -
read_dataset()now forwards thecol_namesconfig key toreadr::read_delim(), so headerless CSVs are read correctly. -
read_dataset()now forwards thequote_charconfig key toreadr::read_delim()asquote =.
Internal
- All
rlang::abort()calls (27 sites) now carry a typed, two-level class hierarchy (c("<specific>", "dqcheckr_error")), letting callers catch errors broadly or precisely. - Consolidated shared test fixtures into
helper.R.
dqcheckr 0.2.0
CRAN release: 2026-06-01
New features
- New
compare_snapshots()andlist_snapshots()functions for drift analysis. Compares any two historical snapshots and optionally renders an HTML drift report with per-column statistical drift, schema changes, and trend charts. - New
resolve_col_type()function: returns the effective type for a column, respecting per-column type overrides set in thecolumn_typesconfig key. - New
QC-15outlier detection check (check_outliers()). Configured viamax_z_scoreand/oriqr_fence_multiplier; skipped silently when neither is set. -
check_key_uniqueness()(QC-12) now supports composite keys: setkey_columnsto a character vector in the dataset YAML. -
check_min_row_count()(QC-14) gainsmax_row_countandmax_file_size_mbthresholds. -
col_threshold()andtable_threshold()added as internal helpers;column_rulesper-column threshold overrides are now correctly stored incolumn_snapshots.threshold(G-01).
Behaviour changes
- Reports migrated from rmarkdown to Quarto.
render_report()usesquarto::quarto_render()and returnsNULLwith a warning when Quarto CLI is not installed. -
compare_schema()(CP-02) split into three separate result objects: CP-02a (new columns), CP-02b (dropped columns), CP-02c (type changes). -
compare_non_numeric_rate()(CP-07) now always emits a result for every eligible column, including PASS for columns where the rate did not increase (G-02). -
run_timestampis now stored in ISO-8601 UTC format (2026-01-01T12:00:00Z) rather than local time (RC-07). -
snapshotstable gains three columns on first use:comparison_mode,render_status, andtype_changed_cols_vs_previous. Existing 0.1.x databases are auto-migrated on the first 0.2.0 run. - SQLite foreign key enforcement is now explicitly enabled on every connection (
PRAGMA foreign_keys = ON). -
detect_files()uses filename as a secondary sort when two files share the same modification time, making folder-mode ordering deterministic (RC-01). -
check_allowed_values()(QC-09),compare_new_values()(CP-05), andcompare_dropped_values()(CP-06) capobservedat 20 values with an"... and N more"suffix (RC-04, RC-05). -
check_non_numeric()(QC-11) gains awarn_non_numeric_rateconfig key for a separate WARN threshold (C-01). -
compare_missing_rate()(CP-03) gains amissing_rate_change_severityconfig key (warn/fail) (B-07). -
compare_column_order()(CP-08) gains acolumn_order_severityconfig key that overrides the format-based default (C-02). -
compute_col_stats()stores the numeric mean under the keynumeric_parseable_mean(renamed fromnumeric_mean) to clarify that non-parseable values are excluded from the calculation (C-04). - Comparison summary in the HTML report now lists all FAIL/WARN messages as a bullet list rather than picking the single worst result (C-05).
-
compare_snapshots()uses the full merged per-dataset config for drift threshold comparisons, so***markers match the original check run for datasets withrule_overrides(G-05/G-06). -
compute_col_stats()unusedqc_resultsparameter removed (D-01).
dqcheckr 0.1.1
-
flag_new_columns,flag_dropped_columns,flag_type_changes(in CP-02) andflag_column_order_change(CP-08) are now honoured. Setting any flag tofalsesuppresses the corresponding check from the report. Schema changes are still tracked in the SQLite snapshot regardless of flags. -
type_inference_thresholdis now configurable per dataset viarule_overridesin the dataset YAML (ordefault_rulesin the global config). Previously fixed at 90%, it now defaults to 90% if not set. Affects QC-06, QC-07, QC-08, QC-11, CP-02, CP-04, CP-05, CP-06, and CP-07.
dqcheckr 0.1.0
Initial release.
- Single-snapshot quality checks: QC-01 to QC-14 (missing rate, empty columns, duplicate rows, row/column counts, inferred types, numeric stats, distinct counts, allowed values, numeric bounds, non-numeric rate, key uniqueness, regex pattern, minimum row count) and SC-01/SC-02 (schema contract).
- Version comparison checks: CP-01 to CP-08 (row count change, schema diff, missing rate change, numeric mean shift, new/dropped distinct values, non-numeric rate change, column order).
- Custom organisation-specific checks via a plain R file.
- Self-contained HTML report with check tables, historical trend charts, and column statistics appendix.
- SQLite snapshot database for long-term trend tracking.
- Supports CSV and fixed-width (FWF) file formats.
- Configuration via global
dqcheckr.ymland per-dataset YAML files.