| # 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. |
| |
| require_relative "buffer-alignable" |
| |
| module ArrowFormat |
| class StreamingWriter |
| include BufferAlignable |
| |
| ALIGNMENT_SIZE = IO::Buffer.size_of(:u64) |
| CONTINUATION = "\xFF\xFF\xFF\xFF".b.freeze |
| EOS = "\xFF\xFF\xFF\xFF\x00\x00\x00\x00".b.freeze |
| METADATA_LARGEST_PADDING = "\x00" * 7 |
| |
| def initialize(output) |
| @output = output |
| @offset = 0 |
| @fb_dictionary_blocks = [] |
| @fb_record_batch_blocks = [] |
| @written_dictionary_offsets = {} |
| end |
| |
| def start(schema) |
| write_message(build_metadata(schema.to_flatbuffers, |
| custom_metadata: schema.message_metadata)) |
| end |
| |
| def write_record_batch(record_batch) |
| record_batch.schema.fields.each_with_index do |field, i| |
| next unless field.type.is_a?(DictionaryType) |
| dictionary_array = record_batch.columns[i] |
| write_dictionary(field.type.id, dictionary_array) |
| end |
| |
| write_record_batch_based_message(record_batch, |
| record_batch.to_flatbuffers, |
| @fb_record_batch_blocks) |
| end |
| |
| def finish |
| write_data(EOS) |
| @output |
| end |
| |
| private |
| def write_data(data) |
| case data |
| when IO::Buffer |
| # TODO: We should use IO::Buffer#write to avoid needless copy. |
| # data.write(@output) |
| @output << data.get_string |
| @offset += data.size |
| else |
| @output << data |
| @offset += data.bytesize |
| end |
| end |
| |
| def write_buffer(buffer) |
| write_data(buffer) |
| padding_size = buffer_padding_size(buffer) |
| write_data(padding(padding_size)) if padding_size > 0 |
| end |
| |
| def build_metadata(header, body_length=0, custom_metadata: nil) |
| fb_message = FB::Message::Data.new |
| fb_message.version = FB::MetadataVersion::V5 |
| fb_message.header = header |
| fb_message.body_length = body_length |
| fb_message.custom_metadata = FB.build_custom_metadata(custom_metadata) |
| metadata = FB::Message.serialize(fb_message) |
| metadata_size = metadata.bytesize |
| padding_size = compute_padding_size(metadata_size, ALIGNMENT_SIZE) |
| metadata_size += padding_size |
| align!(metadata, ALIGNMENT_SIZE) |
| metadata |
| end |
| |
| def write_record_batch_based_message(record_batch, fb_header, fb_blocks) |
| body_length = 0 |
| record_batch.all_buffers_enumerator.each do |buffer| |
| body_length += aligned_buffer_size(buffer) if buffer |
| end |
| metadata = build_metadata(fb_header, body_length, |
| custom_metadata: record_batch.message_metadata) |
| fb_block = FB::Block::Data.new |
| fb_block.offset = @offset |
| fb_block.meta_data_length = |
| CONTINUATION.bytesize + |
| MessagePullReader::METADATA_LENGTH_SIZE + |
| metadata.bytesize |
| fb_block.body_length = body_length |
| fb_blocks << fb_block |
| write_message(metadata) do |
| record_batch.all_buffers_enumerator.each do |buffer| |
| write_buffer(buffer) if buffer |
| end |
| end |
| end |
| |
| def write_dictionary(id, dictionary_array) |
| value_type = dictionary_array.type.value_type |
| base_offset = 0 |
| dictionary_array.dictionaries.each do |dictionary| |
| data = dictionary.array |
| written_offset = @written_dictionary_offsets[id] || 0 |
| current_base_offset = base_offset |
| next_base_offset = base_offset + data.size |
| base_offset = next_base_offset |
| |
| next if next_base_offset <= written_offset |
| |
| is_delta = (not written_offset.zero?) |
| if current_base_offset < written_offset |
| data = data.slice(written_offset - current_base_offset) |
| end |
| |
| schema = Schema.new([Field.new("dummy", value_type)]) |
| size = data.size |
| record_batch = |
| RecordBatch.new(schema, size, [data], |
| message_metadata: dictionary.message_metadata) |
| fb_dictionary_batch = FB::DictionaryBatch::Data.new |
| fb_dictionary_batch.id = id |
| fb_dictionary_batch.data = record_batch.to_flatbuffers |
| fb_dictionary_batch.delta = is_delta |
| write_record_batch_based_message(record_batch, |
| fb_dictionary_batch, |
| @fb_dictionary_blocks) |
| @written_dictionary_offsets[id] = written_offset + data.size |
| end |
| end |
| |
| def write_message(metadata) |
| write_data(CONTINUATION) |
| metadata_size = metadata.bytesize |
| write_data([metadata_size].pack("l<")) |
| write_data(metadata) |
| yield if block_given? |
| end |
| end |
| end |