parquet 0.5.12 → 0.6.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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +295 -98
  3. data/Cargo.toml +1 -1
  4. data/Gemfile +1 -0
  5. data/README.md +94 -3
  6. data/ext/parquet/Cargo.toml +8 -5
  7. data/ext/parquet/src/adapter_ffi.rs +156 -0
  8. data/ext/parquet/src/lib.rs +13 -21
  9. data/ext/parquet-core/Cargo.toml +23 -0
  10. data/ext/parquet-core/src/arrow_conversion.rs +1133 -0
  11. data/ext/parquet-core/src/error.rs +163 -0
  12. data/ext/parquet-core/src/lib.rs +60 -0
  13. data/ext/parquet-core/src/reader.rs +263 -0
  14. data/ext/parquet-core/src/schema.rs +283 -0
  15. data/ext/parquet-core/src/test_utils.rs +308 -0
  16. data/ext/parquet-core/src/traits/mod.rs +5 -0
  17. data/ext/parquet-core/src/traits/schema.rs +151 -0
  18. data/ext/parquet-core/src/value.rs +209 -0
  19. data/ext/parquet-core/src/writer.rs +839 -0
  20. data/ext/parquet-core/tests/arrow_conversion_tests.rs +423 -0
  21. data/ext/parquet-core/tests/binary_data.rs +437 -0
  22. data/ext/parquet-core/tests/column_projection.rs +557 -0
  23. data/ext/parquet-core/tests/complex_types.rs +821 -0
  24. data/ext/parquet-core/tests/compression_tests.rs +434 -0
  25. data/ext/parquet-core/tests/concurrent_access.rs +430 -0
  26. data/ext/parquet-core/tests/decimal_tests.rs +488 -0
  27. data/ext/parquet-core/tests/edge_cases_corner_cases.rs +322 -0
  28. data/ext/parquet-core/tests/error_handling_comprehensive_tests.rs +547 -0
  29. data/ext/parquet-core/tests/null_handling_tests.rs +430 -0
  30. data/ext/parquet-core/tests/performance_memory.rs +181 -0
  31. data/ext/parquet-core/tests/primitive_types.rs +547 -0
  32. data/ext/parquet-core/tests/real_world_patterns.rs +777 -0
  33. data/ext/parquet-core/tests/roundtrip_correctness.rs +279 -0
  34. data/ext/parquet-core/tests/schema_comprehensive_tests.rs +534 -0
  35. data/ext/parquet-core/tests/temporal_tests.rs +518 -0
  36. data/ext/parquet-core/tests/test_helpers.rs +132 -0
  37. data/ext/parquet-core/tests/writer_tests.rs +545 -0
  38. data/ext/parquet-ruby-adapter/Cargo.toml +22 -0
  39. data/ext/parquet-ruby-adapter/build.rs +5 -0
  40. data/ext/parquet-ruby-adapter/examples/try_into_value_demo.rs +98 -0
  41. data/ext/parquet-ruby-adapter/src/batch_manager.rs +116 -0
  42. data/ext/parquet-ruby-adapter/src/chunk_reader.rs +237 -0
  43. data/ext/parquet-ruby-adapter/src/converter.rs +1685 -0
  44. data/ext/parquet-ruby-adapter/src/error.rs +148 -0
  45. data/ext/{parquet/src/ruby_reader.rs → parquet-ruby-adapter/src/io.rs} +190 -56
  46. data/ext/parquet-ruby-adapter/src/lib.rs +90 -0
  47. data/ext/parquet-ruby-adapter/src/logger.rs +64 -0
  48. data/ext/parquet-ruby-adapter/src/metadata.rs +427 -0
  49. data/ext/parquet-ruby-adapter/src/reader.rs +317 -0
  50. data/ext/parquet-ruby-adapter/src/schema.rs +810 -0
  51. data/ext/parquet-ruby-adapter/src/string_cache.rs +106 -0
  52. data/ext/parquet-ruby-adapter/src/try_into_value.rs +91 -0
  53. data/ext/parquet-ruby-adapter/src/types.rs +94 -0
  54. data/ext/parquet-ruby-adapter/src/utils.rs +186 -0
  55. data/ext/parquet-ruby-adapter/src/writer.rs +435 -0
  56. data/lib/parquet/schema.rb +19 -0
  57. data/lib/parquet/version.rb +1 -1
  58. metadata +50 -24
  59. data/ext/parquet/src/enumerator.rs +0 -68
  60. data/ext/parquet/src/header_cache.rs +0 -99
  61. data/ext/parquet/src/logger.rs +0 -171
  62. data/ext/parquet/src/reader/common.rs +0 -111
  63. data/ext/parquet/src/reader/mod.rs +0 -211
  64. data/ext/parquet/src/reader/parquet_column_reader.rs +0 -44
  65. data/ext/parquet/src/reader/parquet_row_reader.rs +0 -43
  66. data/ext/parquet/src/reader/unified/mod.rs +0 -363
  67. data/ext/parquet/src/types/core_types.rs +0 -120
  68. data/ext/parquet/src/types/mod.rs +0 -100
  69. data/ext/parquet/src/types/parquet_value.rs +0 -1275
  70. data/ext/parquet/src/types/record_types.rs +0 -603
  71. data/ext/parquet/src/types/schema_converter.rs +0 -290
  72. data/ext/parquet/src/types/schema_node.rs +0 -424
  73. data/ext/parquet/src/types/timestamp.rs +0 -285
  74. data/ext/parquet/src/types/type_conversion.rs +0 -1949
  75. data/ext/parquet/src/types/writer_types.rs +0 -329
  76. data/ext/parquet/src/utils.rs +0 -184
  77. data/ext/parquet/src/writer/mod.rs +0 -505
  78. data/ext/parquet/src/writer/write_columns.rs +0 -238
  79. data/ext/parquet/src/writer/write_rows.rs +0 -488
@@ -0,0 +1,106 @@
1
+ use std::collections::HashMap;
2
+ use std::sync::{Arc, Mutex};
3
+
4
+ use magnus::RString;
5
+
6
+ /// A cache for interning strings in the Ruby VM to reduce memory usage
7
+ /// when there are many repeated strings
8
+ #[derive(Debug)]
9
+ pub struct StringCache {
10
+ /// The actual cache is shared behind an Arc<Mutex> to allow cloning
11
+ /// while maintaining a single global cache
12
+ cache: Arc<Mutex<HashMap<String, &'static str>>>,
13
+ enabled: bool,
14
+ hits: Arc<Mutex<usize>>,
15
+ misses: Arc<Mutex<usize>>,
16
+ }
17
+
18
+ impl StringCache {
19
+ /// Create a new string cache
20
+ pub fn new(enabled: bool) -> Self {
21
+ Self {
22
+ cache: Arc::new(Mutex::new(HashMap::new())),
23
+ enabled,
24
+ hits: Arc::new(Mutex::new(0)),
25
+ misses: Arc::new(Mutex::new(0)),
26
+ }
27
+ }
28
+
29
+ /// Intern a string in Ruby's VM, returning the same string for tracking
30
+ /// Note: We return the input string to maintain API compatibility,
31
+ /// but internally we ensure it's interned in Ruby's VM
32
+ pub fn intern(&mut self, s: String) -> Arc<str> {
33
+ if !self.enabled {
34
+ return Arc::from(s.as_str());
35
+ }
36
+
37
+ // Try to get or create the interned string
38
+ let result = (|| -> Result<(), String> {
39
+ let mut cache = self.cache.lock().map_err(|e| e.to_string())?;
40
+
41
+ if cache.contains_key(&s) {
42
+ let mut hits = self.hits.lock().map_err(|e| e.to_string())?;
43
+ *hits += 1;
44
+ } else {
45
+ // Create Ruby string and intern it
46
+ let rstring = RString::new(&s);
47
+ let interned = rstring.to_interned_str();
48
+ let static_str = interned.as_str().map_err(|e| e.to_string())?;
49
+
50
+ cache.insert(s.clone(), static_str);
51
+
52
+ let mut misses = self.misses.lock().map_err(|e| e.to_string())?;
53
+ *misses += 1;
54
+ }
55
+ Ok(())
56
+ })();
57
+
58
+ // Log any errors but don't fail - just return the string
59
+ if let Err(e) = result {
60
+ eprintln!("String cache error: {}", e);
61
+ }
62
+
63
+ Arc::from(s.as_str())
64
+ }
65
+
66
+ /// Get cache statistics
67
+ pub fn stats(&self) -> CacheStats {
68
+ let cache_size = self.cache.lock().map(|c| c.len()).unwrap_or(0);
69
+ let hits = self.hits.lock().map(|h| *h).unwrap_or(0);
70
+ let misses = self.misses.lock().map(|m| *m).unwrap_or(0);
71
+
72
+ CacheStats {
73
+ enabled: self.enabled,
74
+ size: cache_size,
75
+ hits,
76
+ misses,
77
+ hit_rate: if hits + misses > 0 {
78
+ hits as f64 / (hits + misses) as f64
79
+ } else {
80
+ 0.0
81
+ },
82
+ }
83
+ }
84
+
85
+ /// Clear the cache
86
+ pub fn clear(&mut self) {
87
+ if let Ok(mut cache) = self.cache.lock() {
88
+ cache.clear();
89
+ }
90
+ if let Ok(mut hits) = self.hits.lock() {
91
+ *hits = 0;
92
+ }
93
+ if let Ok(mut misses) = self.misses.lock() {
94
+ *misses = 0;
95
+ }
96
+ }
97
+ }
98
+
99
+ #[derive(Debug)]
100
+ pub struct CacheStats {
101
+ pub enabled: bool,
102
+ pub size: usize,
103
+ pub hits: usize,
104
+ pub misses: usize,
105
+ pub hit_rate: f64,
106
+ }
@@ -0,0 +1,91 @@
1
+ use crate::{error::Result, RubyAdapterError};
2
+ use magnus::{value::ReprValue, IntoValue, Ruby, Value};
3
+
4
+ /// Trait for converting Rust values to Ruby values with error handling
5
+ ///
6
+ /// This is similar to Magnus's `IntoValue` trait but allows for returning errors
7
+ /// instead of panicking or returning invalid values.
8
+ pub trait TryIntoValue: Sized {
9
+ /// Convert `self` to a Ruby value with error handling
10
+ fn try_into_value(self, handle: &Ruby) -> Result<Value>;
11
+
12
+ /// Convert `self` to a Ruby value with error handling, using the Ruby runtime from the current thread
13
+ fn try_into_value_with_current_thread(self) -> Result<Value> {
14
+ let ruby =
15
+ Ruby::get().map_err(|_| RubyAdapterError::runtime("Failed to get Ruby runtime"))?;
16
+ self.try_into_value(&ruby)
17
+ }
18
+ }
19
+
20
+ // Note: We don't provide a blanket implementation for all IntoValue types
21
+ // because some types may want to provide custom error handling.
22
+ // Types that need TryIntoValue should implement it explicitly.
23
+
24
+ // Convenience implementations for common types
25
+ impl TryIntoValue for String {
26
+ fn try_into_value(self, handle: &Ruby) -> Result<Value> {
27
+ Ok(self.into_value_with(handle))
28
+ }
29
+ }
30
+
31
+ impl TryIntoValue for &str {
32
+ fn try_into_value(self, handle: &Ruby) -> Result<Value> {
33
+ Ok(self.into_value_with(handle))
34
+ }
35
+ }
36
+
37
+ impl TryIntoValue for i32 {
38
+ fn try_into_value(self, handle: &Ruby) -> Result<Value> {
39
+ Ok(self.into_value_with(handle))
40
+ }
41
+ }
42
+
43
+ impl TryIntoValue for i64 {
44
+ fn try_into_value(self, handle: &Ruby) -> Result<Value> {
45
+ Ok(self.into_value_with(handle))
46
+ }
47
+ }
48
+
49
+ impl TryIntoValue for f32 {
50
+ fn try_into_value(self, handle: &Ruby) -> Result<Value> {
51
+ Ok(self.into_value_with(handle))
52
+ }
53
+ }
54
+
55
+ impl TryIntoValue for f64 {
56
+ fn try_into_value(self, handle: &Ruby) -> Result<Value> {
57
+ Ok(self.into_value_with(handle))
58
+ }
59
+ }
60
+
61
+ impl TryIntoValue for bool {
62
+ fn try_into_value(self, handle: &Ruby) -> Result<Value> {
63
+ Ok(self.into_value_with(handle))
64
+ }
65
+ }
66
+
67
+ impl<T> TryIntoValue for Vec<T>
68
+ where
69
+ T: TryIntoValue,
70
+ {
71
+ fn try_into_value(self, handle: &Ruby) -> Result<Value> {
72
+ let array = handle.ary_new();
73
+ for item in self {
74
+ let ruby_value = item.try_into_value(handle)?;
75
+ array.push(ruby_value)?;
76
+ }
77
+ Ok(handle.into_value(array))
78
+ }
79
+ }
80
+
81
+ impl<T> TryIntoValue for Option<T>
82
+ where
83
+ T: TryIntoValue,
84
+ {
85
+ fn try_into_value(self, handle: &Ruby) -> Result<Value> {
86
+ match self {
87
+ Some(value) => value.try_into_value(handle),
88
+ None => Ok(handle.qnil().as_value()),
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,94 @@
1
+ use magnus::Value;
2
+ use std::fs::File;
3
+ use std::str::FromStr;
4
+ use tempfile::NamedTempFile;
5
+
6
+ /// Arguments for writing Parquet files
7
+ #[derive(Debug)]
8
+ pub struct ParquetWriteArgs {
9
+ pub read_from: Value,
10
+ pub write_to: Value,
11
+ pub schema_value: Value,
12
+ pub batch_size: Option<usize>,
13
+ pub flush_threshold: Option<usize>,
14
+ pub compression: Option<String>,
15
+ pub sample_size: Option<usize>,
16
+ pub logger: Option<Value>,
17
+ pub string_cache: Option<bool>,
18
+ }
19
+
20
+ /// Arguments for creating row enumerators
21
+ pub struct RowEnumeratorArgs {
22
+ pub rb_self: Value,
23
+ pub to_read: Value,
24
+ pub result_type: ParserResultType,
25
+ pub columns: Option<Vec<String>>,
26
+ pub strict: bool,
27
+ pub logger: Option<Value>,
28
+ }
29
+
30
+ /// Arguments for creating column enumerators
31
+ pub struct ColumnEnumeratorArgs {
32
+ pub rb_self: Value,
33
+ pub to_read: Value,
34
+ pub result_type: ParserResultType,
35
+ pub columns: Option<Vec<String>>,
36
+ pub batch_size: Option<usize>,
37
+ pub strict: bool,
38
+ pub logger: Option<Value>,
39
+ }
40
+
41
+ /// Enum to handle different writer outputs
42
+ pub enum WriterOutput {
43
+ File(parquet_core::Writer<File>),
44
+ TempFile(parquet_core::Writer<File>, NamedTempFile, Value), // Writer, temp file, IO object
45
+ }
46
+
47
+ /// Result type for parser output
48
+ #[derive(Copy, Clone, Debug, PartialEq, Eq)]
49
+ pub enum ParserResultType {
50
+ Hash,
51
+ Array,
52
+ }
53
+
54
+ impl ParserResultType {
55
+ pub fn iter() -> impl Iterator<Item = Self> {
56
+ [Self::Hash, Self::Array].into_iter()
57
+ }
58
+ }
59
+
60
+ impl FromStr for ParserResultType {
61
+ type Err = String;
62
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
63
+ Self::try_from(s)
64
+ }
65
+ }
66
+
67
+ impl TryFrom<&str> for ParserResultType {
68
+ type Error = String;
69
+
70
+ fn try_from(value: &str) -> Result<Self, Self::Error> {
71
+ match value {
72
+ "hash" => Ok(ParserResultType::Hash),
73
+ "array" => Ok(ParserResultType::Array),
74
+ _ => Err(format!("Invalid parser result type: {}", value)),
75
+ }
76
+ }
77
+ }
78
+
79
+ impl TryFrom<String> for ParserResultType {
80
+ type Error = String;
81
+
82
+ fn try_from(value: String) -> Result<Self, Self::Error> {
83
+ Self::try_from(value.as_str())
84
+ }
85
+ }
86
+
87
+ impl std::fmt::Display for ParserResultType {
88
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89
+ match self {
90
+ ParserResultType::Hash => write!(f, "hash"),
91
+ ParserResultType::Array => write!(f, "array"),
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,186 @@
1
+ use magnus::value::ReprValue;
2
+ use magnus::{
3
+ scan_args::{get_kwargs, scan_args},
4
+ Error as MagnusError, KwArgs, RArray, RHash, Ruby, Symbol, Value,
5
+ };
6
+ use parquet::basic::Compression;
7
+ use parquet_core::ParquetValue;
8
+
9
+ use crate::types::{ColumnEnumeratorArgs, ParquetWriteArgs, RowEnumeratorArgs};
10
+
11
+ /// Estimate the memory size of a ParquetValue
12
+ pub fn estimate_parquet_value_size(value: &ParquetValue) -> usize {
13
+ match value {
14
+ ParquetValue::Null => 1,
15
+ ParquetValue::Boolean(_) => 1,
16
+ ParquetValue::Int8(_) => 1,
17
+ ParquetValue::Int16(_) => 2,
18
+ ParquetValue::Int32(_) => 4,
19
+ ParquetValue::Int64(_) => 8,
20
+ ParquetValue::UInt8(_) => 1,
21
+ ParquetValue::UInt16(_) => 2,
22
+ ParquetValue::UInt32(_) => 4,
23
+ ParquetValue::UInt64(_) => 8,
24
+ ParquetValue::Float16(_) => 4,
25
+ ParquetValue::Float32(_) => 4,
26
+ ParquetValue::Float64(_) => 8,
27
+ ParquetValue::String(s) => s.len() + 24, // String overhead
28
+ ParquetValue::Bytes(b) => b.len() + 24, // Vec overhead
29
+ ParquetValue::Date32(_) => 4,
30
+ ParquetValue::Date64(_) => 8,
31
+ ParquetValue::Decimal128(_, _) => 16 + 1, // value + scale
32
+ ParquetValue::Decimal256(_, _) => 32 + 1, // approx size for BigInt + scale
33
+ ParquetValue::TimestampSecond(_, tz) => 8 + tz.as_ref().map_or(0, |s| s.len() + 24),
34
+ ParquetValue::TimestampMillis(_, tz) => 8 + tz.as_ref().map_or(0, |s| s.len() + 24),
35
+ ParquetValue::TimestampMicros(_, tz) => 8 + tz.as_ref().map_or(0, |s| s.len() + 24),
36
+ ParquetValue::TimestampNanos(_, tz) => 8 + tz.as_ref().map_or(0, |s| s.len() + 24),
37
+ ParquetValue::TimeMillis(_) => 4,
38
+ ParquetValue::TimeMicros(_) => 8,
39
+ ParquetValue::List(items) => {
40
+ 24 + items.iter().map(estimate_parquet_value_size).sum::<usize>()
41
+ }
42
+ ParquetValue::Map(entries) => {
43
+ 48 + entries
44
+ .iter()
45
+ .map(|(k, v)| estimate_parquet_value_size(k) + estimate_parquet_value_size(v))
46
+ .sum::<usize>()
47
+ }
48
+ ParquetValue::Record(fields) => {
49
+ 48 + fields
50
+ .iter()
51
+ .map(|(k, v)| k.len() + 24 + estimate_parquet_value_size(v))
52
+ .sum::<usize>()
53
+ }
54
+ }
55
+ }
56
+
57
+ /// Estimate the memory size of a row
58
+ pub fn estimate_row_size(row: &[ParquetValue]) -> usize {
59
+ row.iter().map(estimate_parquet_value_size).sum()
60
+ }
61
+
62
+ /// Parse compression type from string
63
+ pub fn parse_compression(compression: Option<String>) -> Result<Compression, MagnusError> {
64
+ match compression.map(|s| s.to_lowercase()).as_deref() {
65
+ Some("none") | Some("uncompressed") => Ok(Compression::UNCOMPRESSED),
66
+ Some("snappy") => Ok(Compression::SNAPPY),
67
+ Some("gzip") => Ok(Compression::GZIP(parquet::basic::GzipLevel::default())),
68
+ Some("lz4") => Ok(Compression::LZ4),
69
+ Some("zstd") => Ok(Compression::ZSTD(parquet::basic::ZstdLevel::default())),
70
+ Some("brotli") => Ok(Compression::BROTLI(parquet::basic::BrotliLevel::default())),
71
+ None => Ok(Compression::SNAPPY), // Default to SNAPPY
72
+ Some(other) => Err(MagnusError::new(
73
+ magnus::exception::arg_error(),
74
+ format!("Invalid compression option: '{}'. Valid options are: none, snappy, gzip, lz4, zstd, brotli", other),
75
+ )),
76
+ }
77
+ }
78
+
79
+ /// Parse arguments for Parquet writing
80
+ pub fn parse_parquet_write_args(
81
+ _ruby: &Ruby,
82
+ args: &[Value],
83
+ ) -> Result<ParquetWriteArgs, MagnusError> {
84
+ let parsed_args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
85
+ let (read_from,) = parsed_args.required;
86
+
87
+ let kwargs = get_kwargs::<
88
+ _,
89
+ (Value, Value),
90
+ (
91
+ Option<Option<usize>>,
92
+ Option<Option<usize>>,
93
+ Option<Option<String>>,
94
+ Option<Option<usize>>,
95
+ Option<Option<Value>>,
96
+ Option<Option<bool>>,
97
+ ),
98
+ (),
99
+ >(
100
+ parsed_args.keywords,
101
+ &["schema", "write_to"],
102
+ &[
103
+ "batch_size",
104
+ "flush_threshold",
105
+ "compression",
106
+ "sample_size",
107
+ "logger",
108
+ "string_cache",
109
+ ],
110
+ )?;
111
+
112
+ Ok(ParquetWriteArgs {
113
+ read_from,
114
+ write_to: kwargs.required.1,
115
+ schema_value: kwargs.required.0,
116
+ batch_size: kwargs.optional.0.flatten(),
117
+ flush_threshold: kwargs.optional.1.flatten(),
118
+ compression: kwargs.optional.2.flatten(),
119
+ sample_size: kwargs.optional.3.flatten(),
120
+ logger: kwargs.optional.4.flatten(),
121
+ string_cache: kwargs.optional.5.flatten(),
122
+ })
123
+ }
124
+
125
+ /// Handle block or enumerator creation
126
+ pub fn handle_block_or_enum<F, T>(
127
+ block_given: bool,
128
+ create_enum: F,
129
+ ) -> Result<Option<T>, MagnusError>
130
+ where
131
+ F: FnOnce() -> Result<T, MagnusError>,
132
+ {
133
+ if !block_given {
134
+ let enum_value = create_enum()?;
135
+ return Ok(Some(enum_value));
136
+ }
137
+ Ok(None)
138
+ }
139
+
140
+ /// Create a row enumerator
141
+ pub fn create_row_enumerator(args: RowEnumeratorArgs) -> Result<magnus::Enumerator, MagnusError> {
142
+ let kwargs = RHash::new();
143
+ kwargs.aset(
144
+ Symbol::new("result_type"),
145
+ Symbol::new(args.result_type.to_string()),
146
+ )?;
147
+ if let Some(columns) = args.columns {
148
+ kwargs.aset(Symbol::new("columns"), RArray::from_vec(columns))?;
149
+ }
150
+ if args.strict {
151
+ kwargs.aset(Symbol::new("strict"), true)?;
152
+ }
153
+ if let Some(logger) = args.logger {
154
+ kwargs.aset(Symbol::new("logger"), logger)?;
155
+ }
156
+ Ok(args
157
+ .rb_self
158
+ .enumeratorize("each_row", (args.to_read, KwArgs(kwargs))))
159
+ }
160
+
161
+ /// Create a column enumerator
162
+ #[inline]
163
+ pub fn create_column_enumerator(
164
+ args: ColumnEnumeratorArgs,
165
+ ) -> Result<magnus::Enumerator, MagnusError> {
166
+ let kwargs = RHash::new();
167
+ kwargs.aset(
168
+ Symbol::new("result_type"),
169
+ Symbol::new(args.result_type.to_string()),
170
+ )?;
171
+ if let Some(columns) = args.columns {
172
+ kwargs.aset(Symbol::new("columns"), RArray::from_vec(columns))?;
173
+ }
174
+ if let Some(batch_size) = args.batch_size {
175
+ kwargs.aset(Symbol::new("batch_size"), batch_size)?;
176
+ }
177
+ if args.strict {
178
+ kwargs.aset(Symbol::new("strict"), true)?;
179
+ }
180
+ if let Some(logger) = args.logger {
181
+ kwargs.aset(Symbol::new("logger"), logger)?;
182
+ }
183
+ Ok(args
184
+ .rb_self
185
+ .enumeratorize("each_column", (args.to_read, KwArgs(kwargs))))
186
+ }