From e0194a3816c582a522a06486c32d5c885765be00 Mon Sep 17 00:00:00 2001 From: ericyuanhui <285521263@qq.com> Date: Mon, 31 Aug 2026 14:29:55 +0800 Subject: [PATCH 1/3] fix iceberg extension REST catalog file binder exception Signed-off-by: ericyuanhui <285521263@qq.com> --- src/binder/bind/bind_file_scan.cpp | 42 ++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/binder/bind/bind_file_scan.cpp b/src/binder/bind/bind_file_scan.cpp index d8b041642..bfdb60217 100644 --- a/src/binder/bind/bind_file_scan.cpp +++ b/src/binder/bind/bind_file_scan.cpp @@ -1,3 +1,5 @@ +#include + #include "binder/binder.h" #include "binder/bound_scan_source.h" #include "binder/expression/literal_expression.h" @@ -120,12 +122,48 @@ bool handleFileViaFunction(main::ClientContext* context, std::vector& options) { + // Keep this workaround scoped to a loaded Iceberg REST configuration. + auto warehouseOption = context->getExtensionOption("iceberg_warehouse"); + if (warehouseOption == nullptr || + context->getCurrentSetting("iceberg_warehouse").toString().empty()) { + return false; + } + auto format = options.find(FileScanInfo::FILE_FORMAT_OPTION_NAME); + if (format == options.end() || StringUtils::getLower(format->second.toString()) != "iceberg") { + return false; + } + if (source.find('/') != std::string::npos || source.find('\\') != std::string::npos) { + return false; + } + idx_t dot_count = 0; + for (idx_t i = 0; i < source.size(); i++) { + auto c = static_cast(source[i]); + if (source[i] == '.') { + dot_count++; + continue; + } + if ((i == 0 || source[i - 1] == '.') && !std::isalpha(c) && source[i] != '_') { + return false; + } + if (!std::isalnum(c) && source[i] != '_' && source[i] != '$') { + return false; + } + } + return dot_count == 1 || dot_count == 2; +} + std::unique_ptr Binder::bindFileScanSource(const BaseScanSource& scanSource, const options_t& options, const std::vector& columnNames, const std::vector& columnTypes) { auto fileSource = scanSource.constPtrCast(); - auto filePaths = bindFilePaths(fileSource->filePaths); auto boundOptions = bindParsingOptions(options); + bool catalogName = fileSource->filePaths.size() == 1 && + isIcebergCatalogName(clientContext, fileSource->filePaths[0], boundOptions); + auto filePaths = catalogName ? fileSource->filePaths : bindFilePaths(fileSource->filePaths); FileTypeInfo fileTypeInfo; if (boundOptions.contains(FileScanInfo::FILE_FORMAT_OPTION_NAME)) { @@ -136,7 +174,7 @@ std::unique_ptr Binder::bindFileScanSource(const BaseScanSo } // If we defined a certain FileType, we have to ensure the path is a file, not something else // (e.g. an existed directory) - if (fileTypeInfo.fileType != FileType::UNKNOWN) { + if (fileTypeInfo.fileType != FileType::UNKNOWN && !catalogName) { for (const auto& filePath : filePaths) { if (!LocalFileSystem::fileExists(filePath) && LocalFileSystem::isLocalPath(filePath)) { throw BinderException{std::format("Provided path is not a file: {}.", filePath)}; From cd347de97c052837e2ee08e0ef3214aa253fb024 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Tue, 1 Sep 2026 08:53:28 -0700 Subject: [PATCH 2/3] Generalize iceberg to any extension defined path --- src/binder/bind/bind_file_scan.cpp | 70 +++++++++++------------------- src/include/binder/binder.h | 3 +- 2 files changed, 28 insertions(+), 45 deletions(-) diff --git a/src/binder/bind/bind_file_scan.cpp b/src/binder/bind/bind_file_scan.cpp index bfdb60217..8297f1f5c 100644 --- a/src/binder/bind/bind_file_scan.cpp +++ b/src/binder/bind/bind_file_scan.cpp @@ -1,5 +1,3 @@ -#include - #include "binder/binder.h" #include "binder/bound_scan_source.h" #include "binder/expression/literal_expression.h" @@ -47,7 +45,8 @@ FileTypeInfo Binder::bindFileTypeInfo(const std::vector& filePaths) return expectedFileType; } -std::vector Binder::bindFilePaths(const std::vector& filePaths) const { +std::vector Binder::bindFilePaths(const std::vector& filePaths, + bool passThroughOnNoMatch) const { std::vector boundFilePaths; for (auto& filePath : filePaths) { // This is a temporary workaround because we use duckdb to read from iceberg/delta/azure. @@ -66,6 +65,13 @@ std::vector Binder::bindFilePaths(const std::vector& f auto globbedFilePaths = VirtualFileSystem::GetUnsafe(*clientContext)->glob(clientContext, filePath); if (globbedFilePaths.empty()) { + if (passThroughOnNoMatch) { + // The format's scan function owns the interpretation of the scan source, so + // leave the source unmodified for the extension to resolve (e.g. as a + // REST catalog table identifier) and to produce its own error if invalid. + boundFilePaths.push_back(filePath); + continue; + } throw BinderException{ std::format("No file found that matches the pattern: {}.", filePath)}; } @@ -122,59 +128,35 @@ bool handleFileViaFunction(main::ClientContext* context, std::vector& options) { - // Keep this workaround scoped to a loaded Iceberg REST configuration. - auto warehouseOption = context->getExtensionOption("iceberg_warehouse"); - if (warehouseOption == nullptr || - context->getCurrentSetting("iceberg_warehouse").toString().empty()) { - return false; - } - auto format = options.find(FileScanInfo::FILE_FORMAT_OPTION_NAME); - if (format == options.end() || StringUtils::getLower(format->second.toString()) != "iceberg") { - return false; - } - if (source.find('/') != std::string::npos || source.find('\\') != std::string::npos) { - return false; - } - idx_t dot_count = 0; - for (idx_t i = 0; i < source.size(); i++) { - auto c = static_cast(source[i]); - if (source[i] == '.') { - dot_count++; - continue; - } - if ((i == 0 || source[i - 1] == '.') && !std::isalpha(c) && source[i] != '_') { - return false; - } - if (!std::isalnum(c) && source[i] != '_' && source[i] != '$') { - return false; - } - } - return dot_count == 1 || dot_count == 2; -} - std::unique_ptr Binder::bindFileScanSource(const BaseScanSource& scanSource, const options_t& options, const std::vector& columnNames, const std::vector& columnTypes) { auto fileSource = scanSource.constPtrCast(); auto boundOptions = bindParsingOptions(options); - bool catalogName = fileSource->filePaths.size() == 1 && - isIcebergCatalogName(clientContext, fileSource->filePaths[0], boundOptions); - auto filePaths = catalogName ? fileSource->filePaths : bindFilePaths(fileSource->filePaths); + auto formatOption = boundOptions.find(FileScanInfo::FILE_FORMAT_OPTION_NAME); + // An explicitly declared format that is not one of the built-in file types is provided by + // an extension (e.g. 'iceberg', 'delta'). The _SCAN table function registered by + // that extension owns the interpretation of the scan source: it may be a filesystem path, + // but it can also be a logical identifier (e.g. an Iceberg REST catalog table name) that + // must not be validated against the filesystem. When globbing finds no match, the source + // is therefore passed through verbatim so the extension can resolve it and produce its + // own error if invalid. + const bool extensionFormat = + formatOption != boundOptions.end() && + FileTypeUtils::fromString(formatOption->second.toString()) == FileType::UNKNOWN; + auto filePaths = + bindFilePaths(fileSource->filePaths, extensionFormat /* passThroughOnNoMatch */); FileTypeInfo fileTypeInfo; - if (boundOptions.contains(FileScanInfo::FILE_FORMAT_OPTION_NAME)) { - auto fileFormat = boundOptions.at(FileScanInfo::FILE_FORMAT_OPTION_NAME).toString(); + if (formatOption != boundOptions.end()) { + auto fileFormat = formatOption->second.toString(); fileTypeInfo = FileTypeInfo{FileTypeUtils::fromString(fileFormat), fileFormat}; } else { fileTypeInfo = bindFileTypeInfo(filePaths); } // If we defined a certain FileType, we have to ensure the path is a file, not something else - // (e.g. an existed directory) - if (fileTypeInfo.fileType != FileType::UNKNOWN && !catalogName) { + // (e.g. an existed directory). Extension-provided formats interpret the source themselves. + if (fileTypeInfo.fileType != FileType::UNKNOWN) { for (const auto& filePath : filePaths) { if (!LocalFileSystem::fileExists(filePath) && LocalFileSystem::isLocalPath(filePath)) { throw BinderException{std::format("Provided path is not a file: {}.", filePath)}; diff --git a/src/include/binder/binder.h b/src/include/binder/binder.h index e487eb63b..11038f09c 100644 --- a/src/include/binder/binder.h +++ b/src/include/binder/binder.h @@ -188,7 +188,8 @@ class Binder { common::case_insensitive_map_t bindParsingOptions( const parser::options_t& parsingOptions); common::FileTypeInfo bindFileTypeInfo(const std::vector& filePaths) const; - std::vector bindFilePaths(const std::vector& filePaths) const; + std::vector bindFilePaths(const std::vector& filePaths, + bool passThroughOnNoMatch = false) const; /*** bind query ***/ std::unique_ptr bindQuery(const parser::Statement& statement); From 300b67da4adffc8fdb2202ce9d8c1cb2b58a4183 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Tue, 1 Sep 2026 10:34:50 -0700 Subject: [PATCH 3/3] Update extension submodule --- extension | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension b/extension index 371e60889..988456458 160000 --- a/extension +++ b/extension @@ -1 +1 @@ -Subproject commit 371e60889597bfc96759e57ad945af06635874fc +Subproject commit 988456458e64b26cc3c4bb3663fe2e2f2f21d347