|
8 | 8 | #include "duckdb/function/table_function.hpp" |
9 | 9 | #include "duckdb/main/client_context.hpp" |
10 | 10 | #include "duckdb/main/extension_helper.hpp" |
| 11 | +#include "duckdb/parser/tableref/table_function_ref.hpp" |
11 | 12 | #include "duckdb/planner/tableref/bound_at_clause.hpp" |
12 | 13 | #include "duckdb/storage/table_storage_info.hpp" |
13 | 14 |
|
|
21 | 22 |
|
22 | 23 | namespace duckdb { |
23 | 24 |
|
| 25 | +// VgiNativeDelegationMarkerBindData is declared in storage/vgi_table_entry.hpp |
| 26 | +// so VgiRequiredFiltersOptimizer (vgi_extension.cpp) can dynamic_cast against |
| 27 | +// it. See the header for full docstring and lifecycle. |
| 28 | + |
| 29 | +namespace { |
| 30 | + |
| 31 | +// Marker placeholder TableFunction — never executed; the optimizer |
| 32 | +// extension must replace it. Mirrors MakeMultiBranchMarkerFunction in |
| 33 | +// vgi_multi_scan_rewriter.cpp. |
| 34 | +void NativeDelegationMarkerExecute(ClientContext &, TableFunctionInput &, DataChunk &) { |
| 35 | + throw InternalException( |
| 36 | + "VgiRequiredFiltersOptimizer did not fire — native-delegation placeholder " |
| 37 | + "reached execution. Check that the optimizer extension is registered and " |
| 38 | + "that no other pass dropped the marker. This is a bug — please report it."); |
| 39 | +} |
| 40 | + |
| 41 | +} // namespace |
| 42 | + |
| 43 | +TableFunction MakeNativeDelegationMarkerFunction() { |
| 44 | + TableFunction fn("vgi_native_delegation_marker", {}, NativeDelegationMarkerExecute); |
| 45 | + // No bind callback — bind_data is supplied externally by GetScanFunctionImpl. |
| 46 | + // No init_global / init_local — the marker should never be executed. |
| 47 | + // filter_pushdown=true so DuckDB's FilterPushdown still installs filters |
| 48 | + // on this LogicalGet's table_filters; the rewriter then hands them off to |
| 49 | + // the real native function on the rewritten LogicalGet. |
| 50 | + fn.filter_pushdown = true; |
| 51 | + fn.projection_pushdown = true; |
| 52 | + return fn; |
| 53 | +} |
| 54 | + |
24 | 55 | VgiTableEntry::VgiTableEntry(Catalog &catalog, SchemaCatalogEntry &schema, CreateTableInfo &info, |
25 | 56 | const vgi::VgiTableInfo &table_info) |
26 | 57 | : TableCatalogEntry(catalog, schema, info), table_info_(table_info), catalog_(catalog) { |
@@ -482,6 +513,111 @@ TableFunction VgiTableEntry::GetScanFunctionImpl(ClientContext &context, unique_ |
482 | 513 | context, default_schema, scan_result.function_name, OnEntryNotFound::RETURN_NULL); |
483 | 514 | } |
484 | 515 | } |
| 516 | + bool from_system_catalog = false; |
| 517 | + if (!func_entry) { |
| 518 | + // Last-resort fallback to the system catalog for built-in DuckDB |
| 519 | + // table functions like `read_parquet` or `iceberg_scan` that workers |
| 520 | + // declare via ScanFunctionResult. The multi-branch rewriter |
| 521 | + // (vgi_multi_scan_rewriter.cpp:203) already does this fallback — |
| 522 | + // mirror it for the single-branch path so workers can delegate scans |
| 523 | + // to native DuckDB functions without going through a UNION ALL. |
| 524 | + EntryLookupInfo lookup(CatalogType::TABLE_FUNCTION_ENTRY, scan_result.function_name); |
| 525 | + auto sys_entry = Catalog::GetEntry(context, SYSTEM_CATALOG, DEFAULT_SCHEMA, lookup, |
| 526 | + OnEntryNotFound::RETURN_NULL); |
| 527 | + if (sys_entry) { |
| 528 | + func_entry = &sys_entry->Cast<TableFunctionCatalogEntry>(); |
| 529 | + from_system_catalog = true; |
| 530 | + } |
| 531 | + } |
| 532 | + |
| 533 | + if (from_system_catalog && func_entry) { |
| 534 | + // Native delegation: bind the system function eagerly here, then return |
| 535 | + // a marker carrying the bound function + bind_data + return shapes. |
| 536 | + // VgiRequiredFiltersOptimizer (vgi_extension.cpp) enforces this table's |
| 537 | + // `required_field_filter_paths` against the LogicalGet's table_filters, |
| 538 | + // then swaps `function` / `bind_data` / `returned_types` / `names` in |
| 539 | + // place to the stashed native ones. Subsequent passes see a vanilla |
| 540 | + // native scan. Matches VgiMultiScanRewriter's per-arm binding shape |
| 541 | + // (vgi_multi_scan_rewriter.cpp:219-247) but for the single-branch path. |
| 542 | + vector<LogicalType> arg_types; |
| 543 | + arg_types.reserve(scan_result.positional_arguments.size()); |
| 544 | + for (const auto &v : scan_result.positional_arguments) { |
| 545 | + arg_types.push_back(v.type()); |
| 546 | + } |
| 547 | + TableFunction native_tf = |
| 548 | + func_entry->functions.GetFunctionByArguments(context, arg_types); |
| 549 | + vector<Value> parameters(scan_result.positional_arguments.begin(), |
| 550 | + scan_result.positional_arguments.end()); |
| 551 | + named_parameter_map_t named_parameters; |
| 552 | + for (auto &kv : scan_result.named_arguments) { |
| 553 | + named_parameters.emplace(kv.first, kv.second); |
| 554 | + } |
| 555 | + vector<LogicalType> input_table_types; |
| 556 | + vector<string> input_table_names; |
| 557 | + TableFunctionRef ref; |
| 558 | + TableFunctionBindInput bind_input(parameters, named_parameters, input_table_types, |
| 559 | + input_table_names, native_tf.function_info.get(), |
| 560 | + nullptr, native_tf, ref); |
| 561 | + vector<LogicalType> return_types; |
| 562 | + vector<string> return_names; |
| 563 | + auto native_bind = native_tf.bind(context, bind_input, return_types, return_names); |
| 564 | + virtual_column_map_t native_virtual_columns; |
| 565 | + if (native_tf.get_virtual_columns) { |
| 566 | + native_virtual_columns = native_tf.get_virtual_columns(context, native_bind.get()); |
| 567 | + } |
| 568 | + |
| 569 | + // Validate the catalog's declared columns match the native bind's |
| 570 | + // output by position+name. The LogicalGet that DuckDB constructs uses |
| 571 | + // the catalog's column list for FilterPushdown's column_ids / |
| 572 | + // table_filters keys; if the native function emits a different shape, |
| 573 | + // those indices mis-resolve once VgiRequiredFiltersOptimizer rewrites |
| 574 | + // the marker. Two common causes: |
| 575 | + // - The worker's pa.Schema source omits Hive-partition columns that |
| 576 | + // the native bind appends (read_parquet on `theme=…/type=…/*`). |
| 577 | + // - The worker introspected against a different release than what |
| 578 | + // the URL points at. |
| 579 | + // Either way the right move is to fail loudly here so the worker |
| 580 | + // author sees the mismatch immediately instead of silent column |
| 581 | + // misrouting at scan time. |
| 582 | + { |
| 583 | + const auto &decl_columns = GetColumns(); |
| 584 | + const auto decl_count = decl_columns.LogicalColumnCount(); |
| 585 | + if (decl_count != return_names.size()) { |
| 586 | + throw BinderException( |
| 587 | + "VGI native delegation for '%s.%s.%s' (function '%s'): catalog declares " |
| 588 | + "%llu column(s) but the native bind returned %llu. The catalog's columns " |
| 589 | + "must match exactly what the native function emits at scan time (positions " |
| 590 | + "+ names). Common cause: Hive-partition columns that read_parquet appends " |
| 591 | + "but the worker's schema source omitted.", |
| 592 | + catalog_.GetName(), ParentSchema().name, name, scan_result.function_name, |
| 593 | + static_cast<unsigned long long>(decl_count), |
| 594 | + static_cast<unsigned long long>(return_names.size())); |
| 595 | + } |
| 596 | + for (idx_t i = 0; i < decl_count; ++i) { |
| 597 | + const auto &decl_name = decl_columns.GetColumn(LogicalIndex(i)).Name(); |
| 598 | + if (decl_name != return_names[i]) { |
| 599 | + throw BinderException( |
| 600 | + "VGI native delegation for '%s.%s.%s' (function '%s'): catalog " |
| 601 | + "declared column %llu as '%s' but the native bind returned '%s'. " |
| 602 | + "Names must match by position.", |
| 603 | + catalog_.GetName(), ParentSchema().name, name, scan_result.function_name, |
| 604 | + static_cast<unsigned long long>(i), decl_name, return_names[i]); |
| 605 | + } |
| 606 | + } |
| 607 | + } |
| 608 | + |
| 609 | + auto function_name_log = scan_result.function_name; |
| 610 | + bind_data = make_uniq<VgiNativeDelegationMarkerBindData>( |
| 611 | + *this, std::move(native_tf), std::move(native_bind), std::move(return_types), |
| 612 | + std::move(return_names), std::move(native_virtual_columns), std::move(scan_result), |
| 613 | + attach_params->worker_path()); |
| 614 | + |
| 615 | + VGI_LOG(context, "vgi.scan_function.native_delegation_marker", |
| 616 | + {{"schema", ParentSchema().name}, |
| 617 | + {"table", name}, |
| 618 | + {"function", function_name_log}}); |
| 619 | + return MakeNativeDelegationMarkerFunction(); |
| 620 | + } |
485 | 621 | if (func_entry) { |
486 | 622 | for (auto &tf : func_entry->functions.functions) { |
487 | 623 | has_projection_pushdown = has_projection_pushdown || tf.projection_pushdown; |
|
0 commit comments