blob: e265c280f806a00de261225ac1a108c8c072c734 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#pragma once
#include <deque>
#include <memory>
#include <vector>
#include "arrow/type.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/reader/batch_reader.h"
#include "paimon/reader/file_batch_reader.h"
#include "paimon/result.h"
#include "paimon/status.h"
#include "paimon/utils/range.h"
namespace arrow {
class Array;
class StructArray;
} // namespace arrow
namespace paimon {
/// Merges the blob files of one data-evolution blob bunch that span multiple max sequence
/// number layers, resolving placeholder entries row by row. Aligned with Java's
/// BlobFallbackRecordReader / AllPlaceholdersRecordReader.
///
/// A data-evolution partial update rewrites only the touched rows of a blob column; the new blob
/// file records every untouched row as a placeholder entry. Reading therefore needs, per row, the
/// value from the newest layer that holds a real (non-placeholder) entry:
///
/// 1. The caller groups the blob files by max sequence number (one group per layer, newest
/// first) and, inside each group, orders them by first row id. Row id ranges the group's
/// files do not cover are represented by gap segments, which stand for all-placeholder rows.
/// 2. All groups span the same overall row id range, so with the same row-ranges selection
/// applied they yield the same number of rows and can be stepped in lockstep.
/// 3. Each output row takes the first group, in max-sequence order, whose row is not a
/// placeholder. Placeholder rows are identified by exact equality with the
/// BlobDefs::kPlaceholderSentinel bytes, emitted by the blob format reader when
/// BlobDefs::kEmitPlaceholderSentinelKey is set.
/// 4. A row that is a placeholder in every group degrades to a null blob: it keeps its
/// _ROW_ID, reports -1 as its _SEQUENCE_NUMBER, and returns null for every other field.
class BlobFallbackBatchReader : public BatchReader {
public:
/// One piece of a sequence group: either a reader over a single blob file (already wrapped
/// with the usual per-file mapping readers and row selection), or a virtual gap standing for
/// row ids the group's files do not cover. Segments must be ordered by ascending row id.
struct Segment {
/// File segment: emits the rows of one blob file. Null for a gap segment.
std::unique_ptr<FileBatchReader> reader;
/// Gap segment only: the selected row ids the gap emits (all placeholders), as sorted
/// disjoint ranges. Must not be empty for a gap segment.
std::vector<Range> gap_selected_ranges;
};
/// `sequence_groups` must be ordered by descending max sequence number and contain at least
/// two groups (a single group needs no fallback). `read_schema` is the schema every file
/// reader emits; it must contain exactly one blob field and may additionally contain the
/// row-tracking fields _ROW_ID and _SEQUENCE_NUMBER (completed per file by
/// CompleteRowTrackingFieldsBatchReader), which stay correct for rows that are a
/// placeholder in every layer.
static Result<std::unique_ptr<BlobFallbackBatchReader>> Create(
std::vector<std::vector<Segment>>&& sequence_groups,
const std::shared_ptr<arrow::Schema>& read_schema, int32_t read_batch_size,
const std::shared_ptr<MemoryPool>& pool);
Result<ReadBatch> NextBatch() override;
Result<ReadBatchWithBitmap> NextBatchWithBitmap() override;
void Close() override;
std::shared_ptr<Metrics> GetReaderMetrics() const override;
private:
/// A run of consecutive rows already fetched from one group: `array` rows
/// [offset, offset + length) for a file segment, or `length` placeholder rows for a gap
/// segment (array is null).
struct Chunk {
std::shared_ptr<arrow::StructArray> array;
int64_t offset = 0;
int64_t length = 0;
/// Gap chunk only: the row id of each of the `length` rows, filled when the read
/// schema contains _ROW_ID so all-placeholder rows can keep their row id.
std::vector<int64_t> gap_row_ids;
};
/// Read progress of one sequence group. Move-only, matching Segment's unique_ptr member.
struct GroupCursor {
GroupCursor() = default;
GroupCursor(const GroupCursor&) = delete;
GroupCursor& operator=(const GroupCursor&) = delete;
GroupCursor(GroupCursor&&) = default;
GroupCursor& operator=(GroupCursor&&) = default;
std::vector<Segment> segments;
size_t segment_idx = 0;
/// Position inside the current gap segment: index into gap_selected_ranges and the
/// number of rows already emitted from that range.
size_t gap_range_idx = 0;
int64_t gap_range_pos = 0;
/// Rows fetched from the current file segment but not yet consumed.
std::deque<std::shared_ptr<arrow::StructArray>> pending;
int64_t pending_pos = 0;
};
BlobFallbackBatchReader(std::vector<GroupCursor>&& groups,
const std::shared_ptr<arrow::Schema>& read_schema,
int32_t blob_field_idx, int32_t row_id_field_idx,
int32_t seq_num_field_idx, int32_t read_batch_size,
const std::shared_ptr<MemoryPool>& pool);
/// Collects up to `want` rows from the group into chunks. Only the first group may come up
/// short (which defines the window size); any later group ending early is a misalignment.
Result<int64_t> FillWindow(size_t group_idx, int64_t want, std::vector<Chunk>* chunks);
/// Flags each of the `row_count` window rows of the given chunks as placeholder or not.
Result<std::vector<bool>> ComputePlaceholderFlags(const std::vector<Chunk>& chunks,
int64_t row_count) const;
/// Assembles one output column by stitching, per run of rows choosing the same group,
/// slices of that group's chunks. For rows choosing no group (placeholder in every layer),
/// _ROW_ID is kept, _SEQUENCE_NUMBER becomes -1, and every other field becomes null.
Result<std::shared_ptr<arrow::Array>> AssembleColumn(
int32_t field_idx, const std::vector<int32_t>& group_choice,
const std::vector<std::vector<Chunk>>& group_chunks) const;
/// Assembles the _ROW_ID values of rows [run_start, run_end) from the given group's chunks;
/// gap chunks contribute their synthesized row ids.
Result<std::shared_ptr<arrow::Array>> AssembleRowIdRun(const std::vector<Chunk>& chunks,
int64_t run_start,
int64_t run_end) const;
std::vector<GroupCursor> groups_;
std::shared_ptr<arrow::Schema> read_schema_;
const int32_t blob_field_idx_;
/// Index of _ROW_ID / _SEQUENCE_NUMBER in the read schema, -1 when not read.
const int32_t row_id_field_idx_;
const int32_t seq_num_field_idx_;
const int32_t read_batch_size_;
std::shared_ptr<arrow::MemoryPool> arrow_pool_;
bool closed_ = false;
};
} // namespace paimon