From ee32855b9e77e934e2e1e0b5250a8c6bc3cf7a5b Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Tue, 11 Aug 2026 17:53:59 -0400 Subject: [PATCH 1/2] feat(sf_core): expose typed GetObjects metadata --- .../apis/database_driver_v1/get_objects.rs | 444 ++++++++++++++++-- sf_core/src/apis/database_driver_v1/mod.rs | 15 +- 2 files changed, 424 insertions(+), 35 deletions(-) diff --git a/sf_core/src/apis/database_driver_v1/get_objects.rs b/sf_core/src/apis/database_driver_v1/get_objects.rs index bfa88ca50e..28ed27717f 100644 --- a/sf_core/src/apis/database_driver_v1/get_objects.rs +++ b/sf_core/src/apis/database_driver_v1/get_objects.rs @@ -150,6 +150,8 @@ pub fn nested_get_objects_schema() -> SchemaRef { // Public request type // --------------------------------------------------------------------------- +/// Request parameters shared by the Arrow and typed GetObjects APIs. +#[derive(Debug, Clone)] pub struct GetObjectsRequest { pub conn_handle: Handle, pub depth: i32, @@ -160,6 +162,35 @@ pub struct GetObjectsRequest { pub column_name: Option, } +/// An owned catalog node in the typed GetObjects metadata tree. +/// +/// `db_schemas` is `None` when the requested depth stops at catalogs and is +/// `Some` (possibly empty) when schemas were requested. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CatalogMetadata { + pub catalog_name: String, + pub db_schemas: Option>, +} + +/// An owned schema node in the typed GetObjects metadata tree. +/// +/// `tables` is `None` when the requested depth stops at schemas and is `Some` +/// (possibly empty) when tables were requested. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DbSchemaMetadata { + pub db_schema_name: String, + pub tables: Option>, +} + +/// An owned table node in the typed GetObjects metadata tree. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TableMetadata { + pub table_name: String, + pub table_type: String, + /// Columns are empty when the requested depth stops at tables. + pub columns: Vec, +} + // --------------------------------------------------------------------------- // kind → TABLE_TYPE normalization // --------------------------------------------------------------------------- @@ -199,10 +230,40 @@ fn escape_show_like(pattern: &str) -> String { // --------------------------------------------------------------------------- impl DatabaseDriverV1 { + /// Returns GetObjects metadata as an owned, depth-aware tree. + /// + /// Unlike [`Self::connection_get_objects`], this API does not encode the + /// metadata into an Arrow result-set handle. A `None` child collection + /// means the requested depth stopped at its parent; `Some(Vec::new())` + /// means that level was requested but matched no objects. + pub async fn connection_get_objects_typed( + &self, + req: GetObjectsRequest, + ) -> Result, ApiError> { + let (_, metadata) = self.fetch_get_objects_metadata(req).await?; + Ok(metadata) + } + pub async fn connection_get_objects( &self, req: GetObjectsRequest, ) -> Result { + let (conn_ptr, metadata) = self.fetch_get_objects_metadata(req).await?; + let batch = build_metadata_batch(metadata)?; + let http_client = { + let conn = conn_ptr.lock().await; + conn.http_client + .clone() + .context(ConnectionNotInitializedSnafu)? + }; + + self.register_arrow_batch_as_result_set(&batch, http_client) + } + + async fn fetch_get_objects_metadata( + &self, + req: GetObjectsRequest, + ) -> Result<(Arc>, Vec), ApiError> { let conn_ptr = self .connections .get_obj(req.conn_handle) @@ -215,10 +276,7 @@ impl DatabaseDriverV1 { apply_connection_context(&conn, req.catalog, req.db_schema).await }; - // `depth` arrives as a raw proto i32. Dispatch only the implemented depths - // explicitly; reject COLUMNS (deferred) and unknown values rather than - // letting them fall through to a TABLES catch-all. - let batch = match req.depth { + let metadata = match req.depth { DEPTH_CATALOGS => fetch_catalogs(&conn_ptr, catalog_filter.as_deref()).await?, DEPTH_DB_SCHEMAS => { fetch_schemas( @@ -257,14 +315,7 @@ impl DatabaseDriverV1 { } }; - let http_client = { - let conn = conn_ptr.lock().await; - conn.http_client - .clone() - .context(ConnectionNotInitializedSnafu)? - }; - - self.register_arrow_batch_as_result_set(&batch, http_client) + Ok((conn_ptr, metadata)) } } @@ -305,11 +356,11 @@ async fn apply_connection_context( async fn fetch_catalogs( conn_ptr: &Arc>, catalog_filter: Option<&str>, -) -> Result { +) -> Result, ApiError> { let sql = "SHOW DATABASES IN ACCOUNT".to_string(); let rows = execute_show(conn_ptr, &sql).await?; - let catalog_names: Vec> = rows + Ok(rows .iter() .filter_map(|row| { let name = get_column(row, "name")?; @@ -318,11 +369,12 @@ async fn fetch_catalogs( { return None; } - Some(Some(name.to_string())) + Some(CatalogMetadata { + catalog_name: name.to_string(), + db_schemas: None, + }) }) - .collect(); - - build_catalogs_batch(catalog_names) + .collect()) } // --------------------------------------------------------------------------- @@ -333,7 +385,7 @@ async fn fetch_schemas( conn_ptr: &Arc>, catalog_filter: Option<&str>, schema_filter: Option<&str>, -) -> Result { +) -> Result, ApiError> { // Pick tightest scope: exact catalog -> IN DATABASE "db", else IN ACCOUNT let sql = if let Some(pattern) = catalog_filter { if let Some(literal) = like_pattern::is_exact(pattern) { @@ -377,7 +429,21 @@ async fn fetch_schemas( by_catalog.entry(db_name).or_default().push(schema_name); } - build_schemas_batch(by_catalog) + Ok(by_catalog + .into_iter() + .map(|(catalog_name, schemas)| CatalogMetadata { + catalog_name, + db_schemas: Some( + schemas + .into_iter() + .map(|db_schema_name| DbSchemaMetadata { + db_schema_name, + tables: None, + }) + .collect(), + ), + }) + .collect()) } // --------------------------------------------------------------------------- @@ -419,13 +485,13 @@ async fn fetch_tables( schema_filter: Option<&str>, table_name_filter: Option<&str>, table_types: &TableTypeFilter, -) -> Result { +) -> Result, ApiError> { if matches!(table_types, TableTypeFilter::Unsupported) { - return build_tables_batch(BTreeMap::new()); + return Ok(Vec::new()); } // Empty string means "match nothing"; skip the server query (like_pattern::matches("", _) is false). if matches!(table_name_filter, Some("")) { - return build_tables_batch(BTreeMap::new()); + return Ok(Vec::new()); } let exact_catalog = catalog_filter @@ -501,7 +567,30 @@ async fn fetch_tables( .push((tbl_name, normalized_type)); } - build_tables_batch(by_cat_sch) + Ok(by_cat_sch + .into_iter() + .map(|(catalog_name, schemas)| CatalogMetadata { + catalog_name, + db_schemas: Some( + schemas + .into_iter() + .map(|(db_schema_name, tables)| DbSchemaMetadata { + db_schema_name, + tables: Some( + tables + .into_iter() + .map(|(table_name, table_type)| TableMetadata { + table_name, + table_type, + columns: Vec::new(), + }) + .collect(), + ), + }) + .collect(), + ), + }) + .collect()) } /// Build a `LIKE '…'` clause for `SHOW` commands. @@ -719,6 +808,100 @@ fn get_column<'a>(row: &'a [(String, String)], name: &str) -> Option<&'a str> { // Arrow batch builders // --------------------------------------------------------------------------- +fn build_metadata_batch(metadata: Vec) -> Result { + let schemas_requested = metadata.iter().any(|catalog| catalog.db_schemas.is_some()); + if !schemas_requested { + return build_catalogs_batch( + metadata + .into_iter() + .map(|catalog| Some(catalog.catalog_name)) + .collect(), + ); + } + + let tables_requested = metadata.iter().any(|catalog| { + catalog + .db_schemas + .as_ref() + .is_some_and(|schemas| schemas.iter().any(|schema| schema.tables.is_some())) + }); + if !tables_requested { + let by_catalog = metadata + .into_iter() + .map(|catalog| { + let schemas = catalog + .db_schemas + .unwrap_or_default() + .into_iter() + .map(|schema| schema.db_schema_name) + .collect(); + (catalog.catalog_name, schemas) + }) + .collect(); + return build_schemas_batch(by_catalog); + } + + // The legacy COLUMNS representation uses an empty table type, whereas + // TABLES always contains TABLE or VIEW. Empty metadata produces the same + // zero-row RecordBatch at every depth, so no depth marker is needed there. + let columns_requested = metadata.iter().any(|catalog| { + catalog.db_schemas.as_ref().is_some_and(|schemas| { + schemas.iter().any(|schema| { + schema + .tables + .as_ref() + .is_some_and(|tables| tables.iter().any(|table| table.table_type.is_empty())) + }) + }) + }); + + if columns_requested { + let by_catalog = metadata + .into_iter() + .map(|catalog| { + let schemas = catalog + .db_schemas + .unwrap_or_default() + .into_iter() + .map(|schema| { + let tables = schema + .tables + .unwrap_or_default() + .into_iter() + .map(|table| (table.table_name, table.columns)) + .collect(); + (schema.db_schema_name, tables) + }) + .collect(); + (catalog.catalog_name, schemas) + }) + .collect(); + build_columns_batch(by_catalog) + } else { + let by_catalog = metadata + .into_iter() + .map(|catalog| { + let schemas = catalog + .db_schemas + .unwrap_or_default() + .into_iter() + .map(|schema| { + let tables = schema + .tables + .unwrap_or_default() + .into_iter() + .map(|table| (table.table_name, table.table_type)) + .collect(); + (schema.db_schema_name, tables) + }) + .collect(); + (catalog.catalog_name, schemas) + }) + .collect(); + build_tables_batch(by_catalog) + } +} + fn build_catalogs_batch(catalog_names: Vec>) -> Result { let schema = nested_get_objects_schema(); let n = catalog_names.len(); @@ -1027,7 +1210,7 @@ struct ShowColumnDataType { /// Decoded canonical column descriptor — the sf_core representation of a /// single SHOW COLUMNS row, with Snowflake-wire JSON fully parsed. /// No raw JSON crosses into the `odbc` crate. -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ColumnDescriptor { pub column_name: String, pub ordinal_position: i32, @@ -1123,10 +1306,10 @@ async fn fetch_columns( schema_filter: Option<&str>, table_name_filter: Option<&str>, column_name_filter: Option<&str>, -) -> Result { +) -> Result, ApiError> { // Empty string means "match nothing". if matches!(table_name_filter, Some("")) || matches!(column_name_filter, Some("")) { - return build_columns_batch(BTreeMap::new()); + return Ok(Vec::new()); } let exact_catalog = catalog_filter @@ -1234,7 +1417,31 @@ async fn fetch_columns( table_cols.push(descriptor); } - build_columns_batch(by_cat_sch_tbl) + Ok(by_cat_sch_tbl + .into_iter() + .map(|(catalog_name, schemas)| CatalogMetadata { + catalog_name, + db_schemas: Some( + schemas + .into_iter() + .map(|(db_schema_name, tables)| DbSchemaMetadata { + db_schema_name, + tables: Some( + tables + .into_iter() + .map(|(table_name, columns)| TableMetadata { + table_name, + // Preserve the existing COLUMNS Arrow representation. + table_type: String::new(), + columns, + }) + .collect(), + ), + }) + .collect(), + ), + }) + .collect()) } fn build_columns_batch( @@ -1889,4 +2096,185 @@ mod tests { .unwrap(); assert_eq!(logical_type_arr.value(0), "FIXED"); } + + #[test] + fn typed_catalog_tree_preserves_order_and_null_schema_depth() { + let metadata = vec![ + CatalogMetadata { + catalog_name: "ZDB".to_string(), + db_schemas: None, + }, + CatalogMetadata { + catalog_name: "ADB".to_string(), + db_schemas: None, + }, + ]; + + let batch = build_metadata_batch(metadata).unwrap(); + let names = batch + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(names.value(0), "ZDB"); + assert_eq!(names.value(1), "ADB"); + + let schemas = batch + .column(1) + .as_any() + .downcast_ref::() + .unwrap(); + assert!(schemas.is_null(0)); + assert!(schemas.is_null(1)); + } + + #[test] + fn typed_schema_tree_preserves_null_table_depth() { + let metadata = vec![CatalogMetadata { + catalog_name: "DB".to_string(), + db_schemas: Some(vec![DbSchemaMetadata { + db_schema_name: "PUBLIC".to_string(), + tables: None, + }]), + }]; + + let batch = build_metadata_batch(metadata).unwrap(); + let schemas = batch + .column(1) + .as_any() + .downcast_ref::() + .unwrap(); + assert!(!schemas.is_null(0)); + let schema_values = schemas + .values() + .as_any() + .downcast_ref::() + .unwrap(); + let tables = schema_values + .column_by_name(FIELD_DB_SCHEMA_TABLES) + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + assert!(tables.is_null(0)); + } + + #[test] + fn typed_table_tree_preserves_empty_non_null_columns() { + let metadata = vec![CatalogMetadata { + catalog_name: "DB".to_string(), + db_schemas: Some(vec![DbSchemaMetadata { + db_schema_name: "PUBLIC".to_string(), + tables: Some(vec![TableMetadata { + table_name: "T".to_string(), + table_type: "TABLE".to_string(), + columns: Vec::new(), + }]), + }]), + }]; + + let batch = build_metadata_batch(metadata).unwrap(); + let schemas = batch + .column(1) + .as_any() + .downcast_ref::() + .unwrap(); + let schema_values = schemas + .values() + .as_any() + .downcast_ref::() + .unwrap(); + let tables = schema_values + .column_by_name(FIELD_DB_SCHEMA_TABLES) + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + let table_values = tables + .values() + .as_any() + .downcast_ref::() + .unwrap(); + let columns = table_values + .column_by_name(FIELD_TABLE_COLUMNS) + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + assert!(!columns.is_null(0)); + assert_eq!(columns.value_length(0), 0); + } + + #[test] + fn typed_column_tree_preserves_column_and_empty_table_type() { + let column = ColumnDescriptor { + column_name: "ID".to_string(), + ordinal_position: 1, + logical_type: "FIXED".to_string(), + precision: Some(38), + scale: Some(0), + char_length: None, + byte_length: None, + nullable: false, + column_def: None, + remarks: Some("identifier".to_string()), + }; + let metadata = vec![CatalogMetadata { + catalog_name: "DB".to_string(), + db_schemas: Some(vec![DbSchemaMetadata { + db_schema_name: "PUBLIC".to_string(), + tables: Some(vec![TableMetadata { + table_name: "T".to_string(), + table_type: String::new(), + columns: vec![column.clone()], + }]), + }]), + }]; + + assert_eq!( + metadata[0].db_schemas.as_ref().unwrap()[0] + .tables + .as_ref() + .unwrap()[0] + .columns, + vec![column] + ); + let batch = build_metadata_batch(metadata).unwrap(); + let schemas = batch + .column(1) + .as_any() + .downcast_ref::() + .unwrap(); + let schema_values = schemas + .values() + .as_any() + .downcast_ref::() + .unwrap(); + let tables = schema_values + .column_by_name(FIELD_DB_SCHEMA_TABLES) + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + let table_values = tables + .values() + .as_any() + .downcast_ref::() + .unwrap(); + let table_types = table_values + .column_by_name(FIELD_TABLE_TYPE) + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(table_types.value(0), ""); + assert!(!table_types.is_null(0)); + let columns = table_values + .column_by_name(FIELD_TABLE_COLUMNS) + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(columns.value_length(0), 1); + } } diff --git a/sf_core/src/apis/database_driver_v1/mod.rs b/sf_core/src/apis/database_driver_v1/mod.rs index 617507b545..a8d3b51837 100644 --- a/sf_core/src/apis/database_driver_v1/mod.rs +++ b/sf_core/src/apis/database_driver_v1/mod.rs @@ -36,13 +36,14 @@ pub use async_query_registry::AsyncQueryRegistry; pub use connection::{Connection, ConnectionInfo, RefreshContext, with_valid_session}; pub use error::ApiError; pub use get_objects::{ - ColumnDescriptor, DEPTH_CATALOGS, DEPTH_COLUMNS, DEPTH_DB_SCHEMAS, DEPTH_TABLES, - FIELD_CATALOG_DB_SCHEMAS, FIELD_CATALOG_NAME, FIELD_COLUMN_BYTE_LENGTH, - FIELD_COLUMN_CHAR_LENGTH, FIELD_COLUMN_DEF, FIELD_COLUMN_LOGICAL_TYPE, FIELD_COLUMN_NAME, - FIELD_COLUMN_NULLABLE, FIELD_COLUMN_ORDINAL_POSITION, FIELD_COLUMN_PRECISION, - FIELD_COLUMN_REMARKS, FIELD_COLUMN_SCALE, FIELD_DB_SCHEMA_NAME, FIELD_DB_SCHEMA_TABLES, - FIELD_TABLE_COLUMNS, FIELD_TABLE_CONSTRAINTS, FIELD_TABLE_NAME, FIELD_TABLE_TYPE, - GetObjectsRequest, nested_get_objects_schema, + CatalogMetadata, ColumnDescriptor, DEPTH_CATALOGS, DEPTH_COLUMNS, DEPTH_DB_SCHEMAS, + DEPTH_TABLES, DbSchemaMetadata, FIELD_CATALOG_DB_SCHEMAS, FIELD_CATALOG_NAME, + FIELD_COLUMN_BYTE_LENGTH, FIELD_COLUMN_CHAR_LENGTH, FIELD_COLUMN_DEF, + FIELD_COLUMN_LOGICAL_TYPE, FIELD_COLUMN_NAME, FIELD_COLUMN_NULLABLE, + FIELD_COLUMN_ORDINAL_POSITION, FIELD_COLUMN_PRECISION, FIELD_COLUMN_REMARKS, + FIELD_COLUMN_SCALE, FIELD_DB_SCHEMA_NAME, FIELD_DB_SCHEMA_TABLES, FIELD_TABLE_COLUMNS, + FIELD_TABLE_CONSTRAINTS, FIELD_TABLE_NAME, FIELD_TABLE_TYPE, GetObjectsRequest, TableMetadata, + nested_get_objects_schema, }; pub use global_state::{DatabaseDriverV1, DriverProviders, PutGetResultsetFlavor, WrapperPresets}; pub use like_pattern::ESCAPE_CHAR; From 3a22ff43d9e33e35019971150c9c07175db0f45a Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Tue, 11 Aug 2026 18:11:12 -0400 Subject: [PATCH 2/2] fix(sf_core): make metadata depth explicit --- .../apis/database_driver_v1/get_objects.rs | 201 ++++++++---------- 1 file changed, 94 insertions(+), 107 deletions(-) diff --git a/sf_core/src/apis/database_driver_v1/get_objects.rs b/sf_core/src/apis/database_driver_v1/get_objects.rs index 28ed27717f..d523968936 100644 --- a/sf_core/src/apis/database_driver_v1/get_objects.rs +++ b/sf_core/src/apis/database_driver_v1/get_objects.rs @@ -186,7 +186,9 @@ pub struct DbSchemaMetadata { #[derive(Debug, Clone, PartialEq, Eq)] pub struct TableMetadata { pub table_name: String, - pub table_type: String, + /// `Some("TABLE")` or `Some("VIEW")` when table metadata was requested; + /// `None` at column depth because `SHOW COLUMNS` does not return table type. + pub table_type: Option, /// Columns are empty when the requested depth stops at tables. pub columns: Vec, } @@ -248,8 +250,9 @@ impl DatabaseDriverV1 { &self, req: GetObjectsRequest, ) -> Result { + let depth = req.depth; let (conn_ptr, metadata) = self.fetch_get_objects_metadata(req).await?; - let batch = build_metadata_batch(metadata)?; + let batch = build_metadata_batch(metadata, depth)?; let http_client = { let conn = conn_ptr.lock().await; conn.http_client @@ -581,7 +584,7 @@ async fn fetch_tables( .into_iter() .map(|(table_name, table_type)| TableMetadata { table_name, - table_type, + table_type: Some(table_type), columns: Vec::new(), }) .collect(), @@ -808,97 +811,84 @@ fn get_column<'a>(row: &'a [(String, String)], name: &str) -> Option<&'a str> { // Arrow batch builders // --------------------------------------------------------------------------- -fn build_metadata_batch(metadata: Vec) -> Result { - let schemas_requested = metadata.iter().any(|catalog| catalog.db_schemas.is_some()); - if !schemas_requested { - return build_catalogs_batch( +fn build_metadata_batch( + metadata: Vec, + depth: i32, +) -> Result { + match depth { + DEPTH_CATALOGS => build_catalogs_batch( metadata .into_iter() .map(|catalog| Some(catalog.catalog_name)) .collect(), - ); - } - - let tables_requested = metadata.iter().any(|catalog| { - catalog - .db_schemas - .as_ref() - .is_some_and(|schemas| schemas.iter().any(|schema| schema.tables.is_some())) - }); - if !tables_requested { - let by_catalog = metadata - .into_iter() - .map(|catalog| { - let schemas = catalog - .db_schemas - .unwrap_or_default() - .into_iter() - .map(|schema| schema.db_schema_name) - .collect(); - (catalog.catalog_name, schemas) - }) - .collect(); - return build_schemas_batch(by_catalog); - } - - // The legacy COLUMNS representation uses an empty table type, whereas - // TABLES always contains TABLE or VIEW. Empty metadata produces the same - // zero-row RecordBatch at every depth, so no depth marker is needed there. - let columns_requested = metadata.iter().any(|catalog| { - catalog.db_schemas.as_ref().is_some_and(|schemas| { - schemas.iter().any(|schema| { - schema - .tables - .as_ref() - .is_some_and(|tables| tables.iter().any(|table| table.table_type.is_empty())) - }) - }) - }); - - if columns_requested { - let by_catalog = metadata - .into_iter() - .map(|catalog| { - let schemas = catalog - .db_schemas - .unwrap_or_default() - .into_iter() - .map(|schema| { - let tables = schema - .tables - .unwrap_or_default() - .into_iter() - .map(|table| (table.table_name, table.columns)) - .collect(); - (schema.db_schema_name, tables) - }) - .collect(); - (catalog.catalog_name, schemas) - }) - .collect(); - build_columns_batch(by_catalog) - } else { - let by_catalog = metadata - .into_iter() - .map(|catalog| { - let schemas = catalog - .db_schemas - .unwrap_or_default() - .into_iter() - .map(|schema| { - let tables = schema - .tables - .unwrap_or_default() - .into_iter() - .map(|table| (table.table_name, table.table_type)) - .collect(); - (schema.db_schema_name, tables) - }) - .collect(); - (catalog.catalog_name, schemas) - }) - .collect(); - build_tables_batch(by_catalog) + ), + DEPTH_DB_SCHEMAS => { + let by_catalog = metadata + .into_iter() + .map(|catalog| { + let schemas = catalog + .db_schemas + .unwrap_or_default() + .into_iter() + .map(|schema| schema.db_schema_name) + .collect(); + (catalog.catalog_name, schemas) + }) + .collect(); + build_schemas_batch(by_catalog) + } + DEPTH_TABLES => { + let by_catalog = metadata + .into_iter() + .map(|catalog| { + let schemas = catalog + .db_schemas + .unwrap_or_default() + .into_iter() + .map(|schema| { + let tables = schema + .tables + .unwrap_or_default() + .into_iter() + .map(|table| { + (table.table_name, table.table_type.unwrap_or_default()) + }) + .collect(); + (schema.db_schema_name, tables) + }) + .collect(); + (catalog.catalog_name, schemas) + }) + .collect(); + build_tables_batch(by_catalog) + } + DEPTH_COLUMNS => { + let by_catalog = metadata + .into_iter() + .map(|catalog| { + let schemas = catalog + .db_schemas + .unwrap_or_default() + .into_iter() + .map(|schema| { + let tables = schema + .tables + .unwrap_or_default() + .into_iter() + .map(|table| (table.table_name, table.columns)) + .collect(); + (schema.db_schema_name, tables) + }) + .collect(); + (catalog.catalog_name, schemas) + }) + .collect(); + build_columns_batch(by_catalog) + } + other => InvalidArgumentSnafu { + argument: format!("GetObjects depth {other} is invalid (expected 1..=4)"), + } + .fail(), } } @@ -1431,8 +1421,7 @@ async fn fetch_columns( .into_iter() .map(|(table_name, columns)| TableMetadata { table_name, - // Preserve the existing COLUMNS Arrow representation. - table_type: String::new(), + table_type: None, columns, }) .collect(), @@ -2110,7 +2099,7 @@ mod tests { }, ]; - let batch = build_metadata_batch(metadata).unwrap(); + let batch = build_metadata_batch(metadata, DEPTH_CATALOGS).unwrap(); let names = batch .column(0) .as_any() @@ -2138,7 +2127,7 @@ mod tests { }]), }]; - let batch = build_metadata_batch(metadata).unwrap(); + let batch = build_metadata_batch(metadata, DEPTH_DB_SCHEMAS).unwrap(); let schemas = batch .column(1) .as_any() @@ -2167,13 +2156,13 @@ mod tests { db_schema_name: "PUBLIC".to_string(), tables: Some(vec![TableMetadata { table_name: "T".to_string(), - table_type: "TABLE".to_string(), + table_type: Some("TABLE".to_string()), columns: Vec::new(), }]), }]), }]; - let batch = build_metadata_batch(metadata).unwrap(); + let batch = build_metadata_batch(metadata, DEPTH_TABLES).unwrap(); let schemas = batch .column(1) .as_any() @@ -2206,7 +2195,7 @@ mod tests { } #[test] - fn typed_column_tree_preserves_column_and_empty_table_type() { + fn typed_column_tree_marks_table_type_unavailable() { let column = ColumnDescriptor { column_name: "ID".to_string(), ordinal_position: 1, @@ -2225,21 +2214,19 @@ mod tests { db_schema_name: "PUBLIC".to_string(), tables: Some(vec![TableMetadata { table_name: "T".to_string(), - table_type: String::new(), + table_type: None, columns: vec![column.clone()], }]), }]), }]; - assert_eq!( - metadata[0].db_schemas.as_ref().unwrap()[0] - .tables - .as_ref() - .unwrap()[0] - .columns, - vec![column] - ); - let batch = build_metadata_batch(metadata).unwrap(); + let table = &metadata[0].db_schemas.as_ref().unwrap()[0] + .tables + .as_ref() + .unwrap()[0]; + assert_eq!(table.columns, vec![column]); + assert_eq!(table.table_type, None); + let batch = build_metadata_batch(metadata, DEPTH_COLUMNS).unwrap(); let schemas = batch .column(1) .as_any()