galaaz 2.1.0 → 2.1.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.
@@ -59,9 +59,11 @@ CRuby when you prefer MRI. R remains the same **GNU R** you use interactively—
59
59
  compiled extensions and Bioconductor. Earlier GraalVM / TruffleRuby / FastR experiments
60
60
  are no longer the focus.
61
61
 
62
- The bridge handles **communication and typing** between the two worlds; large tables can
63
- also flow through **Apache Arrow** on the R side when you use the optional helpers described
64
- later in this manual.
62
+ The bridge handles **communication and typing** between the two worlds. Large tables can use
63
+ **Apache Arrow** in two shipped modes (described later): **Stage A** copies Ruby batches into an
64
+ R-side Arrow table (`R::Arrow.from_ruby_batches`); **Stage B** writes an Arrow IPC file and only
65
+ the **path** crosses NewBridge (`Galaaz::ArrowIpc` + `R::Arrow.open_ipc` / `write_ipc`). Shared-heap
66
+ zero-copy is **Stage C** and is not shipped.
65
67
 
66
68
  ## R-on-Rails: the one-person app for R scientists
67
69
 
@@ -340,7 +342,7 @@ The supported install is **`gem install` + compile the gatekeeper**. You do not
340
342
  make -C "${gem_dir}/ext/new_bridge" all
341
343
  ```
342
344
 
343
- 5. Ensure **`R`** starts GNU R and can install packages (network access to CRAN when you first call `R.install_and_loads`). For **Apache Arrow** on Java 9+, pass `-J--add-opens=java.base/java.nio=ALL-UNNAMED` to JRuby (from a checkout, `bin/galaaz-jruby` does this; on CRuby this flag is not needed).
345
+ 5. Ensure **`R`** starts GNU R and can install packages (network access to CRAN when you first call `R.install_and_loads`). For **Apache Arrow** on **JRuby** (Java 9+), the child JVM needs `--add-opens=java.base/java.nio=ALL-UNNAMED` via **`JAVA_OPTS`** (from a checkout, `bin/galaaz-jruby` and `mise.toml` set this; a leading `jruby -J... -S bundle exec` does **not** pass `-J` to rspec). On **CRuby**, install Apache Arrow GLib (`libarrow-glib-dev` from the [Apache Arrow APT](https://arrow.apache.org/install/) repo) and `gem install red-arrow` matching `pkg-config --modversion arrow-glib`. Do not install the unrelated Rubygems package named `arrow`.
344
346
 
345
347
  For **gKnit**, **knitr**, **rmarkdown**, and LaTeX (PDF output), install the corresponding R packages, **Pandoc**, and a TeX distribution if you need PDF; the repository includes helpers such as **`bin/install-tinytex`** where appropriate.
346
348
 
@@ -442,13 +444,15 @@ A practical pattern is:
442
444
 
443
445
  1. Use threads (or a connection pool) to read from **multiple databases or shards** in parallel.
444
446
  2. Merge the rows in Ruby under a `Mutex` if you collect into one structure.
445
- 3. Hand the merged table to R **once** (for example with `R::Arrow.from_ruby_batches` and dplyr,
446
- or by building a data frame) so heavy statistics run in R with fewer bridge round-trips.
447
+ 3. Hand the merged table to R **once**: **`R::Arrow.from_ruby_batches`** (Stage A: copy into R) or
448
+ **`Galaaz::ArrowIpc.write` / `write_batches`** then **`R::Arrow.open_ipc`** (Stage B: IPC file;
449
+ only the path crosses the bridge). Then run dplyr in R.
447
450
 
448
451
  A runnable sketch lives in
449
452
  `examples/multithread_shards_to_r/shards_to_r.rb` (simulated shard queries; swap in your DB
450
453
  driver). For concurrency tests on the bridge itself, see `specs/bridge_concurrent_spec.rb` and
451
- `specs/arrow_from_ruby_batches_spec.rb`.
454
+ `specs/arrow_from_ruby_batches_spec.rb`. Stage B IPC tests: `specs/arrow_ipc_handoff_spec.rb`,
455
+ `specs/arrow_ipc_export_spec.rb`.
452
456
 
453
457
  ## Long-running R calls and a completion block
454
458
 
@@ -4025,35 +4029,77 @@ ans = flights[:all, E.list(R[:arr_delay], R[:dep_delay])]
4025
4029
  # Apache Arrow
4026
4030
 
4027
4031
  [Apache Arrow](https://arrow.apache.org/) is a **columnar** in-memory format used heavily in R
4028
- and Python for analytics. In Galaaz, **Ruby does not hold an Arrow C++ table itself**; instead you
4029
- build ordinary Ruby structures (arrays of row hashes), and **`R::Arrow.from_ruby_batches`** creates
4030
- a real **Arrow `Table` inside GNU R**. From there you use R’s **`arrow`** and **`dplyr`** packages
4031
- as usual: **`group_by`** on the Arrow table, **`summarise`** for aggregates, then **`collect()`** to
4032
- materialize a tibble when you need in-memory R rows.
4033
-
4034
- That pattern matches production use: **JRuby threads** (or sequential code) assemble many rows in
4035
- Ruby; you pay **one** bridge-heavy handoff to R; **dplyr** runs vectorised work on the Arrow table
4036
- in R.
4032
+ and Python for analytics. GNU R still runs in a **separate process**. Ruby does **not** hold a
4033
+ shared Arrow C++ table with R. Stages:
4034
+
4035
+ 1. **Stage A (copy over the bridge):** Ruby row hashes **`R::Arrow.from_ruby_batches`** builds
4036
+ an Arrow `Table` **inside GNU R**. You get a **proxy**.
4037
+ 2. **Stage B1 (Ruby → R IPC file):** **`Galaaz::ArrowIpc.write`** / **`write_batches`** writes an
4038
+ Arrow IPC file (prefer **`/dev/shm`**); **`R::Arrow.open_ipc(path)`** opens it in R. Only the
4039
+ **path** crosses NewBridge. This is **mmap/IPC file handoff**, not a shared heap.
4040
+ 3. **Stage B2 (R → Ruby IPC file):** **`R::Arrow.write_ipc(obj)`** writes uncompressed IPC; Ruby
4041
+ reads with **`Galaaz::ArrowIpc.read`** (column hash) or **`read_batches`** (row hashes). Call
4042
+ **`Galaaz::ArrowIpc.release(path)`** when finished.
4043
+ 4. **Stage C (not shipped):** named shared-memory bus. Do not claim 0 ms shared RAM until then.
4044
+ See **`Documentation/ROADMAP_ARROW_RUBY_R.md`**.
4045
+
4046
+ After ingest, use R’s **`arrow`** / **`dplyr`** on the proxy (`group_by`, `summarise`, `collect`)
4047
+ and unbox only KPIs you need in Ruby.
4048
+
4049
+ **Optional Ruby backends for Stage B**
4050
+
4051
+ * **CRuby:** Apache **red-arrow** — `gem install red-arrow` pinned to the same major as
4052
+ `pkg-config --modversion arrow-glib`, plus system **Arrow GLib** (`libarrow-glib-dev` from the
4053
+ [Apache Arrow APT](https://arrow.apache.org/install/) repo). Do **not** install the unrelated
4054
+ legacy Rubygems package named `arrow`. `bundle exec` still sees a user-installed `red-arrow`
4055
+ via Galaaz’s load-path helper.
4056
+ * **JRuby:** Apache Arrow **Java** JARs — **`GALAAZ_ARROW_JARS`**, `~/arrow_jars`, or
4057
+ `jar-dependencies`. Export **`JAVA_OPTS=--add-opens=java.base/java.nio=ALL-UNNAMED`** on the
4058
+ **child** JVM (`bin/galaaz-jruby`, `mise.toml`). `jruby -J... -S bundle exec rspec` does **not**
4059
+ pass `-J` to rspec.
4060
+
4061
+ **R packages:** **`arrow`** and **`dplyr`**. B2 writes IPC with **`compression: 'uncompressed'`**
4062
+ so JRuby Arrow Java can read without extra compression JARs.
4063
+
4064
+ **Tests:** `specs/arrow_from_ruby_batches_spec.rb` (A);
4065
+ `specs/arrow_ipc_handoff_spec.rb`, `specs/arrow_ipc_export_spec.rb` (B, sync);
4066
+ `new_bridge_specs/arrow_ipc_async_spec.rb`, `new_bridge_specs/arrow_ipc_export_async_spec.rb` (B, async).
4067
+
4068
+ ## `R::Arrow` and `Galaaz::ArrowIpc`
4069
+
4070
+ * **`R::Arrow.from_ruby_batches`** — Stage A ingest.
4071
+ * **`R::Arrow.open_ipc(path)`** — Stage B1: IPC file → R Table proxy.
4072
+ * **`R::Arrow.write_ipc(obj, path = nil)`** — Stage B2: R Table/tibble → IPC path (scratch if omitted).
4073
+ * **`Galaaz::ArrowIpc.write` / `write_batches` / `read` / `read_batches` / `allocate_path` / `release` / `available?`**
4074
+ * **`R::Arrow.table_from(df)`** — wrap an R `data.frame` / tibble as an Arrow table.
4075
+ * **`R::Arrow.read_feather` / `write_feather`**, **`read_parquet`**, **`dataset(path)`** — file and
4076
+ dataset IO on paths visible to R.
4037
4077
 
4038
- **Prerequisites:** install R packages **`arrow`** and **`dplyr`**. Run scripts with
4039
- **`bin/galaaz-jruby`** (or the same JVM flags as in **`docs/testing.md`**) so the Arrow JNI stack is
4040
- available.
4078
+ ## Example: Stage B round-trip (IPC file)
4041
4079
 
4042
- ## Other `R::Arrow` helpers
4080
+ Requires `Galaaz::ArrowIpc.available?` (red-arrow or Arrow JARs) and R **`arrow`**. Not knitted
4081
+ below so a machine without the optional backend still builds this manual.
4043
4082
 
4044
- The Ruby module **`R::Arrow`** (see `lib/R_interface/r_arrow.rb`) also includes:
4083
+ ```ruby
4084
+ path = Galaaz::ArrowIpc.write(id: [1, 2, 3], grp: %w[a a b], value: [1.0, 2.0, 3.5])
4085
+ tbl = R::Arrow.open_ipc(path)
4086
+ Galaaz::ArrowIpc.release(path)
4045
4087
 
4046
- * **`R::Arrow.table_from(df)`** — wrap an R `data.frame` / tibble as an Arrow table.
4047
- * **`R::Arrow.read_feather` / `write_feather`**, **`read_parquet`**, **`dataset(path)`** — file and
4048
- dataset IO on paths visible to R.
4088
+ summed = R.dplyr___summarise(R.dplyr___group_by(tbl, :grp), total: E.sum(:value))
4089
+ out_path = R::Arrow.write_ipc(summed)
4090
+ rows = Galaaz::ArrowIpc.read_batches(out_path)
4091
+ Galaaz::ArrowIpc.release(out_path)
4092
+ # rows => [{:grp=>"a", :total=>3.0}, {:grp=>"b", :total=>3.5}] (illustrative)
4093
+ ```
4049
4094
 
4050
4095
  ## Example: many Ruby rows → Arrow in R → grouped statistics
4051
4096
 
4052
4097
  The repository test **`slow-specs/arrow_large_pipeline_spec.rb`** builds **200k rows** in parallel
4053
- (eight threads × 25,000 rows), pushes them through **`R::Arrow.from_ruby_batches`**, then checks that
4054
- **dplyr** group summaries match a Ruby reference calculation. The same logic appears below at a
4055
- **smaller scale** so this manual can knit quickly; increase `thread_count` and `rows_per_thread`
4056
- when experimenting locally.
4098
+ (eight threads × 25,000 rows), pushes them through **`R::Arrow.from_ruby_batches`** (Stage A), then
4099
+ checks that **dplyr** group summaries match a Ruby reference calculation. The same logic appears
4100
+ below at a **smaller scale** so this manual can knit quickly; increase `thread_count` and
4101
+ `rows_per_thread` when experimenting locally. For the same ingest **without** copying every cell
4102
+ over NewBridge, use Stage B (`write_batches` + `open_ipc`) instead of `from_ruby_batches`.
4057
4103
 
4058
4104
 
4059
4105
  ``` ruby
@@ -4119,10 +4165,10 @@ end
4119
4165
  ```
4120
4166
 
4121
4167
  **What to notice:** (1) Ruby only sees **`Hash`** rows and Ruby **`Thread`** objects; (2) a single
4122
- **`from_ruby_batches`** call creates the Arrow table in R; (3) **`dplyr___group_by`** /
4168
+ **`from_ruby_batches`** call **copies** those columns into an Arrow table in R; (3) **`dplyr___group_by`** /
4123
4169
  **`dplyr___summarise`** / **`dplyr___collect`** mirror **`dplyr::group_by`** /
4124
4170
  **`dplyr::summarise`** / **`dplyr::collect`** on an Arrow-backed table. For a lighter test, see
4125
- **`specs/arrow_from_ruby_batches_spec.rb`**; for the full-size benchmark, run
4171
+ **`specs/arrow_from_ruby_batches_spec.rb`**; for the full-size Stage A benchmark, run
4126
4172
  **`bin/run_slow_rspec slow-specs/arrow_large_pipeline_spec.rb`**.
4127
4173
 
4128
4174
  # Bioconductor and DESeq2
@@ -4260,8 +4306,9 @@ Practical tips:
4260
4306
  glue.
4261
4307
  * **Reuse one process**: running many short scripts cold-starts Ruby, the JVM, and R each time;
4262
4308
  a long-lived process or repeated calls in one run amortize setup (see benchmarks below).
4263
- * **Batch data**: merge shards in Ruby, then call **`R::Arrow.from_ruby_batches`** (or build one
4264
- data frame) instead of millions of tiny R calls.
4309
+ * **Batch data**: merge shards in Ruby, then **`R::Arrow.from_ruby_batches`** (Stage A) or
4310
+ **`Galaaz::ArrowIpc`** + **`R::Arrow.open_ipc`** (Stage B) instead of millions of tiny R calls.
4311
+ When Ruby needs a bulky result table back, **`R::Arrow.write_ipc`** + **`Galaaz::ArrowIpc.read_batches`**.
4265
4312
 
4266
4313
  For measured discussion (including DESeq2-style workloads and warm comparisons), see
4267
4314
  **`docs/performance.md`** and **`docs/deseq2_airway_benchmark.md`** in the Galaaz repository.
@@ -63,6 +63,43 @@ module R
63
63
  R.arrow___as_arrow_table(r_df)
64
64
  end
65
65
 
66
+ # Open an Arrow IPC file (written by Galaaz::ArrowIpc or compatible) as an
67
+ # R-side Arrow Table. Materializes the table in R so the Ruby process may
68
+ # unlink the path immediately after this call returns (Stage B1 lifetime).
69
+ #
70
+ # This is IPC/mmap file handoff — not zero-copy shared heap with Ruby.
71
+ #
72
+ # @param path [String] filesystem path visible to the R process
73
+ # @return [R::Object] Arrow Table proxy
74
+ def self.open_ipc(path)
75
+ path = File.expand_path(path.to_s)
76
+ ok = R::Support.eval("requireNamespace('arrow', quietly=TRUE)")
77
+ unless ok == true
78
+ raise LoadError, "R package 'arrow' is required for R::Arrow.open_ipc"
79
+ end
80
+
81
+ R.arrow___read_ipc_file(path, as_data_frame: false)
82
+ end
83
+
84
+ # Write an R Arrow Table / data.frame to an Arrow IPC file Ruby can read
85
+ # with Galaaz::ArrowIpc.read (Stage B2). Only the path should cross NewBridge.
86
+ #
87
+ # @param r_obj [R::Object] Table, RecordBatch, or data.frame/tibble in R
88
+ # @param path [String, nil] destination; default a new scratch path
89
+ # @return [String] absolute path (fsync'd from Ruby after R returns)
90
+ def self.write_ipc(r_obj, path = nil)
91
+ ok = R::Support.eval("requireNamespace('arrow', quietly=TRUE)")
92
+ unless ok == true
93
+ raise LoadError, "R package 'arrow' is required for R::Arrow.write_ipc"
94
+ end
95
+
96
+ path = path.nil? || path.to_s.empty? ? Galaaz::ArrowIpc.allocate_path : File.expand_path(path.to_s)
97
+ # uncompressed so JRuby Arrow Java can read without arrow-compression JARs
98
+ R.arrow___write_ipc_file(r_obj, path, compression: 'uncompressed')
99
+ File.open(path, 'rb') { |f| f.fsync }
100
+ path
101
+ end
102
+
66
103
  # Open a Parquet/Feather directory or file as an Arrow Dataset
67
104
  # using arrow::open_dataset().
68
105
  #
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##########################################################################################
4
+ # JRuby Arrow IPC writer via Apache Arrow Java.
5
+ #
6
+ # JAR loading order:
7
+ # 1. GALAAZ_ARROW_JARS directory (all *.jar)
8
+ # 2. ~/arrow_jars if present
9
+ # 3. jar-dependencies require_jar for pinned Arrow 18.1.0 artifacts
10
+ ##########################################################################################
11
+
12
+ module Galaaz
13
+ module ArrowIpc
14
+ module JavaArrowBackend
15
+ ARROW_JAVA_VERSION = '18.1.0'
16
+
17
+ module_function
18
+
19
+ def available?
20
+ return false unless RUBY_ENGINE == 'jruby'
21
+
22
+ load_jars!
23
+ true
24
+ rescue LoadError, StandardError
25
+ false
26
+ end
27
+
28
+ def write_columns(columns_hash, path)
29
+ load_jars!
30
+ lengths = columns_hash.values.map(&:length).uniq
31
+ raise ArgumentError, "all columns must have the same length (got #{lengths.inspect})" if lengths.size != 1
32
+
33
+ n = lengths.first
34
+ alloc = org.apache.arrow.memory.RootAllocator.new(java.lang.Long::MAX_VALUE)
35
+ fields = []
36
+ kinds = {}
37
+
38
+ columns_hash.each do |name, values|
39
+ kind = infer_kind(values)
40
+ kinds[name] = kind
41
+ fields << org.apache.arrow.vector.types.pojo.Field.nullable(name, arrow_type(kind))
42
+ end
43
+
44
+ schema = org.apache.arrow.vector.types.pojo.Schema.new(fields)
45
+ root = org.apache.arrow.vector.VectorSchemaRoot.create(schema, alloc)
46
+
47
+ begin
48
+ columns_hash.each do |name, values|
49
+ fill_vector(root.getVector(name), kinds[name], values, n)
50
+ end
51
+ root.setRowCount(n)
52
+
53
+ fos = java.io.FileOutputStream.new(path)
54
+ begin
55
+ channel = java.nio.channels.Channels.newChannel(fos)
56
+ writer = org.apache.arrow.vector.ipc.ArrowFileWriter.new(root, nil, channel)
57
+ begin
58
+ writer.start
59
+ writer.writeBatch
60
+ writer.end
61
+ ensure
62
+ writer.close
63
+ end
64
+ ensure
65
+ fos.close
66
+ end
67
+ ensure
68
+ root.close
69
+ alloc.close
70
+ end
71
+ end
72
+
73
+ def read_columns(path)
74
+ load_jars!
75
+ alloc = org.apache.arrow.memory.RootAllocator.new(java.lang.Long::MAX_VALUE)
76
+ fis = java.io.FileInputStream.new(path)
77
+ columns = nil
78
+ begin
79
+ reader = org.apache.arrow.vector.ipc.ArrowFileReader.new(fis.getChannel, alloc)
80
+ begin
81
+ loop do
82
+ break unless reader.loadNextBatch
83
+
84
+ root = reader.getVectorSchemaRoot
85
+ columns ||= root.getSchema.getFields.map { |f| f.getName }.each_with_object({}) { |n, h| h[n] = [] }
86
+ n = root.getRowCount
87
+ root.getFieldVectors.each do |vec|
88
+ name = vec.getName
89
+ n.times { |i| columns[name] << java_value(vec, i) }
90
+ end
91
+ end
92
+ ensure
93
+ reader.close
94
+ end
95
+ ensure
96
+ fis.close
97
+ alloc.close
98
+ end
99
+ raise ArgumentError, "no record batches in #{path}" if columns.nil?
100
+
101
+ columns
102
+ end
103
+
104
+ def load_jars!
105
+ return if defined?(@jars_loaded) && @jars_loaded
106
+
107
+ unless RUBY_ENGINE == 'jruby'
108
+ raise LoadError, 'Galaaz::ArrowIpc::JavaArrowBackend requires JRuby'
109
+ end
110
+
111
+ loaded = load_jars_from_dir(ENV['GALAAZ_ARROW_JARS'])
112
+ loaded ||= load_jars_from_dir(File.expand_path('~/arrow_jars'))
113
+ loaded ||= load_jars_via_jar_dependencies
114
+
115
+ unless loaded
116
+ raise LoadError,
117
+ 'Apache Arrow Java JARs not found. Set GALAAZ_ARROW_JARS to a directory of ' \
118
+ "Arrow #{ARROW_JAVA_VERSION} JARs, place them in ~/arrow_jars, or install " \
119
+ 'jar-dependencies so require_jar can resolve them.'
120
+ end
121
+
122
+ @jars_loaded = true
123
+ end
124
+ private_class_method :load_jars!
125
+
126
+ def load_jars_from_dir(dir)
127
+ return false if dir.nil? || dir.empty?
128
+ return false unless File.directory?(dir)
129
+
130
+ jars = Dir[File.join(dir, '*.jar')]
131
+ return false if jars.empty?
132
+
133
+ jars.each { |jar| require jar }
134
+ true
135
+ end
136
+ private_class_method :load_jars_from_dir
137
+
138
+ def load_jars_via_jar_dependencies
139
+ require 'jar-dependencies'
140
+ require_jar 'org.apache.arrow', 'arrow-format', ARROW_JAVA_VERSION
141
+ require_jar 'org.apache.arrow', 'arrow-memory-core', ARROW_JAVA_VERSION
142
+ require_jar 'org.apache.arrow', 'arrow-memory-netty', ARROW_JAVA_VERSION
143
+ require_jar 'org.apache.arrow', 'arrow-memory-unsafe', ARROW_JAVA_VERSION
144
+ require_jar 'org.apache.arrow', 'arrow-vector', ARROW_JAVA_VERSION
145
+ true
146
+ rescue LoadError
147
+ false
148
+ end
149
+ private_class_method :load_jars_via_jar_dependencies
150
+
151
+ def arrow_type(kind)
152
+ case kind
153
+ when :float64
154
+ org.apache.arrow.vector.types.pojo.ArrowType::FloatingPoint.new(
155
+ org.apache.arrow.vector.types.FloatingPointPrecision::DOUBLE
156
+ )
157
+ when :int32
158
+ org.apache.arrow.vector.types.pojo.ArrowType::Int.new(32, true)
159
+ when :utf8
160
+ org.apache.arrow.vector.types.pojo.ArrowType::Utf8.new
161
+ else
162
+ raise ArgumentError, "unsupported kind #{kind.inspect}"
163
+ end
164
+ end
165
+ private_class_method :arrow_type
166
+
167
+ def fill_vector(vector, kind, values, n)
168
+ vector.allocateNew
169
+ case kind
170
+ when :float64
171
+ values.each_with_index do |v, i|
172
+ if v.nil?
173
+ vector.setNull(i)
174
+ else
175
+ vector.setSafe(i, v.to_f)
176
+ end
177
+ end
178
+ when :int32
179
+ values.each_with_index do |v, i|
180
+ if v.nil?
181
+ vector.setNull(i)
182
+ else
183
+ vector.setSafe(i, Integer(v))
184
+ end
185
+ end
186
+ when :utf8
187
+ values.each_with_index do |v, i|
188
+ if v.nil?
189
+ vector.setNull(i)
190
+ else
191
+ bytes = v.to_s.to_java_bytes
192
+ vector.setSafe(i, bytes)
193
+ end
194
+ end
195
+ end
196
+ vector.setValueCount(n)
197
+ end
198
+ private_class_method :fill_vector
199
+
200
+ def infer_kind(values)
201
+ sample = values.find { |v| !v.nil? }
202
+ return :float64 if sample.nil?
203
+
204
+ case sample
205
+ when Float
206
+ :float64
207
+ when Integer
208
+ values.any? { |v| v.is_a?(Float) } ? :float64 : :int32
209
+ when String, Symbol
210
+ :utf8
211
+ when TrueClass, FalseClass
212
+ raise ArgumentError, 'boolean columns are not supported in B1 (use int32/float64/utf8)'
213
+ else
214
+ if sample.is_a?(Numeric)
215
+ sample.is_a?(Integer) ? :int32 : :float64
216
+ else
217
+ :utf8
218
+ end
219
+ end
220
+ end
221
+ private_class_method :infer_kind
222
+
223
+ def java_value(vector, i)
224
+ return nil if vector.isNull(i)
225
+
226
+ minor = vector.getMinorType.toString
227
+ case minor
228
+ when 'FLOAT8', 'FLOAT4'
229
+ vector.get(i).to_f
230
+ when 'INT', 'SMALLINT', 'TINYINT', 'UINT1', 'UINT2', 'UINT4'
231
+ vector.get(i).to_i
232
+ when 'BIGINT', 'UINT8'
233
+ vector.get(i).to_i
234
+ when 'VARCHAR', 'VARBINARY'
235
+ bytes = vector.get(i)
236
+ bytes.nil? ? nil : String.from_java_bytes(bytes)
237
+ else
238
+ obj = vector.respond_to?(:getObject) ? vector.getObject(i) : vector.get(i)
239
+ return obj if obj.nil?
240
+ return obj.to_s if obj.is_a?(String) || obj.java_kind_of?(java.lang.CharSequence)
241
+ return obj.to_f if obj.is_a?(Float) || obj.java_kind_of?(java.lang.Double) || obj.java_kind_of?(java.lang.Float)
242
+ return obj.to_i if obj.is_a?(Integer) || obj.java_kind_of?(java.lang.Number)
243
+
244
+ obj.to_s
245
+ end
246
+ end
247
+ private_class_method :java_value
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rbconfig'
4
+
5
+ ##########################################################################################
6
+ # CRuby Arrow IPC writer via red-arrow (gem "arrow").
7
+ ##########################################################################################
8
+
9
+ module Galaaz
10
+ module ArrowIpc
11
+ module RedArrowBackend
12
+ module_function
13
+
14
+ def available?
15
+ load!
16
+ true
17
+ rescue LoadError, StandardError
18
+ false
19
+ end
20
+
21
+ def write_columns(columns_hash, path)
22
+ load!
23
+ lengths = columns_hash.values.map(&:length).uniq
24
+ raise ArgumentError, "all columns must have the same length (got #{lengths.inspect})" if lengths.size != 1
25
+
26
+ table_hash = {}
27
+ columns_hash.each do |name, values|
28
+ table_hash[name] = build_array(name, values)
29
+ end
30
+ table = ::Arrow::Table.new(table_hash)
31
+ table.save(path)
32
+ end
33
+
34
+ def read_columns(path)
35
+ load!
36
+ table = ::Arrow::Table.load(path)
37
+ table.schema.fields.each_with_object({}) do |field, out|
38
+ col = table[field.name]
39
+ values = col.respond_to?(:to_a) ? col.to_a : col.data.to_a
40
+ out[field.name] = values
41
+ end
42
+ end
43
+
44
+ def load!
45
+ return if defined?(@loaded) && @loaded
46
+
47
+ begin
48
+ require 'arrow'
49
+ rescue LoadError
50
+ prepend_user_red_arrow_load_path!
51
+ begin
52
+ require 'arrow'
53
+ rescue LoadError => e
54
+ raise LoadError,
55
+ "Galaaz::ArrowIpc on CRuby requires the red-arrow gem " \
56
+ "(gem install red-arrow, matching pkg-config arrow-glib). #{e.message}"
57
+ end
58
+ end
59
+ @loaded = true
60
+ end
61
+ private_class_method :load!
62
+
63
+ # bundle exec only exposes Gemfile gems; red-arrow is optional and often
64
+ # user-installed. Put its lib + native extension on $LOAD_PATH.
65
+ def prepend_user_red_arrow_load_path!
66
+ names = '{red-arrow,gobject-introspection,glib2,gio2,native-package-installer,pkg-config,extpp}'
67
+ roots = []
68
+ roots << Gem.user_dir if Gem.respond_to?(:user_dir)
69
+ rubylibdir = RbConfig::CONFIG['rubylibdir']
70
+ if rubylibdir
71
+ roots << File.join(File.dirname(rubylibdir), 'gems', RbConfig::CONFIG['ruby_version'])
72
+ end
73
+ roots.concat(Array(Gem.path))
74
+ roots.compact.uniq.each do |root|
75
+ next unless File.directory?(root)
76
+
77
+ Dir.glob(File.join(root, 'gems', "#{names}-*", 'lib')).sort.each do |lib|
78
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
79
+ end
80
+ Dir.glob(File.join(root, 'extensions', '**', "#{names}-*")).select { |p| File.directory?(p) }.sort.each do |ext|
81
+ $LOAD_PATH.unshift(ext) unless $LOAD_PATH.include?(ext)
82
+ end
83
+ end
84
+ end
85
+ private_class_method :prepend_user_red_arrow_load_path!
86
+
87
+ def build_array(name, values)
88
+ kind = infer_kind(values)
89
+ case kind
90
+ when :float64
91
+ ::Arrow::DoubleArray.new(values.map { |v| v.nil? ? nil : v.to_f })
92
+ when :int32
93
+ ::Arrow::Int32Array.new(values.map { |v| v.nil? ? nil : Integer(v) })
94
+ when :utf8
95
+ ::Arrow::StringArray.new(values.map { |v| v.nil? ? nil : v.to_s })
96
+ else
97
+ raise ArgumentError, "unsupported column type for #{name.inspect}"
98
+ end
99
+ end
100
+ private_class_method :build_array
101
+
102
+ def infer_kind(values)
103
+ sample = values.find { |v| !v.nil? }
104
+ return :float64 if sample.nil?
105
+
106
+ case sample
107
+ when Float
108
+ :float64
109
+ when Integer
110
+ values.any? { |v| v.is_a?(Float) } ? :float64 : :int32
111
+ when String, Symbol
112
+ :utf8
113
+ when TrueClass, FalseClass
114
+ raise ArgumentError, 'boolean columns are not supported in B1 (use int32/float64/utf8)'
115
+ else
116
+ if sample.respond_to?(:to_f) && sample.is_a?(Numeric)
117
+ sample.is_a?(Integer) ? :int32 : :float64
118
+ else
119
+ :utf8
120
+ end
121
+ end
122
+ end
123
+ private_class_method :infer_kind
124
+ end
125
+ end
126
+ end