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.
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,30 +31,53 @@ catalog.create_namespace("main")
31
31
  Create a table
32
32
 
33
33
  ```ruby
34
- catalog.create_table("main.events") do |t|
35
- t.bigint "id"
36
- t.float "value"
37
- end
34
+ table =
35
+ catalog.create_table("events") do |t|
36
+ t.bigint "id"
37
+ t.float "value"
38
+ end
39
+ ```
40
+
41
+ Append data
42
+
43
+ ```ruby
44
+ table.append([{"id" => 1, "value" => 3.0}, {"id" => 2, "value" => 4.0}])
38
45
  ```
39
46
 
40
47
  Or with [Polars](https://github.com/ankane/ruby-polars)
41
48
 
42
49
  ```ruby
43
50
  df = Polars::DataFrame.new({"id" => [1, 2], "value" => [3.0, 4.0]})
44
- table = catalog.create_table("main.events", schema: df.schema)
51
+ table = catalog.create_table("events", schema: df.schema)
45
52
  table.append(df)
46
53
  ```
47
54
 
55
+ Or with [Nanoarrow](https://github.com/ankane/nanoarrow-ruby)
56
+
57
+ ```ruby
58
+ arr =
59
+ Nanoarrow::Array.new(
60
+ [{"id" => 1, "value" => 3.0}, {"id" => 2, "value" => 4.0}],
61
+ Nanoarrow.struct({"a" => Nanoarrow.int64, "b" => Nanoarrow.string})
62
+ )
63
+ table = catalog.create_table("events", schema: arr.schema)
64
+ table.append(arr)
65
+ ```
66
+
48
67
  Load a table
49
68
 
50
69
  ```ruby
51
- table = catalog.load_table("main.events")
70
+ table = catalog.load_table("events")
52
71
  ```
53
72
 
54
73
  Query a table
55
74
 
56
75
  ```ruby
76
+ table.to_a
77
+ # or
57
78
  table.to_polars.collect
79
+ # or
80
+ table.scan.to_arrow
58
81
  ```
59
82
 
60
83
  ## Catalog Types
@@ -67,6 +90,22 @@ Iceberg::RestCatalog.new(
67
90
  )
68
91
  ```
69
92
 
93
+ ### S3 Tables
94
+
95
+ ```ruby
96
+ Iceberg::S3TablesCatalog.new(
97
+ arn: "arn:aws:s3tables:..."
98
+ )
99
+ ```
100
+
101
+ ### Glue
102
+
103
+ ```ruby
104
+ Iceberg::GlueCatalog.new(
105
+ warehouse: "s3://my-bucket"
106
+ )
107
+ ```
108
+
70
109
  ### SQL
71
110
 
72
111
  ```ruby
@@ -133,7 +172,7 @@ catalog.list_tables("main")
133
172
  Create a table
134
173
 
135
174
  ```ruby
136
- catalog.create_table("main.events") do |t|
175
+ catalog.create_table("events") do |t|
137
176
  t.integer "id"
138
177
  t.float "value"
139
178
  end
@@ -142,31 +181,31 @@ end
142
181
  Load a table
143
182
 
144
183
  ```ruby
145
- catalog.load_table("main.events")
184
+ catalog.load_table("events")
146
185
  ```
147
186
 
148
187
  Check if a table exists
149
188
 
150
189
  ```ruby
151
- catalog.table_exists?("main.events")
190
+ catalog.table_exists?("events")
152
191
  ```
153
192
 
154
193
  Rename a table
155
194
 
156
195
  ```ruby
157
- catalog.rename_table("main.events", "main.events2")
196
+ catalog.rename_table("events", "events2")
158
197
  ```
159
198
 
160
199
  Register a table
161
200
 
162
201
  ```ruby
163
- catalog.register_table("main.events", "metadata.json")
202
+ catalog.register_table("events", "metadata.json")
164
203
  ```
165
204
 
166
205
  Drop a table
167
206
 
168
207
  ```ruby
169
- catalog.drop_table("main.events")
208
+ catalog.drop_table("events")
170
209
  ```
171
210
 
172
211
  ## Static Tables
@@ -196,7 +235,7 @@ To get started with development:
196
235
  git clone https://github.com/ankane/iceberg-ruby.git
197
236
  cd iceberg-ruby
198
237
  bundle install
199
- bundle exec rake compile
238
+ bundle exec rake compile:dev
200
239
 
201
240
  # memory catalog
202
241
  bundle exec rake test:memory
@@ -208,4 +247,10 @@ bundle exec rake test:rest
208
247
  # SQL catalog
209
248
  createdb iceberg_ruby_test
210
249
  bundle exec rake test:sql
250
+
251
+ # S3 Tables catalog
252
+ bundle exec rake test:s3tables
253
+
254
+ # Glue catalog
255
+ bundle exec rake test:glue
211
256
  ```
@@ -1,10 +1,10 @@
1
1
  [package]
2
2
  name = "iceberg-ruby"
3
- version = "0.11.2"
3
+ version = "0.12.1"
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.95"
8
8
  publish = false
9
9
 
10
10
  [lib]
@@ -12,25 +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.4"
16
+ arrow-array = { version = "58.4", features = ["ffi"] }
17
+ arrow-cast = "58.4"
18
+ arrow-schema = "58.4"
19
+ datafusion = { version = "54", optional = true }
18
20
  futures = "0.3"
19
- iceberg = "=0.9.1"
20
- iceberg-catalog-glue = { version = "=0.9.1", optional = true }
21
- iceberg-catalog-rest = { version = "=0.9.1", optional = true }
22
- iceberg-catalog-s3tables = { version = "=0.9.1", optional = true }
23
- iceberg-catalog-sql = { version = "=0.9.1", optional = true }
24
- iceberg-datafusion = { version = "=0.9.1", optional = true }
25
- magnus = "0.8"
26
- parquet = "57"
21
+ iceberg = "=0.11.0"
22
+ iceberg-catalog-glue = { version = "=0.11.0", optional = true }
23
+ iceberg-catalog-rest = { version = "=0.11.0", optional = true }
24
+ iceberg-catalog-s3tables = { version = "=0.11.0", optional = true }
25
+ iceberg-catalog-sql = { version = "=0.11.0", optional = true }
26
+ iceberg-datafusion = { version = "=0.11.0", optional = true }
27
+ iceberg-storage-opendal = "=0.11.0"
28
+ magnus = "0.9"
29
+ parquet = "58.4"
27
30
  rb-sys = "0.9"
28
- sqlx = { version = "0.8", features = ["postgres", "runtime-tokio", "sqlite"], default-features = false, optional = true }
31
+ sqlx = { version = "0.8", features = ["postgres", "runtime-tokio", "sqlite", "tls-rustls-aws-lc-rs"], default-features = false, optional = true }
29
32
  tokio = { version = "1", features = ["rt-multi-thread"] }
30
33
  uuid = { version = "1", features = ["v4"] }
31
34
 
32
35
  [features]
33
- default = ["rest", "sql"]
36
+ default = ["glue", "rest", "s3tables", "sql"]
34
37
  datafusion = ["dep:datafusion", "dep:iceberg-datafusion"]
35
38
  glue = ["dep:iceberg-catalog-glue"]
36
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
+ }