herringbone 0.9.1 → 1.0.0

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.
@@ -86,8 +86,18 @@ module Herringbone
86
86
  # returns and the block's value is returned; if the block raises, the writer is aborted and no
87
87
  # footer is written. Without a block, call #close to finish. The IO is never closed.
88
88
  #
89
+ # Without a schema, the column types are inferred from the first Schema::INFER_SAMPLE rows
90
+ # (see InferringWriter), and +headers:+ names the columns of Array rows, as with the CSV gem:
91
+ #
92
+ # Herringbone::Writer.open(io, headers: [:id, :name, :age]) do |w|
93
+ # w << [123, "John", 12]
94
+ # w << {id: 124, name: "Jane"} # Hashes work too; missing columns are nulls
95
+ # end
96
+ #
89
97
  # @param io [IO, #write] destination
90
- # @param schema [Schema] schema of the rows
98
+ # @param schema [Schema, nil] schema of the rows; inferred from them when nil
99
+ # @param headers [Array<String, Symbol>, nil] column names, in the order Array rows list their
100
+ # values, when the schema is inferred
91
101
  # @param options [Hash{Symbol => Object}] see the class description and #initialize
92
102
  # @option options [Symbol] :compression (:snappy) codec, see Herringbone.codecs
93
103
  # @option options [Integer, nil] :compression_level (nil) level for :zstd, :gzip or :brotli
@@ -106,12 +116,21 @@ module Herringbone
106
116
  # @option options [EncryptionConfiguration, Hash{Symbol => Object}, nil] :encryption (nil) modular
107
117
  # encryption settings, see #initialize
108
118
  # @yield [writer] the open writer
109
- # @yieldparam writer [Writer] writer to append rows to
119
+ # @yieldparam writer [Writer, InferringWriter] writer to append rows to
110
120
  # @yieldreturn [Object] returned by open
111
- # @return [Writer, Object] the writer without a block, the block's value with one
112
- # @raise [ArgumentError] for an invalid IO, schema or option
113
- def self.open(io, schema, **options)
114
- writer = new(io, schema, **options)
121
+ # @return [Writer, InferringWriter, Object] the writer without a block, the block's value with one
122
+ # @raise [ArgumentError] for an invalid IO, schema or option, or +headers:+ given with a schema
123
+ def self.open(io, schema = nil, headers: nil, **options)
124
+ if schema && headers
125
+ raise ArgumentError, "headers: names the columns of Array rows whose types are inferred, but the " \
126
+ "schema already names the columns (Array rows list their values in schema order). Pass one or the other."
127
+ end
128
+ writer = if schema
129
+ new(io, schema, **options)
130
+ else
131
+ fix = "Herringbone::Writer.open(io, Herringbone::Schema.define { |s| ...; s.%s })"
132
+ InferringWriter.new(io, fix: fix, headers: headers, **options) { |sample| Schema.infer(sample) }
133
+ end
115
134
  return writer unless block_given?
116
135
  begin
117
136
  result = yield writer
data/lib/herringbone.rb CHANGED
@@ -32,8 +32,8 @@ module Herringbone
32
32
  end
33
33
 
34
34
  # A row does not fit a schema that was inferred from earlier rows (Herringbone.write without
35
- # +schema:+, SimpleWriter). The message says what was inferred, from how many rows, and how to
36
- # declare the column instead.
35
+ # +schema:+, Writer.open without a schema). The message says what was inferred, from how many
36
+ # rows, and how to declare the column instead.
37
37
  class SchemaMismatch < EncodeError; end
38
38
 
39
39
  # The file (or the writer configuration) uses a Parquet feature Herringbone does not implement,
@@ -99,7 +99,6 @@ module Herringbone
99
99
  autoload :Redaction, "#{LIB}/redaction"
100
100
  autoload :RestrictedReadableIO, "#{LIB}/restricted_readable_io"
101
101
  autoload :Schema, "#{LIB}/schema"
102
- autoload :SimpleWriter, "#{LIB}/simple_writer"
103
102
  autoload :Thrift, "#{LIB}/thrift"
104
103
  autoload :Types, "#{LIB}/types"
105
104
  autoload :Visualizer, "#{LIB}/visualizer"
@@ -153,10 +152,21 @@ module Herringbone
153
152
  # File.open("orders.parquet", "wb") { |f| Herringbone.write(f, Order.where(created_at: 1.year.ago..)) }
154
153
  # Herringbone.write(io, events.lazy.map(&:to_h)) { |s| s.json :payload }
155
154
  #
155
+ # +headers:+ names the columns of Array rows, as with the CSV gem, and the types are inferred.
156
+ # Rows produced in a loop can be given through an Enumerator:
157
+ #
158
+ # rows = Enumerator.new do |y|
159
+ # y << [123, "John", 12]
160
+ # y << {id: 124, name: "Jane"} # Hashes work too; missing columns are nulls
161
+ # end
162
+ # Herringbone.write(io, rows, headers: [:id, :name, :age])
163
+ #
156
164
  # @param io [IO, #write] destination; written sequentially, never closed
157
165
  # @param records [Enumerable<Hash, Array, Object>, Class, #find_each] rows (Hashes, Arrays in schema
158
166
  # order, or objects responding to #attributes or #to_h), or an ActiveRecord model or relation
159
167
  # @param schema [Schema, nil] schema to write with; derived from +records+ when nil
168
+ # @param headers [Array<String, Symbol>, nil] column names, in the order Array rows list their
169
+ # values; the types are inferred, so it can't be combined with +schema+
160
170
  # @param options [Hash{Symbol => Object}] passed to Writer.new
161
171
  # @option options [Symbol] :compression (:snappy) codec, see Herringbone.codecs
162
172
  # @option options [Integer, nil] :compression_level (nil) level for :zstd, :gzip or :brotli
@@ -178,20 +188,26 @@ module Herringbone
178
188
  # @yieldreturn [void]
179
189
  # @return [Integer] number of rows written
180
190
  # @raise [ArgumentError] when the schema has to be inferred and +records+ is empty or holds Array
181
- # rows, an option is invalid, or the block takes no parameter
191
+ # rows without +headers+, +headers+ is given with +schema+ or an ActiveRecord source, a row does
192
+ # not match the headers, an option is invalid, or the block takes no parameter
182
193
  # @raise [EncodeError] when a row does not fit the schema
183
194
  # @raise [SchemaMismatch] when a row does not fit the inferred schema; the file is left unfinished
184
- def write(io, records, schema: nil, **options, &overrides)
195
+ def write(io, records, schema: nil, headers: nil, **options, &overrides)
185
196
  Schema::Builder.check_block!(overrides, "Herringbone.write(io, rows) { |s| s.json :payload }")
186
197
  model = if records.respond_to?(:klass) then records.klass
187
198
  elsif records.respond_to?(:columns) && records.respond_to?(:find_each) then records
188
199
  end
200
+ if headers && (schema || model)
201
+ raise ArgumentError, "headers: names the columns of Array rows whose types are inferred, but " \
202
+ "#{schema ? "schema: already names the columns (Array rows list their values in schema order)" : "an ActiveRecord source has its own columns"}. " \
203
+ "Pass one or the other."
204
+ end
189
205
  schema ||= Schema.from_active_record(model) if model
190
206
  writer = if schema
191
207
  Writer.new(io, schema, **options)
192
208
  else
193
- fix = "Herringbone.write(io, rows) { |s| s.%s }"
194
- InferringWriter.new(io, fix: fix, **options) { |sample| Schema.infer(sample, &overrides) }
209
+ fix = "Herringbone.write(io, rows#{", headers: [...]" if headers}) { |s| s.%s }"
210
+ InferringWriter.new(io, fix: fix, headers: headers, **options) { |sample| Schema.infer(sample, &overrides) }
195
211
  end
196
212
  begin
197
213
  if records.respond_to?(:find_each)