Skip to content

Support REST catalog views and SQL functions#494

Merged
JingsongLi merged 9 commits into
apache:mainfrom
JingsongLi:codex/rest-catalog-views-sql-functions
Jul 10, 2026
Merged

Support REST catalog views and SQL functions#494
JingsongLi merged 9 commits into
apache:mainfrom
JingsongLi:codex/rest-catalog-views-sql-functions

Conversation

@JingsongLi

Copy link
Copy Markdown
Contributor

What changed

  • add Paimon REST Catalog models, resource paths, pagination, and read APIs for persistent views and functions
  • expose existing REST views through DataFusion catalog providers with owning-namespace resolution, declared-schema alignment, and recursive dependency protection
  • expand deterministic REST SQL scalar functions before DataFusion planning, supporting bare and fully qualified names, nested functions, argument validation, return coercion, statement-local metadata caching, and cycle detection
  • document REST Catalog view and SQL function behavior in the DataFusion README and SQL integration guide

Why

paimon-datafusion previously treated persistent REST Catalog views as missing tables and could not resolve SQL functions stored in the catalog. This adds read-and-execute support for existing objects without introducing view or function DDL.

User impact

Queries executed through SQLContext can now read persistent REST views and call existing SQL scalar functions as either function(...) in the current namespace or catalog.database.function(...). Unsupported definitions and unsafe shapes fail during planning with explicit errors.

Validation

  • cargo test -p paimon-datafusion --lib rest_ — 16 passed
  • cargo test -p paimon --test rest_object_models_test — 6 passed
  • cargo test -p paimon --test rest_api_test — 21 passed
  • RESTCatalog-focused tests — 31 passed
  • cargo clippy -p paimon -p paimon-datafusion --lib --tests -- -D warnings
  • cargo fmt --all -- --check
  • mkdocs build --strict

The full paimon-datafusion library test run also reached 222 passing tests; seven existing scan tests could not load their expected /tmp/paimon-warehouse fixture metadata in this local environment.

@JingsongLi JingsongLi force-pushed the codex/rest-catalog-views-sql-functions branch from cca29e5 to 30a283b Compare July 10, 2026 08:16
@JingsongLi JingsongLi marked this pull request as ready for review July 10, 2026 08:17

@leaves12138 leaves12138 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the overall structure is clear, but I found several blocking correctness and compatibility issues. I reproduced the CTE and HTTP 501 cases with focused local regression tests, and the Python integration CI currently exposes the no-catalog regression. Please address the inline comments and add coverage for these cases.

"REST SQL function expansion exceeded the reference limit of {MAX_FUNCTION_REFERENCES}"
)));
}
let catalog = catalogs.get(&reference.0).ok_or_else(|| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lookup regresses SQLContext usages that have no registered Paimon catalog. Every function call is treated as a REST-function candidate, current_catalog is then DataFusion's default datafusion, and this branch returns Unknown catalog 'datafusion' before registered/built-in UDF resolution. The current Python integration CI fails video_snapshot and vector_from_json tests for exactly this reason. Please make expansion a no-op/fallback when the current catalog is not in the Paimon catalog map (while retaining an appropriate error for explicitly invalid qualification), and add a no-catalog built-in regression test.

Ok(function) => Some(function),
Err(paimon::Error::FunctionNotExist { .. })
| Err(paimon::Error::Unsupported { .. }) => None,
Err(error) => return Err(crate::to_datafusion_error(error)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please preserve compatibility with REST servers that do not implement the new function endpoint. This code probes get_function for every scalar/aggregate call, including built-ins such as abs and count; RESTCatalog::get_function currently leaves HTTP 501 as Error::RestApi(NotImplemented), so SELECT abs(-1) fails instead of falling through to DataFusion. I reproduced this with a catalog returning RestError::NotImplemented. Please map 501 to Unsupported at the REST catalog boundary or handle it as a non-persistent function here, and add a regression test. The same compatibility consideration applies to the new view endpoint.

}

let mut identifiers = Vec::new();
let _: std::ops::ControlFlow<()> = visit_relations(&statements, |relation| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

visit_relations also visits references to local CTE names, but this code treats every unqualified relation as a catalog object. For example, a persistent view named cte_view with WITH cte_view AS (SELECT CAST(42 AS BIGINT) AS answer) SELECT * FROM cte_view is rejected as default.cte_view -> default.cte_view recursion; I reproduced this locally. Please exclude CTE aliases according to their SQL scope (including nested CTEs) before catalog lookup, and add a regression test.

match expand_call(function, call, &reference.0, &functions) {
Ok(expanded) => {
*expr = expanded;
expanded_count += 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current limits bound nesting depth and the number of distinct function definitions, but not the number of expanded call sites. A chain such as f0(x) = f1(x) + f1(x), ..., f31(x) = x creates roughly 2^31 subexpressions while using only 32 references and 32 rounds, so the process can exhaust memory before the depth guard fires. Please add a total expanded-call/AST-size budget checked before replacement, with a branching-chain regression test using a small configured limit.

@leaves12138 leaves12138 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the four issues from the previous review are addressed, and the focused regressions for CTE scoping, unsupported endpoints, no-catalog function resolution, and bounded expansion now pass. On this second pass I found additional source-compatibility and SQL-resolution gaps described inline. I also ran the rest_ DataFusion tests (18 passed), REST object model tests (6 passed), the HTTP 501 catalog fallback test, and cargo fmt --all -- --check.

}

pub(crate) fn with_dynamic_options(
pub fn new(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please preserve the existing public PaimonCatalogProvider::new(Arc<dyn Catalog>) API. This changes it to a five-argument constructor, so every downstream crate using the provider directly stops compiling; I confirmed the old call now fails with E0061. The extra session state is only needed for the SQLContext integration, so keeping new(catalog) and adding a crate-private/session-aware constructor would avoid an unrelated source-breaking change. PaimonSchemaProvider::new is changed similarly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for calling this out. We intentionally do not preserve source compatibility for these provider constructors: PaimonCatalogProvider and PaimonSchemaProvider are implementation details of the DataFusion integration, while SQLContext is the supported public API. The constructors therefore keep the session-aware parameters required for persistent REST view planning. I have removed the compatibility wrappers.

}
}

fn sql_identifier_key(identifier: &Ident) -> SqlIdentifierKey {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please match DataFusion's identifier normalization rather than treating quote_style as part of the CTE identity. With default normalization, WITH cte_view AS (...) SELECT * FROM "cte_view" references the same CTE, but these keys differ (None versus Some('\"')). If the persistent view itself is named cte_view, dependency validation reports a false self-cycle. I reproduced this locally: recursive REST catalog view dependency detected: default.cte_view -> default.cte_view. The same normalization should be applied when relation parts are converted to catalog/database/object names.

.name
.0
.iter()
.map(|part| part.as_ident().map(|ident| ident.value.clone()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please apply DataFusion identifier normalization before REST function lookup and parameter substitution. A catalog function stored as plus_one is not expanded for SELECT PLUS_ONE(41) because the raw Ident.value is used; DataFusion lowercases unquoted identifiers by default, so the query should resolve the same function. I reproduced the current behavior as Invalid function 'plus_one'. Catalog/database components and unquoted parameter identifiers inside stored definitions need the same treatment.

)
.await
}
Statement::Query(_) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please ensure REST function expansion is used by the other query paths as well. In particular, sql() returns through handle_time_travel_query before reaching this arm, and that handler calls the raw SessionContext, so a query such as SELECT plus_one(id) FROM t VERSION AS OF 1 bypasses expansion and fails function resolution. EXPLAIN SELECT plus_one(1) also does not enter the Statement::Query arm. The PR states that queries executed through SQLContext can call these functions, so these existing query forms should not silently lose the feature (or the limitation should be explicit and intentional).

@leaves12138 leaves12138 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the CTE/relation normalization, function call normalization, and EXPLAIN/time-travel expansion issues are addressed, and the new focused tests pass locally. I also accept the provider-constructor break as an intentional API decision based on the explanation in the thread. One parameter-binding normalization edge remains, described inline.

let replacements: HashMap<String, SqlExpr> = input_params
.iter()
.zip(values)
.map(|(field, value)| (normalize_identifier(&Ident::new(field.name())), value))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please preserve the exact metadata parameter name when building this binding map. DataField::name() is already the catalog name, not an unquoted SQL token; wrapping an uppercase name such as X in Ident::new normalizes it to x. A valid definition using the exact quoted identifier ("X" + 1) then fails with references undeclared identifier 'X'. I reproduced this with a focused test. This also collapses distinct parameters such as x and X. Keeping field.name().to_string() as the key while normalizing only identifiers parsed from the SQL definition preserves both unquoted lookup semantics and quoted exact names.

@leaves12138 leaves12138 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, all previously reported issues are addressed. I rechecked the REST view CTE scoping, unsupported-endpoint fallback, function expansion budget, DataFusion identifier normalization, EXPLAIN/time-travel paths, and quoted metadata parameter binding. The focused local regressions pass, and all CI checks are green.

@JingsongLi JingsongLi merged commit 1bf775e into apache:main Jul 10, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants