iceberg_datafusion/table/
table_provider_factory.rs1use std::borrow::Cow;
19use std::collections::HashMap;
20use std::sync::Arc;
21
22use async_trait::async_trait;
23use datafusion::catalog::{Session, TableProvider, TableProviderFactory};
24use datafusion::common::TableReference;
25use datafusion::error::Result as DFResult;
26use datafusion::logical_expr::CreateExternalTable;
27use iceberg::io::{FileIOBuilder, LocalFsStorageFactory, StorageFactory};
28use iceberg::table::StaticTable;
29use iceberg::{Error, ErrorKind, Result, TableIdent};
30
31use super::IcebergStaticTableProvider;
32use crate::to_datafusion_error;
33
34#[derive(Debug, Default)]
100pub struct IcebergTableProviderFactory {
101 storage_factory: Option<Arc<dyn StorageFactory>>,
102}
103
104impl IcebergTableProviderFactory {
105 pub fn new() -> Self {
106 Self {
107 storage_factory: None,
108 }
109 }
110
111 pub fn new_with_storage_factory(storage_factory: Arc<dyn StorageFactory>) -> Self {
113 Self {
114 storage_factory: Some(storage_factory),
115 }
116 }
117}
118
119#[async_trait]
120impl TableProviderFactory for IcebergTableProviderFactory {
121 async fn create(
122 &self,
123 _state: &dyn Session,
124 cmd: &CreateExternalTable,
125 ) -> DFResult<Arc<dyn TableProvider>> {
126 let metadata_file_path = check_cmd(cmd).map_err(to_datafusion_error)?;
127
128 let table_name = &cmd.name;
129 let options = &cmd.options;
130
131 let table_name_with_ns = complement_namespace_if_necessary(table_name);
132
133 let storage_factory = self
134 .storage_factory
135 .clone()
136 .unwrap_or_else(|| Arc::new(LocalFsStorageFactory));
137
138 let table = create_static_table(
139 table_name_with_ns,
140 metadata_file_path,
141 options,
142 storage_factory,
143 )
144 .await
145 .map_err(to_datafusion_error)?
146 .into_table();
147
148 let provider = IcebergStaticTableProvider::try_new_from_table(table)
149 .await
150 .map_err(to_datafusion_error)?;
151
152 Ok(Arc::new(provider))
153 }
154}
155
156fn check_cmd(cmd: &CreateExternalTable) -> Result<&str> {
157 let CreateExternalTable {
158 schema,
159 table_partition_cols,
160 order_exprs,
161 constraints,
162 column_defaults,
163 ..
164 } = cmd;
165
166 let is_invalid = !schema.fields().is_empty()
168 || !table_partition_cols.is_empty()
169 || !order_exprs.is_empty()
170 || !constraints.is_empty()
171 || !column_defaults.is_empty();
172
173 if is_invalid {
174 return Err(Error::new(
175 ErrorKind::FeatureUnsupported,
176 "Currently we only support reading existing icebergs tables in external table command. To create new table, please use catalog provider.",
177 ));
178 }
179
180 match cmd.locations.as_slice() {
181 [location] => Ok(location),
182 _ => Err(Error::new(
183 ErrorKind::FeatureUnsupported,
184 "Iceberg external tables require exactly one metadata location.",
185 )),
186 }
187}
188
189fn complement_namespace_if_necessary(table_name: &TableReference) -> Cow<'_, TableReference> {
200 match table_name {
201 TableReference::Bare { table } => {
202 Cow::Owned(TableReference::partial("default", table.as_ref()))
203 }
204 other => Cow::Borrowed(other),
205 }
206}
207
208async fn create_static_table(
209 table_name: Cow<'_, TableReference>,
210 metadata_file_path: &str,
211 props: &HashMap<String, String>,
212 storage_factory: Arc<dyn StorageFactory>,
213) -> Result<StaticTable> {
214 let table_ident = TableIdent::from_strs(table_name.to_vec())?;
215 let file_io = FileIOBuilder::new(storage_factory)
216 .with_props(props)
217 .build();
218 StaticTable::from_metadata_file(metadata_file_path, table_ident, file_io).await
219}
220
221#[cfg(test)]
222mod tests {
223
224 use datafusion::arrow::datatypes::{DataType, Field, Schema};
225 use datafusion::catalog::TableProviderFactory;
226 use datafusion::common::{Constraints, DFSchema, TableReference};
227 use datafusion::execution::session_state::SessionStateBuilder;
228 use datafusion::logical_expr::CreateExternalTable;
229 use datafusion::parquet::arrow::PARQUET_FIELD_ID_META_KEY;
230 use datafusion::prelude::SessionContext;
231
232 use super::*;
233
234 fn table_metadata_v2_schema() -> Schema {
235 Schema::new(vec![
236 Field::new("x", DataType::Int64, false).with_metadata(HashMap::from([(
237 PARQUET_FIELD_ID_META_KEY.to_string(),
238 "1".to_string(),
239 )])),
240 Field::new("y", DataType::Int64, false).with_metadata(HashMap::from([(
241 PARQUET_FIELD_ID_META_KEY.to_string(),
242 "2".to_string(),
243 )])),
244 Field::new("z", DataType::Int64, false).with_metadata(HashMap::from([(
245 PARQUET_FIELD_ID_META_KEY.to_string(),
246 "3".to_string(),
247 )])),
248 ])
249 }
250
251 fn table_metadata_location() -> String {
252 format!(
253 "{}/testdata/table_metadata/{}",
254 env!("CARGO_MANIFEST_DIR"),
255 "TableMetadataV2.json"
256 )
257 }
258
259 fn create_external_table_cmd() -> CreateExternalTable {
260 let metadata_file_path = table_metadata_location();
261
262 CreateExternalTable {
263 name: TableReference::partial("static_ns", "static_table"),
264 locations: vec![metadata_file_path],
265 schema: Arc::new(DFSchema::empty()),
266 file_type: "iceberg".to_string(),
267 options: Default::default(),
268 table_partition_cols: Default::default(),
269 order_exprs: Default::default(),
270 constraints: Constraints::default(),
271 column_defaults: Default::default(),
272 if_not_exists: Default::default(),
273 or_replace: false,
274 temporary: false,
275 definition: Default::default(),
276 unbounded: Default::default(),
277 }
278 }
279
280 #[tokio::test]
281 async fn test_schema_of_created_table() {
282 let factory = IcebergTableProviderFactory::new();
283
284 let state = SessionStateBuilder::new().build();
285 let cmd = create_external_table_cmd();
286
287 let table_provider = factory
288 .create(&state, &cmd)
289 .await
290 .expect("create table failed");
291
292 let expected_schema = table_metadata_v2_schema();
293 let actual_schema = table_provider.schema();
294
295 assert_eq!(actual_schema.as_ref(), &expected_schema);
296 }
297
298 #[tokio::test]
299 async fn test_schema_of_created_external_table_sql() {
300 let mut state = SessionStateBuilder::new().with_default_features().build();
301 state.table_factories_mut().insert(
302 "ICEBERG".to_string(),
303 Arc::new(IcebergTableProviderFactory::new()),
304 );
305 let ctx = SessionContext::new_with_state(state);
306
307 let table_ref = TableReference::bare("static_table");
310
311 let sql = format!(
313 "CREATE EXTERNAL TABLE {} STORED AS ICEBERG LOCATION '{}'",
314 table_ref,
315 table_metadata_location()
316 );
317 let _df = ctx.sql(&sql).await.expect("create table failed");
318
319 let table_provider = ctx
321 .table_provider(table_ref)
322 .await
323 .expect("table not found");
324
325 let expected_schema = table_metadata_v2_schema();
327 let actual_schema = table_provider.schema();
328
329 assert_eq!(actual_schema.as_ref(), &expected_schema);
330 }
331}