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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -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/ext/new_bridge/Makefile +10 -10
- 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 +552 -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 +17 -9
|
@@ -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
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
##########################################################################################
|
|
8
|
+
# Stage B: Ruby ↔ R Arrow IPC file handoff (B1 ingest, B2 export).
|
|
9
|
+
#
|
|
10
|
+
# NewBridge carries only the path. This is IPC/mmap file handoff — not zero-copy
|
|
11
|
+
# shared heap between Ruby and R.
|
|
12
|
+
##########################################################################################
|
|
13
|
+
|
|
14
|
+
module Galaaz
|
|
15
|
+
class ArrowIpcError < StandardError; end
|
|
16
|
+
|
|
17
|
+
module ArrowIpc
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# @return [Boolean] true when the current Ruby engine's Arrow backend can load
|
|
21
|
+
def available?
|
|
22
|
+
backend.available?
|
|
23
|
+
rescue StandardError
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Write columnar data to an Arrow IPC file.
|
|
28
|
+
#
|
|
29
|
+
# @param columns_hash [Hash{String,Symbol => Array}] column name → values
|
|
30
|
+
# @return [String] absolute path to the IPC file (fsync'd / closed)
|
|
31
|
+
def write(columns_hash)
|
|
32
|
+
raise ArgumentError, 'columns_hash must be a Hash' unless columns_hash.is_a?(Hash)
|
|
33
|
+
raise ArgumentError, 'columns_hash must not be empty' if columns_hash.empty?
|
|
34
|
+
|
|
35
|
+
normalized = normalize_columns(columns_hash)
|
|
36
|
+
path = next_path
|
|
37
|
+
backend.write_columns(normalized, path)
|
|
38
|
+
fsync_path(path)
|
|
39
|
+
path
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Convenience: Array of row Hashes → columnar write.
|
|
43
|
+
#
|
|
44
|
+
# @param row_hashes [Enumerable<Hash>]
|
|
45
|
+
# @return [String] path
|
|
46
|
+
def write_batches(row_hashes)
|
|
47
|
+
rows = []
|
|
48
|
+
row_hashes.each do |row|
|
|
49
|
+
raise ArgumentError, 'each row must be a Hash' unless row.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
rows << row
|
|
52
|
+
end
|
|
53
|
+
raise ArgumentError, 'write_batches requires at least one row' if rows.empty?
|
|
54
|
+
|
|
55
|
+
keys = rows.flat_map(&:keys).map(&:to_s).uniq
|
|
56
|
+
columns = keys.each_with_object({}) { |k, h| h[k] = [] }
|
|
57
|
+
rows.each do |row|
|
|
58
|
+
key_map = row.each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
|
59
|
+
keys.each { |k| columns[k] << key_map[k] }
|
|
60
|
+
end
|
|
61
|
+
write(columns)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Read an Arrow IPC file into a column hash (String keys → Arrays).
|
|
65
|
+
# B2 types: float64, int32/int64, utf8 (nulls preserved).
|
|
66
|
+
#
|
|
67
|
+
# @param path [String]
|
|
68
|
+
# @return [Hash{String => Array}]
|
|
69
|
+
def read(path)
|
|
70
|
+
path = File.expand_path(path.to_s)
|
|
71
|
+
raise ArgumentError, "Arrow IPC file not found: #{path}" unless File.file?(path)
|
|
72
|
+
|
|
73
|
+
backend.read_columns(path)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Read an Arrow IPC file as an Array of row Hashes (symbol keys).
|
|
77
|
+
#
|
|
78
|
+
# @param path [String]
|
|
79
|
+
# @return [Array<Hash>]
|
|
80
|
+
def read_batches(path)
|
|
81
|
+
columns = read(path)
|
|
82
|
+
raise ArgumentError, 'IPC file has no columns' if columns.empty?
|
|
83
|
+
|
|
84
|
+
n = columns.values.map(&:length).uniq
|
|
85
|
+
raise ArgumentError, "ragged columns after read: #{n.inspect}" if n.size != 1
|
|
86
|
+
|
|
87
|
+
n.first.times.map do |i|
|
|
88
|
+
columns.each_with_object({}) { |(name, values), row| row[name.to_sym] = values[i] }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Unique scratch path for a new IPC file (used by R::Arrow.write_ipc when path omitted).
|
|
93
|
+
#
|
|
94
|
+
# @return [String]
|
|
95
|
+
def allocate_path
|
|
96
|
+
File.join(scratch_dir, "galaaz_ipc_#{Process.pid}_#{SecureRandom.hex(8)}.arrow")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Unlink an IPC scratch file if present.
|
|
100
|
+
#
|
|
101
|
+
# @param path [String]
|
|
102
|
+
# @return [void]
|
|
103
|
+
def release(path)
|
|
104
|
+
return if path.nil? || path.to_s.empty?
|
|
105
|
+
|
|
106
|
+
File.unlink(path) if File.exist?(path)
|
|
107
|
+
rescue Errno::ENOENT
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Prefer /dev/shm when writable; else Dir.tmpdir / gem tmp.
|
|
112
|
+
#
|
|
113
|
+
# @return [String]
|
|
114
|
+
def scratch_dir
|
|
115
|
+
@scratch_dir ||= begin
|
|
116
|
+
candidates = []
|
|
117
|
+
candidates << '/dev/shm' if File.directory?('/dev/shm')
|
|
118
|
+
candidates << File.join(Dir.tmpdir, 'galaaz_arrow_ipc')
|
|
119
|
+
candidates << File.expand_path('../../../tmp/galaaz_arrow_ipc', __dir__)
|
|
120
|
+
|
|
121
|
+
chosen = candidates.find do |dir|
|
|
122
|
+
begin
|
|
123
|
+
FileUtils.mkdir_p(dir)
|
|
124
|
+
File.writable?(dir)
|
|
125
|
+
rescue StandardError
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
raise ArrowIpcError, 'no writable scratch directory for Arrow IPC' unless chosen
|
|
130
|
+
|
|
131
|
+
chosen
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @return [Module] RedArrowBackend or JavaArrowBackend
|
|
136
|
+
def backend
|
|
137
|
+
@backend ||=
|
|
138
|
+
if RUBY_ENGINE == 'jruby'
|
|
139
|
+
require_relative 'arrow_ipc/java_arrow_backend'
|
|
140
|
+
JavaArrowBackend
|
|
141
|
+
else
|
|
142
|
+
require_relative 'arrow_ipc/red_arrow_backend'
|
|
143
|
+
RedArrowBackend
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def normalize_columns(columns_hash)
|
|
148
|
+
columns_hash.each_with_object({}) do |(name, values), out|
|
|
149
|
+
key = name.to_s
|
|
150
|
+
raise ArgumentError, "column #{key.inspect} values must be an Array" unless values.is_a?(Array)
|
|
151
|
+
|
|
152
|
+
out[key] = values
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
private_class_method :normalize_columns
|
|
156
|
+
|
|
157
|
+
def next_path
|
|
158
|
+
allocate_path
|
|
159
|
+
end
|
|
160
|
+
private_class_method :next_path
|
|
161
|
+
|
|
162
|
+
def fsync_path(path)
|
|
163
|
+
File.open(path, 'rb') { |f| f.fsync }
|
|
164
|
+
end
|
|
165
|
+
private_class_method :fsync_path
|
|
166
|
+
end
|
|
167
|
+
end
|