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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -4
- data/README.md +79 -32
- data/bin/galaaz +10 -2
- data/bin/galaaz-jruby +0 -0
- data/bin/galaaz-ruby +0 -0
- data/bin/galaaz_jruby_env.inc.sh +19 -3
- data/bin/galaaz_ruby_env.inc.sh +2 -0
- data/bin/gbookdown +0 -0
- data/blogs/R-on-Rails-Planning-Document.md +79 -112
- data/blogs/manual/manual.md +79 -32
- data/lib/R_interface/r_arrow.rb +37 -0
- data/lib/galaaz/arrow_ipc/java_arrow_backend.rb +250 -0
- data/lib/galaaz/arrow_ipc/red_arrow_backend.rb +126 -0
- data/lib/galaaz/arrow_ipc.rb +167 -0
- data/lib/galaaz/cli.rb +531 -0
- data/lib/galaaz.rb +6 -0
- data/lib/galaaz_jruby.rb +17 -2
- data/new_bridge_specs/arrow_ipc_async_spec.rb +90 -0
- data/new_bridge_specs/arrow_ipc_export_async_spec.rb +68 -0
- data/r_requires/arrow.txt +3 -0
- data/r_requires/knit-extras.txt +4 -0
- data/r_requires/knit.txt +4 -0
- data/specs/arrow_ipc_export_spec.rb +62 -0
- data/specs/arrow_ipc_handoff_spec.rb +97 -0
- data/version.rb +1 -1
- metadata +13 -2
data/blogs/manual/manual.md
CHANGED
|
@@ -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
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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
|
|
446
|
-
|
|
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.
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
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
|
|
4054
|
-
**dplyr** group summaries match a Ruby reference calculation. The same logic appears
|
|
4055
|
-
**smaller scale** so this manual can knit quickly; increase `thread_count` and
|
|
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
|
|
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
|
|
4264
|
-
|
|
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.
|
data/lib/R_interface/r_arrow.rb
CHANGED
|
@@ -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
|