Skip to contents

Writes a sidecar lock file recording a data frame's row count, column count, column names, column types and a content hash. Committed next to the analysis, it turns "the input has not changed" from an assumption into something pr_verify_lock() can check.

Usage

pr_lock_table(x, path, algo = "md5")

Arguments

x

A data frame (or tibble::tibble) to lock.

path

Path to the lock file to write. The parent directory must exist. A .lock extension is conventional but not required.

algo

Hash algorithm. Only "md5" is available, because tools::md5sum() is the only hash in base R and this package takes no extra dependencies. Anything else is an error.

Value

Invisibly, a one-row tibble::tibble with columns path, algo, nrow, ncol, columns (list of character), types (list of character) and content (the hash).

Details

The hash is md5 — tools::md5sum() is the only hash function in base R — computed over a byte stream this function writes itself: a fixed magic string, then for each column an NA mask of one byte per row followed by the values as big-endian IEEE-754 doubles, 4-byte big-endian integers, or NUL-terminated UTF-8 strings.

That stream is used in preference to the two obvious alternatives because both are unstable in ways that would make a lock fail for the wrong reason:

  • utils::write.csv() renders doubles as decimal text. R 4.3.0 changed how as.character() formats doubles, so identical numbers can produce different bytes under different R versions, and decimal text does not round-trip a double in any case.

  • serialize() writes the R version that produced the stream into its own header, so the same object hashes differently under R 4.3 and R 4.6.

Pinning the byte order makes x86 and ARM agree; writing doubles as bits removes every formatting decision; carrying NA in a side mask means no real value can impersonate a missing one. Before hashing, each column is reduced to a bare vector — factors to their labels, Date/POSIXct to seconds, all other attributes dropped — so a tibble::tibble and a data.frame holding the same numbers hash alike, and a class change is reported through the types line rather than as a content change.

Column names and types are recorded but are deliberately not part of the content hash. That is what lets pr_verify_lock() distinguish a pure rename ("columns changed, content unchanged") from an edit to the data.

No timestamp is written. Re-locking an unchanged table leaves the file byte-identical, so a lock file under version control produces a diff only when the table really moved.

List columns cannot be hashed and are rejected with an error rather than skipped, because a silently unhashed column is worse than no lock at all.

See also

pr_verify_lock() to check a table against a lock file.

Other provenance functions: pr_design_gaps(), pr_factor_contract(), pr_validate_summary(), pr_verify_lock()

Examples

design <- data.frame(
  ID = c("ID001", "ID001", "ID003"),
  Mode = c("MH", "MS", "MG"),
  n_frames = c(23432L, 10662L, 8000L)
)
path <- tempfile(fileext = ".lock")
lock <- pr_lock_table(design, path)
lock$content
#> [1] "1efdd17da2a4f8741ab4a22ba9696da3"

# Locking is deterministic: the same table gives the same file.
path2 <- tempfile(fileext = ".lock")
pr_lock_table(design, path2)
identical(readLines(path), readLines(path2))
#> [1] TRUE

unlink(c(path, path2))