Skip to content

Commit 4df2961

Browse files
committed
feat(bigquery): parse WITH CONNECTION on CREATE EXTERNAL TABLE
1 parent 9833c03 commit 4df2961

8 files changed

Lines changed: 129 additions & 2 deletions

File tree

src/ast/ddl.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3021,6 +3021,9 @@ pub struct CreateTable {
30213021
/// Snowflake "EXTERNAL_VOLUME" clause for Iceberg tables
30223022
/// <https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table>
30233023
pub external_volume: Option<String>,
3024+
/// BigQuery `WITH CONNECTION` clause for external tables
3025+
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_external_table_statement>
3026+
pub with_connection: Option<ObjectName>,
30243027
/// Snowflake "BASE_LOCATION" clause for Iceberg tables
30253028
/// <https://docs.snowflake.com/en/sql-reference/sql/create-iceberg-table>
30263029
pub base_location: Option<String>,
@@ -3250,6 +3253,9 @@ impl fmt::Display for CreateTable {
32503253
if let Some(cluster_by) = self.cluster_by.as_ref() {
32513254
write!(f, " CLUSTER BY {cluster_by}")?;
32523255
}
3256+
if let Some(with_connection) = &self.with_connection {
3257+
write!(f, " WITH CONNECTION {with_connection}")?;
3258+
}
32533259
if let options @ CreateTableOptions::Options(_) = &self.table_options {
32543260
write!(f, " {options}")?;
32553261
}

src/ast/helpers/stmt_create_table.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ pub struct CreateTableBuilder {
157157
pub base_location: Option<String>,
158158
/// Optional external volume identifier.
159159
pub external_volume: Option<String>,
160+
/// BigQuery `WITH CONNECTION` clause for external tables.
161+
/// <https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_external_table_statement>
162+
pub with_connection: Option<ObjectName>,
160163
/// Optional catalog name.
161164
pub catalog: Option<String>,
162165
/// Optional catalog synchronization option.
@@ -235,6 +238,7 @@ impl CreateTableBuilder {
235238
with_tags: None,
236239
base_location: None,
237240
external_volume: None,
241+
with_connection: None,
238242
catalog: None,
239243
catalog_sync: None,
240244
storage_serialization_policy: None,
@@ -488,6 +492,11 @@ impl CreateTableBuilder {
488492
self.external_volume = external_volume;
489493
self
490494
}
495+
/// Set the BigQuery `WITH CONNECTION` clause for external tables.
496+
pub fn with_connection(mut self, with_connection: Option<ObjectName>) -> Self {
497+
self.with_connection = with_connection;
498+
self
499+
}
491500
/// Set the catalog name for the table.
492501
pub fn catalog(mut self, catalog: Option<String>) -> Self {
493502
self.catalog = catalog;
@@ -605,6 +614,7 @@ impl CreateTableBuilder {
605614
with_tags: self.with_tags,
606615
base_location: self.base_location,
607616
external_volume: self.external_volume,
617+
with_connection: self.with_connection,
608618
catalog: self.catalog,
609619
catalog_sync: self.catalog_sync,
610620
storage_serialization_policy: self.storage_serialization_policy,
@@ -686,6 +696,7 @@ impl From<CreateTable> for CreateTableBuilder {
686696
with_tags: table.with_tags,
687697
base_location: table.base_location,
688698
external_volume: table.external_volume,
699+
with_connection: table.with_connection,
689700
catalog: table.catalog,
690701
catalog_sync: table.catalog_sync,
691702
storage_serialization_policy: table.storage_serialization_policy,

src/ast/spans.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,7 @@ impl Spanned for CreateTable {
589589
with_storage_lifecycle_policy: _, // todo, Snowflake specific
590590
with_tags: _, // todo, Snowflake specific
591591
external_volume: _, // todo, Snowflake specific
592+
with_connection: _, // todo, BigQuery external table connection
592593
base_location: _, // todo, Snowflake specific
593594
catalog: _, // todo, Snowflake specific
594595
catalog_sync: _, // todo, Snowflake specific

src/parser/mod.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6416,8 +6416,28 @@ impl<'a> Parser<'a> {
64166416
None
64176417
};
64186418
let location = hive_formats.as_ref().and_then(|hf| hf.location.clone());
6419-
let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?;
6420-
let table_options = if !table_properties.is_empty() {
6419+
6420+
// BigQuery external tables support `WITH CONNECTION <name>` and `OPTIONS(...)`
6421+
// clauses after the (optional) column list.
6422+
let with_connection = if self.parse_keywords(&[Keyword::WITH, Keyword::CONNECTION]) {
6423+
Some(self.parse_object_name(false)?)
6424+
} else {
6425+
None
6426+
};
6427+
// BigQuery uses OPTIONS(...); Hive uses TBLPROPERTIES(...). They are
6428+
// mutually exclusive in practice, and `parse_options` returns an empty
6429+
// vec when the keyword isn't present, so trying OPTIONS first and
6430+
// falling back to TBLPROPERTIES preserves the existing Hive path
6431+
// without accepting both in the same statement.
6432+
let options = self.parse_options(Keyword::OPTIONS)?;
6433+
let table_properties = if options.is_empty() {
6434+
self.parse_options(Keyword::TBLPROPERTIES)?
6435+
} else {
6436+
vec![]
6437+
};
6438+
let table_options = if !options.is_empty() {
6439+
CreateTableOptions::Options(options)
6440+
} else if !table_properties.is_empty() {
64216441
CreateTableOptions::TableProperties(table_properties)
64226442
} else if let Some(options) = self.maybe_parse_options(Keyword::OPTIONS)? {
64236443
CreateTableOptions::Options(options)
@@ -6430,6 +6450,7 @@ impl<'a> Parser<'a> {
64306450
.hive_distribution(hive_distribution)
64316451
.hive_formats(hive_formats)
64326452
.table_options(table_options)
6453+
.with_connection(with_connection)
64336454
.or_replace(or_replace)
64346455
.if_not_exists(if_not_exists)
64356456
.external(true)

tests/sqlparser_bigquery.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2203,6 +2203,90 @@ fn parse_big_query_declare() {
22032203
);
22042204
}
22052205

2206+
#[test]
2207+
fn parse_bigquery_create_external_table_with_connection_and_options() {
2208+
let sql = concat!(
2209+
"CREATE OR REPLACE EXTERNAL TABLE `proj.ds.tbl` ",
2210+
"WITH CONNECTION `projects/proj/locations/us/connections/c` ",
2211+
r#"OPTIONS(format = "ICEBERG", uris = ["gs://b/m.json"])"#,
2212+
);
2213+
let stmts = bigquery().parse_sql_statements(sql).unwrap();
2214+
assert_eq!(stmts.len(), 1);
2215+
let Statement::CreateTable(ct) = &stmts[0] else {
2216+
panic!("expected CreateTable, got {:?}", stmts[0]);
2217+
};
2218+
assert!(ct.external);
2219+
assert!(ct.or_replace);
2220+
assert!(ct.columns.is_empty());
2221+
let with_connection = ct.with_connection.as_ref().expect("with_connection set");
2222+
assert_eq!(
2223+
with_connection.to_string(),
2224+
"`projects/proj/locations/us/connections/c`"
2225+
);
2226+
let CreateTableOptions::Options(opts) = &ct.table_options else {
2227+
panic!("expected OPTIONS, got {:?}", ct.table_options);
2228+
};
2229+
assert_eq!(opts.len(), 2);
2230+
}
2231+
2232+
#[test]
2233+
fn parse_bigquery_create_external_table_with_connection_variants() {
2234+
// WITH CONNECTION alone — no OPTIONS, no explicit column list.
2235+
// Display normalizes the bare form by adding an empty column list, so
2236+
// use `one_statement_parses_to` to assert the normalized output.
2237+
let stmt = bigquery().one_statement_parses_to(
2238+
"CREATE EXTERNAL TABLE t WITH CONNECTION c",
2239+
"CREATE EXTERNAL TABLE t () WITH CONNECTION c",
2240+
);
2241+
let Statement::CreateTable(ct) = stmt else {
2242+
panic!("expected CreateTable");
2243+
};
2244+
assert!(ct.external);
2245+
assert!(!ct.or_replace);
2246+
assert!(ct.columns.is_empty());
2247+
assert_eq!(
2248+
ct.with_connection
2249+
.as_ref()
2250+
.expect("with_connection set")
2251+
.to_string(),
2252+
"c"
2253+
);
2254+
assert!(matches!(ct.table_options, CreateTableOptions::None));
2255+
2256+
// With explicit columns + OPTIONS. Exercises the Display round-trip of
2257+
// the `(columns) WITH CONNECTION <name> OPTIONS(...)` ordering so a
2258+
// future refactor that reshuffles clause order will fail this test.
2259+
let sql = concat!(
2260+
"CREATE EXTERNAL TABLE t (a INT64, b STRING) ",
2261+
r#"WITH CONNECTION c OPTIONS(uris = ["gs://x"])"#,
2262+
);
2263+
let stmt = bigquery().verified_stmt(sql);
2264+
let Statement::CreateTable(ct) = stmt else {
2265+
panic!("expected CreateTable");
2266+
};
2267+
assert_eq!(ct.columns.len(), 2);
2268+
assert_eq!(
2269+
ct.with_connection
2270+
.as_ref()
2271+
.expect("with_connection set")
2272+
.to_string(),
2273+
"c"
2274+
);
2275+
let CreateTableOptions::Options(opts) = &ct.table_options else {
2276+
panic!("expected OPTIONS, got {:?}", ct.table_options);
2277+
};
2278+
assert_eq!(opts.len(), 1);
2279+
2280+
// Baseline: no WITH CONNECTION. The new parser branch must not produce
2281+
// a spurious with_connection when the keyword pair is absent.
2282+
let stmt =
2283+
bigquery().verified_stmt("CREATE EXTERNAL TABLE t (a INT64) OPTIONS(uris = [\"gs://x\"])");
2284+
let Statement::CreateTable(ct) = stmt else {
2285+
panic!("expected CreateTable");
2286+
};
2287+
assert!(ct.with_connection.is_none());
2288+
}
2289+
22062290
fn bigquery() -> TestedDialects {
22072291
TestedDialects::new(vec![Box::new(BigQueryDialect {})])
22082292
}

tests/sqlparser_duckdb.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,7 @@ fn test_duckdb_union_datatype() {
780780
with_tags: Default::default(),
781781
base_location: Default::default(),
782782
external_volume: Default::default(),
783+
with_connection: Default::default(),
783784
catalog: Default::default(),
784785
catalog_sync: Default::default(),
785786
storage_serialization_policy: Default::default(),

tests/sqlparser_mssql.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1999,6 +1999,7 @@ fn parse_create_table_with_valid_options() {
19991999
with_tags: None,
20002000
base_location: None,
20012001
external_volume: None,
2002+
with_connection: None,
20022003
catalog: None,
20032004
catalog_sync: None,
20042005
storage_serialization_policy: None,
@@ -2173,6 +2174,7 @@ fn parse_create_table_with_identity_column() {
21732174
with_tags: None,
21742175
base_location: None,
21752176
external_volume: None,
2177+
with_connection: None,
21762178
catalog: None,
21772179
catalog_sync: None,
21782180
storage_serialization_policy: None,

tests/sqlparser_postgres.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6702,6 +6702,7 @@ fn parse_trigger_related_functions() {
67026702
with_tags: None,
67036703
base_location: None,
67046704
external_volume: None,
6705+
with_connection: None,
67056706
catalog: None,
67066707
catalog_sync: None,
67076708
storage_serialization_policy: None,

0 commit comments

Comments
 (0)