Repository navigation
[feat] add Parquet metadata TVF - #58972
Conversation
|
Thank you for your contribution to Apache Doris. Please clearly describe your PR:
|
| META_STATISTICS_MAX | ||
| }; | ||
|
|
||
| std::string join_path(const std::vector<std::string>& items) { |
There was a problem hiding this comment.
Use join() in be/src/util/string_util.h
| } | ||
|
|
||
| std::string encodings_to_string(const std::vector<tparquet::Encoding::type>& encodings) { | ||
| std::ostringstream oss; |
There was a problem hiding this comment.
use fmt lib instead of std::ostringstream
| } | ||
|
|
||
| template <typename ColumnType, typename T> | ||
| void insert_numeric_impl(MutableColumnPtr& column, T value) { |
There was a problem hiding this comment.
There are some util method in be/src/exec/schema_scanner/schema_scanner_helper.cpp. You can reuse them
| } | ||
| } | ||
|
|
||
| std::string physical_type_to_string(tparquet::Type::type type) { |
There was a problem hiding this comment.
extract these xx_to_xx methods to a new file parquet_utils.h. So that they can be used in other places, maybe.
|
|
||
| } // namespace |
There was a problem hiding this comment.
All methods in this namespaces should be moved to somewhere else, like parquet_utils.h
| explicit ParquetSchemaModeHandler(RuntimeState* state) : ModeHandler(state) {} | ||
|
|
||
| void init_slot_pos_map(const std::vector<SlotDescriptor*>& slots) override { | ||
| std::unordered_map<std::string, int> name_to_pos = _build_name_to_pos_map(slots); |
There was a problem hiding this comment.
| std::unordered_map<std::string, int> name_to_pos = _build_name_to_pos_map(slots); | |
| const auto& name_to_pos = _build_name_to_pos_map(slots); |
| if (params.__isset.file_type) { | ||
| _file_type = params.file_type; | ||
| } else if (!_paths.empty()) { | ||
| std::string lower_path = to_lower(_paths.front()); |
There was a problem hiding this comment.
the file_type should be handled on FE side. No need to handle it on BE side
| case TASKS: | ||
| return TasksTableValuedFunction.getColumnIndexFromColumnName(columnName, params); | ||
| case PARQUET: | ||
| return null; |
| } | ||
| StorageProperties storageProperties = null; | ||
| if (Strings.isNullOrEmpty(scheme) || "file".equalsIgnoreCase(scheme)) { | ||
| this.fileType = TFileType.FILE_LOCAL; |
There was a problem hiding this comment.
We have LocalProperties, so I don't think we need this if..else
| if (Strings.isNullOrEmpty(rawPath)) { | ||
| throw new AnalysisException("Property 'path' is required for parquet_metadata"); | ||
| } | ||
| List<String> parsedPaths = Arrays.stream(rawPath.split(",")) |
There was a problem hiding this comment.
I think we don't need file list with comma, only:
- full file name
- file name with wildcard
| } | ||
|
|
||
| // Expand any glob patterns (e.g. *.parquet) to concrete file paths. | ||
| normalizedPaths = expandGlobPaths(normalizedPaths, storageProperties, this.fileType); |
There was a problem hiding this comment.
You can simply refer to BrokerUtil.parseFile(path, brokerDesc, fileStatuses);.
See parseFile() method in ExternalFileTableValuedFunction
| @@ -0,0 +1,7 @@ | |||
| #!/bin/bash | |||
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 11 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| unset http_proxy | ||
| unset https_proxy | ||
| cd /mnt/disk1/chenjunwei/doris_build/doris/output/be |
There was a problem hiding this comment.
This file appears to be a personal development script with a hardcoded path (/mnt/disk1/chenjunwei/doris_build/doris/output/be) that should not be committed to the repository. This poses several issues: it exposes the developer's username and local directory structure, won't work on other machines, and is unrelated to the Parquet metadata feature being added.
| cd /mnt/disk1/chenjunwei/doris_build/doris/output/be | |
| # Set BE_DIR from first argument, environment variable, or default to current directory | |
| BE_DIR="${1:-${BE_DIR:-$(pwd)}}" | |
| cd "$BE_DIR" |
| String rawMode = normalizedParams.getOrDefault(MODE, MODE_METADATA); | ||
| mode = rawMode.toLowerCase(); | ||
| if (!SUPPORTED_MODES.contains(mode)) { | ||
| throw new AnalysisException("Unsupported mode '" + rawMode + "' for parquet_metadata"); |
There was a problem hiding this comment.
The error message references 'parquet_metadata' but the actual function name is 'parquet_meta'. This should be updated for consistency with the function name users actually invoke.
| expanded.addAll(expandSingleGlob(path, storageProperties, fileType)); | ||
| } | ||
| if (expanded.isEmpty()) { | ||
| throw new AnalysisException("No files matched parquet_metadata path patterns: " + inputPaths); |
There was a problem hiding this comment.
The error message references 'parquet_metadata' but the actual function name is 'parquet_meta'. This should be updated for consistency with the function name users actually invoke.
| throw new AnalysisException("No files matched parquet_metadata path patterns: " + inputPaths); | |
| throw new AnalysisException("No files matched parquet_meta path patterns: " + inputPaths); |
| case AZURE: | ||
| return TFileType.FILE_S3; | ||
| default: | ||
| throw new AnalysisException("Unsupported storage type for parquet_metadata: " + type); |
There was a problem hiding this comment.
The error message references 'parquet_metadata' but the actual function name is 'parquet_meta'. This should be updated for consistency with the function name users actually invoke.
| throw new AnalysisException("Unsupported storage type for parquet_metadata: " + type); | |
| throw new AnalysisException("Unsupported storage type for parquet_meta: " + type); |
| Status ParquetMetadataReader::_init_from_scan_range(const TMetaScanRange& scan_range) { | ||
| if (!scan_range.__isset.parquet_params) { | ||
| return Status::InvalidArgument( | ||
| "Missing parquet parameters for parquet_metadata table function"); |
There was a problem hiding this comment.
The error message references 'parquet_metadata table function' but the actual function name is 'parquet_meta'. For consistency and clarity, the error message should use 'parquet_meta' to match the function name users actually invoke.
| "Missing parquet parameters for parquet_metadata table function"); | |
| "Missing parquet parameters for parquet_meta table function"); |
|
|
||
| private: | ||
|
|
||
|
|
There was a problem hiding this comment.
There are two consecutive blank lines after the 'private:' access specifier. This is inconsistent with typical C++ style guides which recommend a single blank line. Consider removing one of the blank lines to maintain consistent formatting.
| )); | ||
| String rawPath = normalizedParams.get(PATH); | ||
| if (Strings.isNullOrEmpty(rawPath)) { | ||
| throw new AnalysisException("Property 'path' is required for parquet_metadata"); |
There was a problem hiding this comment.
The error message references 'parquet_metadata' but the actual function name is 'parquet_meta' (as defined in line 64). This inconsistency could confuse users. All error messages should consistently use 'parquet_meta' to match the actual function name used in SQL queries.
| throw new AnalysisException( | ||
| "Failed to parse storage properties for parquet_metadata: " + e.getMessage(), e); |
There was a problem hiding this comment.
The error message references 'parquet_metadata' but the actual function name is 'parquet_meta'. This should be updated for consistency with the function name users actually invoke.
| throw new AnalysisException( | ||
| "Failed to normalize parquet_metadata paths: " + e.getMessage(), e); |
There was a problem hiding this comment.
The error message references 'parquet_metadata' but the actual function name is 'parquet_meta'. This should be updated for consistency with the function name users actually invoke.
| } | ||
| const TParquetMetadataParams& params = scan_range.parquet_params; | ||
| if (!params.__isset.paths || params.paths.empty()) { | ||
| return Status::InvalidArgument("Property 'path' must be set for parquet_metadata"); |
There was a problem hiding this comment.
The error message references 'parquet_metadata' but the actual function name is 'parquet_meta'. For consistency and clarity, the error message should use 'parquet_meta' to match the function name users actually invoke.
| return Status::InvalidArgument("Property 'path' must be set for parquet_metadata"); | |
| return Status::InvalidArgument("Property 'path' must be set for parquet_meta"); |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** | ||
| * Table-valued function for reading Parquet metadata. | ||
| * Currently works in two modes: | ||
| * - parquet_metadata: row-group/column statistics similar to DuckDB parquet_metadata() | ||
| * - parquet_schema: logical schema similar to DuckDB parquet_schema() | ||
| */ |
There was a problem hiding this comment.
The class documentation describes modes as "parquet_metadata" and "parquet_schema" which matches the internal implementation, but the function name is "parquet_meta" (line 64). Consider updating the documentation to clarify that the function is called "parquet_meta" and accepts a "mode" parameter with values "parquet_metadata" or "parquet_schema".
|
|
||
| private: | ||
|
|
||
|
|
There was a problem hiding this comment.
There are extra blank lines between the private access specifier and the first private method declaration. Remove one of the blank lines for consistency with the codebase style.
| #!/bin/bash | ||
| set -euo pipefail | ||
|
|
||
| unset http_proxy | ||
| unset https_proxy | ||
| cd /mnt/disk1/chenjunwei/doris_build/doris/output/be | ||
| ./bin/start_be.sh --daemon |
There was a problem hiding this comment.
This file appears to be a personal development script with hardcoded paths (e.g., /mnt/disk1/chenjunwei/doris_build/doris/output/be) and should not be included in the PR. It should be removed from the repository.
| #!/bin/bash | |
| set -euo pipefail | |
| unset http_proxy | |
| unset https_proxy | |
| cd /mnt/disk1/chenjunwei/doris_build/doris/output/be | |
| ./bin/start_be.sh --daemon |
| try { | ||
| backend = Env.getCurrentSystemInfo().getBackendsByCurrentCluster().values().stream() | ||
| .filter(Backend::isAlive) | ||
| .findFirst() | ||
| .orElse(null); | ||
| } catch (AnalysisException e) { | ||
| throw e; | ||
| } |
There was a problem hiding this comment.
The catch block unnecessarily catches and re-throws the same AnalysisException without adding any value. The try-catch block should be removed since getBackendsByCurrentCluster() already throws AnalysisException which will propagate naturally.
| try { | |
| backend = Env.getCurrentSystemInfo().getBackendsByCurrentCluster().values().stream() | |
| .filter(Backend::isAlive) | |
| .findFirst() | |
| .orElse(null); | |
| } catch (AnalysisException e) { | |
| throw e; | |
| } | |
| backend = Env.getCurrentSystemInfo().getBackendsByCurrentCluster().values().stream() | |
| .filter(Backend::isAlive) | |
| .findFirst() | |
| .orElse(null); |
| case AZURE: | ||
| return TFileType.FILE_S3; | ||
| default: | ||
| throw new AnalysisException("Unsupported storage type for parquet_metadata: " + type); |
There was a problem hiding this comment.
Error messages reference "parquet_metadata" instead of the actual function name "parquet_meta". This inconsistency appears in multiple error messages (lines 125, 132, 139, 162, 174, 207, 226, 338). All error messages should use the actual function name "parquet_meta" for clarity.
| throw new AnalysisException("Unsupported storage type for parquet_metadata: " + type); | |
| throw new AnalysisException("Unsupported storage type for parquet_meta: " + type); |
| "Missing parquet parameters for parquet_metadata table function"); | ||
| } | ||
| const TParquetMetadataParams& params = scan_range.parquet_params; | ||
| if (!params.__isset.paths || params.paths.empty()) { | ||
| return Status::InvalidArgument("Property 'path' must be set for parquet_metadata"); |
There was a problem hiding this comment.
Error messages in the backend reference "parquet_metadata" instead of the actual function name "parquet_meta". This is inconsistent with the function name defined on the frontend (NAME = "parquet_meta"). Update these error messages to use "parquet_meta" for consistency.
| "Missing parquet parameters for parquet_metadata table function"); | |
| } | |
| const TParquetMetadataParams& params = scan_range.parquet_params; | |
| if (!params.__isset.paths || params.paths.empty()) { | |
| return Status::InvalidArgument("Property 'path' must be set for parquet_metadata"); | |
| "Missing parquet parameters for parquet_meta table function"); | |
| } | |
| const TParquetMetadataParams& params = scan_range.parquet_params; | |
| if (!params.__isset.paths || params.paths.empty()) { | |
| return Status::InvalidArgument("Property 'path' must be set for parquet_meta"); |
| public ParquetMetadataTableValuedFunction(Map<String, String> params) throws AnalysisException { | ||
| Map<String, String> normalizedParams = params.entrySet().stream().collect(Collectors.toMap( | ||
| entry -> entry.getKey().toLowerCase(), | ||
| Map.Entry::getValue, | ||
| (value1, value2) -> value2 | ||
| )); | ||
| String rawPath = normalizedParams.get(PATH); | ||
| if (Strings.isNullOrEmpty(rawPath)) { | ||
| throw new AnalysisException("Property 'path' is required for parquet_metadata"); | ||
| } | ||
| List<String> parsedPaths = Arrays.stream(rawPath.split(",")) | ||
| .map(String::trim) | ||
| .filter(token -> !token.isEmpty()) | ||
| .collect(Collectors.toList()); | ||
| if (parsedPaths.isEmpty()) { | ||
| throw new AnalysisException("Property 'path' must contain at least one location"); | ||
| } | ||
| List<String> normalizedPaths = parsedPaths; | ||
|
|
||
| String rawMode = normalizedParams.getOrDefault(MODE, MODE_METADATA); | ||
| mode = rawMode.toLowerCase(); | ||
| if (!SUPPORTED_MODES.contains(mode)) { | ||
| throw new AnalysisException("Unsupported mode '" + rawMode + "' for parquet_metadata"); | ||
| } | ||
|
|
||
| String firstPath = parsedPaths.get(0); | ||
| String scheme = null; | ||
| try { | ||
| scheme = new URI(firstPath).getScheme(); | ||
| } catch (URISyntaxException e) { | ||
| scheme = null; | ||
| } | ||
| StorageProperties storageProperties = null; | ||
| if (Strings.isNullOrEmpty(scheme) || "file".equalsIgnoreCase(scheme)) { | ||
| this.fileType = TFileType.FILE_LOCAL; | ||
| this.properties = Collections.emptyMap(); | ||
| } else { | ||
| Map<String, String> storageParams = new HashMap<>(normalizedParams); | ||
| // StorageProperties detects provider by "uri", we also hint support by scheme. | ||
| storageParams.put("uri", firstPath); | ||
| forceStorageSupport(storageParams, scheme.toLowerCase()); | ||
| try { | ||
| storageProperties = StorageProperties.createPrimary(storageParams); | ||
| } catch (RuntimeException e) { | ||
| throw new AnalysisException( | ||
| "Failed to parse storage properties for parquet_metadata: " + e.getMessage(), e); | ||
| } | ||
| this.fileType = mapToFileType(storageProperties.getType()); | ||
| Map<String, String> backendProps = storageProperties.getBackendConfigProperties(); | ||
| try { | ||
| List<String> tmpPaths = new ArrayList<>(parsedPaths.size()); | ||
| for (String path : parsedPaths) { | ||
| tmpPaths.add(storageProperties.validateAndNormalizeUri(path)); | ||
| } | ||
| normalizedPaths = tmpPaths; | ||
| } catch (UserException e) { | ||
| throw new AnalysisException( | ||
| "Failed to normalize parquet_metadata paths: " + e.getMessage(), e); | ||
| } | ||
| if (this.fileType == TFileType.FILE_HTTP && !backendProps.containsKey("uri")) { | ||
| backendProps = new HashMap<>(backendProps); | ||
| backendProps.put("uri", normalizedPaths.get(0)); | ||
| } | ||
| this.properties = backendProps; | ||
| } | ||
|
|
||
| // Expand any glob patterns (e.g. *.parquet) to concrete file paths. | ||
| normalizedPaths = expandGlobPaths(normalizedPaths, storageProperties, this.fileType); | ||
|
|
||
| this.paths = ImmutableList.copyOf(normalizedPaths); | ||
| } |
There was a problem hiding this comment.
The new ParquetMetadataTableValuedFunction lacks test coverage. Given that other table-valued functions in the repository have test coverage (e.g., ExternalFileTableValuedFunctionTest.java), unit tests should be added to verify the function's behavior, including parameter validation, path expansion, mode handling, and error cases.
| // TINYINT/SMALLINT also need conversion via LittleIntPhysicalConverter. | ||
| switch (type) { | ||
| case TYPE_BOOLEAN: | ||
| case TYPE_TINYINT: |
There was a problem hiding this comment.
why adding these new types?
| std::stringstream ss; | ||
| // clang-format off | ||
| // clang-format off | ||
| #if defined(NDEBUG) |
| if (!Strings.isNullOrEmpty(envDorisHome)) { | ||
| securePrefixCandidates.add(envDorisHome); | ||
| } | ||
| // 兜底:用路径所在目录作为前缀尝试剥离(针对在安全目录内的绝对路径) |
| Map.Entry::getValue, | ||
| (value1, value2) -> value2 | ||
| )); | ||
| String rawPath = normalizedParams.get(PATH); |
There was a problem hiding this comment.
Use ExternalFileTableValuedFunction.URI_KEY instead of PATH
| String tmpBloomColumn = null; | ||
| String tmpBloomLiteral = null; | ||
| if (MODE_BLOOM_PROBE.equals(mode)) { | ||
| tmpBloomColumn = normalizedParams.get(COLUMN); |
There was a problem hiding this comment.
what is different between COLUMN and COLUMN_NAME?
| if (Strings.isNullOrEmpty(scheme) || "file".equalsIgnoreCase(scheme)) { | ||
| storageParams.put(StorageProperties.FS_LOCAL_SUPPORT, "true"); | ||
| } else { | ||
| forceStorageSupport(storageParams, scheme.toLowerCase()); |
There was a problem hiding this comment.
I don't think we need to force set fs.xx.support.
torageProperties.createPrimary can find the real storage type
| if (parsedPath.isEmpty()) { | ||
| throw new AnalysisException("Property 'path' must contain at least one location"); | ||
| } | ||
| List<String> parsedPaths = Collections.singletonList(parsedPath); |
There was a problem hiding this comment.
There is only one path, so no need to use List
| Map<String, String> storageParams, | ||
| TFileType fileType) throws AnalysisException { | ||
| if (fileType == TFileType.FILE_LOCAL) { | ||
| return globLocal(pattern); |
There was a problem hiding this comment.
Why not reuse LocalTableValuedFunction.getFileListFromBackend() to get file list?
|
run buildall |
Cloud UT Coverage ReportIncrement line coverage Increment coverage report
|
FE UT Coverage ReportIncrement line coverage |
BE UT Coverage ReportIncrement line coverage Increment coverage report
|
|
run buildall |
Cloud UT Coverage ReportIncrement line coverage Increment coverage report
|
FE UT Coverage ReportIncrement line coverage |
BE UT Coverage ReportIncrement line coverage Increment coverage report
|
|
run buildall |
Cloud UT Coverage ReportIncrement line coverage Increment coverage report
|
BE UT Coverage ReportIncrement line coverage Increment coverage report
|
BE Regression && UT Coverage ReportIncrement line coverage Increment coverage report
|
FE Regression Coverage ReportIncrement line coverage |
|
PR approved by at least one committer and no changes requested. |
BE Regression && UT Coverage ReportIncrement line coverage Increment coverage report
|
### What problem does this PR solve? Expose Parquet file metadata via a table-valued function for inspection and debugging. ### Issue Number: close #xxx ### Related PR: #xxx ### Problem Summary: - Add a Parquet metadata TVF so users can query Parquet file metadata via SQL. - Backend adds a Parquet metadata reader and scan path; frontend wires the TVF definition. - Enables easy inspection of partitions/row groups/column stats to aid troubleshooting.
### What problem does this PR solve? Expose Parquet file metadata via a table-valued function for inspection and debugging. ### Issue Number: close #xxx ### Related PR: #xxx ### Problem Summary: - Add a Parquet metadata TVF so users can query Parquet file metadata via SQL. - Backend adds a Parquet metadata reader and scan path; frontend wires the TVF definition. - Enables easy inspection of partitions/row groups/column stats to aid troubleshooting.
### What problem does this PR solve? Expose Parquet file metadata via a table-valued function for inspection and debugging. ### Issue Number: close #xxx ### Related PR: #xxx ### Problem Summary: - Add a Parquet metadata TVF so users can query Parquet file metadata via SQL. - Backend adds a Parquet metadata reader and scan path; frontend wires the TVF definition. - Enables easy inspection of partitions/row groups/column stats to aid troubleshooting.
### What problem does this PR solve? Expose Parquet file metadata via a table-valued function for inspection and debugging. ### Issue Number: close #xxx ### Related PR: #xxx ### Problem Summary: - Add a Parquet metadata TVF so users can query Parquet file metadata via SQL. - Backend adds a Parquet metadata reader and scan path; frontend wires the TVF definition. - Enables easy inspection of partitions/row groups/column stats to aid troubleshooting.
### What problem does this PR solve? Expose Parquet file metadata via a table-valued function for inspection and debugging. ### Issue Number: close #xxx ### Related PR: #xxx ### Problem Summary: - Add a Parquet metadata TVF so users can query Parquet file metadata via SQL. - Backend adds a Parquet metadata reader and scan path; frontend wires the TVF definition. - Enables easy inspection of partitions/row groups/column stats to aid troubleshooting.
### What problem does this PR solve? Expose Parquet file metadata via a table-valued function for inspection and debugging. ### Issue Number: close #xxx ### Related PR: #xxx ### Problem Summary: - Add a Parquet metadata TVF so users can query Parquet file metadata via SQL. - Backend adds a Parquet metadata reader and scan path; frontend wires the TVF definition. - Enables easy inspection of partitions/row groups/column stats to aid troubleshooting.
Expose Parquet file metadata via a table-valued function for inspection and debugging. - Add a Parquet metadata TVF so users can query Parquet file metadata via SQL. - Backend adds a Parquet metadata reader and scan path; frontend wires the TVF definition. - Enables easy inspection of partitions/row groups/column stats to aid troubleshooting.
Expose Parquet file metadata via a table-valued function for inspection and debugging. - Add a Parquet metadata TVF so users can query Parquet file metadata via SQL. - Backend adds a Parquet metadata reader and scan path; frontend wires the TVF definition. - Enables easy inspection of partitions/row groups/column stats to aid troubleshooting.
Expose Parquet file metadata via a table-valued function for inspection and debugging. - Add a Parquet metadata TVF so users can query Parquet file metadata via SQL. - Backend adds a Parquet metadata reader and scan path; frontend wires the TVF definition. - Enables easy inspection of partitions/row groups/column stats to aid troubleshooting.
Expose Parquet file metadata via a table-valued function for inspection and debugging. - Add a Parquet metadata TVF so users can query Parquet file metadata via SQL. - Backend adds a Parquet metadata reader and scan path; frontend wires the TVF definition. - Enables easy inspection of partitions/row groups/column stats to aid troubleshooting.
) (#61474) Replacement for #61446 because the original PR head branch could not be updated from this environment. This branch contains: - [feat] add Parquet metadata TVF (#58972) - [fix](regression) stabilize parquet_meta tvf assertions across s3 buckets (#60938) - [fix] remove useless const_cast and explain const_cast for vec (4) (#56603) - [fix] fix parquet metadata TVF compile on branch-4.1 Please review this PR instead of #61446. --------- Co-authored-by: admiring_xm <[email protected]>
What problem does this PR solve?
Expose Parquet file metadata via a table-valued function for inspection and debugging.
Issue Number: close #xxx
Related PR: #xxx
Problem Summary:
Release note
None
Check List (For Author)
Test
Behavior changed:
Does this need documentation?