blob: 9fc1443ccbbca6c534162ae69b735479031ec5e8 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
module WriterHelper
def convert_time_unit(red_arrow_time_unit)
if red_arrow_time_unit.nick == "second"
red_arrow_time_unit.nick.to_sym
else
:"#{red_arrow_time_unit.nick}second"
end
end
def convert_type(red_arrow_type)
case red_arrow_type
when Arrow::NullDataType
ArrowFormat::NullType.singleton
when Arrow::BooleanDataType
ArrowFormat::BooleanType.singleton
when Arrow::Int8DataType
ArrowFormat::Int8Type.singleton
when Arrow::UInt8DataType
ArrowFormat::UInt8Type.singleton
when Arrow::Int16DataType
ArrowFormat::Int16Type.singleton
when Arrow::UInt16DataType
ArrowFormat::UInt16Type.singleton
when Arrow::Int32DataType
ArrowFormat::Int32Type.singleton
when Arrow::UInt32DataType
ArrowFormat::UInt32Type.singleton
when Arrow::Int64DataType
ArrowFormat::Int64Type.singleton
when Arrow::UInt64DataType
ArrowFormat::UInt64Type.singleton
when Arrow::FloatDataType
ArrowFormat::Float32Type.singleton
when Arrow::DoubleDataType
ArrowFormat::Float64Type.singleton
when Arrow::Date32DataType
ArrowFormat::Date32Type.singleton
when Arrow::Date64DataType
ArrowFormat::Date64Type.singleton
when Arrow::Time32DataType
ArrowFormat::Time32Type.new(convert_time_unit(red_arrow_type.unit))
when Arrow::Time64DataType
ArrowFormat::Time64Type.new(convert_time_unit(red_arrow_type.unit))
when Arrow::TimestampDataType
ArrowFormat::TimestampType.new(convert_time_unit(red_arrow_type.unit),
red_arrow_type.time_zone&.identifier)
when Arrow::MonthIntervalDataType
ArrowFormat::YearMonthIntervalType.singleton
when Arrow::DayTimeIntervalDataType
ArrowFormat::DayTimeIntervalType.singleton
when Arrow::MonthDayNanoIntervalDataType
ArrowFormat::MonthDayNanoIntervalType.singleton
when Arrow::DurationDataType
ArrowFormat::DurationType.new(convert_time_unit(red_arrow_type.unit))
when Arrow::BinaryDataType
ArrowFormat::BinaryType.singleton
when Arrow::LargeBinaryDataType
ArrowFormat::LargeBinaryType.singleton
when Arrow::StringDataType
ArrowFormat::UTF8Type.singleton
when Arrow::LargeStringDataType
ArrowFormat::LargeUTF8Type.singleton
when Arrow::Decimal128DataType
ArrowFormat::Decimal128Type.new(red_arrow_type.precision,
red_arrow_type.scale)
when Arrow::Decimal256DataType
ArrowFormat::Decimal256Type.new(red_arrow_type.precision,
red_arrow_type.scale)
when Arrow::FixedSizeBinaryDataType
ArrowFormat::FixedSizeBinaryType.new(red_arrow_type.byte_width)
when Arrow::MapDataType
ArrowFormat::MapType.new(convert_field(red_arrow_type.field),
red_arrow_type.keys_sorted?)
when Arrow::ListDataType
ArrowFormat::ListType.new(convert_field(red_arrow_type.field))
when Arrow::LargeListDataType
ArrowFormat::LargeListType.new(convert_field(red_arrow_type.field))
when Arrow::FixedSizeListDataType
ArrowFormat::FixedSizeListType.new(convert_field(red_arrow_type.field),
red_arrow_type.list_size)
when Arrow::StructDataType
fields = red_arrow_type.fields.collect do |field|
convert_field(field)
end
ArrowFormat::StructType.new(fields)
when Arrow::DenseUnionDataType
fields = red_arrow_type.fields.collect do |field|
convert_field(field)
end
ArrowFormat::DenseUnionType.new(fields, red_arrow_type.type_codes)
when Arrow::SparseUnionDataType
fields = red_arrow_type.fields.collect do |field|
convert_field(field)
end
ArrowFormat::SparseUnionType.new(fields, red_arrow_type.type_codes)
when Arrow::DictionaryDataType
@dictionary_id ||= 0
dictionary_id = @dictionary_id
@dictionary_id += 1
index_type = convert_type(red_arrow_type.index_data_type)
value_type = convert_type(red_arrow_type.value_data_type)
ArrowFormat::DictionaryType.new(dictionary_id,
index_type,
value_type,
red_arrow_type.ordered?)
else
raise "Unsupported type: #{red_arrow_type.inspect}"
end
end
def convert_field(red_arrow_field)
type = convert_type(red_arrow_field.data_type)
ArrowFormat::Field.new(red_arrow_field.name,
type,
nullable: red_arrow_field.nullable?,
metadata: red_arrow_field.metadata)
end
def convert_buffer(buffer)
return nil if buffer.nil?
IO::Buffer.for(buffer.data.to_s.dup)
end
def convert_array(red_arrow_array)
type = convert_type(red_arrow_array.value_data_type)
case type
when ArrowFormat::NullType
type.build_array(red_arrow_array.size)
when ArrowFormat::PrimitiveType
type.build_array(red_arrow_array.size,
convert_buffer(red_arrow_array.null_bitmap),
convert_buffer(red_arrow_array.data_buffer))
when ArrowFormat::VariableSizeBinaryType
type.build_array(red_arrow_array.size,
convert_buffer(red_arrow_array.null_bitmap),
convert_buffer(red_arrow_array.offsets_buffer),
convert_buffer(red_arrow_array.data_buffer))
when ArrowFormat::FixedSizeBinaryType
type.build_array(red_arrow_array.size,
convert_buffer(red_arrow_array.null_bitmap),
convert_buffer(red_arrow_array.data_buffer))
when ArrowFormat::VariableSizeListType
type.build_array(red_arrow_array.size,
convert_buffer(red_arrow_array.null_bitmap),
convert_buffer(red_arrow_array.value_offsets_buffer),
convert_array(red_arrow_array.values_raw))
when ArrowFormat::FixedSizeListType
type.build_array(red_arrow_array.size,
convert_buffer(red_arrow_array.null_bitmap),
convert_array(red_arrow_array.values_raw))
when ArrowFormat::StructType
children = red_arrow_array.fields.collect do |red_arrow_field|
convert_array(red_arrow_field)
end
type.build_array(red_arrow_array.size,
convert_buffer(red_arrow_array.null_bitmap),
children)
when ArrowFormat::DenseUnionType
types_buffer = convert_buffer(red_arrow_array.type_ids.data_buffer)
offsets_buffer = convert_buffer(red_arrow_array.value_offsets.data_buffer)
children = red_arrow_array.fields.collect do |red_arrow_field|
convert_array(red_arrow_field)
end
type.build_array(red_arrow_array.size,
types_buffer,
offsets_buffer,
children)
when ArrowFormat::SparseUnionType
types_buffer = convert_buffer(red_arrow_array.type_ids.data_buffer)
children = red_arrow_array.fields.collect do |red_arrow_field|
convert_array(red_arrow_field)
end
type.build_array(red_arrow_array.size,
types_buffer,
children)
when ArrowFormat::DictionaryType
validity_buffer = convert_buffer(red_arrow_array.null_bitmap)
indices_buffer = convert_buffer(red_arrow_array.indices.data_buffer)
dictionary_array = convert_array(red_arrow_array.dictionary)
dictionary = ArrowFormat::Dictionary.new(0, dictionary_array)
type.build_array(red_arrow_array.size,
validity_buffer,
indices_buffer,
[dictionary])
else
raise "Unsupported array #{red_arrow_array.inspect}"
end
end
def write(writer, *inputs)
inputs.each_with_index do |input, i|
case input
when ArrowFormat::RecordBatch
record_batch = input
else
red_arrow_array = input
array = convert_array(red_arrow_array)
red_arrow_field = Arrow::Field.new("value",
red_arrow_array.value_data_type,
true)
fields = [convert_field(red_arrow_field)]
schema = ArrowFormat::Schema.new(fields)
record_batch = ArrowFormat::RecordBatch.new(schema,
array.size,
[array])
end
writer.start(record_batch.schema) if i.zero?
writer.write_record_batch(record_batch)
end
writer.finish
end
def roundtrip(*inputs)
Dir.mktmpdir do |tmp_dir|
path = File.join(tmp_dir, "data.#{file_extension}")
File.open(path, "wb") do |output|
writer = writer_class.new(output)
write(writer, *inputs)
end
# pp(read(path)) # debug
data = File.open(path, "rb", &:read).freeze
case file_extension
when "arrow"
format = :arrow_file
else
format = :arrow_streaming
end
table = Arrow::Table.load(Arrow::Buffer.new(data), format: format)
if inputs[0].is_a?(Arrow::Array)
[table.value.data_type, table.value.values]
else
table
end
end
end
end
module WriterTests
def test_custom_metadata_field
field = ArrowFormat::Field.new("value",
ArrowFormat::BooleanType.new,
metadata: {
"key1" => "value1",
"key2" => "value2",
})
schema = ArrowFormat::Schema.new([field])
column = convert_array(Arrow::BooleanArray.new([true, nil, false]))
record_batch = ArrowFormat::RecordBatch.new(schema, 3, [column])
table = roundtrip(record_batch)
assert_equal({
"key1" => "value1",
"key2" => "value2",
},
table.schema.fields[0].metadata)
end
def test_custom_metadata_schema
field = ArrowFormat::Field.new("value", ArrowFormat::BooleanType.new)
schema = ArrowFormat::Schema.new([field],
metadata: {
"key1" => "value1",
"key2" => "value2",
})
column = convert_array(Arrow::BooleanArray.new([true, nil, false]))
record_batch = ArrowFormat::RecordBatch.new(schema, 3, [column])
table = roundtrip(record_batch)
assert_equal({
"key1" => "value1",
"key2" => "value2",
},
table.schema.metadata)
end
def test_custom_metadata_message_record_batch
field = ArrowFormat::Field.new("value", ArrowFormat::BooleanType.new)
schema = ArrowFormat::Schema.new([field])
column = convert_array(Arrow::BooleanArray.new([true, nil, false]))
record_batch = ArrowFormat::RecordBatch.new(schema, 3, [column],
message_metadata: {
"key1" => "value1",
"key2" => "value2",
})
output = StringIO.new(+"".b)
writer = writer_class.new(output)
write(writer, record_batch)
writer.finish
reader = reader_class.new(output.string)
assert_equal({
"key1" => "value1",
"key2" => "value2",
},
reader.first.message_metadata)
end
def test_null
array = Arrow::NullArray.new(3)
type, values = roundtrip(array)
assert_equal(["null", [nil, nil, nil]],
[type.to_s, values])
end
def test_boolean
array = Arrow::BooleanArray.new([true, nil, false])
type, values = roundtrip(array)
assert_equal(["bool", [true, nil, false]],
[type.to_s, values])
end
def test_int8
array = Arrow::Int8Array.new([-128, nil, 127])
type, values = roundtrip(array)
assert_equal(["int8", [-128, nil, 127]],
[type.to_s, values])
end
def test_uint8
array = Arrow::UInt8Array.new([0, nil, 255])
type, values = roundtrip(array)
assert_equal(["uint8", [0, nil, 255]],
[type.to_s, values])
end
def test_int16
array = Arrow::Int16Array.new([-32768, nil, 32767])
type, values = roundtrip(array)
assert_equal(["int16", [-32768, nil, 32767]],
[type.to_s, values])
end
def test_uint16
array = Arrow::UInt16Array.new([0, nil, 65535])
type, values = roundtrip(array)
assert_equal(["uint16", [0, nil, 65535]],
[type.to_s, values])
end
def test_int32
array = Arrow::Int32Array.new([-2147483648, nil, 2147483647])
type, values = roundtrip(array)
assert_equal(["int32", [-2147483648, nil, 2147483647]],
[type.to_s, values])
end
def test_uint32
array = Arrow::UInt32Array.new([0, nil, 4294967295])
type, values = roundtrip(array)
assert_equal(["uint32", [0, nil, 4294967295]],
[type.to_s, values])
end
def test_int64
array = Arrow::Int64Array.new([
-9223372036854775808,
nil,
9223372036854775807
])
type, values = roundtrip(array)
assert_equal([
"int64",
[
-9223372036854775808,
nil,
9223372036854775807
],
],
[type.to_s, values])
end
def test_uint64
array = Arrow::UInt64Array.new([0, nil, 18446744073709551615])
type, values = roundtrip(array)
assert_equal(["uint64", [0, nil, 18446744073709551615]],
[type.to_s, values])
end
def test_float32
array = Arrow::FloatArray.new([-0.5, nil, 0.5])
type, values = roundtrip(array)
assert_equal(["float", [-0.5, nil, 0.5]],
[type.to_s, values])
end
def test_float64
array = Arrow::DoubleArray.new([-0.5, nil, 0.5])
type, values = roundtrip(array)
assert_equal(["double", [-0.5, nil, 0.5]],
[type.to_s, values])
end
def test_date32
date_2017_08_28 = 17406
date_2025_12_09 = 20431
array = Arrow::Date32Array.new([
date_2017_08_28,
nil,
date_2025_12_09,
])
type, values = roundtrip(array)
assert_equal([
"date32[day]",
[Date.new(2017, 8, 28), nil, Date.new(2025, 12, 9)],
],
[type.to_s, values])
end
def test_date64
date_2017_08_28_00_00_00 = 1503878400000
date_2025_12_10_00_00_00 = 1765324800000
array = Arrow::Date64Array.new([
date_2017_08_28_00_00_00,
nil,
date_2025_12_10_00_00_00,
])
type, values = roundtrip(array)
assert_equal([
"date64[ms]",
[
DateTime.new(2017, 8, 28, 0, 0, 0),
nil,
DateTime.new(2025, 12, 10, 0, 0, 0),
],
],
[type.to_s, values])
end
def test_time32_second
time_00_00_10 = 10
time_00_01_10 = 60 + 10
array = Arrow::Time32Array.new(:second,
[time_00_00_10, nil, time_00_01_10])
type, values = roundtrip(array)
assert_equal([
"time32[s]",
[
Arrow::Time.new(:second, time_00_00_10),
nil,
Arrow::Time.new(:second, time_00_01_10),
],
],
[type.to_s, values])
end
def test_time32_millisecond
time_00_00_10_000 = 10 * 1000
time_00_01_10_000 = (60 + 10) * 1000
array = Arrow::Time32Array.new(:milli,
[
time_00_00_10_000,
nil,
time_00_01_10_000,
])
type, values = roundtrip(array)
assert_equal([
"time32[ms]",
[
Arrow::Time.new(:milli, time_00_00_10_000),
nil,
Arrow::Time.new(:milli, time_00_01_10_000),
],
],
[type.to_s, values])
end
def test_time64_microsecond
time_00_00_10_000_000 = 10 * 1_000_000
time_00_01_10_000_000 = (60 + 10) * 1_000_000
array = Arrow::Time64Array.new(:micro,
[
time_00_00_10_000_000,
nil,
time_00_01_10_000_000,
])
type, values = roundtrip(array)
assert_equal([
"time64[us]",
[
Arrow::Time.new(:micro, time_00_00_10_000_000),
nil,
Arrow::Time.new(:micro, time_00_01_10_000_000),
],
],
[type.to_s, values])
end
def test_time64_nanosecond
time_00_00_10_000_000_000 = 10 * 1_000_000_000
time_00_01_10_000_000_000 = (60 + 10) * 1_000_000_000
array = Arrow::Time64Array.new(:nano,
[
time_00_00_10_000_000_000,
nil,
time_00_01_10_000_000_000,
])
type, values = roundtrip(array)
assert_equal([
"time64[ns]",
[
Arrow::Time.new(:nano, time_00_00_10_000_000_000),
nil,
Arrow::Time.new(:nano, time_00_01_10_000_000_000),
],
],
[type.to_s, values])
end
def test_timestamp_second
timestamp_2019_11_17_15_09_11 = 1574003351
timestamp_2025_12_16_05_33_58 = 1765863238
array = Arrow::TimestampArray.new(:second,
[
timestamp_2019_11_17_15_09_11,
nil,
timestamp_2025_12_16_05_33_58,
])
type, values = roundtrip(array)
assert_equal([
"timestamp[s]",
[
Time.at(timestamp_2019_11_17_15_09_11),
nil,
Time.at(timestamp_2025_12_16_05_33_58),
],
],
[type.to_s, values])
end
def test_timestamp_millisecond
timestamp_2019_11_17_15_09_11 = 1574003351 * 1_000
timestamp_2025_12_16_05_33_58 = 1765863238 * 1_000
array = Arrow::TimestampArray.new(:milli,
[
timestamp_2019_11_17_15_09_11,
nil,
timestamp_2025_12_16_05_33_58,
])
type, values = roundtrip(array)
assert_equal([
"timestamp[ms]",
[
Time.at(timestamp_2019_11_17_15_09_11 / 1_000),
nil,
Time.at(timestamp_2025_12_16_05_33_58 / 1_000),
],
],
[type.to_s, values])
end
def test_timestamp_microsecond
timestamp_2019_11_17_15_09_11 = 1574003351 * 1_000_000
timestamp_2025_12_16_05_33_58 = 1765863238 * 1_000_000
array = Arrow::TimestampArray.new(:micro,
[
timestamp_2019_11_17_15_09_11,
nil,
timestamp_2025_12_16_05_33_58,
])
type, values = roundtrip(array)
assert_equal([
"timestamp[us]",
[
Time.at(timestamp_2019_11_17_15_09_11 / 1_000_000),
nil,
Time.at(timestamp_2025_12_16_05_33_58 / 1_000_000),
],
],
[type.to_s, values])
end
def test_timestamp_nanosecond
timestamp_2019_11_17_15_09_11 = 1574003351 * 1_000_000_000
timestamp_2025_12_16_05_33_58 = 1765863238 * 1_000_000_000
array = Arrow::TimestampArray.new(:nano,
[
timestamp_2019_11_17_15_09_11,
nil,
timestamp_2025_12_16_05_33_58,
])
type, values = roundtrip(array)
assert_equal([
"timestamp[ns]",
[
Time.at(timestamp_2019_11_17_15_09_11 / 1_000_000_000),
nil,
Time.at(timestamp_2025_12_16_05_33_58 / 1_000_000_000),
],
],
[type.to_s, values])
end
def test_timestamp_time_zone
time_zone = "UTC"
timestamp_2019_11_17_15_09_11 = 1574003351
timestamp_2025_12_16_05_33_58 = 1765863238
data_type = Arrow::TimestampDataType.new(:second, time_zone)
array = Arrow::TimestampArray.new(data_type,
[
timestamp_2019_11_17_15_09_11,
nil,
timestamp_2025_12_16_05_33_58,
])
type, values = roundtrip(array)
assert_equal([
"timestamp[s, tz=#{time_zone}]",
[
Time.at(timestamp_2019_11_17_15_09_11),
nil,
Time.at(timestamp_2025_12_16_05_33_58),
],
],
[type.to_s, values])
end
def test_year_month_interval
array = Arrow::MonthIntervalArray.new([0, nil, 100])
type, values = roundtrip(array)
assert_equal(["month_interval", [0, nil, 100]],
[type.to_s, values])
end
def test_day_time_interval
array =
Arrow::DayTimeIntervalArray.new([
{day: 1, millisecond: 100},
nil,
{day: 3, millisecond: 300},
])
type, values = roundtrip(array)
assert_equal([
"day_time_interval",
[
{day: 1, millisecond: 100},
nil,
{day: 3, millisecond: 300},
],
],
[type.to_s, values])
end
def test_month_day_nano_interval
array =
Arrow::MonthDayNanoIntervalArray.new([
{
month: 1,
day: 1,
nanosecond: 100,
},
nil,
{
month: 3,
day: 3,
nanosecond: 300,
},
])
type, values = roundtrip(array)
assert_equal([
"month_day_nano_interval",
[
{
month: 1,
day: 1,
nanosecond: 100,
},
nil,
{
month: 3,
day: 3,
nanosecond: 300,
},
],
],
[type.to_s, values])
end
def test_duration_second
array = Arrow::DurationArray.new(:second, [0, nil, 100])
type, values = roundtrip(array)
assert_equal(["duration[s]", [0, nil, 100]],
[type.to_s, values])
end
def test_duration_millisecond
array = Arrow::DurationArray.new(:milli, [0, nil, 100])
type, values = roundtrip(array)
assert_equal(["duration[ms]", [0, nil, 100]],
[type.to_s, values])
end
def test_duration_microsecond
array = Arrow::DurationArray.new(:micro, [0, nil, 100])
type, values = roundtrip(array)
assert_equal(["duration[us]", [0, nil, 100]],
[type.to_s, values])
end
def test_duration_nanosecond
array = Arrow::DurationArray.new(:nano, [0, nil, 100])
type, values = roundtrip(array)
assert_equal(["duration[ns]", [0, nil, 100]],
[type.to_s, values])
end
def test_binary
array = Arrow::BinaryArray.new(["Hello".b, nil, "World".b])
type, values = roundtrip(array)
assert_equal(["binary", ["Hello".b, nil, "World".b]],
[type.to_s, values])
end
def test_large_binary
array = Arrow::LargeBinaryArray.new(["Hello".b, nil, "World".b])
type, values = roundtrip(array)
assert_equal(["large_binary", ["Hello".b, nil, "World".b]],
[type.to_s, values])
end
def test_utf8
array = Arrow::StringArray.new(["Hello", nil, "World"])
type, values = roundtrip(array)
assert_equal(["string", ["Hello", nil, "World"]],
[type.to_s, values])
end
def test_large_utf8
array = Arrow::LargeStringArray.new(["Hello", nil, "World"])
type, values = roundtrip(array)
assert_equal(["large_string", ["Hello", nil, "World"]],
[type.to_s, values])
end
def test_fixed_size_binary
data_type = Arrow::FixedSizeBinaryDataType.new(4)
array = Arrow::FixedSizeBinaryArray.new(data_type,
["0124".b, nil, "abcd".b])
type, values = roundtrip(array)
assert_equal(["fixed_size_binary[4]", ["0124".b, nil, "abcd".b]],
[type.to_s, values])
end
def test_decimal128
positive_small = "1.200"
positive_large = ("1234567890" * 3) + "12345.678"
negative_small = "-1.200"
negative_large = "-" + ("1234567890" * 3) + "12345.678"
array = Arrow::Decimal128Array.new({precision: 38, scale: 3},
[
positive_large,
positive_small,
nil,
negative_small,
negative_large,
])
type, values = roundtrip(array)
assert_equal([
"decimal128(38, 3)",
[
BigDecimal(positive_large),
BigDecimal(positive_small),
nil,
BigDecimal(negative_small),
BigDecimal(negative_large),
],
],
[type.to_s, values])
end
def test_decimal256
positive_small = "1.200"
positive_large = ("1234567890" * 7) + "123.456"
negative_small = "-1.200"
negative_large = "-" + ("1234567890" * 7) + "123.456"
array = Arrow::Decimal256Array.new({precision: 76, scale: 3},
[
positive_large,
positive_small,
nil,
negative_small,
negative_large,
])
type, values = roundtrip(array)
assert_equal([
"decimal256(76, 3)",
[
BigDecimal(positive_large),
BigDecimal(positive_small),
nil,
BigDecimal(negative_small),
BigDecimal(negative_large),
],
],
[type.to_s, values])
end
def test_list
data_type = Arrow::ListDataType.new(name: "count", type: :int8)
array = Arrow::ListArray.new(data_type,
[[-128, 127], nil, [-1, 0, 1]])
type, values = roundtrip(array)
assert_equal([
"list<count: int8>",
[[-128, 127], nil, [-1, 0, 1]],
],
[type.to_s, values])
end
def test_large_list
data_type = Arrow::LargeListDataType.new(name: "count",
type: :int8)
array = Arrow::LargeListArray.new(data_type,
[[-128, 127], nil, [-1, 0, 1]])
type, values = roundtrip(array)
assert_equal([
"large_list<count: int8>",
[[-128, 127], nil, [-1, 0, 1]],
],
[type.to_s, values])
end
def test_fixed_size_list
data_type = Arrow::FixedSizeListDataType.new({
name: "count",
type: :int8,
},
2)
array = Arrow::FixedSizeListArray.new(data_type,
[[-128, 127], nil, [-1, 1]])
type, values = roundtrip(array)
assert_equal([
"fixed_size_list<count: int8>[2]",
[[-128, 127], nil, [-1, 1]],
],
[type.to_s, values])
end
def test_map
data_type = Arrow::MapDataType.new(:string, :int8)
array = Arrow::MapArray.new(data_type,
[
{"a" => -128, "b" => 127},
nil,
{"c" => nil},
])
type, values = roundtrip(array)
assert_equal([
"map<string, int8>",
[
{"a" => -128, "b" => 127},
nil,
{"c" => nil},
],
],
[type.to_s, values])
end
def test_struct
data_type = Arrow::StructDataType.new(count: :int8,
visible: :boolean)
array = Arrow::StructArray.new(data_type,
[[-128, nil], nil, [nil, true]])
type, values = roundtrip(array)
assert_equal([
"struct<count: int8, visible: bool>",
[
{"count" => -128, "visible" => nil},
nil,
{"count" => nil, "visible" => true},
],
],
[type.to_s, values])
end
def test_dense_union
fields = [
Arrow::Field.new("number", :int8),
Arrow::Field.new("text", :string),
]
type_ids = [11, 13]
data_type = Arrow::DenseUnionDataType.new(fields, type_ids)
types = Arrow::Int8Array.new([11, 13, 11, 13, 13])
value_offsets = Arrow::Int32Array.new([0, 0, 1, 1, 2])
children = [
Arrow::Int8Array.new([1, nil]),
Arrow::StringArray.new(["a", "b", "c"])
]
array = Arrow::DenseUnionArray.new(data_type,
types,
value_offsets,
children)
type, values = roundtrip(array)
assert_equal([
"dense_union<number: int8=11, text: string=13>",
[1, "a", nil, "b", "c"],
],
[type.to_s, values])
end
def test_sparse_union
fields = [
Arrow::Field.new("number", :int8),
Arrow::Field.new("text", :string),
]
type_ids = [11, 13]
data_type = Arrow::SparseUnionDataType.new(fields, type_ids)
types = Arrow::Int8Array.new([11, 13, 11, 13, 11])
children = [
Arrow::Int8Array.new([1, nil, nil, nil, 5]),
Arrow::StringArray.new([nil, "b", nil, "d", nil])
]
array = Arrow::SparseUnionArray.new(data_type, types, children)
type, values = roundtrip(array)
assert_equal([
"sparse_union<number: int8=11, text: string=13>",
[1, "b", nil, "d", 5],
],
[type.to_s, values])
end
def test_dictionary
values = ["a", "b", "c", nil, "a"]
string_array = Arrow::StringArray.new(values)
array = string_array.dictionary_encode
type, values = roundtrip(array)
assert_equal([
"dictionary<values=string, " +
"indices=int32, " +
"ordered=0>",
["a", "b", "c", nil, "a"],
],
[type.to_s, values])
end
end
module FileWriterTests
def test_custom_metadata_footer
output = StringIO.new(+"".b)
writer = writer_class.new(output)
field = ArrowFormat::Field.new("value", ArrowFormat::BooleanType.new)
schema = ArrowFormat::Schema.new([field])
writer.start(schema)
metadata = {
"key1" => "value1",
"key2" => "value2",
}
writer.finish(metadata)
buffer = Arrow::Buffer.new(output.string)
Arrow::BufferInputStream.open(buffer) do |input|
reader = Arrow::RecordBatchFileReader.new(input)
assert_equal(metadata, reader.metadata)
end
end
end
module StreamingWriterTests
def test_custom_metadata_message_schema
field = ArrowFormat::Field.new("value", ArrowFormat::BooleanType.new)
schema = ArrowFormat::Schema.new([field],
message_metadata: {
"key1" => "value1",
"key2" => "value2",
})
column = convert_array(Arrow::BooleanArray.new([true, nil, false]))
record_batch = ArrowFormat::RecordBatch.new(schema, 3, [column])
output = StringIO.new(+"".b)
writer = writer_class.new(output)
write(writer, record_batch)
writer.finish
input = StringIO.new(output.string)
reader = reader_class.new(input)
assert_equal({
"key1" => "value1",
"key2" => "value2",
},
reader.schema.message_metadata)
end
end
module WriterDictionaryDeltaTests
def build_schema(value_type)
index_type = ArrowFormat::Int32Type.singleton
dictionary_id = 1
ordered = false
type = ArrowFormat::DictionaryType.new(dictionary_id,
index_type,
value_type,
ordered)
field = ArrowFormat::Field.new("value", type)
ArrowFormat::Schema.new([field])
end
def build_dictionary_array(type, indices, dictionaries)
indices_buffer = IO::Buffer.for(indices.pack("l<*"))
ArrowFormat::DictionaryArray.new(type,
indices.size,
nil,
indices_buffer,
dictionaries)
end
def build_record_batches(red_arrow_value_type, values1, values2)
value_type = convert_type(red_arrow_value_type)
schema = build_schema(value_type)
type = schema.fields[0].type
dictionary_id = 1
# The first record batch with new dictionary.
raw_dictionary = values1.uniq
red_arrow_dictionary =
red_arrow_value_type.build_array(raw_dictionary)
dictionary_array = convert_array(red_arrow_dictionary)
dictionary =
ArrowFormat::Dictionary.new(dictionary_id, dictionary_array,
message_metadata: {
"key1" => "value1",
"key2" => "value2",
})
indices1 = values1.collect do |value|
raw_dictionary.index(value)
end
array1 = build_dictionary_array(type, indices1, [dictionary])
record_batch =
ArrowFormat::RecordBatch.new(schema, array1.size, [array1])
if chunked_dictionaries?
# The second record batch with the first dictionary and
# a delta dictionary.
raw_dictionary_delta = (values2.uniq - raw_dictionary)
raw_dictionary_more = raw_dictionary + raw_dictionary_delta
red_arrow_dictionary_delta =
red_arrow_value_type.build_array(raw_dictionary_delta)
dictionary_array_delta = convert_array(red_arrow_dictionary_delta)
dictionary_delta =
ArrowFormat::Dictionary.new(dictionary_id, dictionary_array_delta)
indices2 = values2.collect do |value|
raw_dictionary_more.index(value)
end
array2 = build_dictionary_array(type,
indices2,
[dictionary, dictionary_delta])
else
# The second record batch with the combined dictionary.
raw_dictionary_more = raw_dictionary | values2.uniq
red_arrow_dictionary_more =
red_arrow_value_type.build_array(raw_dictionary_more)
dictionary_array_more = convert_array(red_arrow_dictionary_more)
dictionary_more = ArrowFormat::Dictionary.new(dictionary_id,
dictionary_array_more)
indices2 = values2.collect do |value|
raw_dictionary_more.index(value)
end
array2 = build_dictionary_array(type,
indices2,
[dictionary_more])
end
record_batch_delta =
ArrowFormat::RecordBatch.new(schema, array2.size, [array2])
[record_batch, record_batch_delta]
end
def roundtrip(value_type, values1, values2)
record_batches = build_record_batches(value_type, values1, values2)
GC.start
table = super(*record_batches)
[table.value.data_type, table.value.values]
end
def test_boolean
value_type = Arrow::BooleanDataType.new
values1 = [true, true]
values2 = [false, true, false]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=bool, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_int8
value_type = Arrow::Int8DataType.new
values1 = [-128, 0, -128]
values2 = [127, -128, 0, 127]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=int8, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_uint8
value_type = Arrow::UInt8DataType.new
values1 = [1, 0, 1]
values2 = [255, 0, 1, 255]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=uint8, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_int16
value_type = Arrow::Int16DataType.new
values1 = [-32768, 0, -32768]
values2 = [32767, -32768, 0, 32767]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=int16, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_uint16
value_type = Arrow::UInt16DataType.new
values1 = [1, 0, 1]
values2 = [65535, 0, 1, 65535]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=uint16, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_int32
value_type = Arrow::Int32DataType.new
values1 = [-2147483648, 0, -2147483648]
values2 = [2147483647, -2147483648, 0, 2147483647]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=int32, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_uint32
value_type = Arrow::UInt32DataType.new
values1 = [1, 0, 1]
values2 = [4294967295, 0, 1, 4294967295]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=uint32, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_int64
value_type = Arrow::Int64DataType.new
values1 = [
-9223372036854775808,
0,
-9223372036854775808,
]
values2 = [
9223372036854775807,
-9223372036854775808,
0,
9223372036854775807,
]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=int64, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_uint64
value_type = Arrow::UInt64DataType.new
values1 = [1, 0, 1]
values2 = [
18446744073709551615,
0,
1,
18446744073709551615,
]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=uint64, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_float32
value_type = Arrow::FloatDataType.new
values1 = [-0.5, 0.0, -0.5]
values2 = [0.5, -0.5, 0.0, 0.5]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=float, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_float64
value_type = Arrow::DoubleDataType.new
values1 = [-0.5, 0.0, -0.5]
values2 = [0.5, -0.5, 0.0, 0.5]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=double, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_date32
date_2017_08_28 = 17406
date_2025_12_09 = 20431
value_type = Arrow::Date32DataType.new
values1 = [date_2017_08_28, date_2017_08_28]
values2 = [date_2025_12_09, date_2017_08_28, date_2025_12_09]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=date32[day], " +
"indices=int32, " +
"ordered=0>",
[
Date.new(2017, 8, 28),
Date.new(2017, 8, 28),
Date.new(2025, 12, 9),
Date.new(2017, 8, 28),
Date.new(2025, 12, 9),
],
],
[type.to_s, values])
end
def test_date64
date_2017_08_28_00_00_00 = 1503878400000
date_2025_12_10_00_00_00 = 1765324800000
value_type = Arrow::Date64DataType.new
values1 = [date_2017_08_28_00_00_00, date_2017_08_28_00_00_00]
values2 = [
date_2025_12_10_00_00_00,
date_2017_08_28_00_00_00,
date_2025_12_10_00_00_00,
]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=date64[ms], " +
"indices=int32, " +
"ordered=0>",
[
DateTime.new(2017, 8, 28),
DateTime.new(2017, 8, 28),
DateTime.new(2025, 12, 10),
DateTime.new(2017, 8, 28),
DateTime.new(2025, 12, 10),
],
],
[type.to_s, values])
end
def test_time32
time_00_00_10 = 10
time_00_01_10 = 60 + 10
value_type = Arrow::Time32DataType.new(:second)
values1 = [time_00_00_10, time_00_00_10]
values2 = [time_00_01_10, time_00_00_10, time_00_01_10]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=time32[s], " +
"indices=int32, " +
"ordered=0>",
[
Arrow::Time.new(:second, time_00_00_10),
Arrow::Time.new(:second, time_00_00_10),
Arrow::Time.new(:second, time_00_01_10),
Arrow::Time.new(:second, time_00_00_10),
Arrow::Time.new(:second, time_00_01_10),
],
],
[type.to_s, values])
end
def test_time64
time_00_00_10_000_000 = 10 * 1_000_000
time_00_01_10_000_000 = (60 + 10) * 1_000_000
value_type = Arrow::Time64DataType.new(:micro)
values1 = [time_00_00_10_000_000, time_00_00_10_000_000]
values2 = [
time_00_01_10_000_000,
time_00_00_10_000_000,
time_00_01_10_000_000,
]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=time64[us], " +
"indices=int32, " +
"ordered=0>",
[
Arrow::Time.new(:micro, time_00_00_10_000_000),
Arrow::Time.new(:micro, time_00_00_10_000_000),
Arrow::Time.new(:micro, time_00_01_10_000_000),
Arrow::Time.new(:micro, time_00_00_10_000_000),
Arrow::Time.new(:micro, time_00_01_10_000_000),
],
],
[type.to_s, values])
end
def test_timestamp
timestamp_2019_11_17_15_09_11 = 1574003351
timestamp_2025_12_16_05_33_58 = 1765863238
value_type = Arrow::TimestampDataType.new(:second)
values1 = [
timestamp_2019_11_17_15_09_11,
timestamp_2019_11_17_15_09_11,
]
values2 = [
timestamp_2025_12_16_05_33_58,
timestamp_2019_11_17_15_09_11,
timestamp_2025_12_16_05_33_58,
]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=timestamp[s], " +
"indices=int32, " +
"ordered=0>",
[
Time.at(timestamp_2019_11_17_15_09_11),
Time.at(timestamp_2019_11_17_15_09_11),
Time.at(timestamp_2025_12_16_05_33_58),
Time.at(timestamp_2019_11_17_15_09_11),
Time.at(timestamp_2025_12_16_05_33_58),
],
],
[type.to_s, values])
end
def test_year_month_interval
value_type = Arrow::MonthIntervalDataType.new
values1 = [100, 0, 100]
values2 = [1000, 100, 0, 1000]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=month_interval, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_day_time_interval
value_type = Arrow::DayTimeIntervalDataType.new
values1 = [
{day: 1, millisecond: 100},
{day: 1, millisecond: 100},
]
values2 = [
{day: 3, millisecond: 300},
{day: 1, millisecond: 100},
{day: 3, millisecond: 300},
]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=day_time_interval, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_month_day_nano_interval
value_type = Arrow::MonthDayNanoIntervalDataType.new
values1 = [
{month: 1, day: 1, nanosecond: 100},
{month: 1, day: 1, nanosecond: 100},
]
values2 = [
{month: 3, day: 3, nanosecond: 300},
{month: 1, day: 1, nanosecond: 100},
{month: 3, day: 3, nanosecond: 300},
]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=month_day_nano_interval, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_duration
value_type = Arrow::DurationDataType.new(:second)
values1 = [100, 0, 100]
values2 = [1000, 100, 0, 1000]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=duration[s], " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_binary
value_type = Arrow::BinaryDataType.new
values1 = ["ab".b, "c".b, "ab".b]
values2 = ["c".b, "de".b, "ab".b, "de".b]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=binary, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_large_binary
value_type = Arrow::LargeBinaryDataType.new
values1 = ["ab".b, "c".b, "ab".b]
values2 = ["c".b, "de".b, "ab".b, "de".b]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=large_binary, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_utf8
value_type = Arrow::StringDataType.new
values1 = ["ab", "c", "ab"]
values2 = ["c", "de", "ab", "de"]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=string, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_large_utf8
value_type = Arrow::LargeStringDataType.new
values1 = ["ab", "c", "ab"]
values2 = ["c", "de", "ab", "de"]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=large_string, " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_fixed_size_binary
value_type = Arrow::FixedSizeBinaryDataType.new(2)
values1 = ["ab".b, "cd".b, "ab".b]
values2 = ["ef".b, "cd".b, "ab".b, "ef".b]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=fixed_size_binary[2], " +
"indices=int32, " +
"ordered=0>",
values1 + values2,
],
[type.to_s, values])
end
def test_decimal128
positive_small = "1.200"
positive_large = ("1234567890" * 3) + "12345.678"
negative_small = "-1.200"
negative_large = "-" + ("1234567890" * 3) + "12345.678"
value_type = Arrow::Decimal128DataType.new(precision: 38,
scale: 3)
values1 = [positive_small, negative_small, positive_small]
values2 = [
positive_large,
positive_small,
negative_small,
positive_large,
negative_large,
]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=decimal128(38, 3), " +
"indices=int32, " +
"ordered=0>",
(values1 + values2).collect {|v| BigDecimal(v)},
],
[type.to_s, values])
end
def test_decimal256
positive_small = "1.200"
positive_large = ("1234567890" * 7) + "123.456"
negative_small = "-1.200"
negative_large = "-" + ("1234567890" * 7) + "123.456"
value_type = Arrow::Decimal256DataType.new(precision: 76,
scale: 3)
values1 = [positive_small, negative_small, positive_small]
values2 = [
positive_large,
positive_small,
negative_small,
positive_large,
negative_large,
]
type, values = roundtrip(value_type, values1, values2)
assert_equal([
"dictionary<values=decimal256(76, 3), " +
"indices=int32, " +
"ordered=0>",
(values1 + values2).collect {|v| BigDecimal(v)},
],
[type.to_s, values])
end
end
class TestFileWriter < Test::Unit::TestCase
include WriterHelper
def file_extension
"arrow"
end
def writer_class
ArrowFormat::FileWriter
end
def reader_class
ArrowFormat::FileReader
end
def read(path)
File.open(path, "rb") do |input|
reader = reader_class.new(input)
reader.collect do |record_batch|
record_batch.to_h.tap do |hash|
hash.each do |key, value|
hash[key] = value.to_a
end
end
end
end
end
sub_test_case("Basic") do
include WriterTests
include FileWriterTests
end
sub_test_case("Dictionary: delta") do
include WriterDictionaryDeltaTests
def chunked_dictionaries?
true
end
end
sub_test_case("Dictionary: delta: slice") do
include WriterDictionaryDeltaTests
def chunked_dictionaries?
false
end
end
end
class TestStreamingWriter < Test::Unit::TestCase
include WriterHelper
def file_extension
"arrows"
end
def writer_class
ArrowFormat::StreamingWriter
end
def reader_class
ArrowFormat::StreamingReader
end
def read(path)
File.open(path, "rb") do |input|
reader = reader_class.new(input)
reader.collect do |record_batch|
record_batch.to_h.tap do |hash|
hash.each do |key, value|
hash[key] = value.to_a
end
end
end
end
end
sub_test_case("Basic") do
include WriterTests
include StreamingWriterTests
end
sub_test_case("Dictionary: delta") do
include WriterDictionaryDeltaTests
def chunked_dictionaries?
true
end
end
sub_test_case("Dictionary: delta: slice") do
include WriterDictionaryDeltaTests
def chunked_dictionaries?
false
end
end
end