mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-07-12 03:12:11 +02:00
iss-merge-rowid-overlap-corrupts-filtered-reads / lance#7444: an
update-style merge_insert over a merge-written fragment legally reuses the
updated rows' stable row ids (row-id-lineage spec: updates preserve
_rowid) while the superseded fragment keeps its full sequence plus a
deletion vector. A later delete leaves the overlapping id range sparsely
tiled, and lance-table 7.0.0's RowIdIndex::new asserted dense tiling —
failing every filtered read that builds the id→address map ("Wrong range"
debug assert; "all columns in a record batch must have the same length"
or a silently-wrong batch in release).
The upstream fix (lance#7480, merged 2026-07-01) landed hours AFTER
v8.0.0 was cut, so no release ≤ 8.0.0 carries it. Consume it now as a
vendored pin: vendor/lance-table is the pristine published 7.0.0 source
plus ONLY the #7480 rowids/index.rs hunk (drop the false tiling assert;
hard-error on the true invariant — one live id claimed by two fragments)
and upstream's regression unit test, wired via [patch.crates-io]. The fix
is read-side only, so already-written graphs become readable as-is — no
data repair.
Removal condition (see vendor/lance-table/README.omnigraph.md): drop the
vendor dir + patch entry at the first Lance bump whose lance-table ships
lance#7480 (9.0.0, or a backported 8.0.1). The surface guard
filtered_scan_tolerates_merge_update_row_id_overlap keeps that honest in
both directions.
Turns the previous commit's red tests green. Full workspace gate passes
(cargo test --workspace --locked --no-fail-fast, 68 suites).
347 lines
11 KiB
Protocol Buffer
347 lines
11 KiB
Protocol Buffer
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
|
|
|
syntax = "proto3";
|
|
|
|
package lance.encodings;
|
|
|
|
import "google/protobuf/empty.proto";
|
|
|
|
// This file contains a specification for encodings that can be used
|
|
// to store and load Arrow data into a Lance file for the 2.0 format. It
|
|
// has been superseded by encodings21.proto which is used for the 2.1 format.
|
|
//
|
|
// # Types
|
|
//
|
|
// This file assumes the user wants to load data into Arrow arrays and
|
|
// explains how to map Arrow arrays into Lance files. Encodings are divided
|
|
// into "array encoding" (which maps to an Arrow array and may contain multiple
|
|
// buffers) and "buffer encoding" (which encodes a single buffer of data).
|
|
//
|
|
// # Encoding Tree
|
|
//
|
|
// Most encodings are layered on top of each other. These form a tree of
|
|
// encodings with a single root node. To encode an array you will typically
|
|
// start with the root node and then take the output from that root encoding
|
|
// and feed it into child encodings. The decoding process works in reverse.
|
|
//
|
|
// # Multi-column Encodings
|
|
//
|
|
// Some Arrow arrays will map to more than one column of Lance data. For
|
|
// example, struct arrays and list arrays. This file only contains encodings
|
|
// for a single column. However, it does describe how multi-column arrays can
|
|
// be encoded.
|
|
|
|
// A pointer to a buffer in a Lance file
|
|
//
|
|
// A writer can place a buffer in three different locations. The buffer
|
|
// can go in the data page, in the column metadata, or in the file metadata.
|
|
// The writer is free to choose whatever is most appropriate (for example, a dictionary
|
|
// that is shared across all pages in a column will probably go in the column
|
|
// metadata). This specification does not dictate where the buffer should go.
|
|
message Buffer {
|
|
// The index of the buffer in the collection of buffers
|
|
uint32 buffer_index = 1;
|
|
// The collection holding the buffer
|
|
enum BufferType {
|
|
// The buffer is stored in the data page itself
|
|
page = 0;
|
|
// The buffer is stored in the column metadata
|
|
column = 1;
|
|
// The buffer is stored in the file metadata
|
|
file = 2;
|
|
};
|
|
BufferType buffer_type = 2;
|
|
}
|
|
|
|
// An encoding that adds nullability to another array encoding
|
|
//
|
|
// This can wrap any array encoding and add nullability information
|
|
message Nullable {
|
|
message NoNull {
|
|
ArrayEncoding values = 1;
|
|
}
|
|
message AllNull {}
|
|
message SomeNull {
|
|
ArrayEncoding validity = 1;
|
|
ArrayEncoding values = 2;
|
|
}
|
|
oneof nullability {
|
|
// The array has no nulls and there is a single buffer needed
|
|
NoNull no_nulls = 1;
|
|
// The array may have nulls and we need two buffers
|
|
SomeNull some_nulls = 2;
|
|
// All values are null (no buffers needed)
|
|
AllNull all_nulls = 3;
|
|
}
|
|
}
|
|
|
|
// An array encoding for variable-length list fields
|
|
message List {
|
|
// An array containing the offsets into an items array.
|
|
//
|
|
// This array will have num_rows items and will never
|
|
// have nulls.
|
|
//
|
|
// If the list at index i is not null then offsets[i] will
|
|
// contain `base + len(list)` where `base` is defined as:
|
|
// i == 0: 0
|
|
// i > 0: (offsets[i-1] % null_offset_adjustment)
|
|
//
|
|
// To help understand we can consider the following example list:
|
|
// [ [A, B], null, [], [C, D, E] ]
|
|
//
|
|
// The offsets will be [2, ?, 2, 5]
|
|
//
|
|
// If the incoming list at index i IS null then offsets[i] will
|
|
// contain `base + len(list) + null_offset_adjustment` where `base`
|
|
// is defined the same as above.
|
|
//
|
|
// To complete the above example let's assume that `null_offset_adjustment`
|
|
// is 7. Then the offsets will be [2, 9, 2, 5]
|
|
//
|
|
// If there are no nulls then the offsets we write here are exactly the
|
|
// same as the offsets in an Arrow list array (except we omit the leading
|
|
// 0 which is redundant)
|
|
//
|
|
// The reason we do this is so that reading a single list at index i only
|
|
// requires us to load the indices at i and i-1.
|
|
//
|
|
// If the offset at index i is greater than `null_offset_adjustment``
|
|
// then the list at index i is null.
|
|
//
|
|
// Otherwise the length of the list is `offsets[i] - base` where
|
|
// base is defined the same as above.
|
|
//
|
|
// Let's consider our example offsets: [2, 9, 2, 5]
|
|
//
|
|
// We can take any range of lists and determine how many list items are
|
|
// referenced by the sublist.
|
|
//
|
|
// 0..3: [_, 5] -> items 0..5 (base = 0* and end is 5)
|
|
// 0..2: [_, 2] -> items 0..2 (base = 0* and end is 2)
|
|
// 0..1: [_, 9] -> items 0..2 (base = 0* and end is 9 % 7)
|
|
// 1..3: [2, 5] -> items 2..5 (base = 2 and end is 5)
|
|
// 1..2: [2, 2] -> items 2..2 (base = 2 and end is 2)
|
|
// 2..3: [9, 5] -> items 2..5 (base = 9 % 7 and end is 5)
|
|
//
|
|
// * When the start of our range is the 0th item the base is always 0 and we only
|
|
// need to load a single index from disk to determine the range.
|
|
//
|
|
// The data type of the offsets array is flexible and does not need
|
|
// to match the data type of the destination array. Please note that the offsets
|
|
// array is very likely to be efficiently encoded by bit packing deltas.
|
|
ArrayEncoding offsets = 1;
|
|
// If a list is null then we add this value to the offset
|
|
//
|
|
// This value must be greater than the length of the items so that
|
|
// (offset + null_offset_adjustment) is never used by a non-null list.
|
|
//
|
|
// Note that this value cannot be equal to the length of the items
|
|
// because then a page with a single list would store [ X ] and we
|
|
// couldn't know if that is a null list or a list with X items.
|
|
//
|
|
// Therefore, the best choice for this value is 1 + # of items.
|
|
// Choosing this will maximize the bit packing that we can apply to the offsets.
|
|
uint64 null_offset_adjustment = 2;
|
|
// How many items are referenced by these offsets. This is needed in
|
|
// order to determine which items pages map to this offsets page.
|
|
uint64 num_items = 3;
|
|
}
|
|
|
|
// An array encoding for fixed-size list fields
|
|
message FixedSizeList {
|
|
/// The number of items in each list
|
|
uint32 dimension = 1;
|
|
/// True if the list is nullable
|
|
bool has_validity = 3;
|
|
/// The items in the list
|
|
ArrayEncoding items = 2;
|
|
}
|
|
|
|
message Compression {
|
|
string scheme = 1;
|
|
optional int32 level = 2;
|
|
}
|
|
|
|
// Fixed width items placed contiguously in a buffer
|
|
message Flat {
|
|
// the number of bits per value, must be greater than 0, does
|
|
// not need to be a multiple of 8
|
|
uint64 bits_per_value = 1;
|
|
// the buffer of values
|
|
Buffer buffer = 2;
|
|
// The Compression message can specify the compression scheme (e.g. zstd) and any
|
|
// other information that is needed for decompression.
|
|
//
|
|
// If this array is compressed then the bits_per_value refers to the uncompressed
|
|
// data.
|
|
Compression compression = 3;
|
|
}
|
|
|
|
// Compression algorithm where all values have a constant value
|
|
message Constant {
|
|
// The value (TODO: define encoding for literals?)
|
|
bytes value = 1;
|
|
}
|
|
|
|
// Items are bitpacked in a buffer
|
|
message Bitpacked {
|
|
// the number of bits used for a value in the buffer
|
|
uint64 compressed_bits_per_value = 1;
|
|
|
|
// the number of bits of the uncompressed value. e.g. for a u32, this will be 32
|
|
uint64 uncompressed_bits_per_value = 2;
|
|
|
|
// The items in the list
|
|
Buffer buffer = 3;
|
|
|
|
// Whether or not a sign bit is included in the bitpacked value
|
|
bool signed = 4;
|
|
}
|
|
|
|
// Items are bitpacked in a buffer
|
|
message BitpackedForNonNeg {
|
|
// the number of bits used for a value in the buffer
|
|
uint64 compressed_bits_per_value = 1;
|
|
|
|
// the number of bits of the uncompressed value. e.g. for a u32, this will be 32
|
|
uint64 uncompressed_bits_per_value = 2;
|
|
|
|
// The items in the list
|
|
Buffer buffer = 3;
|
|
}
|
|
|
|
// Opaque bitpacking variant where the bits per value are stored inline in the chunks themselves
|
|
message InlineBitpacking {
|
|
// the number of bits of the uncompressed value. e.g. for a u32, this will be 32
|
|
uint64 uncompressed_bits_per_value = 2;
|
|
}
|
|
|
|
// Transparent bitpacking variant where the number of bits per value is fixed through the whole buffer
|
|
message OutOfLineBitpacking {
|
|
// the number of bits of the uncompressed value. e.g. for a u32, this will be 32
|
|
uint64 uncompressed_bits_per_value = 2;
|
|
// The number of compressed bits per value, fixed across the entire buffer
|
|
uint64 compressed_bits_per_value = 3;
|
|
}
|
|
|
|
// An array encoding for shredded structs that will never be null
|
|
//
|
|
// There is no actual data in this column.
|
|
//
|
|
// TODO: Struct validity bitmaps will be placed here.
|
|
message SimpleStruct {}
|
|
|
|
// An array encoding for binary fields
|
|
message Binary {
|
|
ArrayEncoding indices = 1;
|
|
ArrayEncoding bytes = 2;
|
|
uint64 null_adjustment = 3;
|
|
}
|
|
|
|
message Variable {
|
|
uint32 bits_per_offset = 1;
|
|
}
|
|
|
|
message Fsst {
|
|
ArrayEncoding binary = 1;
|
|
bytes symbol_table = 2;
|
|
}
|
|
|
|
// An array encoding for dictionary-encoded fields
|
|
message Dictionary {
|
|
ArrayEncoding indices = 1;
|
|
ArrayEncoding items = 2;
|
|
uint32 num_dictionary_items = 3;
|
|
}
|
|
|
|
message PackedStruct {
|
|
repeated ArrayEncoding inner = 1;
|
|
Buffer buffer = 2;
|
|
}
|
|
|
|
message PackedStructFixedWidthMiniBlock {
|
|
ArrayEncoding Flat = 1;
|
|
repeated uint32 bits_per_values = 2;
|
|
}
|
|
|
|
message FixedSizeBinary {
|
|
ArrayEncoding bytes = 1;
|
|
uint32 byte_width = 2;
|
|
}
|
|
|
|
message Block {
|
|
string scheme = 1;
|
|
}
|
|
|
|
// Run-Length Encoding for miniblock format
|
|
message Rle {
|
|
// Number of bits per value (8, 16, 32, 64, or 128)
|
|
uint64 bits_per_value = 1;
|
|
}
|
|
|
|
// Byte Stream Split encoding for floating point values
|
|
message ByteStreamSplit {
|
|
// Number of bits per value (32 for float, 64 for double)
|
|
uint64 bits_per_value = 1;
|
|
}
|
|
|
|
// General miniblock encoding - wraps another miniblock encoding with compression
|
|
message GeneralMiniBlock {
|
|
// The inner miniblock encoding (e.g., Rle, Bitpacked, etc.)
|
|
ArrayEncoding inner = 1;
|
|
// The compression scheme to apply to the miniblock buffers
|
|
Compression compression = 2;
|
|
}
|
|
|
|
// Encodings that decode into an Arrow array
|
|
message ArrayEncoding {
|
|
oneof array_encoding {
|
|
Flat flat = 1;
|
|
Nullable nullable = 2;
|
|
FixedSizeList fixed_size_list = 3;
|
|
List list = 4;
|
|
SimpleStruct struct = 5;
|
|
Binary binary = 6;
|
|
Dictionary dictionary = 7;
|
|
Fsst fsst = 8;
|
|
PackedStruct packed_struct = 9;
|
|
Bitpacked bitpacked = 10;
|
|
FixedSizeBinary fixed_size_binary = 11;
|
|
BitpackedForNonNeg bitpacked_for_non_neg = 12;
|
|
Constant constant = 13;
|
|
InlineBitpacking inline_bitpacking = 14;
|
|
OutOfLineBitpacking out_of_line_bitpacking = 15;
|
|
Variable variable = 16;
|
|
PackedStructFixedWidthMiniBlock packed_struct_fixed_width_mini_block = 17;
|
|
Block block = 18;
|
|
Rle rle = 19;
|
|
GeneralMiniBlock general_mini_block = 20;
|
|
ByteStreamSplit byte_stream_split = 21;
|
|
}
|
|
}
|
|
|
|
// Wraps a column with a zone map index that can be used
|
|
// to apply pushdown filters
|
|
message ZoneIndex {
|
|
uint32 rows_per_zone = 1;
|
|
Buffer zone_map_buffer = 2;
|
|
ColumnEncoding inner = 3;
|
|
}
|
|
|
|
// Marks a column as blob data. It will contain a packed struct
|
|
// with fields position and size (u64)
|
|
message Blob {
|
|
ColumnEncoding inner = 1;
|
|
}
|
|
|
|
// Encodings that describe a column of values
|
|
message ColumnEncoding {
|
|
oneof column_encoding {
|
|
// No special encoding, just column values
|
|
google.protobuf.Empty values = 1;
|
|
ZoneIndex zone_index = 2;
|
|
Blob blob = 3;
|
|
}
|
|
}
|