iceberg 0.11.2 → 0.12.1

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
+
1
4
  #[cfg(feature = "datafusion")]
2
- use datafusion::execution::context::SessionContext;
3
- use iceberg::io::LocalFsStorageFactory;
5
+ use datafusion::common::ScalarValue;
6
+ #[cfg(feature = "datafusion")]
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,13 +27,23 @@ use iceberg_catalog_sql::{
21
27
  };
22
28
  #[cfg(feature = "datafusion")]
23
29
  use iceberg_datafusion::IcebergCatalogProvider;
24
- use std::collections::HashMap;
25
- use std::sync::{Arc, RwLock};
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
+ };
26
36
 
27
37
  use crate::error::to_rb_err;
28
- 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};
29
43
  use crate::utils::Wrap;
30
- use crate::{RbResult, RbTable};
44
+ #[cfg(feature = "datafusion")]
45
+ use crate::utils::date_to_i32;
46
+ use crate::{RbPartitionSpec, RbResult, RbSchema, RbSortOrder, RbTable};
31
47
 
32
48
  pub enum RbCatalogType {
33
49
  #[cfg(feature = "glue")]
@@ -82,7 +98,11 @@ impl RbCatalog {
82
98
  pub fn new_glue(warehouse: String) -> RbResult<Self> {
83
99
  let props = HashMap::from([(GLUE_CATALOG_PROP_WAREHOUSE.to_string(), warehouse)]);
84
100
  let catalog = runtime()
85
- .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
+ )
86
106
  .map_err(to_rb_err)?;
87
107
  Ok(Self {
88
108
  catalog: RbCatalogType::Glue(catalog.into()).into(),
@@ -91,13 +111,14 @@ impl RbCatalog {
91
111
 
92
112
  pub fn new_memory(warehouse: Option<String>) -> RbResult<Self> {
93
113
  let mut props = HashMap::new();
94
- if let Some(v) = warehouse {
95
- 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());
96
116
  }
97
117
  let catalog = runtime()
98
118
  .block_on(
99
119
  MemoryCatalogBuilder::default()
100
- .with_storage_factory(Arc::new(LocalFsStorageFactory))
120
+ .with_runtime(Runtime::new(tokio_runtime()))
121
+ .with_storage_factory(Arc::new(storage_factory(warehouse)))
101
122
  .load("memory", props),
102
123
  )
103
124
  .map_err(to_rb_err)?;
@@ -114,13 +135,14 @@ impl RbCatalog {
114
135
  ) -> RbResult<Self> {
115
136
  let mut props = props;
116
137
  props.insert(REST_CATALOG_PROP_URI.to_string(), uri);
117
- if let Some(v) = warehouse {
118
- 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());
119
140
  }
120
141
  let catalog = runtime()
121
142
  .block_on(
122
143
  RestCatalogBuilder::default()
123
- .with_storage_factory(Arc::new(LocalFsStorageFactory))
144
+ .with_runtime(Runtime::new(tokio_runtime()))
145
+ .with_storage_factory(Arc::new(storage_factory(warehouse)))
124
146
  .load("rest", props),
125
147
  )
126
148
  .map_err(to_rb_err)?;
@@ -134,7 +156,11 @@ impl RbCatalog {
134
156
  let mut props = HashMap::new();
135
157
  props.insert(S3TABLES_CATALOG_PROP_TABLE_BUCKET_ARN.to_string(), arn);
136
158
  let catalog = runtime()
137
- .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
+ )
138
164
  .map_err(to_rb_err)?;
139
165
  Ok(Self {
140
166
  catalog: RbCatalogType::S3Tables(catalog.into()).into(),
@@ -150,7 +176,7 @@ impl RbCatalog {
150
176
  ) -> RbResult<Self> {
151
177
  let mut props = props;
152
178
  props.insert(SQL_CATALOG_PROP_URI.to_string(), uri);
153
- props.insert(SQL_CATALOG_PROP_WAREHOUSE.to_string(), warehouse);
179
+ props.insert(SQL_CATALOG_PROP_WAREHOUSE.to_string(), warehouse.clone());
154
180
  props.insert(
155
181
  SQL_CATALOG_PROP_BIND_STYLE.to_string(),
156
182
  SqlBindStyle::DollarNumeric.to_string(),
@@ -158,7 +184,8 @@ impl RbCatalog {
158
184
  let catalog = runtime()
159
185
  .block_on(
160
186
  SqlCatalogBuilder::default()
161
- .with_storage_factory(Arc::new(LocalFsStorageFactory))
187
+ .with_runtime(Runtime::new(tokio_runtime()))
188
+ .with_storage_factory(Arc::new(storage_factory(Some(warehouse))))
162
189
  .load(name, props),
163
190
  )
164
191
  .map_err(to_rb_err)?;
@@ -279,20 +306,42 @@ impl RbCatalog {
279
306
  .collect())
280
307
  }
281
308
 
309
+ #[allow(clippy::too_many_arguments)]
282
310
  pub fn create_table(
283
- &self,
311
+ ruby: &Ruby,
312
+ rb_self: &Self,
284
313
  name: Wrap<TableIdent>,
285
- schema: Wrap<Schema>,
314
+ schema: &RbSchema,
286
315
  location: Option<String>,
316
+ partition_spec: Option<&RbPartitionSpec>,
317
+ sort_order: Option<&RbSortOrder>,
318
+ properties: HashMap<String, String>,
319
+ format_version: u8,
287
320
  ) -> RbResult<RbTable> {
321
+ let format_version = match format_version {
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
+ };
288
332
  let creation = TableCreation::builder()
289
333
  .name(name.0.name)
290
- .schema(schema.0)
334
+ .schema(schema.schema.clone())
291
335
  .location_opt(location)
336
+ .partition_spec_opt(partition_spec.map(|v| v.spec.clone()))
337
+ .sort_order_opt(sort_order.map(|v| v.order.clone()))
338
+ .properties(properties)
339
+ .format_version(format_version)
292
340
  .build();
293
341
  let table = runtime()
294
342
  .block_on(
295
- self.catalog
343
+ rb_self
344
+ .catalog
296
345
  .read()
297
346
  .unwrap()
298
347
  .as_catalog()
@@ -332,6 +381,19 @@ impl RbCatalog {
332
381
  Ok(())
333
382
  }
334
383
 
384
+ pub fn purge_table(&self, name: Wrap<TableIdent>) -> RbResult<()> {
385
+ runtime()
386
+ .block_on(
387
+ self.catalog
388
+ .read()
389
+ .unwrap()
390
+ .as_catalog()
391
+ .purge_table(&name.0),
392
+ )
393
+ .map_err(to_rb_err)?;
394
+ Ok(())
395
+ }
396
+
335
397
  pub fn table_exists(&self, name: Wrap<TableIdent>) -> RbResult<bool> {
336
398
  let exists = runtime()
337
399
  .block_on(
@@ -376,22 +438,84 @@ impl RbCatalog {
376
438
  }
377
439
 
378
440
  #[cfg(feature = "datafusion")]
379
- pub fn sql(&self, sql: String) -> RbResult<()> {
380
- let runtime = runtime();
381
-
382
- // TODO only create context once
441
+ pub fn session_context(&self, default_namespace: Option<String>) -> RbResult<RbSessionContext> {
383
442
  let catalog = self.catalog.read().unwrap().as_arc();
384
- let provider = runtime
443
+ let provider = runtime()
385
444
  .block_on(IcebergCatalogProvider::try_new(catalog))
386
- .unwrap();
387
- let ctx = SessionContext::new();
445
+ .map_err(to_rb_err)?;
446
+ let mut config = SessionConfig::new();
447
+ if let Some(namespace) = default_namespace {
448
+ config = config.with_default_catalog_and_schema("datafusion", namespace);
449
+ }
450
+ let ctx = SessionContext::new_with_config(config);
388
451
  ctx.register_catalog("datafusion", Arc::new(provider));
452
+ Ok(RbSessionContext { ctx })
453
+ }
454
+ }
389
455
 
390
- let df = runtime.block_on(ctx.sql(&sql)).unwrap();
391
- let _results = runtime.block_on(df.collect()).unwrap();
456
+ #[cfg(feature = "datafusion")]
457
+ #[magnus::wrap(class = "Iceberg::SessionContext")]
458
+ pub struct RbSessionContext {
459
+ pub(crate) ctx: SessionContext,
460
+ }
392
461
 
393
- // println!("{:?}", _results);
462
+ #[cfg(feature = "datafusion")]
463
+ impl RbSessionContext {
464
+ pub fn sql(ruby: &Ruby, rb_self: &Self, sql: String, rb_params: RArray) -> RbResult<Value> {
465
+ let mut params = Vec::new();
466
+ for param in rb_params.into_iter() {
467
+ if param.is_nil() {
468
+ params.push(ScalarValue::Null);
469
+ } else if Qtrue::from_value(param).is_some() {
470
+ params.push(ScalarValue::from(true));
471
+ } else if Qfalse::from_value(param).is_some() {
472
+ params.push(ScalarValue::from(false));
473
+ } else if let Some(v) = Integer::from_value(param) {
474
+ params.push(ScalarValue::from(v.to_i64()?));
475
+ } else if let Some(v) = Float::from_value(param) {
476
+ params.push(ScalarValue::from(v.to_f64()));
477
+ } else if let Some(v) = RString::from_value(param) {
478
+ // TODO support binary strings
479
+ params.push(ScalarValue::from(v.to_string()?));
480
+ } else if unsafe { param.classname() } == "Date" {
481
+ params.push(ScalarValue::Date32(Some(date_to_i32(param)?)));
482
+ } else if unsafe { param.classname() } == "Time" {
483
+ let sec: i64 = param.funcall("to_i", ())?;
484
+ let nsec: i64 = param.funcall("nsec", ())?;
485
+ params.push(ScalarValue::TimestampNanosecond(
486
+ Some(sec * 1_000_000_000 + nsec),
487
+ None,
488
+ ));
489
+ } else {
490
+ return Err(todo_error(param));
491
+ }
492
+ }
394
493
 
395
- Ok(())
494
+ let runtime = runtime();
495
+ let stream = runtime
496
+ .block_on(rb_self.ctx.sql(&sql))
497
+ .map_err(datafusion_error)?
498
+ .with_param_values(params)
499
+ .map_err(datafusion_error)?;
500
+ let batches = runtime
501
+ .block_on(stream.collect())
502
+ .map_err(datafusion_error)?;
503
+ collect_batches(ruby, batches)
504
+ }
505
+ }
506
+
507
+ fn storage_factory(warehouse: Option<String>) -> OpenDalStorageFactory {
508
+ match warehouse {
509
+ Some(ref v) => {
510
+ if v.starts_with("s3://") {
511
+ OpenDalStorageFactory::S3 {
512
+ customized_credential_load: None,
513
+ }
514
+ } else {
515
+ OpenDalStorageFactory::Fs
516
+ }
517
+ }
518
+ // TODO consider OpenDalStorageFactory::Memory
519
+ None => OpenDalStorageFactory::Fs,
396
520
  }
397
521
  }
@@ -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,17 +1,49 @@
1
+ use std::error::Error as _;
2
+
3
+ use iceberg::{Error, ErrorKind};
1
4
  use magnus::{Error as RbErr, RModule, Ruby, prelude::*};
2
5
 
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",
6
+ pub fn to_rb_err(err: Error) -> RbErr {
7
+ let mut class_name = match err.kind() {
8
+ ErrorKind::NamespaceAlreadyExists => "NamespaceAlreadyExistsError",
9
+ ErrorKind::NamespaceNotFound => "NoSuchNamespaceError",
10
+ ErrorKind::TableAlreadyExists => "TableAlreadyExistsError",
11
+ ErrorKind::TableNotFound => "NoSuchTableError",
12
+ ErrorKind::FeatureUnsupported => "UnsupportedFeatureError",
13
+ ErrorKind::DataInvalid => "InvalidDataError",
11
14
  _ => "Error",
12
15
  };
13
16
 
14
- let mut class = Ruby::get()
17
+ // no way to get context separately
18
+ // https://github.com/apache/iceberg-rust/issues/1071
19
+ let mut message = err.message().to_string();
20
+
21
+ let mut source = err.source();
22
+ while let Some(s) = source {
23
+ let m = s.to_string();
24
+ if message.contains(&m) {
25
+ break;
26
+ }
27
+ message.push_str(": ");
28
+ message.push_str(&m);
29
+ source = s.source();
30
+ }
31
+
32
+ if class_name == "Error" {
33
+ let s = err.to_string();
34
+ // for Glue
35
+ if s.contains("EntityNotFoundException") {
36
+ class_name = "NoSuchTableError";
37
+
38
+ // for Glue and S3 Tables
39
+ } else if s.contains("AlreadyExistsException")
40
+ || s.contains("A table with an identical name already exists in the namespace.")
41
+ {
42
+ class_name = "TableAlreadyExistsError";
43
+ }
44
+ }
45
+
46
+ let class = Ruby::get()
15
47
  .unwrap()
16
48
  .class_object()
17
49
  .const_get::<_, RModule>("Iceberg")
@@ -19,20 +51,28 @@ pub fn to_rb_err(err: iceberg::Error) -> RbErr {
19
51
  .const_get(class_name)
20
52
  .unwrap();
21
53
 
22
- // no way to get context separately
23
- // https://github.com/apache/iceberg-rust/issues/1071
24
- let message = err.to_string();
25
-
26
- // TODO remove in 0.12.0
27
- if message.contains("target schema is not superset of current schema") {
28
- class = Ruby::get().unwrap().exception_arg_error();
29
- }
54
+ RbErr::new(class, message)
55
+ }
30
56
 
31
- let message = message
32
- // TODO improve
33
- .strip_prefix("Unexpected => ")
34
- .map(|v| v.to_string())
35
- .unwrap_or(message);
57
+ pub fn todo_error<T: std::fmt::Debug>(message: T) -> RbErr {
58
+ let class = Ruby::get()
59
+ .unwrap()
60
+ .class_object()
61
+ .const_get::<_, RModule>("Iceberg")
62
+ .unwrap()
63
+ .const_get("Todo")
64
+ .unwrap();
65
+ RbErr::new(class, format!("not implemented yet: {:?}", message))
66
+ }
36
67
 
37
- RbErr::new(class, message)
68
+ #[cfg(feature = "datafusion")]
69
+ pub fn datafusion_error(err: datafusion::common::DataFusionError) -> RbErr {
70
+ let class = Ruby::get()
71
+ .unwrap()
72
+ .class_object()
73
+ .const_get::<_, RModule>("Iceberg")
74
+ .unwrap()
75
+ .const_get("Error")
76
+ .unwrap();
77
+ RbErr::new(class, err.to_string())
38
78
  }