iceberg 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,15 @@
1
+ use std::collections::HashMap;
2
+ use std::sync::{Arc, RwLock};
3
+
4
+ #[cfg(feature = "datafusion")]
5
+ use datafusion::common::ScalarValue;
1
6
  #[cfg(feature = "datafusion")]
2
- use datafusion::execution::context::SessionContext;
3
- use iceberg::io::LocalFsStorageFactory;
7
+ use datafusion::execution::context::{SessionConfig, SessionContext};
4
8
  use iceberg::memory::{MEMORY_CATALOG_WAREHOUSE, MemoryCatalogBuilder};
5
- use iceberg::spec::Schema;
6
- use iceberg::{Catalog, CatalogBuilder, MemoryCatalog, NamespaceIdent, TableCreation, TableIdent};
9
+ use iceberg::spec::FormatVersion;
10
+ use iceberg::{
11
+ Catalog, CatalogBuilder, MemoryCatalog, NamespaceIdent, Runtime, TableCreation, TableIdent,
12
+ };
7
13
  #[cfg(feature = "glue")]
8
14
  use iceberg_catalog_glue::{GLUE_CATALOG_PROP_WAREHOUSE, GlueCatalog, GlueCatalogBuilder};
9
15
  #[cfg(feature = "rest")]
@@ -21,14 +27,23 @@ use iceberg_catalog_sql::{
21
27
  };
22
28
  #[cfg(feature = "datafusion")]
23
29
  use iceberg_datafusion::IcebergCatalogProvider;
24
- use std::cell::RefCell;
25
- use std::collections::HashMap;
26
- use std::sync::Arc;
30
+ use iceberg_storage_opendal::OpenDalStorageFactory;
31
+ use magnus::{Error as RbErr, Ruby};
32
+ #[cfg(feature = "datafusion")]
33
+ use magnus::{
34
+ Float, Integer, RArray, RString, Value, value::Qfalse, value::Qtrue, value::ReprValue,
35
+ };
27
36
 
28
37
  use crate::error::to_rb_err;
29
- use crate::runtime::runtime;
38
+ #[cfg(feature = "datafusion")]
39
+ use crate::error::{datafusion_error, todo_error};
40
+ #[cfg(feature = "datafusion")]
41
+ use crate::result::collect_batches;
42
+ use crate::runtime::{runtime, tokio_runtime};
30
43
  use crate::utils::Wrap;
31
- use crate::{RbResult, RbTable};
44
+ #[cfg(feature = "datafusion")]
45
+ use crate::utils::date_to_i32;
46
+ use crate::{RbPartitionSpec, RbResult, RbSchema, RbSortOrder, RbTable};
32
47
 
33
48
  pub enum RbCatalogType {
34
49
  #[cfg(feature = "glue")]
@@ -75,7 +90,7 @@ impl RbCatalogType {
75
90
 
76
91
  #[magnus::wrap(class = "Iceberg::RbCatalog")]
77
92
  pub struct RbCatalog {
78
- pub catalog: RefCell<RbCatalogType>,
93
+ pub catalog: RwLock<RbCatalogType>,
79
94
  }
80
95
 
81
96
  impl RbCatalog {
@@ -83,7 +98,11 @@ impl RbCatalog {
83
98
  pub fn new_glue(warehouse: String) -> RbResult<Self> {
84
99
  let props = HashMap::from([(GLUE_CATALOG_PROP_WAREHOUSE.to_string(), warehouse)]);
85
100
  let catalog = runtime()
86
- .block_on(GlueCatalogBuilder::default().load("glue", props))
101
+ .block_on(
102
+ GlueCatalogBuilder::default()
103
+ .with_runtime(Runtime::new(tokio_runtime()))
104
+ .load("glue", props),
105
+ )
87
106
  .map_err(to_rb_err)?;
88
107
  Ok(Self {
89
108
  catalog: RbCatalogType::Glue(catalog.into()).into(),
@@ -92,13 +111,14 @@ impl RbCatalog {
92
111
 
93
112
  pub fn new_memory(warehouse: Option<String>) -> RbResult<Self> {
94
113
  let mut props = HashMap::new();
95
- if let Some(v) = warehouse {
96
- props.insert(MEMORY_CATALOG_WAREHOUSE.to_string(), v);
114
+ if let Some(ref v) = warehouse {
115
+ props.insert(MEMORY_CATALOG_WAREHOUSE.to_string(), v.clone());
97
116
  }
98
117
  let catalog = runtime()
99
118
  .block_on(
100
119
  MemoryCatalogBuilder::default()
101
- .with_storage_factory(Arc::new(LocalFsStorageFactory))
120
+ .with_runtime(Runtime::new(tokio_runtime()))
121
+ .with_storage_factory(Arc::new(storage_factory(warehouse)))
102
122
  .load("memory", props),
103
123
  )
104
124
  .map_err(to_rb_err)?;
@@ -115,13 +135,14 @@ impl RbCatalog {
115
135
  ) -> RbResult<Self> {
116
136
  let mut props = props;
117
137
  props.insert(REST_CATALOG_PROP_URI.to_string(), uri);
118
- if let Some(v) = warehouse {
119
- props.insert(REST_CATALOG_PROP_WAREHOUSE.to_string(), v);
138
+ if let Some(ref v) = warehouse {
139
+ props.insert(REST_CATALOG_PROP_WAREHOUSE.to_string(), v.clone());
120
140
  }
121
141
  let catalog = runtime()
122
142
  .block_on(
123
143
  RestCatalogBuilder::default()
124
- .with_storage_factory(Arc::new(LocalFsStorageFactory))
144
+ .with_runtime(Runtime::new(tokio_runtime()))
145
+ .with_storage_factory(Arc::new(storage_factory(warehouse)))
125
146
  .load("rest", props),
126
147
  )
127
148
  .map_err(to_rb_err)?;
@@ -135,7 +156,11 @@ impl RbCatalog {
135
156
  let mut props = HashMap::new();
136
157
  props.insert(S3TABLES_CATALOG_PROP_TABLE_BUCKET_ARN.to_string(), arn);
137
158
  let catalog = runtime()
138
- .block_on(S3TablesCatalogBuilder::default().load("s3tables", props))
159
+ .block_on(
160
+ S3TablesCatalogBuilder::default()
161
+ .with_runtime(Runtime::new(tokio_runtime()))
162
+ .load("s3tables", props),
163
+ )
139
164
  .map_err(to_rb_err)?;
140
165
  Ok(Self {
141
166
  catalog: RbCatalogType::S3Tables(catalog.into()).into(),
@@ -151,7 +176,7 @@ impl RbCatalog {
151
176
  ) -> RbResult<Self> {
152
177
  let mut props = props;
153
178
  props.insert(SQL_CATALOG_PROP_URI.to_string(), uri);
154
- props.insert(SQL_CATALOG_PROP_WAREHOUSE.to_string(), warehouse);
179
+ props.insert(SQL_CATALOG_PROP_WAREHOUSE.to_string(), warehouse.clone());
155
180
  props.insert(
156
181
  SQL_CATALOG_PROP_BIND_STYLE.to_string(),
157
182
  SqlBindStyle::DollarNumeric.to_string(),
@@ -159,7 +184,8 @@ impl RbCatalog {
159
184
  let catalog = runtime()
160
185
  .block_on(
161
186
  SqlCatalogBuilder::default()
162
- .with_storage_factory(Arc::new(LocalFsStorageFactory))
187
+ .with_runtime(Runtime::new(tokio_runtime()))
188
+ .with_storage_factory(Arc::new(storage_factory(Some(warehouse))))
163
189
  .load(name, props),
164
190
  )
165
191
  .map_err(to_rb_err)?;
@@ -175,7 +201,8 @@ impl RbCatalog {
175
201
  let namespaces = runtime()
176
202
  .block_on(
177
203
  self.catalog
178
- .borrow()
204
+ .read()
205
+ .unwrap()
179
206
  .as_catalog()
180
207
  .list_namespaces(parent.map(|v| v.0).as_ref()),
181
208
  )
@@ -191,7 +218,8 @@ impl RbCatalog {
191
218
  runtime()
192
219
  .block_on(
193
220
  self.catalog
194
- .borrow()
221
+ .read()
222
+ .unwrap()
195
223
  .as_catalog()
196
224
  .create_namespace(&name.0, props),
197
225
  )
@@ -201,7 +229,13 @@ impl RbCatalog {
201
229
 
202
230
  pub fn namespace_exists(&self, name: Wrap<NamespaceIdent>) -> RbResult<bool> {
203
231
  let exists = runtime()
204
- .block_on(self.catalog.borrow().as_catalog().namespace_exists(&name.0))
232
+ .block_on(
233
+ self.catalog
234
+ .read()
235
+ .unwrap()
236
+ .as_catalog()
237
+ .namespace_exists(&name.0),
238
+ )
205
239
  .map_err(to_rb_err)?;
206
240
  Ok(exists)
207
241
  }
@@ -211,7 +245,13 @@ impl RbCatalog {
211
245
  name: Wrap<NamespaceIdent>,
212
246
  ) -> RbResult<HashMap<String, String>> {
213
247
  let namespace = runtime()
214
- .block_on(self.catalog.borrow().as_catalog().get_namespace(&name.0))
248
+ .block_on(
249
+ self.catalog
250
+ .read()
251
+ .unwrap()
252
+ .as_catalog()
253
+ .get_namespace(&name.0),
254
+ )
215
255
  .map_err(to_rb_err)?;
216
256
  Ok(namespace.properties().clone())
217
257
  }
@@ -224,7 +264,8 @@ impl RbCatalog {
224
264
  runtime()
225
265
  .block_on(
226
266
  self.catalog
227
- .borrow()
267
+ .read()
268
+ .unwrap()
228
269
  .as_catalog()
229
270
  .update_namespace(&name.0, props),
230
271
  )
@@ -234,14 +275,26 @@ impl RbCatalog {
234
275
 
235
276
  pub fn drop_namespace(&self, name: Wrap<NamespaceIdent>) -> RbResult<()> {
236
277
  runtime()
237
- .block_on(self.catalog.borrow().as_catalog().drop_namespace(&name.0))
278
+ .block_on(
279
+ self.catalog
280
+ .read()
281
+ .unwrap()
282
+ .as_catalog()
283
+ .drop_namespace(&name.0),
284
+ )
238
285
  .map_err(to_rb_err)?;
239
286
  Ok(())
240
287
  }
241
288
 
242
289
  pub fn list_tables(&self, namespace: Wrap<NamespaceIdent>) -> RbResult<Vec<Vec<String>>> {
243
290
  let tables = runtime()
244
- .block_on(self.catalog.borrow().as_catalog().list_tables(&namespace.0))
291
+ .block_on(
292
+ self.catalog
293
+ .read()
294
+ .unwrap()
295
+ .as_catalog()
296
+ .list_tables(&namespace.0),
297
+ )
245
298
  .map_err(to_rb_err)?;
246
299
  Ok(tables
247
300
  .iter()
@@ -253,21 +306,47 @@ impl RbCatalog {
253
306
  .collect())
254
307
  }
255
308
 
309
+ #[allow(clippy::too_many_arguments)]
256
310
  pub fn create_table(
257
- &self,
311
+ ruby: &Ruby,
312
+ rb_self: &Self,
258
313
  name: Wrap<TableIdent>,
259
- schema: Wrap<Schema>,
314
+ schema: &RbSchema,
260
315
  location: Option<String>,
316
+ partition_spec: Option<&RbPartitionSpec>,
317
+ sort_order: Option<&RbSortOrder>,
318
+ properties: HashMap<String, String>,
261
319
  ) -> RbResult<RbTable> {
320
+ let format_version = if let Some(v) = properties.get("format-version") {
321
+ match v.as_str() {
322
+ "1" => FormatVersion::V1,
323
+ "2" => FormatVersion::V2,
324
+ "3" => FormatVersion::V3,
325
+ _ => {
326
+ return Err(RbErr::new(
327
+ ruby.exception_arg_error(),
328
+ "Unsupported format version",
329
+ ));
330
+ }
331
+ }
332
+ } else {
333
+ FormatVersion::V2
334
+ };
262
335
  let creation = TableCreation::builder()
263
336
  .name(name.0.name)
264
- .schema(schema.0)
337
+ .schema(schema.schema.clone())
265
338
  .location_opt(location)
339
+ .partition_spec_opt(partition_spec.map(|v| v.spec.clone()))
340
+ .sort_order_opt(sort_order.map(|v| v.order.clone()))
341
+ .properties(properties)
342
+ .format_version(format_version)
266
343
  .build();
267
344
  let table = runtime()
268
345
  .block_on(
269
- self.catalog
270
- .borrow()
346
+ rb_self
347
+ .catalog
348
+ .read()
349
+ .unwrap()
271
350
  .as_catalog()
272
351
  .create_table(&name.0.namespace, creation),
273
352
  )
@@ -279,7 +358,13 @@ impl RbCatalog {
279
358
 
280
359
  pub fn load_table(&self, name: Wrap<TableIdent>) -> RbResult<RbTable> {
281
360
  let table = runtime()
282
- .block_on(self.catalog.borrow().as_catalog().load_table(&name.0))
361
+ .block_on(
362
+ self.catalog
363
+ .read()
364
+ .unwrap()
365
+ .as_catalog()
366
+ .load_table(&name.0),
367
+ )
283
368
  .map_err(to_rb_err)?;
284
369
  Ok(RbTable {
285
370
  table: table.into(),
@@ -288,14 +373,39 @@ impl RbCatalog {
288
373
 
289
374
  pub fn drop_table(&self, name: Wrap<TableIdent>) -> RbResult<()> {
290
375
  runtime()
291
- .block_on(self.catalog.borrow().as_catalog().drop_table(&name.0))
376
+ .block_on(
377
+ self.catalog
378
+ .read()
379
+ .unwrap()
380
+ .as_catalog()
381
+ .drop_table(&name.0),
382
+ )
383
+ .map_err(to_rb_err)?;
384
+ Ok(())
385
+ }
386
+
387
+ pub fn purge_table(&self, name: Wrap<TableIdent>) -> RbResult<()> {
388
+ runtime()
389
+ .block_on(
390
+ self.catalog
391
+ .read()
392
+ .unwrap()
393
+ .as_catalog()
394
+ .purge_table(&name.0),
395
+ )
292
396
  .map_err(to_rb_err)?;
293
397
  Ok(())
294
398
  }
295
399
 
296
400
  pub fn table_exists(&self, name: Wrap<TableIdent>) -> RbResult<bool> {
297
401
  let exists = runtime()
298
- .block_on(self.catalog.borrow().as_catalog().table_exists(&name.0))
402
+ .block_on(
403
+ self.catalog
404
+ .read()
405
+ .unwrap()
406
+ .as_catalog()
407
+ .table_exists(&name.0),
408
+ )
299
409
  .map_err(to_rb_err)?;
300
410
  Ok(exists)
301
411
  }
@@ -304,7 +414,8 @@ impl RbCatalog {
304
414
  runtime()
305
415
  .block_on(
306
416
  self.catalog
307
- .borrow()
417
+ .read()
418
+ .unwrap()
308
419
  .as_catalog()
309
420
  .rename_table(&name.0, &new_name.0),
310
421
  )
@@ -320,7 +431,8 @@ impl RbCatalog {
320
431
  runtime()
321
432
  .block_on(
322
433
  self.catalog
323
- .borrow()
434
+ .read()
435
+ .unwrap()
324
436
  .as_catalog()
325
437
  .register_table(&name.0, metadata_location),
326
438
  )
@@ -329,22 +441,84 @@ impl RbCatalog {
329
441
  }
330
442
 
331
443
  #[cfg(feature = "datafusion")]
332
- pub fn sql(&self, sql: String) -> RbResult<()> {
333
- let runtime = runtime();
334
-
335
- // TODO only create context once
336
- let catalog = self.catalog.borrow().as_arc();
337
- let provider = runtime
444
+ pub fn session_context(&self, default_namespace: Option<String>) -> RbResult<RbSessionContext> {
445
+ let catalog = self.catalog.read().unwrap().as_arc();
446
+ let provider = runtime()
338
447
  .block_on(IcebergCatalogProvider::try_new(catalog))
339
- .unwrap();
340
- let ctx = SessionContext::new();
448
+ .map_err(to_rb_err)?;
449
+ let mut config = SessionConfig::new();
450
+ if let Some(namespace) = default_namespace {
451
+ config = config.with_default_catalog_and_schema("datafusion", namespace);
452
+ }
453
+ let ctx = SessionContext::new_with_config(config);
341
454
  ctx.register_catalog("datafusion", Arc::new(provider));
455
+ Ok(RbSessionContext { ctx })
456
+ }
457
+ }
342
458
 
343
- let df = runtime.block_on(ctx.sql(&sql)).unwrap();
344
- let _results = runtime.block_on(df.collect()).unwrap();
459
+ #[cfg(feature = "datafusion")]
460
+ #[magnus::wrap(class = "Iceberg::SessionContext")]
461
+ pub struct RbSessionContext {
462
+ pub(crate) ctx: SessionContext,
463
+ }
345
464
 
346
- // println!("{:?}", _results);
465
+ #[cfg(feature = "datafusion")]
466
+ impl RbSessionContext {
467
+ pub fn sql(ruby: &Ruby, rb_self: &Self, sql: String, rb_params: RArray) -> RbResult<Value> {
468
+ let mut params = Vec::new();
469
+ for param in rb_params.into_iter() {
470
+ if param.is_nil() {
471
+ params.push(ScalarValue::Null);
472
+ } else if Qtrue::from_value(param).is_some() {
473
+ params.push(ScalarValue::from(true));
474
+ } else if Qfalse::from_value(param).is_some() {
475
+ params.push(ScalarValue::from(false));
476
+ } else if let Some(v) = Integer::from_value(param) {
477
+ params.push(ScalarValue::from(v.to_i64()?));
478
+ } else if let Some(v) = Float::from_value(param) {
479
+ params.push(ScalarValue::from(v.to_f64()));
480
+ } else if let Some(v) = RString::from_value(param) {
481
+ // TODO support binary strings
482
+ params.push(ScalarValue::from(v.to_string()?));
483
+ } else if unsafe { param.classname() } == "Date" {
484
+ params.push(ScalarValue::Date32(Some(date_to_i32(param)?)));
485
+ } else if unsafe { param.classname() } == "Time" {
486
+ let sec: i64 = param.funcall("to_i", ())?;
487
+ let nsec: i64 = param.funcall("nsec", ())?;
488
+ params.push(ScalarValue::TimestampNanosecond(
489
+ Some(sec * 1_000_000_000 + nsec),
490
+ None,
491
+ ));
492
+ } else {
493
+ return Err(todo_error(param));
494
+ }
495
+ }
347
496
 
348
- Ok(())
497
+ let runtime = runtime();
498
+ let stream = runtime
499
+ .block_on(rb_self.ctx.sql(&sql))
500
+ .map_err(datafusion_error)?
501
+ .with_param_values(params)
502
+ .map_err(datafusion_error)?;
503
+ let batches = runtime
504
+ .block_on(stream.collect())
505
+ .map_err(datafusion_error)?;
506
+ collect_batches(ruby, batches)
507
+ }
508
+ }
509
+
510
+ fn storage_factory(warehouse: Option<String>) -> OpenDalStorageFactory {
511
+ match warehouse {
512
+ Some(ref v) => {
513
+ if v.starts_with("s3://") {
514
+ OpenDalStorageFactory::S3 {
515
+ customized_credential_load: None,
516
+ }
517
+ } else {
518
+ OpenDalStorageFactory::Fs
519
+ }
520
+ }
521
+ // TODO consider OpenDalStorageFactory::Memory
522
+ None => OpenDalStorageFactory::Fs,
349
523
  }
350
524
  }
@@ -0,0 +1,32 @@
1
+ use std::collections::HashMap;
2
+
3
+ use iceberg::spec::EncryptedKey;
4
+ use magnus::{IntoValue, Ruby, value::ReprValue};
5
+
6
+ #[magnus::wrap(class = "Iceberg::EncryptedKey")]
7
+ pub struct RbEncryptedKey {
8
+ pub(crate) key: EncryptedKey,
9
+ }
10
+
11
+ impl RbEncryptedKey {
12
+ pub fn key_id(&self) -> &str {
13
+ self.key.key_id()
14
+ }
15
+
16
+ pub fn encrypted_by_id(&self) -> Option<&str> {
17
+ self.key.encrypted_by_id()
18
+ }
19
+
20
+ pub fn properties(&self) -> HashMap<String, String> {
21
+ self.key.properties().clone()
22
+ }
23
+
24
+ pub fn inspect(ruby: &Ruby, rb_self: &Self) -> String {
25
+ format!(
26
+ "#<Iceberg::EncryptedKey key_id={}, encrypted_by_id={}, properties={}>",
27
+ rb_self.key_id().into_value_with(ruby).inspect(),
28
+ rb_self.encrypted_by_id().into_value_with(ruby).inspect(),
29
+ rb_self.properties().into_value_with(ruby).inspect(),
30
+ )
31
+ }
32
+ }
@@ -1,16 +1,35 @@
1
+ use iceberg::{Error, ErrorKind};
1
2
  use magnus::{Error as RbErr, RModule, Ruby, prelude::*};
2
3
 
3
- pub fn to_rb_err(err: iceberg::Error) -> RbErr {
4
- let class_name = match err.kind() {
5
- iceberg::ErrorKind::NamespaceAlreadyExists => "NamespaceAlreadyExistsError",
6
- iceberg::ErrorKind::NamespaceNotFound => "NamespaceNotFoundError",
7
- iceberg::ErrorKind::TableAlreadyExists => "TableAlreadyExistsError",
8
- iceberg::ErrorKind::TableNotFound => "TableNotFoundError",
9
- iceberg::ErrorKind::FeatureUnsupported => "UnsupportedFeatureError",
10
- iceberg::ErrorKind::DataInvalid => "InvalidDataError",
4
+ pub fn to_rb_err(err: Error) -> RbErr {
5
+ let mut class_name = match err.kind() {
6
+ ErrorKind::NamespaceAlreadyExists => "NamespaceAlreadyExistsError",
7
+ ErrorKind::NamespaceNotFound => "NoSuchNamespaceError",
8
+ ErrorKind::TableAlreadyExists => "TableAlreadyExistsError",
9
+ ErrorKind::TableNotFound => "NoSuchTableError",
10
+ ErrorKind::FeatureUnsupported => "UnsupportedFeatureError",
11
+ ErrorKind::DataInvalid => "InvalidDataError",
11
12
  _ => "Error",
12
13
  };
13
14
 
15
+ // no way to get context separately
16
+ // https://github.com/apache/iceberg-rust/issues/1071
17
+ let message = err.message().to_string();
18
+
19
+ if class_name == "Error" {
20
+ let s = err.to_string();
21
+ // for Glue
22
+ if s.contains("EntityNotFoundException") {
23
+ class_name = "NoSuchTableError";
24
+
25
+ // for Glue and S3 Tables
26
+ } else if s.contains("AlreadyExistsException")
27
+ || s.contains("A table with an identical name already exists in the namespace.")
28
+ {
29
+ class_name = "TableAlreadyExistsError";
30
+ }
31
+ }
32
+
14
33
  let class = Ruby::get()
15
34
  .unwrap()
16
35
  .class_object()
@@ -19,14 +38,28 @@ pub fn to_rb_err(err: iceberg::Error) -> RbErr {
19
38
  .const_get(class_name)
20
39
  .unwrap();
21
40
 
22
- // no way to get context separately
23
- // https://github.com/apache/iceberg-rust/issues/1071
24
- let message = err.to_string();
25
- let message = message
26
- // TODO improve
27
- .strip_prefix("Unexpected => ")
28
- .map(|v| v.to_string())
29
- .unwrap_or(message);
30
-
31
41
  RbErr::new(class, message)
32
42
  }
43
+
44
+ pub fn todo_error<T: std::fmt::Debug>(message: T) -> RbErr {
45
+ let class = Ruby::get()
46
+ .unwrap()
47
+ .class_object()
48
+ .const_get::<_, RModule>("Iceberg")
49
+ .unwrap()
50
+ .const_get("Todo")
51
+ .unwrap();
52
+ RbErr::new(class, format!("not implemented yet: {:?}", message))
53
+ }
54
+
55
+ #[cfg(feature = "datafusion")]
56
+ pub fn datafusion_error(err: datafusion::common::DataFusionError) -> RbErr {
57
+ let class = Ruby::get()
58
+ .unwrap()
59
+ .class_object()
60
+ .const_get::<_, RModule>("Iceberg")
61
+ .unwrap()
62
+ .const_get("Error")
63
+ .unwrap();
64
+ RbErr::new(class, err.to_string())
65
+ }