blob: 1307089e3959eb37d267191e989602c8bfdfe1ee [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
/// \file iceberg/table_scan.h
/// \brief Define table scan APIs and scan task types.
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "iceberg/iceberg_export.h"
#include "iceberg/result.h"
#include "iceberg/table_metadata.h"
#include "iceberg/type_fwd.h"
#include "iceberg/util/error_collector.h"
#include "iceberg/util/executor.h"
#include "iceberg/util/stream.h"
namespace iceberg {
/// \brief An abstract scan task.
class ICEBERG_EXPORT ScanTask {
public:
enum class Kind : uint8_t {
kFileScanTask,
kChangelogScanTask,
};
/// \brief The kind of scan task.
virtual Kind kind() const = 0;
/// \brief The number of bytes that should be read by this scan task.
virtual int64_t size_bytes() const = 0;
/// \brief The number of files that should be read by this scan task.
virtual int32_t files_count() const = 0;
/// \brief The number of rows that should be read by this scan task.
virtual int64_t estimated_row_count() const = 0;
virtual ~ScanTask();
};
/// \brief Task representing a data file and its corresponding delete files.
class ICEBERG_EXPORT FileScanTask : public ScanTask {
public:
/// \brief Construct with data file, delete files, and residual filter.
///
/// \param data_file The data file to read.
/// \param delete_files Delete files that apply to this data file.
/// \param filter Optional residual filter to apply after reading.
/// \note The shared data file, delete files, and residual filter must remain
/// unchanged for this task's lifetime. Metadata changes require rebuilding tasks.
explicit FileScanTask(std::shared_ptr<DataFile> data_file,
std::vector<std::shared_ptr<DataFile>> delete_files = {},
std::shared_ptr<Expression> filter = nullptr);
/// \brief Make a task for a validated half-open byte range [start, start + length).
///
/// A range covering the entire file is represented as a whole-file task. An empty
/// range is valid only for an empty whole file. The data file, delete files, and
/// residual filter are shared with the resulting task and must remain unchanged
/// for its lifetime. Metadata changes require rebuilding tasks.
static Result<std::shared_ptr<FileScanTask>> MakeSplit(
std::shared_ptr<DataFile> data_file, int64_t start, int64_t length,
std::vector<std::shared_ptr<DataFile>> delete_files = {},
std::shared_ptr<Expression> filter = nullptr);
/// \brief The inclusive byte offset where this task starts, or zero for a whole file.
int64_t start() const { return range_ ? range_->start : 0; }
/// \brief The byte length of this task, or the size of a whole file.
int64_t length() const;
/// \brief Whether this task reads a proper subset of its data file.
bool is_split() const { return range_.has_value(); }
/// \brief The data file that should be read by this scan task.
const std::shared_ptr<DataFile>& data_file() const { return data_file_; }
/// \brief Delete files that apply to this data file.
const std::vector<std::shared_ptr<DataFile>>& delete_files() const {
return delete_files_;
}
/// \brief Residual filter to apply after reading.
const std::shared_ptr<Expression>& residual_filter() const { return residual_filter_; }
Kind kind() const override { return Kind::kFileScanTask; }
int64_t size_bytes() const override;
int32_t files_count() const override;
/// \brief The estimated number of rows in this task, prorated by byte range.
/// For valid, non-overlapping splits covering a file with unchanged metadata,
/// the estimates sum to the file's record_count.
int64_t estimated_row_count() const override;
private:
// Only set for a partial file; whole-file tasks leave range_ empty.
struct Range {
int64_t start;
int64_t length;
};
FileScanTask(std::shared_ptr<DataFile> data_file,
std::vector<std::shared_ptr<DataFile>> delete_files,
std::shared_ptr<Expression> filter, Range range);
std::shared_ptr<DataFile> data_file_;
std::vector<std::shared_ptr<DataFile>> delete_files_;
std::shared_ptr<Expression> residual_filter_;
std::optional<Range> range_;
};
/// \brief Stream of file scan tasks.
using FileScanTaskStream = Stream<std::shared_ptr<FileScanTask>>;
/// \brief Owning pointer to a file scan task stream.
using FileScanTaskStreamPtr = std::unique_ptr<FileScanTaskStream>;
enum class ChangelogOperation : uint8_t {
kInsert,
kDelete,
kUpdateBefore,
kUpdateAfter,
};
/// \brief A scan task for reading changelog entries between snapshots.
class ICEBERG_EXPORT ChangelogScanTask : public ScanTask {
public:
/// \brief Construct an AddedRowsScanTask.
///
/// \param change_ordinal Position in the changelog order (0-based).
/// \param commit_snapshot_id The snapshot ID that committed this change.
/// \param data_file The data file containing the added rows.
/// \param delete_files Delete files that apply to this data file.
/// \param residual_filter Optional residual filter to apply after reading.
ChangelogScanTask(int32_t change_ordinal, int64_t commit_snapshot_id,
std::shared_ptr<DataFile> data_file,
std::vector<std::shared_ptr<DataFile>> delete_files = {},
std::shared_ptr<Expression> residual_filter = nullptr);
Kind kind() const override { return Kind::kChangelogScanTask; }
int64_t size_bytes() const override;
int32_t files_count() const override;
int64_t estimated_row_count() const override;
virtual ChangelogOperation operation() const = 0;
/// \brief The position of this change in the changelog order (0-based).
int32_t change_ordinal() const { return change_ordinal_; }
/// \brief The snapshot ID that committed this change.
int64_t commit_snapshot_id() const { return commit_snapshot_id_; }
/// \brief Residual filter to apply after reading.
const std::shared_ptr<Expression>& residual_filter() const { return residual_filter_; }
protected:
int32_t change_ordinal_;
int64_t commit_snapshot_id_;
std::shared_ptr<DataFile> data_file_;
std::vector<std::shared_ptr<DataFile>> delete_files_;
std::shared_ptr<Expression> residual_filter_;
};
/// \brief A scan task for inserts generated by adding a data file to the table.
///
/// This task represents data files that were added to the table, along with any
/// delete files that should be applied when reading the data.
///
/// Added data files may have matching delete files. This may happen if a
/// matching position delete file is committed in the same snapshot or if changes
/// for multiple snapshots are squashed together.
///
/// Suppose snapshot S1 adds data files F1, F2, F3 and a position delete file,
/// D1, that marks particular records in F1 as deleted. A scan for changes
/// generated by S1 should include the following tasks:
/// - AddedRowsScanTask(file=F1, deletes=[D1], snapshot=S1)
/// - AddedRowsScanTask(file=F2, deletes=[], snapshot=S1)
/// - AddedRowsScanTask(file=F3, deletes=[], snapshot=S1)
///
/// Readers consuming these tasks should produce added records with metadata
/// like change ordinal and commit snapshot ID.
class ICEBERG_EXPORT AddedRowsScanTask : public ChangelogScanTask {
public:
using ChangelogScanTask::ChangelogScanTask;
ChangelogOperation operation() const override { return ChangelogOperation::kInsert; }
/// \brief The data file containing the added rows.
const std::shared_ptr<DataFile>& data_file() const { return data_file_; }
/// \brief A list of delete files to apply when reading the data file in this task.
///
/// @return A list of delete files to apply
const std::vector<std::shared_ptr<DataFile>>& delete_files() const {
return delete_files_;
}
};
/// \brief A scan task for deletes generated by removing a data file from the table.
///
/// All historical delete files added earlier must be applied while reading the data file.
/// This is required to output only those data records that were live when the data file
/// was removed.
///
/// Suppose snapshot S1 contains data files F1, F2, F3. Then snapshot S2 adds a position
/// delete file, D1, that deletes records from F2 and snapshot S3 removes F2 entirely. A
/// scan for changes generated by S3 should include the following task:
/// - DeletedDataFileScanTask(file=F2, existing-deletes=[D1], snapshot=S3)
///
/// Readers consuming these tasks should produce deleted records with metadata like
/// change ordinal and commit snapshot ID.
class ICEBERG_EXPORT DeletedDataFileScanTask : public ChangelogScanTask {
public:
using ChangelogScanTask::ChangelogScanTask;
ChangelogOperation operation() const override { return ChangelogOperation::kDelete; }
/// \brief The data file that was deleted.
const std::shared_ptr<DataFile>& data_file() const { return data_file_; }
/// \brief A list of previously added delete files to apply when reading the
/// data file in this task.
///
/// \return A list of delete files to apply
const std::vector<std::shared_ptr<DataFile>>& existing_deletes() const {
return delete_files_;
}
};
namespace internal {
// Internal table scan context used by different scan implementations.
struct ICEBERG_EXPORT TableScanContext {
std::optional<int64_t> snapshot_id;
std::shared_ptr<Expression> filter;
bool ignore_residuals{false};
bool case_sensitive{true};
bool return_column_stats{false};
std::unordered_set<int32_t> columns_to_keep_stats;
std::optional<std::vector<std::string>> selected_columns;
std::shared_ptr<Schema> projected_schema;
std::unordered_map<std::string, std::string> options;
bool from_snapshot_id_inclusive{false};
std::optional<int64_t> from_snapshot_id;
std::optional<int64_t> to_snapshot_id;
std::string branch{};
std::optional<int64_t> min_rows_requested;
bool use_snapshot_schema{false};
OptionalExecutor plan_executor;
std::string table_name;
std::shared_ptr<MetricsReporter> metrics_reporter;
// Validate the context parameters to see if they have conflicts.
[[nodiscard]] Status Validate() const;
};
} // namespace internal
// Concept to check if a type is an incremental scan
template <typename T>
concept IsIncrementalScan = std::is_base_of_v<IncrementalScan<FileScanTask>, T> ||
std::is_base_of_v<IncrementalScan<ChangelogScanTask>, T>;
/// \brief Builder class for creating TableScan instances.
template <typename ScanType = DataTableScan>
class ICEBERG_TEMPLATE_CLASS_EXPORT TableScanBuilder : public ErrorCollector {
public:
/// \brief Constructs a TableScanBuilder for the given table.
/// \param table Table whose metadata, FileIO, name, and reporter are captured.
static Result<std::unique_ptr<TableScanBuilder<ScanType>>> Make(const Table& table);
/// \brief Update property that will override the table's behavior
/// based on the incoming pair. Unknown properties will be ignored.
/// \param key name of the table property to be overridden
/// \param value value to override with
TableScanBuilder& Option(std::string key, std::string value);
/// \brief Set the projected schema.
/// \param schema a projection schema
TableScanBuilder& Project(std::shared_ptr<Schema> schema);
/// \brief If data columns are selected via Select(), controls whether
/// the match to the schema will be done with case sensitivity. Default is true.
/// \param case_sensitive whether the scan is case-sensitive
TableScanBuilder& CaseSensitive(bool case_sensitive);
/// \brief Request this scan to load the column stats with each data file.
///
/// Column stats include: value count, null value count, lower bounds, and upper bounds.
TableScanBuilder& IncludeColumnStats();
/// \brief Request this scan to load the column stats for the specific columns with each
/// data file.
///
/// Column stats include: value count, null value count, lower bounds, and upper bounds.
///
/// \param requested_columns column names for which to keep the stats.
TableScanBuilder& IncludeColumnStats(const std::vector<std::string>& requested_columns);
/// \brief Request this scan to read the given data columns.
///
/// This produces an expected schema that includes all fields that are either selected
/// or used by this scan's filter expression.
///
/// An empty list selects no user columns. A list containing `*` selects all columns.
/// If this method is not called, all columns are selected.
///
/// \param column_names column names from the table's schema
TableScanBuilder& Select(const std::vector<std::string>& column_names);
/// \brief Set the expression to filter data.
/// \param filter a filter expression
TableScanBuilder& Filter(std::shared_ptr<Expression> filter);
/// \brief Request data filtering to files but not to rows in those files.
TableScanBuilder& IgnoreResiduals();
/// \brief Request this scan to return at least the given number of rows.
///
/// This is used as a hint and is entirely optional in order to not have to return more
/// rows than necessary. This may return fewer rows if the scan does not contain that
/// many, or it may return more than requested.
///
/// \param num_rows The minimum number of rows requested
TableScanBuilder& MinRowsRequested(int64_t num_rows);
/// \brief Configure an executor for manifest planning.
///
/// The executor is borrowed and must remain alive throughout planning by scans built
/// from this builder and until any stream returned by PlanFilesStream() is destroyed.
///
/// \param executor Executor to use while planning manifests.
/// \return Reference to this for method chaining.
TableScanBuilder& PlanWith(Executor& executor);
/// \brief Request this scan to use the given snapshot by ID.
/// \param snapshot_id a snapshot ID
/// \note InvalidArgument will be returned if the snapshot cannot be found
TableScanBuilder& UseSnapshot(int64_t snapshot_id);
/// \brief Request this scan to use the given reference.
/// \param ref reference
/// \note InvalidArgument will be returned if a reference with the given name
/// could not be found
TableScanBuilder& UseRef(const std::string& ref);
/// \brief Request this scan to use the most recent snapshot as of the given time
/// in milliseconds on the branch in the scan or main if no branch is set.
/// \param timestamp_millis a timestamp in milliseconds.
/// \note InvalidArgument will be returned if the snapshot cannot be found or time
/// travel is attempted on a tag
TableScanBuilder& AsOfTime(int64_t timestamp_millis);
/// \brief Instructs this scan to look for changes starting from a particular snapshot.
///
/// This method is only available for incremental scans.
/// If the start snapshot is not configured, it defaults to the oldest ancestor of
/// the end snapshot (inclusive).
///
/// \param from_snapshot_id the start snapshot ID
/// \param inclusive whether the start snapshot is inclusive, default is false
/// \note InvalidArgument will be returned if the start snapshot is not an ancestor of
/// the end snapshot
TableScanBuilder& FromSnapshot(int64_t from_snapshot_id, bool inclusive = false)
requires IsIncrementalScan<ScanType>;
/// \brief Instructs this scan to look for changes starting from a particular snapshot.
///
/// This method is only available for incremental scans.
/// If the start snapshot is not configured, it defaults to the oldest ancestor of
/// the end snapshot (inclusive).
///
/// \param ref the start ref name that points to a particular snapshot ID
/// \param inclusive whether the start snapshot is inclusive, default is false
/// \note InvalidArgument will be returned if the start snapshot is not an ancestor of
/// the end snapshot
TableScanBuilder& FromSnapshot(const std::string& ref, bool inclusive = false)
requires IsIncrementalScan<ScanType>;
/// \brief Instructs this scan to look for changes up to a particular snapshot
/// (inclusive).
///
/// This method is only available for incremental scans.
/// If the end snapshot is not configured, it defaults to the current table snapshot
/// (inclusive).
///
/// \param to_snapshot_id the end snapshot ID (inclusive)
TableScanBuilder& ToSnapshot(int64_t to_snapshot_id)
requires IsIncrementalScan<ScanType>;
/// \brief Instructs this scan to look for changes up to a particular snapshot ref
/// (inclusive).
///
/// This method is only available for incremental scans.
/// If the end snapshot is not configured, it defaults to the current table snapshot
/// (inclusive).
///
/// \param ref the end snapshot Ref (inclusive)
TableScanBuilder& ToSnapshot(const std::string& ref)
requires IsIncrementalScan<ScanType>;
/// \brief Use the specified branch
///
/// This method is only available for incremental scans.
/// \param branch the branch name
TableScanBuilder& UseBranch(const std::string& branch)
requires IsIncrementalScan<ScanType>;
/// \brief Add a metrics reporter for this scan.
///
/// May be called multiple times; each call combines with the previous reporter
/// via MetricsReporters::Combine().
TableScanBuilder& ReportWith(std::shared_ptr<MetricsReporter> reporter);
/// \brief Builds and returns a TableScan instance.
/// \return A Result containing the TableScan or an error.
virtual Result<std::unique_ptr<ScanType>> Build();
protected:
TableScanBuilder(std::shared_ptr<TableMetadata> metadata, std::shared_ptr<FileIO> io,
std::string table_name,
std::shared_ptr<MetricsReporter> metrics_reporter);
TableScanBuilder(TableScanBuilder&&) = default;
TableScanBuilder& operator=(TableScanBuilder&&) = default;
// Return the snapshot schema for tags and time travel, else the table schema.
Result<std::reference_wrapper<const std::shared_ptr<Schema>>> ResolveSnapshotSchema();
Status ResolveColumnStatsSelection();
std::shared_ptr<TableMetadata> metadata_;
std::shared_ptr<FileIO> io_;
internal::TableScanContext context_;
std::shared_ptr<Schema> snapshot_schema_;
std::optional<std::vector<std::string>> requested_column_stats_;
};
/// \brief Represents a configured scan operation on a table.
class ICEBERG_EXPORT TableScan {
public:
virtual ~TableScan();
/// \brief Returns the table metadata being scanned.
const std::shared_ptr<TableMetadata>& metadata() const;
/// \brief Returns the snapshot to scan. If there is no snapshot, returns nullptr.
Result<std::shared_ptr<Snapshot>> snapshot() const;
/// \brief Returns the projected schema for the scan.
Result<std::shared_ptr<Schema>> schema() const;
/// \brief Returns the scan context.
const internal::TableScanContext& context() const;
/// \brief Returns the file I/O instance used for reading files.
virtual const std::shared_ptr<FileIO>& io() const;
/// \brief Returns this scan's filter expression.
const std::shared_ptr<Expression>& filter() const;
/// \brief Returns whether this scan is case-sensitive.
bool is_case_sensitive() const;
protected:
TableScan(std::shared_ptr<TableMetadata> metadata, std::shared_ptr<Schema> schema,
std::shared_ptr<FileIO> io, internal::TableScanContext context);
Result<std::reference_wrapper<const std::shared_ptr<Schema>>> ResolveProjectedSchema()
const;
virtual const std::vector<std::string>& ScanColumns() const;
const std::shared_ptr<TableMetadata> metadata_;
const std::shared_ptr<Schema> schema_;
const std::shared_ptr<FileIO> io_;
const internal::TableScanContext context_;
mutable std::shared_ptr<Schema> projected_schema_;
};
/// \brief A base template class for incremental scans that read changes between
/// snapshots, and return scan tasks of the specified type.
template <typename ScanTaskType>
class IncrementalScan : public TableScan {
public:
~IncrementalScan() override = default;
virtual Result<std::vector<std::shared_ptr<ScanTaskType>>> PlanFiles() const = 0;
protected:
virtual Result<std::vector<std::shared_ptr<ScanTaskType>>> PlanFiles(
std::optional<int64_t> from_snapshot_id_exclusive,
int64_t to_snapshot_id_inclusive) const = 0;
using TableScan::TableScan;
// Allow the free function ResolvePlanFiles to access protected members.
template <typename T>
friend Result<std::vector<std::shared_ptr<T>>> ResolvePlanFiles(
const IncrementalScan<T>& scan);
};
} // namespace iceberg