Skip to content

feat: Support multiple external table locations#22695

Open
kumarUjjawal wants to merge 2 commits into
apache:mainfrom
kumarUjjawal:feature/multi-location-external-table
Open

feat: Support multiple external table locations#22695
kumarUjjawal wants to merge 2 commits into
apache:mainfrom
kumarUjjawal:feature/multi-location-external-table

Conversation

@kumarUjjawal

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Part of #16303.

Rationale for this change

CREATE EXTERNAL TABLE can reference only one location today. This adds support for listing multiple explicit locations and reading them as one table.

What changes are included in this PR?

  • Adds LOCATION ('a.parquet', 'b.parquet') syntax.
  • Keeps LOCATION 'a,b.parquet' as a single path, so literal commas still work.
  • Carries multiple locations through the logical plan and proto.
  • Updates listing table creation to scan all listed locations.
  • Requires all listed locations to use the same object store and matching fields.
  • Keeps stream tables limited to exactly one location.
  • Updates docs and upgrade notes.

Are these changes tested?

Yes

Are there any user-facing changes?

Yes. CREATE EXTERNAL TABLE now accepts a parenthesized list of locations.

There is also a public API change: CreateExternalTable.location is replaced by CreateExternalTable.locations.

@github-actions github-actions Bot added documentation Improvements or additions to documentation sql SQL Planner logical-expr Logical plan and expressions core Core DataFusion crate sqllogictest SQL Logic Tests (.slt) catalog Related to the catalog crate proto Related to proto crate ffi Changes to the ffi crate labels Jun 1, 2026
@github-actions

github-actions Bot commented Jun 1, 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 v54.0.0 (current)
       Built [ 102.608s] (current)
     Parsing datafusion v54.0.0 (current)
      Parsed [   0.033s] (current)
    Building datafusion v54.0.0 (baseline)
       Built [ 102.965s] (baseline)
     Parsing datafusion v54.0.0 (baseline)
      Parsed [   0.034s] (baseline)
    Checking datafusion v54.0.0 -> v54.0.0 (no change; assume patch)
     Checked [   0.585s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [ 207.862s] datafusion
    Building datafusion-catalog v54.0.0 (current)
       Built [  38.526s] (current)
     Parsing datafusion-catalog v54.0.0 (current)
      Parsed [   0.026s] (current)
    Building datafusion-catalog v54.0.0 (baseline)
       Built [  38.891s] (baseline)
     Parsing datafusion-catalog v54.0.0 (baseline)
      Parsed [   0.026s] (baseline)
    Checking datafusion-catalog v54.0.0 -> v54.0.0 (no change; assume patch)
     Checked [   0.154s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [  79.476s] datafusion-catalog
    Building datafusion-cli v54.0.0 (current)
       Built [ 179.803s] (current)
     Parsing datafusion-cli v54.0.0 (current)
      Parsed [   0.032s] (current)
    Building datafusion-cli v54.0.0 (baseline)
       Built [ 181.549s] (baseline)
     Parsing datafusion-cli v54.0.0 (baseline)
      Parsed [   0.034s] (baseline)
    Checking datafusion-cli v54.0.0 -> v54.0.0 (no change; assume patch)
     Checked [   0.118s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [ 364.104s] datafusion-cli
    Building datafusion-expr v54.0.0 (current)
       Built [  26.521s] (current)
     Parsing datafusion-expr v54.0.0 (current)
      Parsed [   0.075s] (current)
    Building datafusion-expr v54.0.0 (baseline)
       Built [  26.377s] (baseline)
     Parsing datafusion-expr v54.0.0 (baseline)
      Parsed [   0.076s] (baseline)
    Checking datafusion-expr v54.0.0 -> v54.0.0 (no change; assume patch)
     Checked [   1.327s] 223 checks: 221 pass, 2 fail, 0 warn, 30 skip

--- failure constructible_struct_adds_field: externally-constructible struct adds field ---

Description:
A pub struct constructible with a struct literal has a new pub field. Existing struct literals must be updated to include the new field.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field CreateExternalTable.locations in /home/runner/work/datafusion/datafusion/datafusion/expr/src/logical_plan/ddl.rs:219
  field CreateExternalTable.locations in /home/runner/work/datafusion/datafusion/datafusion/expr/src/logical_plan/ddl.rs:219

--- failure struct_pub_field_missing: pub struct's pub field removed or renamed ---

Description:
A publicly-visible struct has at least one public field that is no longer available under its prior name. It may have been renamed or removed entirely.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/struct_pub_field_missing.ron

Failed in:
  field location of struct CreateExternalTable, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d4406bcd5aa1525203a285dec9206551d290fabd/datafusion/expr/src/logical_plan/ddl.rs:215
  field location of struct CreateExternalTable, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d4406bcd5aa1525203a285dec9206551d290fabd/datafusion/expr/src/logical_plan/ddl.rs:215

     Summary semver requires new major version: 2 major and 0 minor checks failed
    Finished [  55.532s] datafusion-expr
    Building datafusion-ffi v54.0.0 (current)
       Built [  59.777s] (current)
     Parsing datafusion-ffi v54.0.0 (current)
      Parsed [   0.061s] (current)
    Building datafusion-ffi v54.0.0 (baseline)
       Built [  59.159s] (baseline)
     Parsing datafusion-ffi v54.0.0 (baseline)
      Parsed [   0.061s] (baseline)
    Checking datafusion-ffi v54.0.0 -> v54.0.0 (no change; assume patch)
     Checked [   0.240s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [ 121.176s] datafusion-ffi
    Building datafusion-proto v54.0.0 (current)
       Built [  58.185s] (current)
     Parsing datafusion-proto v54.0.0 (current)
      Parsed [   0.018s] (current)
    Building datafusion-proto v54.0.0 (baseline)
       Built [  58.310s] (baseline)
     Parsing datafusion-proto v54.0.0 (baseline)
      Parsed [   0.019s] (baseline)
    Checking datafusion-proto v54.0.0 -> v54.0.0 (no change; assume patch)
     Checked [   0.262s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [ 117.988s] datafusion-proto
    Building datafusion-proto-models v54.0.0 (current)
       Built [  24.099s] (current)
     Parsing datafusion-proto-models v54.0.0 (current)
      Parsed [   0.128s] (current)
    Building datafusion-proto-models v54.0.0 (baseline)
       Built [  24.032s] (baseline)
     Parsing datafusion-proto-models v54.0.0 (baseline)
      Parsed [   0.130s] (baseline)
    Checking datafusion-proto-models v54.0.0 -> v54.0.0 (no change; assume patch)
     Checked [   1.638s] 223 checks: 222 pass, 1 fail, 0 warn, 30 skip

--- failure constructible_struct_adds_field: externally-constructible struct adds field ---

Description:
A pub struct constructible with a struct literal has a new pub field. Existing struct literals must be updated to include the new field.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field CreateExternalTableNode.locations in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:260
  field CreateExternalTableNode.locations in /home/runner/work/datafusion/datafusion/datafusion/proto-models/src/generated/prost.rs:260

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  51.076s] datafusion-proto-models
    Building datafusion-sql v54.0.0 (current)
       Built [  41.204s] (current)
     Parsing datafusion-sql v54.0.0 (current)
      Parsed [   0.034s] (current)
    Building datafusion-sql v54.0.0 (baseline)
       Built [  40.928s] (baseline)
     Parsing datafusion-sql v54.0.0 (baseline)
      Parsed [   0.032s] (baseline)
    Checking datafusion-sql v54.0.0 -> v54.0.0 (no change; assume patch)
     Checked [   0.258s] 223 checks: 222 pass, 1 fail, 0 warn, 30 skip

--- failure constructible_struct_adds_field: externally-constructible struct adds field ---

Description:
A pub struct constructible with a struct literal has a new pub field. Existing struct literals must be updated to include the new field.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field CreateExternalTable.locations in /home/runner/work/datafusion/datafusion/datafusion/sql/src/parser.rs:255

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  83.493s] datafusion-sql
    Building datafusion-sqllogictest v54.0.0 (current)
       Built [ 182.484s] (current)
     Parsing datafusion-sqllogictest v54.0.0 (current)
      Parsed [   0.021s] (current)
    Building datafusion-sqllogictest v54.0.0 (baseline)
       Built [ 182.056s] (baseline)
     Parsing datafusion-sqllogictest v54.0.0 (baseline)
      Parsed [   0.022s] (baseline)
    Checking datafusion-sqllogictest v54.0.0 -> v54.0.0 (no change; assume patch)
     Checked [   0.095s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [ 367.461s] datafusion-sqllogictest

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Jun 1, 2026
Comment thread datafusion/core/src/datasource/listing_table_factory.rs Outdated
Comment thread datafusion/proto-models/src/generated/prost.rs
Comment thread datafusion/proto/src/logical_plan/mod.rs Outdated
Comment thread datafusion/core/src/datasource/listing_table_factory.rs Outdated
Comment thread datafusion/core/src/test_util/mod.rs Outdated
Comment thread datafusion/sql/src/parser.rs Outdated
@kumarUjjawal

Copy link
Copy Markdown
Contributor Author

Thank you @martin-g for great feedback. I have addressed the concerns.

@kumarUjjawal
kumarUjjawal force-pushed the feature/multi-location-external-table branch from 24e71e1 to 1544a99 Compare June 18, 2026 14:13
@kumarUjjawal
kumarUjjawal requested a review from martin-g June 18, 2026 14:13
@kumarUjjawal
kumarUjjawal force-pushed the feature/multi-location-external-table branch from 1544a99 to afc19ce Compare June 22, 2026 11:59
@kumarUjjawal

Copy link
Copy Markdown
Contributor Author

@Jefffrey do you have time to review this pr?

@Jefffrey Jefffrey added the api change Changes the API exposed to users of the crate label Jul 16, 2026

@Jefffrey Jefffrey 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.

sorry took a while for me to get around to reviewing this

Comment thread datafusion/sqllogictest/test_files/information_schema.slt
statement ok
CREATE EXTERNAL TABLE multi_loc (c1 int, c2 bigint, c3 boolean)
STORED AS CSV
LOCATION ('../core/tests/data/partitioned_csv/partition-0.csv', '../core/tests/data/partitioned_csv/partition-1.csv')

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.

does anything funky happen if you specify the same location twice?

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 will add a test

Comment thread docs/source/user-guide/sql/ddl.md
columns,
file_type,
location,
location: _,

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.

should we add a comment here explaining why we ignore location?


Ok(LogicalPlan::Ddl(DdlStatement::CreateExternalTable(
Box::new(
PlanCreateExternalTable::builder(name, location, file_type, df_schema)

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.

similarly here do we need to explain why we have to provide the first location via builder but then override (i assume?) using with_locations() after?

name.clone(),
)),
location: location.clone(),
location: locations.first().cloned().unwrap_or_default(),

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.

is it better to just leave location empty? otherwise it could have subtle behaviour like dropping other locations if multiple were specified 🤔

/// File type (Parquet, NDJSON, CSV, etc)
pub file_type: String,
/// Path to file
/// First path to file, retained for backwards compatibility.

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.

i do wonder if we should take this opportunity to do a clean cut to locations since its already an API change or too many downstream users rely on location and prevents us from doing so 🤔

}
}

fn schemas_have_same_fields(left: &SchemaRef, right: &SchemaRef) -> bool {

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.

would a followup be exploring if we should allow union by name instead of only union by order

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.

This would be a good followup. Thanks!

match &inferred_schema {
None => inferred_schema = Some((location, schema)),
Some((existing_location, existing))
if !schemas_have_same_fields(existing, &schema) =>

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.

i suppose a followup issue could be allowing users to customize this, for example filling nulls for missing columns; but this is a good initial rule

@kumarUjjawal
kumarUjjawal force-pushed the feature/multi-location-external-table branch from afc19ce to 8d22b92 Compare July 17, 2026 05:12
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.74242% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.65%. Comparing base (7ca6e54) to head (8d22b92).

Files with missing lines Patch % Lines
datafusion/proto-models/src/generated/pbjson.rs 0.00% 13 Missing ⚠️
datafusion/sql/src/parser.rs 88.46% 4 Missing and 8 partials ⚠️
...usion/core/src/datasource/listing_table_factory.rs 96.63% 3 Missing and 1 partial ⚠️
datafusion/proto/src/logical_plan/mod.rs 76.92% 3 Missing ⚠️
datafusion/expr/src/logical_plan/ddl.rs 77.77% 2 Missing ⚠️
datafusion/sql/src/statement.rs 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #22695      +/-   ##
==========================================
- Coverage   80.66%   80.65%   -0.01%     
==========================================
  Files        1086     1086              
  Lines      366694   366747      +53     
  Branches   366694   366747      +53     
==========================================
+ Hits       295806   295818      +12     
- Misses      53265    53293      +28     
- Partials    17623    17636      +13     

☔ 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.

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

Labels

api change Changes the API exposed to users of the crate auto detected api change Auto detected API change catalog Related to the catalog crate core Core DataFusion crate documentation Improvements or additions to documentation ffi Changes to the ffi crate logical-expr Logical plan and expressions proto Related to proto crate sql SQL Planner sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants