galaaz 2.1.0 → 2.1.2

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.
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Async synchronization for Stage B2 R→Ruby Arrow IPC export.
4
+ # Run: bin/run_all_rspec new_bridge_specs/arrow_ipc_export_async_spec.rb
5
+
6
+ require 'thread'
7
+ require 'galaaz'
8
+
9
+ RSpec.describe 'Arrow IPC export async (Stage B2)' do
10
+ before(:all) do
11
+ skip 'R not on PATH' unless system('command -v R >/dev/null 2>&1')
12
+ skip 'Galaaz::ArrowIpc backend not available' unless Galaaz::ArrowIpc.available?
13
+ ok = R::Support.eval("requireNamespace('arrow', quietly=TRUE)")
14
+ skip 'R package arrow is not available' unless ok == true
15
+ @paths = []
16
+ end
17
+
18
+ after(:each) do
19
+ Array(@paths).each { |p| Galaaz::ArrowIpc.release(p) }
20
+ @paths = []
21
+ end
22
+
23
+ def track(path)
24
+ @paths << path
25
+ path
26
+ end
27
+
28
+ it 'happens-before: async R write_ipc_file then Ruby read matches' do
29
+ in_path = track(Galaaz::ArrowIpc.write(n: [1, 2, 3]))
30
+ out_path = track(Galaaz::ArrowIpc.allocate_path)
31
+
32
+ done = Queue.new
33
+ R.eval_r_async(
34
+ "({ arrow::write_ipc_file(arrow::read_ipc_file(#{in_path.inspect}, as_data_frame=FALSE), #{out_path.inspect}, compression='uncompressed'); 1L })",
35
+ timeout: 30
36
+ ) { |result| done.push(result) }
37
+ r = done.pop
38
+ expect(r).to be_ok
39
+
40
+ cols = Galaaz::ArrowIpc.read(out_path)
41
+ expect(cols['n'].map(&:to_i)).to eq([1, 2, 3])
42
+ end
43
+
44
+ it 'two concurrent R exports do not cross-talk' do
45
+ in_a = track(Galaaz::ArrowIpc.write(tag: ['A']))
46
+ in_b = track(Galaaz::ArrowIpc.write(tag: ['B']))
47
+ out_a = track(Galaaz::ArrowIpc.allocate_path)
48
+ out_b = track(Galaaz::ArrowIpc.allocate_path)
49
+
50
+ done = Queue.new
51
+ R.eval_r_async(
52
+ "({ arrow::write_ipc_file(arrow::read_ipc_file(#{in_a.inspect}, as_data_frame=FALSE), #{out_a.inspect}, compression='uncompressed'); 1L })",
53
+ timeout: 30
54
+ ) { |res| done.push([:a, res]) }
55
+ R.eval_r_async(
56
+ "({ arrow::write_ipc_file(arrow::read_ipc_file(#{in_b.inspect}, as_data_frame=FALSE), #{out_b.inspect}, compression='uncompressed'); 1L })",
57
+ timeout: 30
58
+ ) { |res| done.push([:b, res]) }
59
+
60
+ 2.times do
61
+ _key, res = done.pop
62
+ expect(res).to be_ok
63
+ end
64
+
65
+ expect(Galaaz::ArrowIpc.read(out_a)['tag']).to eq(['A'])
66
+ expect(Galaaz::ArrowIpc.read(out_b)['tag']).to eq(['B'])
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ # CRAN packages for `galaaz add arrow`.
2
+ dplyr
3
+ arrow
@@ -0,0 +1,4 @@
1
+ # Best-effort CRAN extras for `galaaz add knit` (warn if missing).
2
+ ggplot2
3
+ kableExtra
4
+ RColorBrewer
@@ -0,0 +1,4 @@
1
+ # Hard CRAN set for `galaaz add knit` (must succeed).
2
+ dplyr
3
+ knitr
4
+ rmarkdown
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'galaaz'
4
+
5
+ describe 'Arrow IPC export (Stage B2)' do
6
+ before(:all) do
7
+ skip 'Galaaz::ArrowIpc backend not available' unless Galaaz::ArrowIpc.available?
8
+ ok = R::Support.eval("requireNamespace('arrow', quietly=TRUE) && requireNamespace('dplyr', quietly=TRUE)")
9
+ skip 'R packages arrow/dplyr are not available' unless ok == true
10
+ @paths = []
11
+ end
12
+
13
+ after(:each) do
14
+ Array(@paths).each { |p| Galaaz::ArrowIpc.release(p) }
15
+ @paths = []
16
+ end
17
+
18
+ def track(path)
19
+ @paths << path
20
+ path
21
+ end
22
+
23
+ it 'reads columns R wrote via write_ipc (R→Ruby)' do
24
+ in_path = track(Galaaz::ArrowIpc.write(value: [1.5, 2.5], grp: %w[a b]))
25
+ tbl = R::Arrow.open_ipc(in_path)
26
+ out_path = track(R::Arrow.write_ipc(tbl))
27
+ expect(File.exist?(out_path)).to be true
28
+
29
+ cols = Galaaz::ArrowIpc.read(out_path)
30
+ expect(cols['value']).to eq([1.5, 2.5])
31
+ expect(cols['grp']).to eq(%w[a b])
32
+
33
+ rows = Galaaz::ArrowIpc.read_batches(out_path)
34
+ expect(rows).to eq([{ value: 1.5, grp: 'a' }, { value: 2.5, grp: 'b' }])
35
+ end
36
+
37
+ it 'proves DB-style loop: Ruby ingest → R summarise → Ruby reads result table' do
38
+ in_path = track(Galaaz::ArrowIpc.write_batches([
39
+ { id: 1, grp: 'a', value: 10.0 },
40
+ { id: 2, grp: 'b', value: 20.5 },
41
+ { id: 3, grp: 'a', value: 30.0 }
42
+ ]))
43
+ tbl = R::Arrow.open_ipc(in_path)
44
+ grouped = R.dplyr___group_by(tbl, :grp)
45
+ summed = R.dplyr___summarise(grouped, total: E.sum(:value))
46
+ out_path = track(R::Arrow.write_ipc(summed))
47
+
48
+ rows = Galaaz::ArrowIpc.read_batches(out_path)
49
+ by_grp = rows.each_with_object({}) { |r, h| h[r[:grp].to_s] = r[:total].to_f }
50
+ expect(by_grp['a']).to eq(40.0)
51
+ expect(by_grp['b']).to eq(20.5)
52
+
53
+ Galaaz::ArrowIpc.release(out_path)
54
+ expect(File.exist?(out_path)).to be false
55
+ end
56
+
57
+ it 'raises when reading a missing IPC file' do
58
+ expect {
59
+ Galaaz::ArrowIpc.read('/tmp/galaaz_missing_b2.arrow')
60
+ }.to raise_error(ArgumentError, /not found/)
61
+ end
62
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'galaaz'
4
+
5
+ describe 'Arrow IPC handoff (Stage B1)' do
6
+ before(:all) do
7
+ skip 'Galaaz::ArrowIpc writer backend not available' unless Galaaz::ArrowIpc.available?
8
+ ok = R::Support.eval("requireNamespace('arrow', quietly=TRUE) && requireNamespace('dplyr', quietly=TRUE)")
9
+ skip 'R packages arrow/dplyr are not available' unless ok == true
10
+ @paths = []
11
+ end
12
+
13
+ after(:each) do
14
+ Array(@paths).each { |p| Galaaz::ArrowIpc.release(p) }
15
+ @paths = []
16
+ end
17
+
18
+ def track(path)
19
+ @paths << path
20
+ path
21
+ end
22
+
23
+ it 'round-trips a float64 column through open_ipc' do
24
+ values = [1.5, 2.5, -3.0, 0.0]
25
+ path = track(Galaaz::ArrowIpc.write(value: values))
26
+ expect(File.exist?(path)).to be true
27
+
28
+ tbl = R::Arrow.open_ipc(path)
29
+ expect(tbl.rclass.split(' ')).to include('Table')
30
+ expect((R.nrow(tbl) >> 0)).to eq(values.length)
31
+
32
+ collected = R.dplyr___collect(tbl)
33
+ col = collected[['value']]
34
+ got = (1..(col.length >> 0)).map do |i|
35
+ v = col[i]
36
+ v.respond_to?(:>>) ? (v >> 0) : v
37
+ end
38
+ expect(got).to eq(values)
39
+
40
+ Galaaz::ArrowIpc.release(path)
41
+ expect(File.exist?(path)).to be false
42
+ end
43
+
44
+ it 'round-trips multi-column id/grp/value via write_batches' do
45
+ rows = [
46
+ { id: 1, grp: 'a', value: 10.0 },
47
+ { id: 2, grp: 'b', value: 20.5 },
48
+ { id: 3, grp: 'a', value: 30.0 }
49
+ ]
50
+ path = track(Galaaz::ArrowIpc.write_batches(rows))
51
+ tbl = R::Arrow.open_ipc(path)
52
+
53
+ expect((R.nrow(tbl) >> 0)).to eq(3)
54
+ summed = R.dplyr___summarise(tbl, total: E.sum(:value))
55
+ out = R.dplyr___collect(summed)
56
+ total = out[['total']]
57
+ tot = total[1]
58
+ tot = tot.respond_to?(:>>) ? (tot >> 0) : tot
59
+ expect(tot).to eq(60.5)
60
+ end
61
+
62
+ it 'raises on missing R arrow package path for open_ipc with bad path' do
63
+ expect {
64
+ R::Arrow.open_ipc('/tmp/galaaz_missing_ipc_file_does_not_exist.arrow')
65
+ }.to raise_error(StandardError)
66
+ end
67
+
68
+ it 'raises when write is called without a usable backend message path' do
69
+ # available? already gated the suite; still check empty columns
70
+ expect { Galaaz::ArrowIpc.write({}) }.to raise_error(ArgumentError)
71
+ end
72
+
73
+ it 'optional timing smoke vs from_ruby_batches for N≈50k' do
74
+ n = 50_000
75
+ cols = {
76
+ id: (0...n).to_a,
77
+ grp: (0...n).map { |i| "g#{i % 5}" },
78
+ value: (0...n).map { |i| (i % 10) + 1.0 }
79
+ }
80
+
81
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
82
+ path = track(Galaaz::ArrowIpc.write(cols))
83
+ tbl = R::Arrow.open_ipc(path)
84
+ ipc_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000.0
85
+ expect((R.nrow(tbl) >> 0)).to eq(n)
86
+
87
+ rows = (0...n).map { |i| { id: i, grp: "g#{i % 5}", value: (i % 10) + 1.0 } }
88
+ t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
89
+ tbl2 = R::Arrow.from_ruby_batches(rows)
90
+ batch_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t1) * 1000.0
91
+ expect((R.nrow(tbl2) >> 0)).to eq(n)
92
+
93
+ # Not a flaky perf gate — only ensure both paths produce a table.
94
+ expect(ipc_ms).to be > 0
95
+ expect(batch_ms).to be > 0
96
+ end
97
+ end
data/version.rb CHANGED
@@ -1,2 +1,2 @@
1
1
  $gem_name = "galaaz"
2
- $version="2.1.0"
2
+ $version="2.1.2"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: galaaz
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Botafogo
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-09-03 00:00:00.000000000 Z
10
+ date: 2026-09-04 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: msgpack
@@ -87,11 +86,11 @@ description: |
87
86
  email: rodrigo.a.botafogo@gmail.com
88
87
  executables:
89
88
  - galaaz
90
- - gstudio
91
- - gknit
92
89
  - gbookdown
93
- - grun
90
+ - gknit
94
91
  - gknit-draft
92
+ - grun
93
+ - gstudio
95
94
  extensions: []
96
95
  extra_rdoc_files: []
97
96
  files:
@@ -337,6 +336,10 @@ files:
337
336
  - lib/R_interface/runary_operators.rb
338
337
  - lib/R_interface/rvector.rb
339
338
  - lib/galaaz.rb
339
+ - lib/galaaz/arrow_ipc.rb
340
+ - lib/galaaz/arrow_ipc/java_arrow_backend.rb
341
+ - lib/galaaz/arrow_ipc/red_arrow_backend.rb
342
+ - lib/galaaz/cli.rb
340
343
  - lib/galaaz_jruby.rb
341
344
  - lib/galaaz_ruby.rb
342
345
  - lib/gknit.rb
@@ -357,6 +360,8 @@ files:
357
360
  - lib/new_bridge/tcp_framed.rb
358
361
  - lib/util/exec_ruby.rb
359
362
  - lib/util/inline_file.rb
363
+ - new_bridge_specs/arrow_ipc_async_spec.rb
364
+ - new_bridge_specs/arrow_ipc_export_async_spec.rb
360
365
  - new_bridge_specs/benchmark_phase5_5_unboxing_spec.rb
361
366
  - new_bridge_specs/eval_r_async_spec.rb
362
367
  - new_bridge_specs/integration_phase5_1_concurrent_spec.rb
@@ -383,10 +388,15 @@ files:
383
388
  - new_bridge_specs/phase4_2_hardening_spec.rb
384
389
  - new_bridge_specs/phase4_3_r_instance_manager_spec.rb
385
390
  - new_bridge_specs/phase4_nested_callbacks_spec.rb
391
+ - r_requires/arrow.txt
386
392
  - r_requires/ggplot.rb
393
+ - r_requires/knit-extras.txt
394
+ - r_requires/knit.txt
387
395
  - r_requires/knitr.rb
388
396
  - specs/all.rb
389
397
  - specs/arrow_from_ruby_batches_spec.rb
398
+ - specs/arrow_ipc_export_spec.rb
399
+ - specs/arrow_ipc_handoff_spec.rb
390
400
  - specs/arrow_semantics_spec.rb
391
401
  - specs/bridge_concurrent_spec.rb
392
402
  - specs/bridge_nested_spec.rb
@@ -474,7 +484,6 @@ metadata:
474
484
  documentation_uri: https://rbotafogo.github.io/galaaz/
475
485
  changelog_uri: https://github.com/rbotafogo/galaaz/blob/galaaz2_0/CHANGELOG.md
476
486
  yard.run: yri
477
- post_install_message:
478
487
  rdoc_options: []
479
488
  require_paths:
480
489
  - lib
@@ -489,8 +498,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
489
498
  - !ruby/object:Gem::Version
490
499
  version: '0'
491
500
  requirements: []
492
- rubygems_version: 3.5.22
493
- signing_key:
501
+ rubygems_version: 4.0.3
494
502
  specification_version: 4
495
503
  summary: 'R-on-Rails: tightly couple Ruby and GNU R for data science on the web'
496
504
  test_files: []