Skip to content

Partition information schema content per catalog - #25887

Open
pepijnve wants to merge 13 commits into
apache:mainfrom
pepijnve:information-schema-extension
Open

pepijnve wants to merge 13 commits into
apache:mainfrom
pepijnve:information-schema-extension

Conversation

@pepijnve

@pepijnve pepijnve commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

Catalog-qualified information schema queries currently return metadata for object from every registered catalog.

If we follow the letter of the SQL specification, my_catalog.information_schema.tables should describe only my_catalog. This is consistent with PostgreSQL's information schema representing the current database.

When working with multiple catalogs, for instance multiple Iceberg REST catalogs, it is more practical to be able to query just the objects defined in one specific catalog without further filtering.

A special 'system' catalog is optionally provided to be able to query across catalogs. This can be seen as an approximation of SQL's definition schema.

What changes are included in this PR?

  • The catalog list provided to InformationSchemaProvider is now constructed dynamically based on the catalog being queried. An optional 'system' catalog provides the existing global catalog behaviour. All other catalogs are restricted to just their own content.
  • Unqualified information_schema queries now use the configured default catalog instead of returning metadata from all catalogs.

What is the testing strategy for this PR?

Added multi-catalog SQL logic coverage for qualified and unqualified information schema queries.

Are there any user-facing changes?

Yes. Information schema results are now scoped to the resolved catalog.

@github-actions github-actions Bot added sql SQL Planner core Core DataFusion crate sqllogictest SQL Logic Tests (.slt) catalog Related to the catalog crate common Related to common crate execution Related to the execution crate labels Sep 29, 2026
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Thank you for opening this pull request!

Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch).

Details
     Cloning apache/main
    Building datafusion v55.1.0 (current)
       Built [  67.156s] (current)
     Parsing datafusion v55.1.0 (current)
      Parsed [   0.036s] (current)
    Building datafusion v55.1.0 (baseline)
       Built [  65.568s] (baseline)
     Parsing datafusion v55.1.0 (baseline)
      Parsed [   0.037s] (baseline)
    Checking datafusion v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.685s] 229 checks: 229 pass, 31 skip
     Summary no semver update required
    Finished [ 135.463s] datafusion
    Building datafusion-catalog v55.1.0 (current)
       Built [  43.253s] (current)
     Parsing datafusion-catalog v55.1.0 (current)
      Parsed [   0.024s] (current)
    Building datafusion-catalog v55.1.0 (baseline)
       Built [  43.077s] (baseline)
     Parsing datafusion-catalog v55.1.0 (baseline)
      Parsed [   0.025s] (baseline)
    Checking datafusion-catalog v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.127s] 229 checks: 229 pass, 31 skip
     Summary no semver update required
    Finished [  87.758s] datafusion-catalog
    Building datafusion-cli v55.1.0 (current)
       Built [  94.553s] (current)
     Parsing datafusion-cli v55.1.0 (current)
      Parsed [   0.033s] (current)
    Building datafusion-cli v55.1.0 (baseline)
       Built [  94.446s] (baseline)
     Parsing datafusion-cli v55.1.0 (baseline)
      Parsed [   0.034s] (baseline)
    Checking datafusion-cli v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.146s] 229 checks: 229 pass, 31 skip
     Summary no semver update required
    Finished [ 192.021s] datafusion-cli
    Building datafusion-common v55.1.0 (current)
       Built [  35.780s] (current)
     Parsing datafusion-common v55.1.0 (current)
      Parsed [   0.082s] (current)
    Building datafusion-common v55.1.0 (baseline)
       Built [  36.441s] (baseline)
     Parsing datafusion-common v55.1.0 (baseline)
      Parsed [   0.079s] (baseline)
    Checking datafusion-common v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.794s] 229 checks: 228 pass, 1 fail, 0 warn, 31 skip

--- failure constructible_struct_adds_field: struct exhaustively constructible through public API adds field ---

Description:
A pub struct that could be exhaustively constructed with a literal using only public API has a new pub field, breaking existing exhaustive literals.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.51.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field CatalogOptions.system_catalog in /home/runner/work/datafusion/datafusion/datafusion/common/src/config.rs:225

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  74.213s] datafusion-common
    Building datafusion-execution v55.1.0 (current)
       Built [  33.704s] (current)
     Parsing datafusion-execution v55.1.0 (current)
      Parsed [   0.030s] (current)
    Building datafusion-execution v55.1.0 (baseline)
       Built [  34.269s] (baseline)
     Parsing datafusion-execution v55.1.0 (baseline)
      Parsed [   0.033s] (baseline)
    Checking datafusion-execution v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.280s] 229 checks: 229 pass, 31 skip
     Summary no semver update required
    Finished [  69.544s] datafusion-execution
    Building datafusion-sql v55.1.0 (current)
       Built [  48.402s] (current)
     Parsing datafusion-sql v55.1.0 (current)
      Parsed [   0.036s] (current)
    Building datafusion-sql v55.1.0 (baseline)
       Built [  49.042s] (baseline)
     Parsing datafusion-sql v55.1.0 (baseline)
      Parsed [   0.038s] (baseline)
    Checking datafusion-sql v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.255s] 229 checks: 229 pass, 31 skip
     Summary no semver update required
    Finished [  98.752s] datafusion-sql
    Building datafusion-sqllogictest v55.1.0 (current)
       Built [ 109.866s] (current)
     Parsing datafusion-sqllogictest v55.1.0 (current)
      Parsed [   0.016s] (current)
    Building datafusion-sqllogictest v55.1.0 (baseline)
       Built [ 107.996s] (baseline)
     Parsing datafusion-sqllogictest v55.1.0 (baseline)
      Parsed [   0.016s] (baseline)
    Checking datafusion-sqllogictest v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.104s] 229 checks: 229 pass, 31 skip
     Summary no semver update required
    Finished [ 220.718s] datafusion-sqllogictest

@github-actions github-actions Bot added auto detected api change Auto detected API change documentation Improvements or additions to documentation labels Sep 29, 2026
@pepijnve
pepijnve force-pushed the information-schema-extension branch from b5c8c7a to 1b1ed04 Compare October 2, 2026 20:19
@pepijnve
pepijnve force-pushed the information-schema-extension branch from 1ae5b90 to 1e5b40e Compare October 3, 2026 13:24
@codecov-commenter

codecov-commenter commented Oct 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.85047% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.93%. Comparing base (a9129e7) to head (de36547).
⚠️ Report is 21 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/catalog/src/information_schema.rs 78.07% 19 Missing and 6 partials ⚠️
datafusion/sql/src/statement.rs 96.55% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #25887      +/-   ##
==========================================
+ Coverage   82.92%   82.93%   +0.01%     
==========================================
  Files        1155     1154       -1     
  Lines      458197   458155      -42     
  Branches   458197   458155      -42     
==========================================
+ Hits       379968   379985      +17     
+ Misses      55196    55140      -56     
+ Partials    23033    23030       -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pepijnve
pepijnve marked this pull request as ready for review October 3, 2026 13:54
@pepijnve

pepijnve commented Oct 3, 2026

Copy link
Copy Markdown
Contributor Author

@neilconway this branch is now clean. There's still quite a lot that can be improved in the information schema, but I've restricted it to just the scope change here.

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @pepijnve -- I am worried about the behavior change in this PR

I thought postgres had some notion of "default search path" or something that would scope its name resolution rules (and presumably also information_schema visibility). Did you consider something like that for DataFusion?

statement: Statement,
) -> datafusion_common::Result<LogicalPlan> {
let references = self.resolve_table_references(&statement)?;
let mut references = self.resolve_table_references(&statement)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this is a nice change to move it here into the session state

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, that gives me the feeling I'm heading in the right direction.

schema: schema.into(),
table: table.into(),
fn resolve_info_table(&self, table: &str) -> Option<TableReference> {
let table_reference = if let Some(system_catalog) =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe we can adjust the comments here to explain the scoping rules:

Looks first in the system catalog, if defined, and otherwise treats it as unqualified reference

@pepijnve pepijnve Oct 6, 2026 •

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.

See other comment. Might be best to peek ahead at PR #26057 where this goes away entirely. In a nutshell, what I've done there is to change the planning of show ... statements so that they're not dependent on information_schema at all.

{
TableReference::Full {
catalog: system_catalog.deref().into(),
schema: "information_schema".into(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This information_schema reference feels wrong to me -- I thought the design was that the core of Datafusion / the sql parser didn't have any special case for the information_schema and information schema was handled with the same API as the other catalog providers

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.

In this PR and more importantly in main, the references to information_schema, the various tables and their columns all exist here. As an example, see the show_tables_to_plan function on main.

I agree with you that the hardcoding of the information_schema table schema in the sql crate feels wrong. Getting rid of that is something I'm trying in followup PR #26057. This one is smaller in scope and only tries to land the work done in PR #24200 which seems to have stalled.

WHERE table_schema <> 'information_schema';
----
my_other_catalog my_other_schema t3

query TTTT rowsort
SELECT * from information_schema.tables;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why don't all the other tables appear now? That seems ilke people would treat it as a regression 🤔

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.

What I've tried to implement in this PR is what's described at https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-information-schema

In the SYSTEM catalog, the INFORMATION_SCHEMA is a SQL standard schema that provides metadata about objects across all catalogs in the metastore. It does not contain metadata about hive_metastore objects.

Separately, each catalog created in Unity Catalog also automatically includes an information_schema that describes metadata about objects in that catalog only.

The behaviour you're seeing here is that the default catalog was set to my_other_catalog. information_schema.tables then resolves to my_other_catalog.information_schema.tables which only describes the objects in my_other_catalog.

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.

I was curious about how this was theoretically supposed to work per the SQL specification. In ISO 9075-11:2023 at least, the information schema views are indeed segregated per catalog. There's a theoretical notion of a 'definition schema' containing base tables which are allowed to contain everything, and then the information schema view are defined in terms of those.

First we have

CREATE VIEW INFORMATION_SCHEMA_CATALOG_NAME AS
SELECT CATALOG_NAME
FROM DEFINITION_SCHEMA.CATALOG_NAME
WHERE CATALOG_NAME = 'CN';

where CN is the name of the catalog in which this Information Schema resides.

The tables view is defined as (shortened for legibility)

CREATE VIEW TABLES AS
SELECT TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, ...
FROM DEFINITION_SCHEMA.TABLES
WHERE ... 
AND
TABLE_CATALOG
= ( SELECT CATALOG_NAME
FROM INFORMATION_SCHEMA_CATALOG_NAME );

The last predicate is what restricts the set of tables to only those that exist in the same catalog as the information schema itself. I did not check all of them exhaustively, but the same seems to hold for most of the other object views as well.

This is of course just the standards view on things. Logging this here as FYI.

@pepijnve

pepijnve commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

I think you're referring to PostgreSQL's schema search path. I'm not familiar with that, but will look into it.

@pepijnve

pepijnve commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

I spent some time surveying what's out there with AI to get a feel for our options.

We'll need to make a decision on a couple of things:

  1. Should information_schema be global or per catalog?
  2. If global, which catalog is it a child of?
  3. If per catalog, what does an unqualified usage of information_schema resolve to?

Here's what I got from Claude wrt other some other systems out there

System Global? Where it lives? Unqualified?
DuckDB Yes, one view covers all attached databases Once, in the system catalog system.information_schema
SQL Server No; query db.INFORMATION_SCHEMA.TABLES per database, or use sys.databases plus dynamic SQL Each database Current database (USE)
Snowflake No; SNOWFLAKE.ACCOUNT_USAGE views cover the account (with latency) Each database Current database; errors if none is set
Trino No; system.jdbc.* and system.metadata.* cover all catalogs Each catalog (generated per connector) Session catalog; errors if none is set
Databricks (Unity Catalog) Opt-in via system.information_schema Each catalog, plus system.information_schema Current catalog (USE CATALOG)
BigQuery Region-qualified, e.g. `region-us`.INFORMATION_SCHEMA.TABLES Per dataset or region, as a qualifier Generally errors without a qualifier
MySQL / MariaDB Yes, but "databases" are really schemas; table_catalog is always def Once, global The global schema
SQLite No; query aux.sqlite_schema per attachment None; each attached DB has sqlite_schema main.sqlite_schema
StarRocks No; query catalog.information_schema.tables per catalog (external catalogs supported from v3.2) Each catalog (default_catalog and each external catalog) Current catalog (SET CATALOG), default_catalog by default

The current state of DataFusion is somewhere in between all the choices listed above: information_schema lives in each catalog which suggests it's partitioned per catalog, but each 'instance' is identical and is actually global/cross-catalog. This becomes particularly annoying when you're trying to work with remote catalogs. It doesn't seem desirable for a query that looks local to start accessing remote resources (e.g., I try to query the memory catalog via the information schema, and it starts accessing my Iceberg REST catalog over the internet).

Given that a cross-catalog information schema currently exists, I think we at least need to retain that capability. The question then is how we expose that to the outside world.

What I did in this PR is to introduce a new magic catalog named system that holds the global information schema. Because DataFusion only has a single default catalog value at the moment rather than a PostgreSQL style search path, you need to address this explicitly.

An alternative solution could be to add a search path capability for unqualified identifier resolution. By default, we would not expose the per-catalog information schema and add system to the end of the search path by default. This would still introduce a behaviour change though for info schema queries in qualified form (i.e. select * from datafusion.information_schema.tables would not work).

A hybrid solution where we special case the unqualified resolution of information_schema is also an option.

@pepijnve

Copy link
Copy Markdown
Contributor Author

@alamb sorry to bother you, but if you have a moment could you let me know which direction seems the most promising to you? I'm happy to continue refining this PR, but not entirely sure what an acceptable end goal looks like.

@pepijnve pepijnve changed the title Information schema extension Partition information schema content per catalog Oct 16, 2026
@alamb

alamb commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Sorry -- this PR is on my list to review / respond to mroe carefully. I will try and do so soon

@pepijnve

Copy link
Copy Markdown
Contributor Author

@neilconway since you were reviewing the original version of this PR, I was wondering if you had any opinions on the global vs segregated information schema question above.

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @pepijnve

My core concern is that while the change proposed in this PR may likely be better / more standards compliant than what currently exists in DataFusion, it is likely to be treated as a major change by some downstream systems (for example InfluxDB would likely be impacted)

I think we need to add some way (e.g. an enum or InformationSchema view / registration API) to keep the existing behavior.

Another thing that might be more elegant maybe would be to update InformationSchemaProvider so that it doesn't require a catalog_list but instead just wraps a single Catalog.

We would still have to find some way to replicate the existing behavior of listing all the tables in all schemas

/// Retrieve the [`SchemaProvider`] for a specific [`TableReference`], if it
/// exists.
pub fn schema_for_ref(
&self,
table_ref: impl Into<TableReference>,
) -> datafusion_common::Result<Arc<dyn SchemaProvider>> {
let resolved_ref = self.resolve_table_ref(table_ref);
if self.config().information_schema()
&& *resolved_ref.schema == *INFORMATION_SCHEMA
{
return Ok(Arc::new(
InformationSchemaProvider::new(Arc::clone(&self.inner.catalog_list))
.with_table_functions(self.inner.table_functions.clone()),
));
}

🤔

Another idea would be to make some sort of InformationSchema builder or something similar that allowed finer grain control

@alamb

alamb commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

A hybrid solution where we special case the unqualified resolution of information_schema is also an option.

Perthaps you mean here some way to preserve the existing behavior

@pepijnve

Copy link
Copy Markdown
Contributor Author

A hybrid solution where we special case the unqualified resolution of information_schema is also an option.

Perthaps you mean here some way to preserve the existing behavior

Indeed. What I had in mind was that we would use the 'system catalog' configuration option for this. If it's undefined, then we stick to the current behaviour of every catalog containing being an identical, global one. If it is defined, then we make the per catalog instances so that they only describe objects in the same catalog and the global one provides the cross catalog variant. This way the change becomes opt-in.

@pepijnve

Copy link
Copy Markdown
Contributor Author

@alamb the most recent two commits change things so that the only remaining incompatible change is in the CLI where we'll need to decide what the default behaviour is. Should I revert that back to the current behaviour as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto detected api change Auto detected API change catalog Related to the catalog crate common Related to common crate core Core DataFusion crate documentation Improvements or additions to documentation execution Related to the execution crate sql SQL Planner sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

information_schema.tables contains all tables from all catalogs instead of current catalog

4 participants