Repository navigation
Closes #78 Dataset JSON Compressed (DSJC) support - #99
Merged
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## yyjson-native #99 +/- ##
================================================
Coverage ? 97.76%
================================================
Files ? 14
Lines ? 1031
Branches ? 0
================================================
Hits ? 1008
Misses ? 23
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
nicholas-masel
approved these changes
Sep 3, 2026
Closes #78. DSJC is a zLib stream of Dataset NDJSON content with no wrapper of its own, so it sits directly on the NDJSON reader and writer already here. - write_dataset_dsjc() deflates rows as they are serialized, so a large dataset never holds its uncompressed NDJSON in memory. Returns a raw vector when no file is given, since the format is binary. - read_dataset_dsjc() inflates and hands the bytes to the same NDJSON core, so the object returned matches the uncompressed readers exactly. - validate_dataset_dsjc() decompresses and validates the content as Dataset NDJSON. Implemented against zlib rather than memCompress(): R's "gzip" type does produce a conformant zLib stream, but at a fixed level 6, and the specification recommends 9. PKG_LIBS gains -lz; R itself requires zlib on every platform it builds on. Interoperability is verified both ways - streams written here decompress with R's own zlib at every level, and a stream compressed elsewhere reads back correctly. Corrupt and truncated input report rather than crash. 26MB / 300k rows: 10.9 MB at level 9 (41.9%), write 1.05s, read 0.06s. Level 1 writes in a fifth of the time for a file 4% larger.
read_from_url() reads lines, which corrupts a compressed stream, so read_raw_from_url() pulls the bytes through unchanged in chunks. Both read_dataset_dsjc() and validate_dataset_dsjc() accept a URL now. Tested over file://, which exercises the same binary path as http(s) without needing a network. inst/extdata gains dm.dsjc, generated from the dm.json already there so the two stay in step; data-raw/data.R regenerates it. datasetjson_example() picks it up with no change, and the roxygen examples now read a real file rather than writing one first.
The example set covered JSON and the compressed form but not the NDJSON in between, which is the format the compressed one is built from. All three are now generated from dm.json in data-raw, and a test asserts they read back to the same dataset - including that dm.dsjc really is dm.ndjson compressed, once the creation timestamp each file carries is normalised. read_dataset_ndjson() and validate_dataset_ndjson() examples now read a real file rather than writing one first.
Covers what DSJC is, the three functions, the size difference using the shipped example files, the compression level trade-off, and the raw vector return. Sits after the NDJSON section, since DSJC is that format compressed.
The reader inflated into a malloc buffer and then copied the whole thing into R_alloc memory, the copy existing only so a later parse error could not leak the allocation. At scale that copy cost as much resident memory as the decompressed content itself: reading a 419MB file peaked at 2437MB, which is more than reading the 999MB uncompressed NDJSON it came from - the wrong way round for a format whose point is being smaller. Inflating into a PROTECTed raw vector, grown with Rf_xlengthgets, needs no copy and cannot leak if ndjson_core() raises. 1GB dataset: read peak 2437MB -> 2188MB. What remains above the NDJSON reader is the compressed input buffer, which is held whole while parsing; streaming the file through inflate would remove that too.
- Note why memDecompress(type = "gzip") inflates a zLib stream: R's "gzip" covers both RFC 1952 and RFC 1950, and DSJC is the latter. - Say which zLib branch each malformed-input case targets, and cover truncation both ways - a heavy prefix cut and the loss of just the trailing checksum. Both reach the same branch: any prefix of a valid deflate stream is itself a valid partial stream, so inflate() asks for more input rather than reporting corruption. Corruption is covered separately by overwriting bytes mid-stream. - Add a float_as_decimals test for the compressed writer. It shares prepare_dataset_for_write() with the other two, but passes as_decimal to its own C entry point, so it needs its own coverage. Checks the metadata flips to decimal/decimal, the numbers are quoted inside the compressed content, and values still round-trip bit-exactly.
Carries over the fix from #98's review; it lived in the merge commit the rebase onto the rewritten base dropped.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #78. Targets
yyjson-native(#98), notmain— it builds directly on the native NDJSON reader and writer added there.What DSJC is
The specification is deliberately minimal: a DSJC file is the Dataset NDJSON content of a dataset as a zLib stream, with no header, signature or wrapper of its own.
.dsjcextension, MIME typeapplication/vnd.cdisc.dataset-json.compressed, level 9 and a 15-bit window recommended.So it sits directly on the NDJSON work already on #98.
Three functions
write_dataset_dsjc(x, file, level = 9)fileis given, since the format is binaryread_dataset_dsjc(file)validate_dataset_dsjc(x)Implementation note worth a reviewer's attention
memCompress(type = "gzip")does emit a conformant zLib stream despite the name — but at a fixed level 6, and the specification recommends 9. So this uses zlib directly (PKG_LIBS = -lz; R requires zlib on every platform it builds on). That also allows rows to be deflated as they are serialized, so writing a large dataset never holds its uncompressed NDJSON in memory — which is what the specification's memory-efficiency section asks for.If the extra link is unwelcome,
memCompresswould work at level 6 and remove ~120 lines of C, at the cost of buffering the whole payload.Verified
0x78 0xDA— zLib, level 9write_dataset_ndjson()writes to a fileinvalid distances set,compressed stream ended before it was complete)identical()check as the other formatsread_from_url()reads lines, which would corrupt a stream, so there is a separateread_raw_from_url(). Tested overfile://, which exercises the same path ashttp(s)without needing a networkLevel 9 costs 5x the write time for 4% smaller output. Kept as the default because the specification recommends it, with the trade-off documented.
Also here
inst/extdatanow carries all three representations of the same dataset —dm.json,dm.ndjson,dm.dsjc— generated fromdm.jsonindata-raw/data.Rso they cannot drift. A test asserts they read back to the same dataset, including thatdm.dsjcreally isdm.ndjsoncompressed. Theread_dataset_ndjson()andvalidate_dataset_ndjson()examples now read a real file rather than writing one first.The Getting Started vignette gains a "Compressed files" section.
32 tests.
R CMD check --as-cran: 0 errors, 0 warnings, 0 notes.devtools::document()NEWS.md— deferred to the release cleanup passR CMD checklocally and address all errors and warnings