Repository navigation
Conversation
There was a problem hiding this comment.
@lucasfang, I left some comments for this pull request. PTAL.
| std::shared_ptr<FileStorePathFactory> path_factory, | ||
| store_->NewPathFactory(arrow_schema, core_options, | ||
| core_options.GetFileFormat()->Identifier(), memory_pool)); | ||
| return NewTableRead(path_factory, internal_context, memory_pool, executor); |
There was a problem hiding this comment.
@lucasfang, select the fallback reader from the fallback schema. CreateTableRead now dispatches through the main table's NewTableRead implementation, including when constructing the fallback reader. An append-only main table can have a primary-key fallback branch—the existing append_table_with_rt_branch fixture does. Such fallback splits now get AppendOnlyTableRead without a MergeFileSplitRead, so splits requiring merging or delete handling fail with create reader failed, not read match with split. The previous implementation selected the reader from internal_context->GetPrimaryKeys(). Please select the reader using the fallback schema's primary keys.
| GlobalSystemTableContext context; | ||
| context.catalog = this; | ||
| context.fs = fs_; | ||
| context.warehouse = warehouse_; | ||
| context.catalog_options = api_->GetMergedOptions(); | ||
| return GlobalSystemTableLoader::Load(identifier.GetObjectName(), context); |
There was a problem hiding this comment.
@lucasfang, preserve the catalog lifetime for global system tables. The returned system table stores this as a non-owning catalog pointer. After GetTable returns, destroying the catalog leaves sys.tables, sys.partitions and sys.all_table_options with a dangling pointer; their lazy BuildRows calls dereference it during NextBatch. Neither the table nor its reader retains the catalog (InMemorySystemTableRead retains only the table and memory pool). Please retain the required catalog state for the returned table's lifetime.
| for (const auto& [key, value] : dynamic_options) { | ||
| if (key != Options::FILE_SYSTEM && key.rfind("fs.", 0) != 0) { | ||
| continue; |
There was a problem hiding this comment.
@lucasfang, recognize native S3 options when rebuilding managers. This check ignores supported options such as s3.endpoint, s3.access-key and s3.session.token. When the filesystem is resolved from options, Copy reparses these into a new filesystem but retains the old schema and snapshot managers, leaving metadata requests on the old endpoint or credentials while data requests use the new settings. Please include native S3 options in the invalidation check. An isolated check of this helper confirmed that changing s3.endpoint returns false, whereas changing fs.s3.endpoint returns true.
| for (const auto& [key, value] : context->GetOptions()) { | ||
| if (key != Options::TYPE) { | ||
| options[key] = value; | ||
| } | ||
| } |
There was a problem hiding this comment.
@lucasfang, carry copied table options into fallback reads. The fallback merges only context options, whereas the main reader uses the copied table's options. For a primary-key table copied with table-read.sequence-number.enabled=true and a fallback branch, reading _SEQUENCE_NUMBER with an otherwise empty context succeeds for the main reader but fails while constructing the fallback reader because that flag is lost. InternalReadContext then treats _SEQUENCE_NUMBER as a regular field missing from the fallback schema. Please propagate the table's effective dynamic overrides to the fallback as well.
|
|
||
| /// The schema manager the operators read through. It carries the schema cache of `root_path` | ||
| /// on `branch()`. | ||
| const std::shared_ptr<SchemaManager>& schema_manager() const { |
There was a problem hiding this comment.
why these function name is lower_case, not UpperCase?
| const std::string& uuid = "", | ||
| const std::string& branch = BranchManager::DEFAULT_MAIN_BRANCH, | ||
| const std::shared_ptr<SchemaManager>& schema_manager = nullptr, | ||
| const std::shared_ptr<SnapshotManager>& snapshot_manager = nullptr, | ||
| const std::map<std::string, std::string>& fs_scheme_map = {}); |
There was a problem hiding this comment.
Do not use default values(also in some other files, total 10 functions used)
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::ScanContext> scan_context, | ||
| scan_builder.Finish()); | ||
| PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::TableScan> scan, | ||
| paimon::TableScan::Create(std::move(scan_context))); |
There was a problem hiding this comment.
I’m a bit confused—now that expect for a data table, shouldn’t all the other table types use the table->NewScan()-style interfaces?
| class PAIMON_EXPORT TableScan { | ||
| public: | ||
| /// Create an instance of `TableScan`. | ||
| /// Create an instance of `TableScan`: the table the context names is loaded first, then the |
There was a problem hiding this comment.
"the table the context names" -> "the table created by the context"?
| /// @param table The format table to read, as `Catalog::GetFormatTable()` hands it back. | ||
| /// @param table The format table to read, as `Catalog::GetTable()` hands it back. | ||
| explicit ReadContextBuilder(const std::shared_ptr<FormatTable>& table); | ||
|
|
There was a problem hiding this comment.
I’m thinking about removing FormatTable from xxxContextBuilder. Wouldn’t it be better if all special tables also go through table (i.e., create the behavior via table)? Only the data table might be allowed to be created directly via the context.
Otherwise, the current design feels a bit too coupled between context and table.
| ASSERT_TRUE(std::dynamic_pointer_cast<SystemTable>(system_table) != nullptr); | ||
| ASSERT_TRUE(std::dynamic_pointer_cast<FormatTable>(system_table) == nullptr); | ||
| ASSERT_EQ("options", system_table->Name()); | ||
| ASSERT_EQ("options", system_table->FullName()); |
There was a problem hiding this comment.
For system tables, should the full name and name be tbl1$options?
| static Result<std::unique_ptr<InternalReadContext>> Create( | ||
| const std::shared_ptr<ReadContext>& read_context, | ||
| const std::shared_ptr<TableSchema>& table_schema, CoreOptions core_options, | ||
| const std::string* path = nullptr); |
There was a problem hiding this comment.
Prefer not to use default parameters in the production code if possible—otherwise we can’t be sure every call site has been updated. Also, why is CoreOptions not const &?
And adding an extra const std::string* path sounds a bit odd.
| return Status::NotImplemented( | ||
| "a format table has no snapshots, so there is nothing a streaming write could " | ||
| "commit against"); | ||
| } |
There was a problem hiding this comment.
Are these actually “NotImplemented”, or are some of them “Invalid”?
| // default backend. | ||
| .WithFileSystem(context->GetSpecificFileSystem() != nullptr | ||
| ? context->GetSpecificFileSystem() | ||
| : GetFileSystem()) |
There was a problem hiding this comment.
Why doesn’t this consider context->GetFileSystemSchemeToIdentifierMap()?
It seems a bit asymmetric with read.
| // the base holds, so its own read-only schema is adopted before it leaves the | ||
| // loader. | ||
| PAIMON_RETURN_NOT_OK(system_table->AdoptOwnSchema()); | ||
| return system_table; |
There was a problem hiding this comment.
This design is a bit coupled—similar to an init function.
Do we need SystemTable::GetSchema to override with its own logic?
| /// `Table` interface as every other kind of table. `Name()` - which each kind of system table | ||
| /// answers for itself - is its full name as well: the identifier it is opened by belongs to the | ||
| /// data table underneath and would name that one. | ||
| class SystemTable : public Table, public std::enable_shared_from_this<SystemTable> { |
There was a problem hiding this comment.
Why is this designed this way here? In Java it just returns "tbl$options".
| /// The shared ownership a returned table keeps of its catalog, so its lazy `BuildRows` | ||
| /// cannot read through one already destroyed. Empty where the table is transient - used | ||
| /// within the call, before the catalog can go away. | ||
| std::shared_ptr<const Catalog> catalog_retainer; |
There was a problem hiding this comment.
This design is a bit weird—why can’t we just use std::shared_ptr<Catalog> directly?
| class ManifestList; | ||
| class TableSchema; | ||
|
|
||
| /// The storage a managed table is served through: the file system, root path, schema, branch, |
There was a problem hiding this comment.
There are so many sentence "XXX a XXX" in this PR, I think it's difficult to understand
There was a problem hiding this comment.
and there are many long, complex sentences in the comments. Could you simplify them and express them in plain, easy-to-understand language?
| // handed back has to keep it alive or the pointer dangles at the first `BuildRows`. A | ||
| // catalog not owned by a `std::shared_ptr` cannot be kept, so it is refused by name. | ||
| std::shared_ptr<const RestCatalog> self = weak_from_this().lock(); | ||
| if (self == nullptr) { |
There was a problem hiding this comment.
Catalog::Create() returns std::unique_ptr<Catalog>. Converting that result to std::shared_ptr<Catalog> does not initialize enable_shared_from_this<RestCatalog>, because the constructor sees the static type Catalog, which has no such base. Consequently, weak_from_this().lock() remains empty and loading sys.tables fails even when the catalog is owned by a shared_ptr.
The new test converts unique_ptr<RestCatalog> directly to shared_ptr<RestCatalog>, so it does not cover this public factory path. Could we make catalog retention work through Catalog::Create() and add a regression test using that path?
There was a problem hiding this comment.
Confirmed this still occurs at 13fba3780476ce4177d2197e0d7f62816bdd3047. A standalone C++17 reproduction of the same inheritance and ownership conversion leaves weak_from_this() empty after promoting unique_ptr<Catalog> to shared_ptr<Catalog>, while promoting unique_ptr<RestCatalog> initializes it. The current test exercises only the latter, so the public Catalog::Create() path still cannot load global system tables. Please cover that public factory path in the retention fix.
SteNicholas
left a comment
There was a problem hiding this comment.
One additional correctness issue remains in the direct system-table read path.
| Status SystemTable::CheckNoSearch(const std::shared_ptr<ReadContext>& context) { | ||
| if (context->HasVectorOrFullTextSearch()) { | ||
| return Status::NotImplemented( | ||
| "VectorSearch or FullTextSearch does not support system-table reads yet"); | ||
| } | ||
| return Status::OK(); |
There was a problem hiding this comment.
[P2] Reject FullTextMatch on direct system-table reads
HasVectorOrFullTextSearch() only checks TopK requests; it does not detect FullTextMatch predicates supplied through ReadContextBuilder::SetPredicate(). Calling NewRead() on an in-memory system table (for example, t$options) therefore passes this guard and returns all rows without evaluating the predicate. The static TableRead::Create() path rejects the same request with NotImplemented.
Please also check PredicateUtils::ContainsFullTextMatch(context->GetPredicate()) here and add a regression test through the direct Table::NewRead() entry point.
Purpose
Linked issue: close #459
The single
Tableclass dispatched managed, format, and system tables throughifbranches in one 1400-linetable.cpp; every operator call re-merged an options map and re-parsedCoreOptions(Java'sAbstractFileStoreparses once), and the served file system was resolved before the schema's access configuration was known. This splits the hierarchy the way Java splits it and gives context options one clear place to speak, fixing the defects that the ambiguity hid.Changes:
Table(include/paimon/catalog/table.h) becomes an abstract base:Name/FullName/NewXxx/Copyare virtual, the constructor is protected. Implemented byFileStoreTable(withAppendOnlyFileStoreTable/PrimaryKeyFileStoreTablesubclasses,src/paimon/core/table/), byFormatTable(now deriving fromTable, owning its own assembly —format_table_loaderis absorbed), and by theSystemTablefamily.FileStore(src/paimon/core/table/file_store.h/.cpp), the counterpart of Java'sAbstractFileStore: materializesCoreOptionsonce through astatic Create(), resolves the served file system after the options are known (schema-carried access configuration names its own backend; the caller's explicit file system wins over it), carries the file-system scheme map, and hosts the operator factories.FileStoreTableholds its schema/snapshot managers and its branch;Copy()hands the managers over unless a file-system option among the copy's changes the resolution, and never half-appliestypeorbranch;SwitchToBranch()derives a table of another branch.TableScan::Create(ctx)etc.) merges them over the schema's throughTable::Copy()— a table is the product of its load — whileNewXxxon a held table refuses a contradicting option (option 'X' is 'A' in the context but 'B' on the table, values redacted throughSensitiveConfigUtils::RedactValue). The four context builders document the semantics.RestCatalog(src/paimon/rest/rest_catalog.cpp):GetTableserves the global system tables of thesysdatabase locally (they previously reached the managed-table load, which statically cast their read-only schema — undefined behavior), binds per-table system tables toGetTableFileSystem()so data-token credentials reach$snapshotsand friends, andFileStoreTable::Createrefuses a non-managed schema instead of casting it.read_optimized_system_table.cpp,audit_log_system_table.cpp): the underlying scans and reads prefer the context's file system and fall back to the table's own only when the context names neither a file system nor a scheme map (the previous unconditional fallback was refused when a scheme map was present). The scan-fallback read context merges the load's dynamic options, so the fallback reader recognises a sequence-number column the main one does.Tests
src/paimon/core/table/table_test.cpp(expanded): managers of the branch a table was loaded with,SwitchToBranch,Copykeeping that branch when handed abranchoption (both directions, verified to fail before the fix), operators refusing an option that contradicts a held table, a factory-path override of a schema-carried key, cache injection, thesys-database REST open, and the per-table file system.src/paimon/core/table/source/table_read_test.cpp(TestMergeOptions,TestDependencyReadWithoutCatalogUsesReadOptions): factory-path merging of context options into the served schema, and the dependency catalog context seeing the merged header value.src/paimon/core/catalog/file_system_catalog_test.cpp,src/paimon/rest/rest_catalog_test.cpp, and the inte tests (scan_and_read,realtime_write,read,scan,write_and_read_inte_with_index) adapted to theTableAPI.PAIMON_ENABLE_REST=ON):paimon-core-test2492/2492;paimon-rest-test152/152;ctest -j 9632/33 — the only failure ispaimon-write-and-read-inte-testwith its 2("parquet", "jindo")jindo-credential environment cases, unchanged from the baseline;paimon-data-evolution-table-test306/306 andpaimon-scan-and-read-inte-test511/511;pre-commit run --files <changed>passed (clang-format applied);git diff --checkclean.API and Format
No storage format or protocol change. Breaking public API changes:
Tableis now abstract (constructor protected,Name/FullName/NewXxx/Copyvirtual,Table::CurrentBranch()replaced byFileStoreTable::CurrentBranch());FormatTablederives fromTable(staticFormatTable::Copy(table, options)becomes the virtualTable::Copy(options),Location()becomesGetRootPath());FileStoreTable::SwitchToBranchis new. Observable semantics: schema options that do not parse fail at table-build time (previously at the first operator); a table without a schema-carried backend or a caller file system is served by the default local one instead of a null one; context options are merged at load time only.Documentation
No new user-facing feature; the option semantics on the four context builders are documented in the same change, and
docs/source/user_guide/format_table.rstfollows theFormatTableAPI rename.Generative AI tooling
Generated-by: Qoder