Module Talon_parquet
Parquet files, read and written.
A Parquet file stores a table column by column, in row groups, and describes them in a footer at its end. Each row group holds a column chunk per column, a sequence of pages compressed with one codec. A column has a physical type, how its values are stored, and may carry an annotation, what they mean. A group is a field with children.
A format says how talon reads a file's columns and how it writes them: each column's name, its Parquet type and the talon type (Talon.Type) it reads as, with the codec and row-group size encode writes with. A format describes columns only, so one format reads any file with those columns. pp_format prints it.
A read has three steps. Map the file with Rig_disk.of_file, or take a buffer on the host. sniff its footer into a format and declare what the file leaves open with with_type. Read the rows with source, a Talon.Source.t, or with file from a path. A write states a format for a query's schema with format, and encode writes the query's rows as that format says.
Unless a value's doc says otherwise, a function that reads bytes returns Error for bytes it cannot read, and a function given a format or a name raises Invalid_argument for a misuse.
This reads a file and writes its rows with another codec:
let recompress w = let ( let* ) = Result.bind in let* source = Talon_parquet.file "events.parquet" in let q = Talon.Query.of_source source in Talon_parquet.encode (Talon_parquet.format ~compression:Talon_parquet.Zstd (Talon.Query.schema q)) q w
Types
Scalar columns
A column reads as the talon type that holds its values with their meaning:
booleanasbool,floatasfloat32anddoubleasfloat64;int32andint64asint32andint64, or as the type theirINTEGERannotation names,int8touint64;DATEasdate,TIMEasclockof its unit, andTIMESTAMPasdatetimeof its unit, an instant (datetime[u, UTC]) when the timestamp is adjusted to UTC and a wall clock (datetime[u]) otherwise;int96, the legacy timestamps, asdatetime[ns];fixed_len_byte_array(2)annotatedFLOAT16asfloat16;binaryannotatedSTRING,ENUMorJSONasstring, and every otherbinaryandfixed_len_byte_arraycolumn asbinary.
An annotation that does not apply to its column's physical type, such as UUID on an int32, or that talon does not know, is ignored, as Parquet asks of its readers. The column reads as its physical type.
Undeclared decimals
A DECIMAL(p,s) column, the number unscaled × 10{^-s} of at most p digits, is undeclared as a file's decimal is, until with_type declares it. A nested column that holds one is undeclared as a whole.
Alternative types
with_type also reads:
- a column of byte strings, a
binaryorfixed_len_byte_arraycolumn without a numeric annotation, asstring,binaryor a categorical. As astringits values must be valid UTF-8, and as a categorical they must be in its dictionary; - an
int96column as a wall-clockdatetimeof any unit. Its values must be whole numbers of the unit; - a nested column as a list of what its element reads as, or a record of the same field names in order, each reading as its field does.
Values that break these rules fail when the column is read.
Nested columns
A field of a group reads as the column its shape says, a list or a record of columns that each read by these same rules, and a null at any level of a file reads as a null of that level's column:
- a group annotated
LISTas a list, its element chosen by the backward-compatibility rules of Parquet's logical types, so that a list of two levels and one of three read alike; - a group annotated
MAPorMAP_KEY_VALUEas a list of records, the fields of its repeated group, a map'skeyandvalue, or itskeyalone when the file holds no values. A map is its pairs, in the file's order, and two equal keys stay two elements; - any other group as a record of its fields, in order, and null where the file says the group is absent;
- a
repeatedfield that no such group holds as a list of its values, and that list is never null.
A value below a null is unspecified, in a record's field as in a list's offsets.
Refusals
sniff refuses a file that talon cannot read in full:
- a file whose footer or columns are encrypted;
- a file whose column chunks are stored in other files;
- an interval or a variant, at any level of a column;
- a
repeatedgroup annotatedLISTorMAPthat is not the element of a list, a group annotated so that does not hold onerepeatedfield, and a map whoserepeatedfield is not a group; - a group of no fields, and a group with two fields of the same name;
- a column chunk compressed with LZO, Brotli, Hadoop's LZ4 framing (the codec
LZ4;LZ4_RAWis read) or a codec Parquet does not define.
Formats
type formatval sniff : Rig.Buffer.t -> (format, Talon.Error.t) Stdlib.resultsniff b is the format of the Parquet file whose bytes are b, read from its footer alone: each column of the file's schema, in order, reading as its Parquet type says.
b must be a buffer of the disk (Rig_disk.device), as Rig_disk.of_file makes, which sniff maps into host memory (Rig.Buffer.borrow), or a buffer the host addresses, as Rig.Buffer.of_bigarray makes.
Error e if b cannot be borrowed on the host, if b is not a Parquet file or its footer is malformed, or if the file is one talon refuses. When its Thrift encoding does not parse, e gives the bytes of b that failed.
val with_type : string -> Talon.Type.any -> format -> formatwith_type name t f is f with the column name read as t, the type sniff gives it, a declaration if it is undeclared, or one of its alternatives. Any other type is a computation on the column once read, such as Expr.cast.
Raises Invalid_argument if f has no column name, or if that column does not read as t; the message names the types it reads as.
pp_format ppf f formats f for people: a line that counts its columns, then one line per column, with its name and the type it reads as, as Talon.Schema.pp formats a column, and its Parquet type, as Parquet's schema language writes it, aligned after an arrow. A column reads as undeclared until it is declared:
parquet (3 columns) id int32 ← required int32 name string ← optional binary (STRING) price undeclared ← optional int64 (DECIMAL(12,2))
Reading
A source decodes only the columns a request names and those of the conjuncts it applies (Talon.Source), a nested column's every leaf (a column of values below its groups) when it is requested. The file's row groups are its parts, each one batch of the request's columns.
Text conjuncts
The source answers Talon.Source.answer.Exact for a conjunct that reads one column of string or binary, and applies it as it reads. On a chunk with a dictionary page, the conjunct is evaluated once for each value of the dictionary and of the chunk's other pages, and each row takes its value's result. On another chunk, it is evaluated for each row. A part of a request with such a conjunct leaves its number of rows unstated.
Pruning
The source answers Talon.Source.answer.Inexact for another conjunct that row-group statistics can decide, and skips a row group whose statistics show that no row of it passes. A column of nulls passes no comparison. A minimum and a maximum bound a comparison or In on integers (except uint64), dates, datetimes, byte strings, and float32 and float64 in a row group the file states holds no NaN. A nested column's statistics say nothing of its rows, so no conjunct over a nested column prunes a row group.
Failure
A read fails with Error e as sniff does, if the file has no column of the request or one that does not read as the format's type, or if a column chunk fails to decode. A failure in a column chunk names its row group. A page that does not decode also gives its bytes, and a value that a column's layout refuses (Talon.Column.of_layout) names the column and the row of the row group. A failure in a nested column names the column or, for a leaf, its dotted path such as tags.list.element, and a value's position among the leaf's values stands for its row.
val source : format -> Rig.Buffer.t -> Talon.Source.tsource f b is the source, named parquet, of the rows of the Parquet file whose bytes are b, read as f says. b is a buffer as sniff takes. Each read of the source reads b's footer again.
Raises Invalid_argument if f leaves a column undeclared; the message names it and the types it reads as.
val file :
?format:format ->
string ->
(Talon.Source.t, Talon.Error.t) Stdlib.resultfile ?format path is the source of the file path, which it maps (Rig_disk.of_file), read as format, or as sniff gives it when format is absent. It is named parquet "path" and states its number of rows, and its errors name the file.
Error e if the file cannot be mapped, or as sniff.
Raises Invalid_argument as source does, which a file with an undeclared column does when format is absent.
Writing
encode writes every column optional, its nulls as definition levels, and a list column in Parquet's three levels (LIST, list, element). Each optional or repeated field on a column's path adds a definition level, and the column's highest must fit the 16 bits that hold it, at most 65_535.
Written types
bool,float32andfloat64write asboolean,floatanddouble, andfloat16asfixed_len_byte_array(2)annotatedFLOAT16;int32andint64write as themselves, and every other integer asint32orint64annotatedINTEGER;stringwrites asbinaryannotatedSTRING, andbinaryasbinary;datewrites asDATE, and aclockasTIMEof its unit,MILLISonint32andMICROSorNANOSonint64, withisAdjustedToUTCfalse;- a
datetimewrites asTIMESTAMPof its unit, an instant withisAdjustedToUTCtrueand a wall clock withfalse; - a
listwrites as aLISTgroup, arecordas a group of its fields, and atensoras nested lists, one per dimension, row-major.
A type without a Parquet type of its own reads back as another: a categorical as a string, a clock or datetime of seconds as the same type of milliseconds, and an extension as its storage. A duration has no unit a file can record, so format refuses it: write Expr.cast Type.int64 d, its ticks of its own unit, instead.
Statistics
Each column chunk records, as source uses them, its nulls, its NaNs, and its minimum and maximum over the values that are not NaN, -0. as a zero minimum and +0. as a zero maximum. A chunk of only NaNs records no bounds. A byte string bound longer than 64 bytes is truncated to a prefix that ends at a whole UTF-8 character for text, and marked inexact. A truncated maximum is the next value past its prefix. When no value lies past it, as for a prefix of 0xFF bytes, the chunk records no maximum.
val format :
?compression:compression ->
?row_group:int ->
Talon.Schema.t ->
formatformat ?compression ?row_group s is the format of a file that encode writes from a query of schema s. Each column has the type it writes and reads back as (Written types). compression, the pages' codec, defaults to Snappy. row_group, the number of rows of each row group but the last and one batch of source, defaults to 1_048_576.
Raises Invalid_argument if s has no column, if row_group is not positive, or if s holds a record without fields, a tensor whose dtype no talon scalar type stores, such as a complex one, or, naming it, a column that holds a duration, with what writes its ticks: an Expr.cast, or, below an extension, which does not cast, Talon.Column.of_layout over its storage first (Written types), or whose highest definition level does not fit (Writing).
val encode :
format ->
Talon.Query.t ->
Bytesrw.Bytes.Writer.t ->
(unit, Talon.Error.t) Stdlib.resultencode f q w runs q with Talon.Query.fold and writes its rows to w as a Parquet file of format f, then Bytesrw.Bytes.Slice.eod. The bytes depend only on q's rows, in order, and on f, so q's batches do not change them. It writes a row group as soon as it holds its rows, and holds no more than one row group and the batch that completes it.
Error e where the run fails, as Talon.Query.fold does, if writing to w raises Bytesrw.Bytes.Stream.Error or Sys_error, or if a value does not write: a value that a page of Parquet's 2 GiB cannot hold, or a datetime of seconds that does not fit milliseconds in int64. w then holds an incomplete Parquet file.
Raises Invalid_argument if q's columns do not write as f's, names and types, if f leaves a column undeclared, or if a column of f has a Parquet type encode does not write, such as a required one from sniff. A format from format writes.