polars-df 0.25.1 → 0.26.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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -1
  3. data/Cargo.lock +278 -106
  4. data/Cargo.toml +0 -3
  5. data/LICENSE.txt +1 -1
  6. data/README.md +7 -3
  7. data/ext/polars/Cargo.toml +18 -18
  8. data/ext/polars/src/catalog/unity.rs +15 -20
  9. data/ext/polars/src/conversion/any_value.rs +25 -24
  10. data/ext/polars/src/conversion/chunked_array.rs +58 -56
  11. data/ext/polars/src/conversion/datetime.rs +58 -7
  12. data/ext/polars/src/conversion/mod.rs +205 -142
  13. data/ext/polars/src/dataframe/export.rs +15 -12
  14. data/ext/polars/src/dataframe/general.rs +5 -4
  15. data/ext/polars/src/dataframe/map.rs +6 -4
  16. data/ext/polars/src/error.rs +1 -1
  17. data/ext/polars/src/expr/array.rs +0 -24
  18. data/ext/polars/src/expr/datatype.rs +3 -2
  19. data/ext/polars/src/expr/datetime.rs +4 -4
  20. data/ext/polars/src/expr/general.rs +27 -15
  21. data/ext/polars/src/expr/list.rs +0 -26
  22. data/ext/polars/src/functions/business.rs +2 -2
  23. data/ext/polars/src/functions/io.rs +4 -3
  24. data/ext/polars/src/functions/lazy.rs +58 -46
  25. data/ext/polars/src/functions/meta.rs +6 -5
  26. data/ext/polars/src/functions/mod.rs +0 -1
  27. data/ext/polars/src/functions/utils.rs +4 -2
  28. data/ext/polars/src/interop/arrow/mod.rs +4 -2
  29. data/ext/polars/src/interop/arrow/to_rb.rs +17 -12
  30. data/ext/polars/src/interop/numo/to_numo_series.rs +26 -25
  31. data/ext/polars/src/io/scan_options.rs +6 -3
  32. data/ext/polars/src/io/sink_options.rs +2 -0
  33. data/ext/polars/src/lazyframe/general.rs +32 -16
  34. data/ext/polars/src/lazyframe/optflags.rs +2 -1
  35. data/ext/polars/src/lib.rs +21 -37
  36. data/ext/polars/src/map/lazy.rs +5 -2
  37. data/ext/polars/src/map/series.rs +19 -18
  38. data/ext/polars/src/on_startup.rs +16 -7
  39. data/ext/polars/src/ruby/capsule.rs +27 -0
  40. data/ext/polars/src/ruby/mod.rs +1 -0
  41. data/ext/polars/src/ruby/numo.rs +3 -4
  42. data/ext/polars/src/ruby/rb_modules.rs +2 -4
  43. data/ext/polars/src/ruby/ruby_udf.rs +7 -9
  44. data/ext/polars/src/ruby/utils.rs +12 -1
  45. data/ext/polars/src/series/aggregation.rs +13 -1
  46. data/ext/polars/src/series/export.rs +38 -38
  47. data/ext/polars/src/series/general.rs +4 -3
  48. data/ext/polars/src/series/map.rs +3 -2
  49. data/ext/polars/src/series/scatter.rs +4 -4
  50. data/ext/polars/src/utils.rs +31 -7
  51. data/lib/polars/array_expr.rb +23 -7
  52. data/lib/polars/array_name_space.rb +16 -2
  53. data/lib/polars/binary_name_space.rb +32 -0
  54. data/lib/polars/data_frame.rb +83 -11
  55. data/lib/polars/date_time_expr.rb +91 -3
  56. data/lib/polars/date_time_name_space.rb +7 -1
  57. data/lib/polars/expr.rb +122 -44
  58. data/lib/polars/functions/aggregation/horizontal.rb +4 -4
  59. data/lib/polars/functions/business.rb +2 -2
  60. data/lib/polars/functions/eager.rb +80 -7
  61. data/lib/polars/functions/lazy.rb +5 -2
  62. data/lib/polars/iceberg_dataset.rb +81 -14
  63. data/lib/polars/io/csv.rb +27 -5
  64. data/lib/polars/io/delta.rb +4 -0
  65. data/lib/polars/io/ipc.rb +1 -1
  66. data/lib/polars/io/lines.rb +4 -4
  67. data/lib/polars/io/sink_options.rb +4 -2
  68. data/lib/polars/lazy_frame.rb +97 -14
  69. data/lib/polars/list_expr.rb +21 -7
  70. data/lib/polars/list_name_space.rb +16 -2
  71. data/lib/polars/query_opt_flags.rb +22 -5
  72. data/lib/polars/schema.rb +9 -0
  73. data/lib/polars/selectors.rb +1 -1
  74. data/lib/polars/series.rb +106 -19
  75. data/lib/polars/sql_context.rb +2 -2
  76. data/lib/polars/string_cache.rb +19 -72
  77. data/lib/polars/string_expr.rb +1 -7
  78. data/lib/polars/string_name_space.rb +1 -7
  79. data/lib/polars/utils/construction/series.rb +8 -3
  80. data/lib/polars/utils/convert.rb +16 -6
  81. data/lib/polars/utils/parse.rb +7 -0
  82. data/lib/polars/utils/reduce_balanced.rb +43 -0
  83. data/lib/polars/utils/various.rb +5 -0
  84. data/lib/polars/version.rb +1 -1
  85. data/lib/polars.rb +5 -1
  86. metadata +4 -17
  87. data/ext/polars/src/functions/string_cache.rs +0 -24
@@ -16,6 +16,7 @@ use magnus::{
16
16
  use polars::chunked_array::object::PolarsObjectSafe;
17
17
  use polars::chunked_array::ops::{FillNullLimit, FillNullStrategy};
18
18
  use polars::datatypes::AnyValue;
19
+ use polars::frame::PivotColumnNaming;
19
20
  use polars::frame::row::Row;
20
21
  use polars::io::avro::AvroCompression;
21
22
  use polars::prelude::default_values::{
@@ -36,6 +37,9 @@ use polars_utils::total_ord::{TotalEq, TotalHash};
36
37
  use crate::file::{RubyScanSourceInput, get_ruby_scan_source_input};
37
38
  use crate::object::OBJECT_NAME;
38
39
  use crate::rb_modules::pl_series;
40
+ use crate::ruby::gvl::GvlExt;
41
+ use crate::ruby::utils::TryIntoValue;
42
+ use crate::series::import_schema_rbcapsule;
39
43
  use crate::utils::to_rb_err;
40
44
  use crate::{
41
45
  RbDataFrame, RbExpr, RbLazyFrame, RbPolarsErr, RbResult, RbSeries, RbTypeError, RbValueError,
@@ -80,11 +84,9 @@ pub(crate) fn get_series(obj: Value) -> RbResult<Series> {
80
84
  Ok(rbs.series.read().clone())
81
85
  }
82
86
 
83
- pub(crate) fn to_series(rb: &Ruby, s: RbSeries) -> Value {
87
+ pub(crate) fn to_series(rb: &Ruby, s: RbSeries) -> RbResult<Value> {
84
88
  let series = pl_series(rb);
85
- series
86
- .funcall::<_, _, Value>("_from_rbseries", (s,))
87
- .unwrap()
89
+ series.funcall::<_, _, Value>("_from_rbseries", (s,))
88
90
  }
89
91
 
90
92
  impl TryConvert for Wrap<PlSmallStr> {
@@ -116,140 +118,135 @@ impl TryConvert for Wrap<NullValues> {
116
118
  }
117
119
  }
118
120
 
119
- fn struct_dict<'a>(ruby: &Ruby, vals: impl Iterator<Item = AnyValue<'a>>, flds: &[Field]) -> Value {
121
+ fn struct_dict<'a>(
122
+ ruby: &Ruby,
123
+ vals: impl Iterator<Item = AnyValue<'a>>,
124
+ flds: &[Field],
125
+ ) -> RbResult<Value> {
120
126
  let dict = ruby.hash_new();
121
127
  for (fld, val) in flds.iter().zip(vals) {
122
- dict.aset(fld.name().as_str(), Wrap(val)).unwrap()
128
+ dict.aset(fld.name().as_str(), Wrap(val).try_into_value_with(ruby)?)?;
123
129
  }
124
- dict.as_value()
130
+ Ok(dict.as_value())
125
131
  }
126
132
 
127
- impl IntoValue for Wrap<Series> {
128
- fn into_value_with(self, ruby: &Ruby) -> Value {
133
+ impl TryIntoValue for Wrap<Series> {
134
+ fn try_into_value_with(self, ruby: &Ruby) -> RbResult<Value> {
129
135
  to_series(ruby, RbSeries::new(self.0))
130
136
  }
131
137
  }
132
138
 
133
- impl IntoValue for Wrap<DataType> {
134
- fn into_value_with(self, ruby: &Ruby) -> Value {
139
+ impl TryIntoValue for Wrap<DataType> {
140
+ fn try_into_value_with(self, ruby: &Ruby) -> RbResult<Value> {
135
141
  let pl = crate::rb_modules::polars(ruby);
136
142
 
137
143
  match self.0 {
138
144
  DataType::Int8 => {
139
- let class = pl.const_get::<_, Value>("Int8").unwrap();
140
- class.funcall("new", ()).unwrap()
145
+ let class = pl.const_get::<_, Value>("Int8")?;
146
+ class.funcall("new", ())
141
147
  }
142
148
  DataType::Int16 => {
143
- let class = pl.const_get::<_, Value>("Int16").unwrap();
144
- class.funcall("new", ()).unwrap()
149
+ let class = pl.const_get::<_, Value>("Int16")?;
150
+ class.funcall("new", ())
145
151
  }
146
152
  DataType::Int32 => {
147
- let class = pl.const_get::<_, Value>("Int32").unwrap();
148
- class.funcall("new", ()).unwrap()
153
+ let class = pl.const_get::<_, Value>("Int32")?;
154
+ class.funcall("new", ())
149
155
  }
150
156
  DataType::Int64 => {
151
- let class = pl.const_get::<_, Value>("Int64").unwrap();
152
- class.funcall("new", ()).unwrap()
157
+ let class = pl.const_get::<_, Value>("Int64")?;
158
+ class.funcall("new", ())
153
159
  }
154
160
  DataType::Int128 => {
155
- let class = pl.const_get::<_, Value>("Int128").unwrap();
156
- class.funcall("new", ()).unwrap()
161
+ let class = pl.const_get::<_, Value>("Int128")?;
162
+ class.funcall("new", ())
157
163
  }
158
164
  DataType::UInt8 => {
159
- let class = pl.const_get::<_, Value>("UInt8").unwrap();
160
- class.funcall("new", ()).unwrap()
165
+ let class = pl.const_get::<_, Value>("UInt8")?;
166
+ class.funcall("new", ())
161
167
  }
162
168
  DataType::UInt16 => {
163
- let class = pl.const_get::<_, Value>("UInt16").unwrap();
164
- class.funcall("new", ()).unwrap()
169
+ let class = pl.const_get::<_, Value>("UInt16")?;
170
+ class.funcall("new", ())
165
171
  }
166
172
  DataType::UInt32 => {
167
- let class = pl.const_get::<_, Value>("UInt32").unwrap();
168
- class.funcall("new", ()).unwrap()
173
+ let class = pl.const_get::<_, Value>("UInt32")?;
174
+ class.funcall("new", ())
169
175
  }
170
176
  DataType::UInt64 => {
171
- let class = pl.const_get::<_, Value>("UInt64").unwrap();
172
- class.funcall("new", ()).unwrap()
177
+ let class = pl.const_get::<_, Value>("UInt64")?;
178
+ class.funcall("new", ())
173
179
  }
174
180
  DataType::UInt128 => {
175
- let class = pl.const_get::<_, Value>("UInt128").unwrap();
176
- class.funcall("new", ()).unwrap()
181
+ let class = pl.const_get::<_, Value>("UInt128")?;
182
+ class.funcall("new", ())
177
183
  }
178
184
  DataType::Float16 => {
179
- let class = pl.const_get::<_, Value>("Float16").unwrap();
180
- class.funcall("new", ()).unwrap()
185
+ let class = pl.const_get::<_, Value>("Float16")?;
186
+ class.funcall("new", ())
181
187
  }
182
188
  DataType::Float32 => {
183
- let class = pl.const_get::<_, Value>("Float32").unwrap();
184
- class.funcall("new", ()).unwrap()
189
+ let class = pl.const_get::<_, Value>("Float32")?;
190
+ class.funcall("new", ())
185
191
  }
186
192
  DataType::Float64 | DataType::Unknown(UnknownKind::Float) => {
187
- let class = pl.const_get::<_, Value>("Float64").unwrap();
188
- class.funcall("new", ()).unwrap()
193
+ let class = pl.const_get::<_, Value>("Float64")?;
194
+ class.funcall("new", ())
189
195
  }
190
196
  DataType::Decimal(precision, scale) => {
191
- let class = pl.const_get::<_, Value>("Decimal").unwrap();
192
- class
193
- .funcall::<_, _, Value>("new", (precision, scale))
194
- .unwrap()
197
+ let class = pl.const_get::<_, Value>("Decimal")?;
198
+ class.funcall::<_, _, Value>("new", (precision, scale))
195
199
  }
196
200
  DataType::Boolean => {
197
- let class = pl.const_get::<_, Value>("Boolean").unwrap();
198
- class.funcall("new", ()).unwrap()
201
+ let class = pl.const_get::<_, Value>("Boolean")?;
202
+ class.funcall("new", ())
199
203
  }
200
204
  DataType::String | DataType::Unknown(UnknownKind::Str) => {
201
- let class = pl.const_get::<_, Value>("String").unwrap();
202
- class.funcall("new", ()).unwrap()
205
+ let class = pl.const_get::<_, Value>("String")?;
206
+ class.funcall("new", ())
203
207
  }
204
208
  DataType::Binary => {
205
- let class = pl.const_get::<_, Value>("Binary").unwrap();
206
- class.funcall("new", ()).unwrap()
209
+ let class = pl.const_get::<_, Value>("Binary")?;
210
+ class.funcall("new", ())
207
211
  }
208
212
  DataType::Array(inner, size) => {
209
- let class = pl.const_get::<_, Value>("Array").unwrap();
210
- let inner = Wrap(*inner);
213
+ let class = pl.const_get::<_, Value>("Array")?;
214
+ let inner = Wrap(*inner).try_into_value_with(ruby)?;
211
215
  let args = (inner, size);
212
- class.funcall::<_, _, Value>("new", args).unwrap()
216
+ class.funcall::<_, _, Value>("new", args)
213
217
  }
214
218
  DataType::List(inner) => {
215
- let class = pl.const_get::<_, Value>("List").unwrap();
216
- let inner = Wrap(*inner);
217
- class.funcall::<_, _, Value>("new", (inner,)).unwrap()
219
+ let class = pl.const_get::<_, Value>("List")?;
220
+ let inner = Wrap(*inner).try_into_value_with(ruby)?;
221
+ class.funcall::<_, _, Value>("new", (inner,))
218
222
  }
219
223
  DataType::Date => {
220
- let class = pl.const_get::<_, Value>("Date").unwrap();
221
- class.funcall("new", ()).unwrap()
224
+ let class = pl.const_get::<_, Value>("Date")?;
225
+ class.funcall("new", ())
222
226
  }
223
227
  DataType::Datetime(tu, tz) => {
224
- let datetime_class = pl.const_get::<_, Value>("Datetime").unwrap();
225
- datetime_class
226
- .funcall::<_, _, Value>(
227
- "new",
228
- (tu.to_ascii(), tz.as_deref().map(|x| x.as_str())),
229
- )
230
- .unwrap()
228
+ let datetime_class = pl.const_get::<_, Value>("Datetime")?;
229
+ datetime_class.funcall::<_, _, Value>(
230
+ "new",
231
+ (tu.to_ascii(), tz.as_deref().map(|x| x.as_str())),
232
+ )
231
233
  }
232
234
  DataType::Duration(tu) => {
233
- let duration_class = pl.const_get::<_, Value>("Duration").unwrap();
234
- duration_class
235
- .funcall::<_, _, Value>("new", (tu.to_ascii(),))
236
- .unwrap()
235
+ let duration_class = pl.const_get::<_, Value>("Duration")?;
236
+ duration_class.funcall::<_, _, Value>("new", (tu.to_ascii(),))
237
237
  }
238
238
  DataType::Object(_) => {
239
- let class = pl.const_get::<_, Value>("Object").unwrap();
240
- class.funcall("new", ()).unwrap()
239
+ let class = pl.const_get::<_, Value>("Object")?;
240
+ class.funcall("new", ())
241
241
  }
242
242
  DataType::Categorical(cats, _) => {
243
- let categories_class = pl.const_get::<_, Value>("Categories").unwrap();
244
- let categorical_class = pl.const_get::<_, Value>("Categorical").unwrap();
243
+ let categories_class = pl.const_get::<_, Value>("Categories")?;
244
+ let categorical_class = pl.const_get::<_, Value>("Categorical")?;
245
245
  let categories: Value = categories_class
246
- .funcall("_from_rb_categories", (RbCategories::from(cats.clone()),))
247
- .unwrap();
246
+ .funcall("_from_rb_categories", (RbCategories::from(cats.clone()),))?;
248
247
  let kwargs = ruby.hash_new();
249
- kwargs
250
- .aset(ruby.to_symbol("categories"), categories)
251
- .unwrap();
252
- categorical_class.funcall("new", (kwargs,)).unwrap()
248
+ kwargs.aset(ruby.to_symbol("categories"), categories)?;
249
+ categorical_class.funcall("new", (kwargs,))
253
250
  }
254
251
  DataType::Enum(_, mapping) => {
255
252
  let categories = unsafe {
@@ -258,42 +255,38 @@ impl IntoValue for Wrap<DataType> {
258
255
  vec![mapping.to_arrow(true)],
259
256
  )
260
257
  };
261
- let class = pl.const_get::<_, Value>("Enum").unwrap();
262
- let series = to_series(ruby, categories.into_series().into());
263
- class.funcall::<_, _, Value>("new", (series,)).unwrap()
258
+ let class = pl.const_get::<_, Value>("Enum")?;
259
+ let series = to_series(ruby, categories.into_series().into())?;
260
+ class.funcall::<_, _, Value>("new", (series,))
264
261
  }
265
262
  DataType::Time => {
266
- let class = pl.const_get::<_, Value>("Time").unwrap();
267
- class.funcall("new", ()).unwrap()
263
+ let class = pl.const_get::<_, Value>("Time")?;
264
+ class.funcall("new", ())
268
265
  }
269
266
  DataType::Struct(fields) => {
270
- let field_class = pl.const_get::<_, Value>("Field").unwrap();
267
+ let field_class = pl.const_get::<_, Value>("Field")?;
271
268
  let iter = fields.iter().map(|fld| {
272
269
  let name = fld.name().as_str();
273
- let dtype = Wrap(fld.dtype().clone());
274
- field_class
275
- .funcall::<_, _, Value>("new", (name, dtype))
276
- .unwrap()
270
+ let dtype = Wrap(fld.dtype().clone()).try_into_value_with(ruby);
271
+ dtype.and_then(|dt| field_class.funcall::<_, _, Value>("new", (name, dt)))
277
272
  });
278
- let fields = ruby.ary_from_iter(iter);
279
- let struct_class = pl.const_get::<_, Value>("Struct").unwrap();
280
- struct_class
281
- .funcall::<_, _, Value>("new", (fields,))
282
- .unwrap()
273
+ let fields = ruby.ary_try_from_iter(iter)?;
274
+ let struct_class = pl.const_get::<_, Value>("Struct")?;
275
+ struct_class.funcall::<_, _, Value>("new", (fields,))
283
276
  }
284
277
  DataType::Null => {
285
- let class = pl.const_get::<_, Value>("Null").unwrap();
286
- class.funcall("new", ()).unwrap()
278
+ let class = pl.const_get::<_, Value>("Null")?;
279
+ class.funcall("new", ())
287
280
  }
288
281
  DataType::Extension(_typ, _storage) => {
289
282
  todo!();
290
283
  }
291
284
  DataType::Unknown(UnknownKind::Int(v)) => {
292
- Wrap(materialize_dyn_int(v).dtype()).into_value_with(ruby)
285
+ Wrap(materialize_dyn_int(v).dtype()).try_into_value_with(ruby)
293
286
  }
294
287
  DataType::Unknown(_) => {
295
- let class = pl.const_get::<_, Value>("Unknown").unwrap();
296
- class.funcall("new", ()).unwrap()
288
+ let class = pl.const_get::<_, Value>("Unknown")?;
289
+ class.funcall("new", ())
297
290
  }
298
291
  DataType::BinaryOffset => {
299
292
  unimplemented!()
@@ -399,13 +392,12 @@ impl TryConvert for Wrap<DataType> {
399
392
  "Polars::String" => DataType::String,
400
393
  "Polars::Binary" => DataType::Binary,
401
394
  "Polars::Categorical" => {
402
- let categories: Value = ob.funcall("categories", ()).unwrap();
403
- let rb_categories: &RbCategories =
404
- categories.funcall("_categories", ()).unwrap();
395
+ let categories: Value = ob.funcall("categories", ())?;
396
+ let rb_categories: &RbCategories = categories.funcall("_categories", ())?;
405
397
  DataType::from_categories(rb_categories.categories().clone())
406
398
  }
407
399
  "Polars::Enum" => {
408
- let categories: Value = ob.funcall("categories", ()).unwrap();
400
+ let categories: Value = ob.funcall("categories", ())?;
409
401
  let s = get_series(categories)?;
410
402
  let ca = s.str().map_err(RbPolarsErr::from)?;
411
403
  let categories = ca.downcast_iter().next().unwrap().clone();
@@ -417,7 +409,7 @@ impl TryConvert for Wrap<DataType> {
417
409
  "Polars::Date" => DataType::Date,
418
410
  "Polars::Time" => DataType::Time,
419
411
  "Polars::Datetime" => {
420
- let time_unit: Value = ob.funcall("time_unit", ()).unwrap();
412
+ let time_unit: Value = ob.funcall("time_unit", ())?;
421
413
  let time_unit = Wrap::<TimeUnit>::try_convert(time_unit)?.0;
422
414
  let time_zone: Option<String> = ob.funcall("time_zone", ())?;
423
415
  DataType::Datetime(
@@ -426,7 +418,7 @@ impl TryConvert for Wrap<DataType> {
426
418
  )
427
419
  }
428
420
  "Polars::Duration" => {
429
- let time_unit: Value = ob.funcall("time_unit", ()).unwrap();
421
+ let time_unit: Value = ob.funcall("time_unit", ())?;
430
422
  let time_unit = Wrap::<TimeUnit>::try_convert(time_unit)?.0;
431
423
  DataType::Duration(time_unit)
432
424
  }
@@ -437,13 +429,13 @@ impl TryConvert for Wrap<DataType> {
437
429
  DataType::Decimal(precision, scale)
438
430
  }
439
431
  "Polars::List" => {
440
- let inner: Value = ob.funcall("inner", ()).unwrap();
432
+ let inner: Value = ob.funcall("inner", ())?;
441
433
  let inner = Wrap::<DataType>::try_convert(inner)?;
442
434
  DataType::List(Box::new(inner.0))
443
435
  }
444
436
  "Polars::Array" => {
445
- let inner: Value = ob.funcall("inner", ()).unwrap();
446
- let size: Value = ob.funcall("size", ()).unwrap();
437
+ let inner: Value = ob.funcall("inner", ())?;
438
+ let size: Value = ob.funcall("size", ())?;
447
439
  let inner = Wrap::<DataType>::try_convert(inner)?;
448
440
  let size = usize::try_convert(size)?;
449
441
  DataType::Array(Box::new(inner.0), size)
@@ -524,7 +516,28 @@ impl TryConvert for Wrap<Schema> {
524
516
  impl TryConvert for Wrap<ArrowSchema> {
525
517
  fn try_convert(ob: Value) -> RbResult<Self> {
526
518
  let ruby = Ruby::get_with(ob);
527
- // TODO improve
519
+
520
+ if let Ok(schema_capsule) = ob.funcall::<_, _, Value>("arrow_c_schema", ()) {
521
+ let field = import_schema_rbcapsule(schema_capsule)?;
522
+
523
+ let ArrowDataType::Struct(fields) = field.dtype else {
524
+ return Err(RbValueError::new_err(format!(
525
+ "arrow_c_schema of object did not return struct dtype: \
526
+ object: {:?}, dtype: {:?}",
527
+ ob, &field.dtype
528
+ )));
529
+ };
530
+
531
+ let mut schema = ArrowSchema::from_iter_check_duplicates(fields).unwrap();
532
+
533
+ if let Some(md) = field.metadata {
534
+ *schema.metadata_mut() = Arc::unwrap_or_clone(md);
535
+ }
536
+
537
+ return Ok(Wrap(schema));
538
+ }
539
+
540
+ // TODO remove in 0.27.0
528
541
  let ob = RHash::try_convert(ob)?;
529
542
  let fields: RArray = ob.aref(ruby.to_symbol("fields"))?;
530
543
  let mut arrow_schema = ArrowSchema::with_capacity(fields.len());
@@ -555,6 +568,33 @@ impl TryConvert for Wrap<ArrowSchema> {
555
568
  "binary_view" => ArrowDataType::BinaryView,
556
569
  "string_view" => ArrowDataType::Utf8View,
557
570
  "unknown" => ArrowDataType::Unknown,
571
+ "timestamp" => {
572
+ let time_unit: String = f.aref(ruby.to_symbol("time_unit"))?;
573
+ let time_zone: Option<String> = f.aref(ruby.to_symbol("time_zone"))?;
574
+ let arrow_time_unit = match time_unit.as_str() {
575
+ "us" => ArrowTimeUnit::Microsecond,
576
+ "ns" => ArrowTimeUnit::Nanosecond,
577
+ _ => todo!(),
578
+ };
579
+ ArrowDataType::Timestamp(arrow_time_unit, time_zone.map(|v| v.into()))
580
+ }
581
+ "time64" => {
582
+ let time_unit: String = f.aref(ruby.to_symbol("time_unit"))?;
583
+ let arrow_time_unit = match time_unit.as_str() {
584
+ "us" => ArrowTimeUnit::Microsecond,
585
+ _ => todo!(),
586
+ };
587
+ ArrowDataType::Time64(arrow_time_unit)
588
+ }
589
+ "decimal" => {
590
+ let precision: usize = f.aref(ruby.to_symbol("precision"))?;
591
+ let scale: usize = f.aref(ruby.to_symbol("scale"))?;
592
+ ArrowDataType::Decimal(precision, scale)
593
+ }
594
+ "fixed_size_binary" => {
595
+ let limit: usize = f.aref(ruby.to_symbol("limit"))?;
596
+ ArrowDataType::FixedSizeBinary(limit)
597
+ }
558
598
  _ => todo!(),
559
599
  };
560
600
  let is_nullable = f.aref(ruby.to_symbol("nullable"))?;
@@ -638,13 +678,13 @@ impl TryConvert for Wrap<ScanSources> {
638
678
  }
639
679
  }
640
680
 
641
- impl IntoValue for Wrap<Schema> {
642
- fn into_value_with(self, ruby: &Ruby) -> Value {
681
+ impl TryIntoValue for Wrap<Schema> {
682
+ fn try_into_value_with(self, ruby: &Ruby) -> RbResult<Value> {
643
683
  let dict = ruby.hash_new();
644
684
  for (k, v) in self.0.iter() {
645
- dict.aset(k.as_str(), Wrap(v.clone())).unwrap();
685
+ dict.aset(k.as_str(), Wrap(v.clone()).try_into_value_with(ruby)?)?;
646
686
  }
647
- dict.as_value()
687
+ Ok(dict.as_value())
648
688
  }
649
689
  }
650
690
 
@@ -655,18 +695,17 @@ pub struct ObjectValue {
655
695
 
656
696
  impl Debug for ObjectValue {
657
697
  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
658
- f.debug_struct("ObjectValue")
659
- .field("inner", &self.to_value())
660
- .finish()
698
+ write!(f, "{}", self)
661
699
  }
662
700
  }
663
701
 
664
702
  impl Hash for ObjectValue {
665
703
  fn hash<H: Hasher>(&self, state: &mut H) {
666
- let h = self
667
- .to_value()
668
- .funcall::<_, _, isize>("hash", ())
669
- .expect("should be hashable");
704
+ let h = Ruby::attach(|rb| {
705
+ rb.get_inner(self.inner)
706
+ .funcall::<_, _, isize>("hash", ())
707
+ .expect("should be hashable")
708
+ });
670
709
  state.write_isize(h)
671
710
  }
672
711
  }
@@ -675,7 +714,11 @@ impl Eq for ObjectValue {}
675
714
 
676
715
  impl PartialEq for ObjectValue {
677
716
  fn eq(&self, other: &Self) -> bool {
678
- self.to_value().eql(other.to_value()).unwrap_or(false)
717
+ Ruby::attach(|ruby| {
718
+ ruby.get_inner(self.inner)
719
+ .eql(ruby.get_inner(other.inner))
720
+ .unwrap_or(false)
721
+ })
679
722
  }
680
723
  }
681
724
 
@@ -696,7 +739,10 @@ impl TotalHash for ObjectValue {
696
739
 
697
740
  impl Display for ObjectValue {
698
741
  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
699
- write!(f, "{}", self.to_value())
742
+ Ruby::attach(|rb| {
743
+ let v = rb.get_inner(self.inner);
744
+ write!(f, "{}", v)
745
+ })
700
746
  }
701
747
  }
702
748
 
@@ -724,12 +770,6 @@ impl From<&dyn PolarsObjectSafe> for &ObjectValue {
724
770
  }
725
771
  }
726
772
 
727
- impl ObjectValue {
728
- pub fn to_value(&self) -> Value {
729
- self.clone().into_value_with(&Ruby::get().unwrap())
730
- }
731
- }
732
-
733
773
  impl IntoValue for ObjectValue {
734
774
  fn into_value_with(self, ruby: &Ruby) -> Value {
735
775
  ruby.get_inner(self.inner)
@@ -738,9 +778,9 @@ impl IntoValue for ObjectValue {
738
778
 
739
779
  impl Default for ObjectValue {
740
780
  fn default() -> Self {
741
- ObjectValue {
742
- inner: Ruby::get().unwrap().qnil().as_value().into(),
743
- }
781
+ Ruby::attach(|rb| ObjectValue {
782
+ inner: rb.qnil().as_value().into(),
783
+ })
744
784
  }
745
785
  }
746
786
 
@@ -1125,6 +1165,21 @@ impl TryConvert for Wrap<SearchSortedSide> {
1125
1165
  }
1126
1166
  }
1127
1167
 
1168
+ impl TryConvert for Wrap<PivotColumnNaming> {
1169
+ fn try_convert(ob: Value) -> RbResult<Self> {
1170
+ let parsed = match String::try_convert(ob)?.as_str() {
1171
+ "auto" => PivotColumnNaming::Auto,
1172
+ "combine" => PivotColumnNaming::Combine,
1173
+ v => {
1174
+ return Err(RbValueError::new_err(format!(
1175
+ "`column_naming` must be one of {{'auto', 'combine'}}, got {v}",
1176
+ )));
1177
+ }
1178
+ };
1179
+ Ok(Wrap(parsed))
1180
+ }
1181
+ }
1182
+
1128
1183
  impl TryConvert for Wrap<ClosedInterval> {
1129
1184
  fn try_convert(ob: Value) -> RbResult<Self> {
1130
1185
  let parsed = match String::try_convert(ob)?.as_str() {
@@ -1234,15 +1289,25 @@ impl TryConvert for Wrap<CastColumnsPolicy> {
1234
1289
  return Ok(out);
1235
1290
  }
1236
1291
 
1237
- let integer_upcast = match &*ob.funcall::<_, _, String>("integer_cast", ())? {
1238
- "upcast" => true,
1239
- "forbid" => false,
1240
- v => {
1241
- return Err(RbValueError::new_err(format!(
1242
- "unknown option for integer_cast: {v}"
1243
- )));
1292
+ let mut integer_upcast = false;
1293
+ let mut integer_to_float_cast = false;
1294
+
1295
+ let integer_cast_object: Value = ob.funcall("integer_cast", ())?;
1296
+
1297
+ parse_multiple_options("integer_cast", integer_cast_object, |v| {
1298
+ match v {
1299
+ "upcast" => integer_upcast = true,
1300
+ "allow-float" => integer_to_float_cast = true,
1301
+ "forbid" => {}
1302
+ v => {
1303
+ return Err(RbValueError::new_err(format!(
1304
+ "unknown option for integer_cast: {v}"
1305
+ )));
1306
+ }
1244
1307
  }
1245
- };
1308
+
1309
+ Ok(())
1310
+ })?;
1246
1311
 
1247
1312
  let mut float_upcast = false;
1248
1313
  let mut float_downcast = false;
@@ -1318,6 +1383,7 @@ impl TryConvert for Wrap<CastColumnsPolicy> {
1318
1383
 
1319
1384
  return Ok(Wrap(CastColumnsPolicy {
1320
1385
  integer_upcast,
1386
+ integer_to_float_cast,
1321
1387
  float_upcast,
1322
1388
  float_downcast,
1323
1389
  datetime_nanoseconds_downcast,
@@ -1364,12 +1430,9 @@ pub fn parse_fill_null_strategy(
1364
1430
  "zero" => FillNullStrategy::Zero,
1365
1431
  "one" => FillNullStrategy::One,
1366
1432
  e => {
1367
- return Err(magnus::Error::new(
1368
- Ruby::get().unwrap().exception_runtime_error(),
1369
- format!(
1370
- "strategy must be one of {{'forward', 'backward', 'min', 'max', 'mean', 'zero', 'one'}}, got {e}",
1371
- ),
1372
- ));
1433
+ return Err(RbValueError::new_err(format!(
1434
+ "`strategy` must be one of {{'forward', 'backward', 'min', 'max', 'mean', 'zero', 'one'}}, got {e}",
1435
+ )));
1373
1436
  }
1374
1437
  };
1375
1438
  Ok(parsed)
@@ -1,39 +1,42 @@
1
- use magnus::{IntoValue, Ruby, Value, prelude::*};
1
+ use magnus::{Ruby, Value, prelude::*};
2
2
 
3
3
  use super::*;
4
4
  use crate::RbResult;
5
5
  use crate::conversion::{ObjectValue, Wrap};
6
6
  use crate::interop::arrow::to_rb::dataframe_to_stream;
7
+ use crate::ruby::utils::TryIntoValue;
7
8
 
8
9
  impl RbDataFrame {
9
- pub fn row_tuple(ruby: &Ruby, self_: &Self, idx: i64) -> Value {
10
+ pub fn row_tuple(ruby: &Ruby, self_: &Self, idx: i64) -> RbResult<Value> {
10
11
  let idx = if idx < 0 {
11
12
  (self_.df.read().height() as i64 + idx) as usize
12
13
  } else {
13
14
  idx as usize
14
15
  };
15
- ruby.ary_from_iter(self_.df.read().columns().iter().map(|s| match s.dtype() {
16
+ ruby.ary_try_from_iter(self_.df.read().columns().iter().map(|s| match s.dtype() {
16
17
  DataType::Object(_) => {
17
18
  let obj: Option<&ObjectValue> = s.get_object(idx).map(|any| any.into());
18
- obj.unwrap().to_value()
19
+ // TODO remove unwrap and clone
20
+ obj.unwrap().clone().try_into_value_with(ruby)
19
21
  }
20
- _ => Wrap(s.get(idx).unwrap()).into_value_with(ruby),
22
+ _ => Wrap(s.get(idx).unwrap()).try_into_value_with(ruby),
21
23
  }))
22
- .as_value()
24
+ .map(|v| v.as_value())
23
25
  }
24
26
 
25
- pub fn row_tuples(ruby: &Ruby, self_: &Self) -> Value {
27
+ pub fn row_tuples(ruby: &Ruby, self_: &Self) -> RbResult<Value> {
26
28
  let df = &self_.df;
27
- ruby.ary_from_iter((0..df.read().height()).map(|idx| {
28
- ruby.ary_from_iter(self_.df.read().columns().iter().map(|s| match s.dtype() {
29
+ ruby.ary_try_from_iter((0..df.read().height()).map(|idx| {
30
+ ruby.ary_try_from_iter(self_.df.read().columns().iter().map(|s| match s.dtype() {
29
31
  DataType::Object(_) => {
30
32
  let obj: Option<&ObjectValue> = s.get_object(idx).map(|any| any.into());
31
- obj.unwrap().to_value()
33
+ // TODO remove unwrap and clone
34
+ obj.unwrap().clone().try_into_value_with(ruby)
32
35
  }
33
- _ => Wrap(s.get(idx).unwrap()).into_value_with(ruby),
36
+ _ => Wrap(s.get(idx).unwrap()).try_into_value_with(ruby),
34
37
  }))
35
38
  }))
36
- .as_value()
39
+ .map(|v| v.as_value())
37
40
  }
38
41
 
39
42
  pub fn __arrow_c_stream__(ruby: &Ruby, self_: &Self) -> RbResult<Value> {