herringbone 0.1.0 → 0.2.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.
@@ -176,53 +176,34 @@ module Herringbone
176
176
  new(root)
177
177
  end
178
178
 
179
- # Builds a schema with the DSL (see Schema::Builder), from a Hash, or both:
180
- #
181
- # Schema.define(id: {type: :int64, null: false}, name: :string, tags: [:string],
182
- # address: {city: :string, zip: :string}, price: {type: :decimal, precision: 10, scale: 2})
183
- #
184
- # Hash values: a type Symbol; [element] for a list; a Hash with :type (plus DSL options such as
185
- # null:, precision:, of: for lists, key:/value: for maps); or a Hash without :type for a struct.
186
- def self.define(spec = nil, &block)
179
+ # Builds a schema with the DSL, see Schema::Builder
180
+ def self.define(&block)
187
181
  builder = Builder.new
188
- builder.fields(spec) if spec
189
182
  builder.instance_eval(&block) if block
190
183
  raise ArgumentError, "A schema needs at least one field" if builder.nodes.empty?
191
184
  new(Node.new(name: "schema", repetition: :required, children: builder.nodes))
192
185
  end
193
186
 
194
- # Accepts a Schema or a Hash spec for Schema.define
195
- def self.coerce(schema)
196
- case schema
197
- when Schema then schema
198
- when Hash then define(schema)
199
- else raise ArgumentError, "Expected a Herringbone::Schema or a Hash, got #{schema.class}"
200
- end
201
- end
187
+ INFER_SAMPLE = 1000
202
188
 
203
- # Infers a schema from sample rows (Hashes, or objects responding to #attributes or #to_h).
204
- # All fields are nullable. Integer -> int64, Integer mixed with Float -> double,
189
+ # Infers a schema from the first 1000 rows (Hashes, or objects responding to #attributes or
190
+ # #to_h). All fields are nullable. Integer -> int64, Integer mixed with Float -> double,
205
191
  # String/Symbol -> string (binary if not valid UTF-8), true/false -> boolean,
206
192
  # Time/DateTime -> timestamp(micros), Date -> date, BigDecimal -> decimal(38, max scale seen),
207
193
  # Hash -> struct, Array -> list. Columns that are nil in every sampled row become strings.
208
- # +types+ overrides inference for some columns, using the Hash spec of Schema.define:
209
- # Schema.infer(rows, types: { payload: :json, status: { type: :enum, values: %w[a b] } })
210
- def self.infer(rows, sample: 1000, types: {})
211
- sample_rows = rows.first(sample).map { |r| Inference.row_hash(r) }
194
+ # Fields declared in the block (Builder DSL) replace the inferred ones of the same name:
195
+ # Schema.infer(rows) { json :payload }
196
+ def self.infer(rows, &block)
197
+ sample_rows = rows.first(INFER_SAMPLE).map { |r| Inference.row_hash(r) }
212
198
  raise ArgumentError, "Cannot infer a schema from zero rows" if sample_rows.empty?
213
- overrides = types.to_h { |k, v| [k.to_s, v] }
199
+ overrides = Builder.new
200
+ overrides.instance_eval(&block) if block
201
+ declared = overrides.nodes.to_h { |n| [n.name, n] }
214
202
  names = sample_rows.flat_map { |r| r.keys.map(&:to_s) }.uniq
215
203
  nodes = names.map do |name|
216
- if overrides.key?(name)
217
- builder = Builder.new
218
- builder.fields(name => overrides[name])
219
- builder.nodes.first
220
- else
221
- values = sample_rows.map { |r| r.fetch(name) { r[name.to_sym] } }
222
- Inference.node_for(name, values)
223
- end
204
+ declared.delete(name) || Inference.node_for(name, sample_rows.map { |r| r.fetch(name) { r[name.to_sym] } })
224
205
  end
225
- new(Node.new(name: "schema", repetition: :required, children: nodes))
206
+ new(Node.new(name: "schema", repetition: :required, children: nodes + declared.values))
226
207
  end
227
208
 
228
209
  module Inference
@@ -274,7 +255,7 @@ module Herringbone
274
255
  else
275
256
  classes = values.map(&:class).uniq
276
257
  raise ArgumentError, "Cannot infer a Parquet type for #{name} from #{classes.map(&:name).join(", ")}; " \
277
- "pass types: { #{name}: ... }"
258
+ "declare it in a block: Schema.infer(rows) { string :#{name} }"
278
259
  end
279
260
  end
280
261
  end
@@ -314,7 +295,12 @@ module Herringbone
314
295
  else
315
296
  "group"
316
297
  end
317
- ann = node.logical_type ? node.logical_type.kind.first.to_s.upcase : Format::ConvertedType::NAMES[node.converted_type]
298
+ # A logical type this version does not know decodes as an empty union
299
+ kind = node.logical_type&.kind
300
+ ann = if kind then kind.first.to_s.upcase
301
+ elsif node.logical_type then "UNKNOWN LOGICAL TYPE"
302
+ else Format::ConvertedType::NAMES[node.converted_type]
303
+ end
318
304
  lines << "#{" " * depth}#{node.repetition} #{desc} #{node.name}#{ann ? " (#{ann})" : ""}"
319
305
  node.children&.each { |c| walk.call(c, depth + 1) }
320
306
  end
@@ -470,47 +456,6 @@ module Herringbone
470
456
  column(name, :enum, values: values, parquet_enum: parquet_enum, **opts)
471
457
  end
472
458
 
473
- # Declares fields from a Hash spec, see Schema.define
474
- def fields(spec)
475
- spec.each { |name, desc| declare(name, desc) }
476
- self
477
- end
478
-
479
- # Declares one field from a Hash spec value
480
- def declare(name, desc, null: true)
481
- case desc
482
- when Symbol, String
483
- column(name, desc.to_sym, null: null)
484
- when Array
485
- raise ArgumentError, "List spec for #{name} must have exactly one element type" unless desc.size == 1
486
- element = desc.first
487
- list(name, null: null) { declare(:element, element) }
488
- when Hash
489
- opts = desc.to_h { |k, v| [k.to_sym, v] }
490
- type = opts.delete(:type)
491
- null = opts.delete(:null) { null }
492
- # A Hash without :type is a struct; its keys (other than null:) are the fields
493
- return struct(name, null: null) { fields(opts) } if type.nil?
494
- case type.to_sym
495
- when :list
496
- of = opts.delete(:of) or raise ArgumentError, "list #{name} needs of:"
497
- element_null = opts.delete(:element_null) { true }
498
- list(name, null: null, **opts) { declare(:element, of, null: element_null) }
499
- when :map
500
- key = opts.delete(:key) { :string }
501
- value = opts.delete(:value) or raise ArgumentError, "map #{name} needs value:"
502
- value_null = opts.delete(:value_null) { true }
503
- map(name, key, null: null, **opts) { declare(:value, value, null: value_null) }
504
- when :struct
505
- struct(name, null: null) { fields(opts.fetch(:fields)) }
506
- else
507
- column(name, type.to_sym, null: null, **opts)
508
- end
509
- else
510
- raise ArgumentError, "Cannot declare #{name} from #{desc.inspect}"
511
- end
512
- end
513
-
514
459
  private
515
460
 
516
461
  def add(node)
@@ -53,7 +53,7 @@ module Herringbone
53
53
  # Physical attributes (type, logical type etc.) for a DSL type name
54
54
  def physical_attributes(type, **opts)
55
55
  case type.to_sym
56
- when :boolean, :bool then { type: T::BOOLEAN }
56
+ when :boolean then { type: T::BOOLEAN }
57
57
  when :int8 then int_type(8, true)
58
58
  when :int16 then int_type(16, true)
59
59
  when :int32 then { type: T::INT32 }
@@ -65,8 +65,8 @@ module Herringbone
65
65
  when :float then { type: T::FLOAT }
66
66
  when :double then { type: T::DOUBLE }
67
67
  when :float16 then { type: T::FIXED_LEN_BYTE_ARRAY, type_length: 2, logical_type: lt(float16: Format::Float16Type.new) }
68
- when :string, :utf8 then { type: T::BYTE_ARRAY, converted_type: C::UTF8, logical_type: lt(string: Format::StringType.new) }
69
- when :binary, :bytes then { type: T::BYTE_ARRAY }
68
+ when :string then { type: T::BYTE_ARRAY, converted_type: C::UTF8, logical_type: lt(string: Format::StringType.new) }
69
+ when :binary then { type: T::BYTE_ARRAY }
70
70
  when :json then { type: T::BYTE_ARRAY, converted_type: C::JSON, logical_type: lt(json: Format::JsonType.new) }
71
71
  when :bson then { type: T::BYTE_ARRAY, converted_type: C::BSON, logical_type: lt(bson: Format::BsonType.new) }
72
72
  when :enum
@@ -287,7 +287,7 @@ module Herringbone
287
287
  if !b && (type == T::INT32 || type == T::INT64)
288
288
  bits = type == T::INT32 ? 32 : 64
289
289
  check = int_checker(0, (1 << a) - 1)
290
- return ->(v) { Delta.wrap(check.call(v), bits) }
290
+ return ->(v) { Encodings::Delta.wrap(check.call(v), bits) }
291
291
  elsif type == T::INT32 || type == T::INT64
292
292
  return int_checker(-(1 << (a - 1)), (1 << (a - 1)) - 1)
293
293
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Herringbone
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end