iceberg 0.11.2 → 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
+
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,45 @@ 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>,
287
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
+ };
288
335
  let creation = TableCreation::builder()
289
336
  .name(name.0.name)
290
- .schema(schema.0)
337
+ .schema(schema.schema.clone())
291
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)
292
343
  .build();
293
344
  let table = runtime()
294
345
  .block_on(
295
- self.catalog
346
+ rb_self
347
+ .catalog
296
348
  .read()
297
349
  .unwrap()
298
350
  .as_catalog()
@@ -332,6 +384,19 @@ impl RbCatalog {
332
384
  Ok(())
333
385
  }
334
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
+ )
396
+ .map_err(to_rb_err)?;
397
+ Ok(())
398
+ }
399
+
335
400
  pub fn table_exists(&self, name: Wrap<TableIdent>) -> RbResult<bool> {
336
401
  let exists = runtime()
337
402
  .block_on(
@@ -376,22 +441,84 @@ impl RbCatalog {
376
441
  }
377
442
 
378
443
  #[cfg(feature = "datafusion")]
379
- pub fn sql(&self, sql: String) -> RbResult<()> {
380
- let runtime = runtime();
381
-
382
- // TODO only create context once
444
+ pub fn session_context(&self, default_namespace: Option<String>) -> RbResult<RbSessionContext> {
383
445
  let catalog = self.catalog.read().unwrap().as_arc();
384
- let provider = runtime
446
+ let provider = runtime()
385
447
  .block_on(IcebergCatalogProvider::try_new(catalog))
386
- .unwrap();
387
- 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);
388
454
  ctx.register_catalog("datafusion", Arc::new(provider));
455
+ Ok(RbSessionContext { ctx })
456
+ }
457
+ }
389
458
 
390
- let df = runtime.block_on(ctx.sql(&sql)).unwrap();
391
- 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
+ }
392
464
 
393
- // 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
+ }
394
496
 
395
- 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,
396
523
  }
397
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,17 +1,36 @@
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
 
14
- let mut class = Ruby::get()
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
+
33
+ let class = Ruby::get()
15
34
  .unwrap()
16
35
  .class_object()
17
36
  .const_get::<_, RModule>("Iceberg")
@@ -19,20 +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
-
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
- }
41
+ RbErr::new(class, message)
42
+ }
30
43
 
31
- let message = message
32
- // TODO improve
33
- .strip_prefix("Unexpected => ")
34
- .map(|v| v.to_string())
35
- .unwrap_or(message);
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
+ }
36
54
 
37
- RbErr::new(class, message)
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())
38
65
  }