Skip to content

PostgreSQL: Support CREATE FOREIGN TABLE - #2515

Merged
LucaCappelletti94 merged 11 commits into
apache:mainfrom
fmguerreiro:upstream/create-foreign-table
Sep 25, 2026
Merged

LucaCappelletti94 merged 11 commits into
apache:mainfrom
fmguerreiro:upstream/create-foreign-table

Conversation

@fmguerreiro

@fmguerreiro fmguerreiro commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Parses CREATE FOREIGN TABLE [IF NOT EXISTS] name (columns) SERVER server [OPTIONS (...)] (docs).

Split out of #2317 at @LucaCappelletti94's request. That PR now carries CREATE FOREIGN DATA WRAPPER only.

Table-level OPTIONS reuses the list parser pulled out of parse_pg_create_server, which #2317 also moves; whichever lands second needs a rebase there.

Per-column OPTIONS (...), INHERITS, PARTITION OF and ALTER FOREIGN TABLE are out of scope; the rustdoc notes that.

Table-level OPTIONS reuses the list parser lifted out of CREATE SERVER.
Without parens Display emitted a () the input never had, and a persistence modifier was accepted then dropped. Also covers OPTIONS in the span.
OR ALTER, MULTISET, PERSISTENT and the view params were still accepted and then dropped. The error now points at the modifier rather than the table name.
@fmguerreiro
fmguerreiro marked this pull request as ready for review September 16, 2026 06:12
Matching any ParserError would also pass if the guard stopped being reached. LOCAL, SET and the view params were never exercised.
Also says why the or_replace disjunct is kept despite being unreachable today.
@fmguerreiro fmguerreiro changed the title PostgreSQL: add support for CREATE FOREIGN TABLE PostgreSQL: Support CREATE FOREIGN TABLE Sep 16, 2026
@codecov-commenter

codecov-commenter commented Sep 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.38776% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.01%. Comparing base (9352883) to head (b1f9ffe).

Files with missing lines Patch % Lines
src/ast/ddl.rs 48.48% 13 Missing and 4 partials ⚠️
src/parser/mod.rs 79.24% 0 Missing and 11 partials ⚠️
src/ast/spans.rs 81.81% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2515      +/-   ##
==========================================
- Coverage   81.02%   81.01%   -0.02%     
==========================================
  Files          42       42              
  Lines       33455    33541      +86     
  Branches    33455    33541      +86     
==========================================
+ Hits        27108    27173      +65     
- Misses       2789     2800      +11     
- Partials     3558     3568      +10     

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

Comment thread src/ast/spans.rs
Comment on lines +3167 to +3181

#[test]
fn test_create_foreign_table_span_includes_option_keys() {
let dialect = &crate::dialect::PostgreSqlDialect {};
let sql = "CREATE FOREIGN TABLE ft (a INT) SERVER s OPTIONS (schema_name 'public')";
let mut test = SpanTest::new(dialect, sql);

// Ends at the option key, not the statement: a quoted option value is an
// Ident with an empty span, so it contributes nothing to the union.
let stmt = test.0.parse_statement().unwrap();
assert_eq!(
test.get_source(stmt.span()),
"ft (a INT) SERVER s OPTIONS (schema_name"
);
}

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.

You should not pin known-wrong behaviour in tests. This span stops at (schema_name because parse_identifier gives quoted identifiers an empty span.

Suggested change
#[test]
fn test_create_foreign_table_span_includes_option_keys() {
let dialect = &crate::dialect::PostgreSqlDialect {};
let sql = "CREATE FOREIGN TABLE ft (a INT) SERVER s OPTIONS (schema_name 'public')";
let mut test = SpanTest::new(dialect, sql);
// Ends at the option key, not the statement: a quoted option value is an
// Ident with an empty span, so it contributes nothing to the union.
let stmt = test.0.parse_statement().unwrap();
assert_eq!(
test.get_source(stmt.span()),
"ft (a INT) SERVER s OPTIONS (schema_name"
);
}

@fmguerreiro fmguerreiro Sep 24, 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.

shoot, you're right! fixed in 4207b27

@LucaCappelletti94 LucaCappelletti94 added the waiting on contributor The review needs further refinements by its author label Sep 24, 2026
@LucaCappelletti94 LucaCappelletti94 added waiting on reviewer PR needs to be reviewed and removed waiting on contributor The review needs further refinements by its author labels Sep 25, 2026
Comment thread src/ast/spans.rs
Comment on lines +3171 to +3183
#[test]
fn test_create_foreign_table_span_includes_quoted_option_value() {
let dialect = &crate::dialect::PostgreSqlDialect {};
let sql = "CREATE FOREIGN TABLE ft (a INT) SERVER s OPTIONS (schema_name 'public')";
let mut test = SpanTest::new(dialect, sql);

// Span reaches through the quoted value but not the closing paren.
let stmt = test.0.parse_statement().unwrap();
assert_eq!(
test.get_source(stmt.span()),
"ft (a INT) SERVER s OPTIONS (schema_name 'public'"
);
}

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.

You should assert the option spans directly instead of the statement span. The statement span still leaves out CREATE FOREIGN TABLE and the closing ), so this test pins known-wrong behaviour again. The version below fails if parse_identifier drops the span of a quoted identifier.

Suggested change
#[test]
fn test_create_foreign_table_span_includes_quoted_option_value() {
let dialect = &crate::dialect::PostgreSqlDialect {};
let sql = "CREATE FOREIGN TABLE ft (a INT) SERVER s OPTIONS (schema_name 'public')";
let mut test = SpanTest::new(dialect, sql);
// Span reaches through the quoted value but not the closing paren.
let stmt = test.0.parse_statement().unwrap();
assert_eq!(
test.get_source(stmt.span()),
"ft (a INT) SERVER s OPTIONS (schema_name 'public'"
);
}
#[test]
fn test_create_foreign_table_option_spans() {
let dialect = &crate::dialect::PostgreSqlDialect {};
let sql = r#"CREATE FOREIGN TABLE ft (a INT) SERVER s OPTIONS ("schema_name" 'public')"#;
let mut test = SpanTest::new(dialect, sql);
let options = match test.0.parse_statement().unwrap() {
Statement::CreateForeignTable(stmt) => stmt.options.unwrap(),
stmt => panic!("expected CREATE FOREIGN TABLE, got {stmt:?}"),
};
assert_eq!(test.get_source(options[0].key.span), r#""schema_name""#);
assert_eq!(test.get_source(options[0].value.span), "'public'");
}

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.

done in 88cbb5d, asserting the key and value spans directly.

@LucaCappelletti94 LucaCappelletti94 added waiting on contributor The review needs further refinements by its author and removed waiting on reviewer PR needs to be reviewed labels Sep 25, 2026
@LucaCappelletti94 LucaCappelletti94 added waiting on reviewer PR needs to be reviewed and removed waiting on contributor The review needs further refinements by its author labels Sep 25, 2026

@LucaCappelletti94 LucaCappelletti94 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 for splitting CREATE FOREIGN TABLE out of #2317 and finishing up the PR, @fmguerreiro.

If you have some time, we would really appreciate your help with reviewing other open PRs, which is where the project is most short-handed right now. Since you seem interested in PostgreSQL, running the fuzz_postgres_accepts harness would help a lot too. It feeds generated SQL to PostgreSQL's own grammar through pg_query and reports every statement PostgreSQL accepts that PostgreSqlDialect rejects. You can start it from the fuzz directory with

cargo +nightly fuzz run fuzz_postgres_accepts fuzz_seeds -- -max_total_time=600

and docs/fuzzing.md has the details. Minimized reproductions of what it finds, as issues or small fix PRs, are worth much more to us at this point than new syntax.

cc @alamb @iffyio

@LucaCappelletti94
LucaCappelletti94 added this pull request to the merge queue Sep 25, 2026
Merged via the queue into apache:main with commit c23dc05 Sep 25, 2026
12 of 13 checks passed
@LucaCappelletti94 LucaCappelletti94 removed the waiting on reviewer PR needs to be reviewed label Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants