| Title: | Read 'SAS' Data in Chunks and Convert to 'Parquet' |
|---|---|
| Description: | Reads 'SAS' 'sas7bdat' files through a persistent native reader. Processes configurable chunks without reopening the input or restarting the parser between chunks. Supports uncompressed, run-length encoded, and Ross Data Compression data through the bundled 'ReadStat' library. Also streams native decoded batches directly to 'Parquet' through the 'Arrow' C Data Interface and the optional 'arrow' package, without constructing R data frames. |
| Authors: | Jianyun Wu [aut, cre], Evan Miller [aut, cph] (Author of included ReadStat code) |
| Maintainer: | Jianyun Wu <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.0.7 |
| Built: | 2026-10-16 02:59:22 UTC |
| Source: | https://github.com/Fred-Wu/anotherSAS7bdat |
Read a SAS file a group of rows at a time. Use sas7bdat_read() to run a
function on every chunk automatically, or use sas7bdat_open() and
sas7bdat_read_chunk() to request data frames yourself. Inspect the reader
with sas7bdat_info() and close it with sas7bdat_close() when finished.
sas7bdat_read( path, callback, chunk_rows = 100000L, chunk_bytes = NULL, columns = NULL, encoding = NULL, dates = TRUE ) sas7bdat_open( path, chunk_rows = 100000L, chunk_bytes = NULL, columns = NULL, encoding = NULL, dates = TRUE ) sas7bdat_read_chunk(reader, n = NULL) sas7bdat_close(reader) sas7bdat_info(reader)sas7bdat_read( path, callback, chunk_rows = 100000L, chunk_bytes = NULL, columns = NULL, encoding = NULL, dates = TRUE ) sas7bdat_open( path, chunk_rows = 100000L, chunk_bytes = NULL, columns = NULL, encoding = NULL, dates = TRUE ) sas7bdat_read_chunk(reader, n = NULL) sas7bdat_close(reader) sas7bdat_info(reader)
path |
A character string specifying the path to a local |
callback |
A function whose first argument receives each chunk as a data frame. |
chunk_rows |
A positive whole number specifying the maximum rows per chunk.
Fewer rows are returned when the |
chunk_bytes |
Optional approximate decoded size limit for each chunk,
as a numeric byte count or a size string with units B, KB, MB, GB, KiB,
MiB, or GiB (case-insensitive). The default, |
columns |
A character vector of column names, or |
encoding |
A character string specifying the source encoding, or |
dates |
A logical value indicating whether to convert recognized SAS
date/time formats to R date/time classes. Dates become |
reader |
A |
n |
A positive whole number specifying the maximum rows for this read,
or |
With sas7bdat_read(), each chunk is passed to callback as its
first positional argument.
That argument contains the actual data frame for the current chunk. You
choose its name; it does not have to be named chunk. The package calls
your function again with the next data frame until reading finishes.
sas7bdat_read() returns a reading summary, not the dataset or your
callback's results. Assignments inside the callback are local to that
call. To use rows or calculated results after reading, your callback must
save them in an object outside the callback or write them to a file.
Callback return values are not collected; returning exactly FALSE
stops reading.
With sas7bdat_open(), the returned reader is a bookmark attached to the
open file, not a data frame. Each sas7bdat_read_chunk(reader) returns
the next data frame and advances the position. Assigning it to a variable
replaces that variable's previous value; it does not append rows. In a
loop, stop when the result is NULL. Previously returned data frames stay
valid after later reads or closing. sas7bdat_info() inspects the reader
without advancing it.
Information returned by sas7bdat_info() is a snapshot at the time of the
call. A saved list does not update when rows are read or the reader is
closed. Call sas7bdat_info() again to get the current status and counters.
Reopening the file starts from the beginning. Readers cannot be saved with
saveRDS() for later reuse or shared between R processes.
The file is opened once and row reading continues across chunk boundaries without restarting the parser. Metadata setup may seek within the file once at initialization. Internal SAS RLE and RDC compression are detected automatically. The file must be seekable; ZIP/gzip wrappers are unsupported.
After the first chunk request, a native worker decodes ahead while R
converts or processes earlier chunks. Read-ahead is bounded to one queued
batch and one in-progress batch, each limited by the optional chunk_bytes
and the row count requested when that batch starts. Changing n takes effect on output
chunks even when data has already been decoded. Closing cancels the worker
and discards unread data. Returned data frames own their values and remain
valid when native buffers are reused.
Column names are case-sensitive and must be unique. Output columns follow the requested order. Returned text is converted to UTF-8.
chunk_rows sets an upper limit on rows per chunk. Each chunk ends after
a complete row when either the row limit or the approximate decoded size
limit, chunk_bytes, is reached, when supplied. If the byte limit is
reached first,
the chunk contains fewer than chunk_rows rows, even before the end of
the file. Row counts can vary between chunks as decoded row sizes vary.
For sas7bdat_read_chunk(), supplying n overrides the row limit for that
call; the byte target remains in effect. The final chunk may also contain
fewer rows if fewer remain in the file.
With chunk_bytes = NULL, the requested row count controls chunk sizes.
The reader starts with small native buffers and samples the first 256
decoded rows, across chunks if needed. Those rows remain in the normal
output stream. The sample estimates text sizes for reserving buffers for
the rest of the chunk, with 25 percent text headroom. Estimates are then
updated once per batch as text sizes change. Buffers grow if needed and
are reused across chunks. Files with fewer than 256 rows use the rows
available. Sampling does not reopen the file or make an extra parser pass.
Automatic sizing controls buffer allocation; it does not impose a byte
limit. Choose a row count that fits available memory, or supply
chunk_bytes as an additional limit. An explicit byte limit is never
increased by the estimator.
The byte target is approximate and may be exceeded by the last row added to a chunk. It is not a limit on total R memory usage; even a single row may exceed the target. Native buffers for read-ahead and the output chunk can coexist, as can earlier data frames retained by your callback. Reusable native buffers retain their allocated capacity until the reader is closed. MB and GB use powers of 1,000; MiB and GiB use powers of 1,024.
SAS numerics are doubles. Special missing values use haven-compatible tagged
NAs; character padding is trimmed by ReadStat. Column labels and SAS formats
are retained as label and format.sas attributes.
Date/time formats are identified once when the reader opens. With
dates = TRUE, conversion occurs while constructing each output column:
dates use R's epoch, datetimes use UTC, and times retain seconds as hms.
Missing values and their tags are preserved. With dates = FALSE, numeric
values remain in their original SAS units.
User-defined formats and SAS catalog files are not interpreted. Files exceeding 2,147,483,647 physical observations are rejected in this version, before counter overflow.
To retain the entire dataset, you must explicitly save the chunks and
combine them, as shown below. The entire decoded dataset
and temporary copies must then fit in memory.
For faster binding of many chunks, use data.table::rbindlist() from the
optional data.table package in place of do.call(rbind, chunks).
It returns a data.table, which also inherits from data.frame.
sas7bdat_read() returns an invisible reading summary: rows is the
number of rows passed to the callback, chunks is the number of calls,
and completed indicates whether reading reached the end of the file.
An empty file calls the callback zero times and returns a completed
summary with zero rows.
sas7bdat_open() returns a sas7bdat_reader that remembers the reading
position in the open file.
sas7bdat_read_chunk() returns a data frame, or NULL when no rows remain.
sas7bdat_info() returns a snapshot list with rows_total, rows_delivered,
columns (names, types, formats, and labels), compression, and status
(open, eof, error, or closed). eof means end of file. Additional
diagnostic counters are rows_decoded, file_opens, file_closes,
bytes_read, and seeks. Byte and seek counters include metadata setup;
they do not measure R memory use. With read-ahead, rows_decoded can be
greater than rows_delivered.
estimation_rows counts the initial rows used for buffer sizing (up to
256); estimated_row_bytes is the approximate decoded bytes per row,
updated after each batch. Both are zero before sampling begins.
sas7bdat_close() returns NULL, invisibly.
sas7bdat_read() closes its reader automatically at the end, on an error or
interruption, or when the callback returns exactly FALSE.
When using sas7bdat_open(), call sas7bdat_close() when finished. Inside
a function, use on.exit(sas7bdat_close(reader), add = TRUE) for cleanup even
if processing fails. Closing more than once is safe. Reading from a closed
reader is an error; open a new reader to start again.
The package includes three files generated with SAS 9.4:
example_uncompressed.sas7bdat: uncompressed.
example_rle.sas7bdat: RLE compression (COMPRESS=CHAR).
example_rdc.sas7bdat: RDC compression (COMPRESS=BINARY).
Each contains the same 120 rows with numeric values, Unicode text, dates,
times, datetimes, column labels, and ordinary and special missing values.
Use any of these filenames in system.file() in the examples below.
The SAS program used to generate them is included as examples/generate.sas.
SAS is not needed to read the bundled files.
# Read the entire file into sas_data using a callback. path <- system.file("examples", "example_uncompressed.sas7bdat", package = "anotherSAS7bdat") chunks <- list() sas7bdat_read( path, chunk_rows = 25, callback = function(data) { chunks[[length(chunks) + 1L]] <<- data } ) sas_data <- do.call(rbind, chunks) # Faster alternative with data.table installed: # sas_data <- data.table::rbindlist(chunks, use.names = TRUE) rm(chunks) # Read the entire RLE-compressed file using a reader. path <- system.file("examples", "example_rle.sas7bdat", package = "anotherSAS7bdat") reader <- sas7bdat_open(path, chunk_rows = 25) chunks <- list() repeat { data <- sas7bdat_read_chunk(reader) if (is.null(data)) break chunks[[length(chunks) + 1L]] <- data } sas7bdat_close(reader) sas_data <- do.call(rbind, chunks) # Faster alternative with data.table installed: # sas_data <- data.table::rbindlist(chunks, use.names = TRUE) rm(chunks) sas7bdat_info(reader)$status # "closed"# Read the entire file into sas_data using a callback. path <- system.file("examples", "example_uncompressed.sas7bdat", package = "anotherSAS7bdat") chunks <- list() sas7bdat_read( path, chunk_rows = 25, callback = function(data) { chunks[[length(chunks) + 1L]] <<- data } ) sas_data <- do.call(rbind, chunks) # Faster alternative with data.table installed: # sas_data <- data.table::rbindlist(chunks, use.names = TRUE) rm(chunks) # Read the entire RLE-compressed file using a reader. path <- system.file("examples", "example_rle.sas7bdat", package = "anotherSAS7bdat") reader <- sas7bdat_open(path, chunk_rows = 25) chunks <- list() repeat { data <- sas7bdat_read_chunk(reader) if (is.null(data)) break chunks[[length(chunks) + 1L]] <- data } sas7bdat_close(reader) sas_data <- do.call(rbind, chunks) # Faster alternative with data.table installed: # sas_data <- data.table::rbindlist(chunks, use.names = TRUE) rm(chunks) sas7bdat_info(reader)$status # "closed"
Stream a SAS file into one Parquet file without constructing R data frames or R character vectors. Requires the optional arrow package.
sas7bdat_to_parquet( path, output, chunk_rows = 100000L, chunk_bytes = NULL, columns = NULL, encoding = NULL, dates = TRUE, compression = "snappy", missing_tags = FALSE, progress = interactive() )sas7bdat_to_parquet( path, output, chunk_rows = 100000L, chunk_bytes = NULL, columns = NULL, encoding = NULL, dates = TRUE, compression = "snappy", missing_tags = FALSE, progress = interactive() )
path |
A character string specifying the path to a local |
output |
Path to a new local Parquet file. The parent directory must exist. Existing files are never overwritten. |
chunk_rows |
A positive whole number specifying the maximum rows per chunk.
Fewer rows are returned when the |
chunk_bytes |
Optional approximate decoded size limit for each chunk,
as a numeric byte count or a size string with units B, KB, MB, GB, KiB,
MiB, or GiB (case-insensitive). The default, |
columns |
A character vector of column names, or |
encoding |
A character string specifying the source encoding, or |
dates |
Whether to convert recognized SAS dates and datetimes to
Parquet date and UTC timestamp types. SAS times retain double seconds and
are restored as |
compression |
Parquet compression codec, for example |
missing_tags |
Whether to retain SAS special missing tags in additional
character columns. The default, |
progress |
A logical value. Set to |
Native decoded batches are exported through the Arrow C Data Interface.
R coordinates the writer and holds small schema and batch handles; dataset
values remain in native buffers. The persistent SAS reader, column selection,
UTF-8 decoding, adaptive buffer sizing, and bounded read-ahead are shared
with sas7bdat_read(). Arrow is needed only for this conversion function.
Each decoded batch becomes a Parquet row group, with at most chunk_rows
rows. An explicit chunk_bytes can produce smaller groups; the default
NULL lets the row limit control batch sizes. Native output batches are
released after each write; the whole dataset is never collected.
With dates = TRUE, recognized SAS dates become Parquet dates (days since
1970-01-01), and datetimes become UTC timestamps rounded to the nearest
microsecond. Fractional dates and out-of-range temporal values cause an
error. SAS times remain double seconds, preserving negative times, values
beyond 24 hours, and fractional seconds. R metadata restores their hms
class and HH:MM:SS display when read with arrow::read_parquet() or
converted from an Arrow table to an R data frame. With dates = FALSE, all numeric
values retain their original SAS units and double precision, including
sub-microsecond datetime values.
File metadata retains sas.table_name, sas.label, and sas.encoding.
Each field retains sas.label, sas.format, sas.type, sas.units, and
sas.missing_tags; tag fields identify their source in sas.missing_for.
R metadata also restores column label and format.sas attributes and the
file label on import. Other Parquet clients see times as double seconds.
Inspect the schema metadata with
arrow::read_parquet(output, as_data_frame = FALSE)$schema.
Conversion writes a temporary file in the output directory and publishes it only after reading and writing finish successfully. Errors and interrupts close the reader and remove the incomplete temporary output. Empty SAS files produce a valid empty Parquet file with the selected schema.
An invisible list containing path, rows, chunks, completed,
and missing_tag_columns, a named mapping from numeric source columns to
their tag columns (empty when missing_tags = FALSE).
if (requireNamespace("arrow", quietly = TRUE)) { path <- system.file("examples", "example_rle.sas7bdat", package = "anotherSAS7bdat") output <- tempfile(fileext = ".parquet") sas7bdat_to_parquet(path, output) data <- arrow::read_parquet(output) unlink(output) }if (requireNamespace("arrow", quietly = TRUE)) { path <- system.file("examples", "example_rle.sas7bdat", package = "anotherSAS7bdat") output <- tempfile(fileext = ".parquet") sas7bdat_to_parquet(path, output) data <- arrow::read_parquet(output) unlink(output) }