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.
data/Cargo.toml CHANGED
@@ -2,5 +2,12 @@
2
2
  members = ["ext/iceberg"]
3
3
  resolver = "3"
4
4
 
5
+ [profile.dev]
6
+ strip = true
7
+ opt-level = 2 # compiles faster
8
+
5
9
  [profile.release]
6
10
  strip = true
11
+ opt-level = "s"
12
+ lto = true
13
+ codegen-units = 1
data/NOTICE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  Apache Iceberg
3
- Copyright 2017-2025 The Apache Software Foundation
3
+ Copyright 2017-2026 The Apache Software Foundation
4
4
 
5
5
  This product includes software developed at
6
6
  The Apache Software Foundation (http://www.apache.org/).
@@ -8,7 +8,7 @@ The Apache Software Foundation (http://www.apache.org/).
8
8
  --------------------------------------------------------------------------------
9
9
 
10
10
  Apache Iceberg Rust
11
- Copyright 2023-2024 The Apache Software Foundation
11
+ Copyright 2023-2026 The Apache Software Foundation
12
12
 
13
13
  This product includes software developed at
14
14
  The Apache Software Foundation (http://www.apache.org/).
data/README.md CHANGED
@@ -19,10 +19,10 @@ gem "iceberg"
19
19
  Create a client for an Iceberg catalog
20
20
 
21
21
  ```ruby
22
- catalog = Iceberg::RestCatalog.new(uri: "http://localhost:8181")
22
+ catalog = Iceberg::RestCatalog.new(uri: "http://localhost:8181", default_namespace: "main")
23
23
  ```
24
24
 
25
- Create a namespace
25
+ Create a namespace if needed
26
26
 
27
27
  ```ruby
28
28
  catalog.create_namespace("main")
@@ -31,7 +31,7 @@ catalog.create_namespace("main")
31
31
  Create a table
32
32
 
33
33
  ```ruby
34
- catalog.create_table("main.events") do |t|
34
+ catalog.create_table("events") do |t|
35
35
  t.bigint "id"
36
36
  t.float "value"
37
37
  end
@@ -41,14 +41,14 @@ Or with [Polars](https://github.com/ankane/ruby-polars)
41
41
 
42
42
  ```ruby
43
43
  df = Polars::DataFrame.new({"id" => [1, 2], "value" => [3.0, 4.0]})
44
- table = catalog.create_table("main.events", schema: df.schema)
44
+ table = catalog.create_table("events", schema: df.schema)
45
45
  table.append(df)
46
46
  ```
47
47
 
48
48
  Load a table
49
49
 
50
50
  ```ruby
51
- table = catalog.load_table("main.events")
51
+ table = catalog.load_table("events")
52
52
  ```
53
53
 
54
54
  Query a table
@@ -67,6 +67,26 @@ Iceberg::RestCatalog.new(
67
67
  )
68
68
  ```
69
69
 
70
+ ### S3 Tables
71
+
72
+ [unreleased]
73
+
74
+ ```ruby
75
+ Iceberg::S3TablesCatalog.new(
76
+ arn: "arn:aws:s3tables:..."
77
+ )
78
+ ```
79
+
80
+ ### Glue
81
+
82
+ [unreleased]
83
+
84
+ ```ruby
85
+ Iceberg::GlueCatalog.new(
86
+ warehouse: "s3://my-bucket"
87
+ )
88
+ ```
89
+
70
90
  ### SQL
71
91
 
72
92
  ```ruby
@@ -133,7 +153,7 @@ catalog.list_tables("main")
133
153
  Create a table
134
154
 
135
155
  ```ruby
136
- catalog.create_table("main.events") do |t|
156
+ catalog.create_table("events") do |t|
137
157
  t.integer "id"
138
158
  t.float "value"
139
159
  end
@@ -142,31 +162,31 @@ end
142
162
  Load a table
143
163
 
144
164
  ```ruby
145
- catalog.load_table("main.events")
165
+ catalog.load_table("events")
146
166
  ```
147
167
 
148
168
  Check if a table exists
149
169
 
150
170
  ```ruby
151
- catalog.table_exists?("main.events")
171
+ catalog.table_exists?("events")
152
172
  ```
153
173
 
154
174
  Rename a table
155
175
 
156
176
  ```ruby
157
- catalog.rename_table("main.events", "main.events2")
177
+ catalog.rename_table("events", "events2")
158
178
  ```
159
179
 
160
180
  Register a table
161
181
 
162
182
  ```ruby
163
- catalog.register_table("main.events", "metadata.json")
183
+ catalog.register_table("events", "metadata.json")
164
184
  ```
165
185
 
166
186
  Drop a table
167
187
 
168
188
  ```ruby
169
- catalog.drop_table("main.events")
189
+ catalog.drop_table("events")
170
190
  ```
171
191
 
172
192
  ## Static Tables
@@ -196,7 +216,7 @@ To get started with development:
196
216
  git clone https://github.com/ankane/iceberg-ruby.git
197
217
  cd iceberg-ruby
198
218
  bundle install
199
- bundle exec rake compile
219
+ bundle exec rake compile:dev
200
220
 
201
221
  # memory catalog
202
222
  bundle exec rake test:memory
@@ -1,10 +1,10 @@
1
1
  [package]
2
2
  name = "iceberg-ruby"
3
- version = "0.11.1"
3
+ version = "0.12.0"
4
4
  license = "Apache-2.0"
5
5
  authors = ["Andrew Kane <andrew@ankane.org>"]
6
6
  edition = "2024"
7
- rust-version = "1.92"
7
+ rust-version = "1.94"
8
8
  publish = false
9
9
 
10
10
  [lib]
@@ -12,24 +12,28 @@ name = "iceberg"
12
12
  crate-type = ["cdylib"]
13
13
 
14
14
  [dependencies]
15
- arrow-array = { version = "57", features = ["ffi"] }
16
- arrow-schema = "57"
17
- datafusion = { version = "52", optional = true }
15
+ arrow = "58"
16
+ arrow-array = { version = "58", features = ["ffi"] }
17
+ arrow-cast = "58"
18
+ arrow-schema = "58"
19
+ datafusion = { version = "53", optional = true }
18
20
  futures = "0.3"
19
- iceberg = "0.9"
20
- iceberg-catalog-glue = { version = "0.9", optional = true }
21
- iceberg-catalog-rest = { version = "0.9", optional = true }
22
- iceberg-catalog-s3tables = { version = "0.9", optional = true }
23
- iceberg-catalog-sql = { version = "0.9", optional = true }
24
- iceberg-datafusion = { version = "0.9", optional = true }
21
+ iceberg = "=0.10.0"
22
+ iceberg-catalog-glue = { version = "=0.10.0", optional = true }
23
+ iceberg-catalog-rest = { version = "=0.10.0", optional = true }
24
+ iceberg-catalog-s3tables = { version = "=0.10.0", optional = true }
25
+ iceberg-catalog-sql = { version = "=0.10.0", optional = true }
26
+ iceberg-datafusion = { version = "=0.10.0", optional = true }
27
+ iceberg-storage-opendal = "=0.10.0"
25
28
  magnus = "0.8"
26
- parquet = "57"
29
+ parquet = "58"
30
+ rb-sys = "0.9"
27
31
  sqlx = { version = "0.8", features = ["postgres", "runtime-tokio", "sqlite"], default-features = false, optional = true }
28
32
  tokio = { version = "1", features = ["rt-multi-thread"] }
29
33
  uuid = { version = "1", features = ["v4"] }
30
34
 
31
35
  [features]
32
- default = ["rest", "sql"]
36
+ default = ["glue", "rest", "s3tables", "sql"]
33
37
  datafusion = ["dep:datafusion", "dep:iceberg-datafusion"]
34
38
  glue = ["dep:iceberg-catalog-glue"]
35
39
  rest = ["dep:iceberg-catalog-rest"]
@@ -1,11 +1,12 @@
1
1
  use arrow_array::ffi_stream::{ArrowArrayStreamReader, FFI_ArrowArrayStream};
2
+ use arrow_schema::Schema as ArrowSchema;
3
+ use arrow_schema::ffi::FFI_ArrowSchema;
2
4
  use magnus::{Error as RbErr, Ruby, TryConvert, Value, prelude::*};
3
5
 
4
6
  use crate::RbResult;
7
+ use crate::utils::Wrap;
5
8
 
6
- pub struct RbArrowType<T>(pub T);
7
-
8
- impl TryConvert for RbArrowType<ArrowArrayStreamReader> {
9
+ impl TryConvert for Wrap<ArrowArrayStreamReader> {
9
10
  fn try_convert(val: Value) -> RbResult<Self> {
10
11
  let ruby = Ruby::get_with(val);
11
12
  let addr: usize = val.funcall("to_i", ())?;
@@ -14,9 +15,23 @@ impl TryConvert for RbArrowType<ArrowArrayStreamReader> {
14
15
  let stream_ptr =
15
16
  Box::new(unsafe { std::ptr::replace(addr as _, FFI_ArrowArrayStream::empty()) });
16
17
 
17
- Ok(RbArrowType(
18
- ArrowArrayStreamReader::try_new(*stream_ptr)
19
- .map_err(|e| RbErr::new(ruby.exception_arg_error(), e.to_string()))?,
20
- ))
18
+ Ok(Wrap(ArrowArrayStreamReader::try_new(*stream_ptr).map_err(
19
+ |e| RbErr::new(ruby.exception_arg_error(), e.to_string()),
20
+ )?))
21
+ }
22
+ }
23
+
24
+ impl TryConvert for Wrap<ArrowSchema> {
25
+ fn try_convert(val: Value) -> RbResult<Self> {
26
+ let ruby = Ruby::get_with(val);
27
+ let addr: usize = val.funcall("to_i", ())?;
28
+
29
+ // use similar approach as Polars to consume pointer and avoid copy
30
+ let schema_ptr =
31
+ Box::new(unsafe { std::ptr::replace(addr as _, FFI_ArrowSchema::empty()) });
32
+
33
+ Ok(Wrap(ArrowSchema::try_from(&*schema_ptr).map_err(|e| {
34
+ RbErr::new(ruby.exception_arg_error(), e.to_string())
35
+ })?))
21
36
  }
22
37
  }
@@ -0,0 +1,174 @@
1
+ use std::sync::Arc;
2
+
3
+ use arrow::array::{
4
+ Array, BooleanBuilder, Date32Builder, Decimal128Builder, Float32Builder, Float64Builder,
5
+ Int32Builder, Int64Builder, LargeBinaryBuilder, StringBuilder, TimestampMicrosecondBuilder,
6
+ };
7
+ use arrow::datatypes::{DataType as ArrowDataType, Decimal128Type};
8
+ use arrow_array::ffi_stream::FFI_ArrowArrayStream;
9
+ use arrow_array::{RecordBatch, RecordBatchIterator};
10
+ use arrow_cast::parse::parse_decimal;
11
+ use arrow_schema::{Field as ArrowField, Schema as ArrowSchema, TimeUnit};
12
+ use magnus::{RArray, RHash, RString, Ruby, TryConvert, Value, value::ReprValue};
13
+
14
+ use crate::RbResult;
15
+ use crate::capsule::RbCapsule;
16
+ use crate::error::todo_error;
17
+ use crate::utils::{Wrap, date_to_i32};
18
+
19
+ #[magnus::wrap(class = "Iceberg::ArrowRecordBatch")]
20
+ pub struct RbArrowRecordBatch {
21
+ pub(crate) batch: RecordBatch,
22
+ }
23
+
24
+ impl RbArrowRecordBatch {
25
+ pub fn new(ruby: &Ruby, data: RArray, schema: Wrap<ArrowSchema>) -> RbResult<Self> {
26
+ let schema = Arc::new(schema.0);
27
+ let mut columns = Vec::new();
28
+ for field in &schema.fields {
29
+ let array = match field.data_type() {
30
+ ArrowDataType::Boolean => new_array_boolean(ruby, data, field)?,
31
+ ArrowDataType::Int32 => new_array_int32(ruby, data, field)?,
32
+ ArrowDataType::Int64 => new_array_int64(ruby, data, field)?,
33
+ ArrowDataType::Float32 => new_array_float32(ruby, data, field)?,
34
+ ArrowDataType::Float64 => new_array_float64(ruby, data, field)?,
35
+ ArrowDataType::Decimal128(precision, scale) => {
36
+ new_array_decimal128(ruby, data, field, *precision, *scale)?
37
+ }
38
+ ArrowDataType::Date32 => new_array_date32(ruby, data, field)?,
39
+ ArrowDataType::Timestamp(TimeUnit::Microsecond, None) => {
40
+ new_array_timestamp_us(ruby, data, field)?
41
+ }
42
+ ArrowDataType::Utf8 => new_array_utf8(ruby, data, field)?,
43
+ ArrowDataType::LargeBinary => new_array_large_binary(ruby, data, field)?,
44
+ _ => return Err(todo_error(field.data_type())),
45
+ };
46
+ columns.push(array);
47
+ }
48
+ let batch = RecordBatch::try_new(schema.clone(), columns).unwrap();
49
+ Ok(Self { batch })
50
+ }
51
+
52
+ pub fn arrow_c_stream(&self) -> RbCapsule {
53
+ let schema = self.batch.schema();
54
+ let reader = RecordBatchIterator::new([Ok(self.batch.clone())].into_iter(), schema);
55
+ let stream = FFI_ArrowArrayStream::new(Box::new(reader));
56
+ RbCapsule::new(stream, Some("arrow_array_stream".into()))
57
+ }
58
+ }
59
+
60
+ macro_rules! new_array {
61
+ ($name:ident, $builder:ty, $type:ty) => {
62
+ fn $name(ruby: &Ruby, data: RArray, field: &Arc<ArrowField>) -> RbResult<Arc<dyn Array>> {
63
+ let name = ruby.str_new(field.name());
64
+ let mut builder = <$builder>::new();
65
+ for row in data.into_iter() {
66
+ let row = RHash::try_convert(row)?;
67
+ let value: Option<$type> = row.aref(name)?;
68
+ match value {
69
+ Some(v) => builder.append_value(v),
70
+ None => builder.append_null(),
71
+ }
72
+ }
73
+ let array: Arc<dyn Array> = Arc::new(builder.finish());
74
+ Ok(array)
75
+ }
76
+ };
77
+ }
78
+
79
+ new_array!(new_array_boolean, BooleanBuilder, bool);
80
+ new_array!(new_array_int32, Int32Builder, i32);
81
+ new_array!(new_array_int64, Int64Builder, i64);
82
+ new_array!(new_array_float32, Float32Builder, f32);
83
+ new_array!(new_array_float64, Float64Builder, f64);
84
+ new_array!(new_array_utf8, StringBuilder, String);
85
+
86
+ fn new_array_decimal128(
87
+ ruby: &Ruby,
88
+ data: RArray,
89
+ field: &Arc<ArrowField>,
90
+ precision: u8,
91
+ scale: i8,
92
+ ) -> RbResult<Arc<dyn Array>> {
93
+ let name = ruby.str_new(field.name());
94
+ let mut builder = Decimal128Builder::new()
95
+ .with_precision_and_scale(precision, scale)
96
+ .unwrap();
97
+ for row in data.into_iter() {
98
+ let row = RHash::try_convert(row)?;
99
+ let value: Option<Value> = row.aref(name)?;
100
+ match value {
101
+ Some(v) => {
102
+ let s = v.to_r_string()?;
103
+ builder.append_value(
104
+ parse_decimal::<Decimal128Type>(unsafe { s.as_str()? }, precision, scale)
105
+ .unwrap(),
106
+ )
107
+ }
108
+ None => builder.append_null(),
109
+ }
110
+ }
111
+ let array: Arc<dyn Array> = Arc::new(builder.finish());
112
+ Ok(array)
113
+ }
114
+
115
+ fn new_array_date32(
116
+ ruby: &Ruby,
117
+ data: RArray,
118
+ field: &Arc<ArrowField>,
119
+ ) -> RbResult<Arc<dyn Array>> {
120
+ let name = ruby.str_new(field.name());
121
+ let mut builder = Date32Builder::new();
122
+ for row in data.into_iter() {
123
+ let row = RHash::try_convert(row)?;
124
+ let value: Option<Value> = row.aref(name)?;
125
+ match value {
126
+ Some(v) => builder.append_value(date_to_i32(v)?),
127
+ None => builder.append_null(),
128
+ }
129
+ }
130
+ let array: Arc<dyn Array> = Arc::new(builder.finish());
131
+ Ok(array)
132
+ }
133
+
134
+ fn new_array_timestamp_us(
135
+ ruby: &Ruby,
136
+ data: RArray,
137
+ field: &Arc<ArrowField>,
138
+ ) -> RbResult<Arc<dyn Array>> {
139
+ let name = ruby.str_new(field.name());
140
+ let mut builder = TimestampMicrosecondBuilder::new();
141
+ for row in data.into_iter() {
142
+ let row = RHash::try_convert(row)?;
143
+ let value: Option<Value> = row.aref(name)?;
144
+ match value {
145
+ Some(v) => {
146
+ let sec: i64 = v.funcall("to_i", ())?;
147
+ let usec: i64 = v.funcall("usec", ())?;
148
+ builder.append_value(sec * 1_000_000 + usec);
149
+ }
150
+ None => builder.append_null(),
151
+ }
152
+ }
153
+ let array: Arc<dyn Array> = Arc::new(builder.finish());
154
+ Ok(array)
155
+ }
156
+
157
+ fn new_array_large_binary(
158
+ ruby: &Ruby,
159
+ data: RArray,
160
+ field: &Arc<ArrowField>,
161
+ ) -> RbResult<Arc<dyn Array>> {
162
+ let name = ruby.str_new(field.name());
163
+ let mut builder = LargeBinaryBuilder::new();
164
+ for row in data.into_iter() {
165
+ let row = RHash::try_convert(row)?;
166
+ let value: Option<RString> = row.aref(name)?;
167
+ match value {
168
+ Some(v) => builder.append_value(unsafe { v.as_slice() }),
169
+ None => builder.append_null(),
170
+ }
171
+ }
172
+ let array: Arc<dyn Array> = Arc::new(builder.finish());
173
+ Ok(array)
174
+ }
@@ -0,0 +1,24 @@
1
+ use std::any::Any;
2
+
3
+ #[magnus::wrap(class = "Iceberg::Capsule")]
4
+ pub struct RbCapsule {
5
+ value: Box<dyn Any + Send>,
6
+ name: Option<String>,
7
+ }
8
+
9
+ impl RbCapsule {
10
+ pub fn new<T: 'static + Send>(value: T, name: Option<String>) -> Self {
11
+ Self {
12
+ value: Box::new(value),
13
+ name,
14
+ }
15
+ }
16
+
17
+ pub fn to_i(&self) -> usize {
18
+ (&*self.value as *const dyn Any as *const ()) as usize
19
+ }
20
+
21
+ pub fn name(&self) -> Option<&str> {
22
+ self.name.as_deref()
23
+ }
24
+ }