| /* | |
| * 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 |