herringbone 0.7.0 → 0.9.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.
@@ -4,7 +4,8 @@ require "zlib"
4
4
  require "stringio"
5
5
 
6
6
  module Herringbone
7
- # Raised when a file uses (or a writer asks for) a codec whose library is not loaded
7
+ # Raised when a file uses (or a writer asks for) a codec whose library is not loaded. The message
8
+ # says which gem to install, and that it has to be required before the read or write.
8
9
  class MissingCodecError < UnsupportedError
9
10
  # @return [String] codec name (+"ZSTD"+) and name of the gem providing it (+"zstd-ruby"+)
10
11
  attr_reader :codec, :gem_name
@@ -12,18 +13,37 @@ module Herringbone
12
13
  # @param codec [String] codec name, as shown in the message
13
14
  # @param gem_name [String] gem to add to the Gemfile
14
15
  # @param path [String] what to require to load the gem
15
- def initialize(codec, gem_name, path)
16
+ # @param column [String, nil] dotted path of the column being read, when there is one
17
+ def initialize(codec, gem_name, path, column: nil)
16
18
  @codec = codec
17
19
  @gem_name = gem_name
18
- super("#{codec} compression needs the \"#{gem_name}\" gem, which is not loaded. Add " \
19
- "`gem \"#{gem_name}\"` to your Gemfile and `require \"#{path}\"` to use #{codec}.")
20
+ @path = path
21
+ where = column ? " to decompress column #{column}" : ""
22
+ super(<<~MESSAGE.chomp)
23
+ #{codec} compression needs the "#{gem_name}" gem#{where}, but it is not loaded.
24
+ Herringbone does not load optional codec gems by itself. To use #{codec}:
25
+
26
+ 1. Install the gem: add `gem "#{gem_name}"` to your Gemfile, or run `gem install #{gem_name}`.
27
+ 2. Load it with `require "#{path}"` before the read or write that needs #{codec},
28
+ for instance at the top of your script or in an initializer.
29
+ MESSAGE
30
+ end
31
+
32
+ # The same error, naming the column that could not be decompressed
33
+ #
34
+ # @param column [String] dotted path of the column
35
+ # @return [MissingCodecError]
36
+ # @api private
37
+ def for_column(column)
38
+ self.class.new(@codec, @gem_name, @path, column: column).tap { |e| e.set_backtrace(backtrace) if backtrace }
20
39
  end
21
40
  end
22
41
 
23
42
  # Dispatches page (de)compression by Parquet codec id. Snappy and LZ4 are pure Ruby and GZIP
24
43
  # uses zlib, so those always work. LZO is pure Ruby too, but can only be read. ZSTD and Brotli
25
44
  # come from optional gems (zstd-ruby, brotli), which the application requires; if they are
26
- # not loaded, MissingCodecError says what to add.
45
+ # not loaded, MissingCodecError says what to add. ZSTD is the exception when reading: without
46
+ # zstd-ruby, pages are decompressed by Codecs::Zstd, in pure Ruby.
27
47
  # (Snappy also uses the optional snappy gem when it is loaded, see Codecs::Snappy.)
28
48
  #
29
49
  # @api private
@@ -137,7 +157,7 @@ module Herringbone
137
157
  when Format::Codec::LZ4_RAW then Codecs::LZ4.decompress_block(data, uncompressed_size)
138
158
  when Format::Codec::LZ4 then Codecs::LZ4.decompress_hadoop(data, uncompressed_size)
139
159
  when Format::Codec::LZO then Codecs::LZO.decompress_hadoop(data, uncompressed_size)
140
- when Format::Codec::ZSTD then library(codec).decompress(data)
160
+ when Format::Codec::ZSTD then unzstd(data, uncompressed_size)
141
161
  when Format::Codec::BROTLI then library(codec).inflate(data)
142
162
  else
143
163
  raise UnsupportedError, "Unsupported compression codec #{Format::Codec::NAMES[codec] || codec}"
@@ -204,6 +224,15 @@ module Herringbone
204
224
  stream.finish
205
225
  end
206
226
 
227
+ # @param data [String] one or more Zstandard frames
228
+ # @param uncompressed_size [Integer] expected decompressed size in bytes
229
+ # @return [String] decompressed bytes, from zstd-ruby when it is loaded, else pure Ruby
230
+ # @raise [Codecs::Zstd::Error] on corrupt data, when decompressed in pure Ruby
231
+ def unzstd(data, uncompressed_size)
232
+ zstd = loaded_library(:Zstd)
233
+ zstd ? zstd.decompress(data) : Codecs::Zstd.decompress(data, uncompressed_size)
234
+ end
235
+
207
236
  # Codec output relabelled as binary without copying it, unless it is frozen
208
237
  #
209
238
  # @param str [String] bytes
@@ -158,7 +158,7 @@ module Herringbone
158
158
  type = case kind
159
159
  when nil then Format::Type::NAMES[column.type].downcase
160
160
  when :integer then "#{details[1] ? "int" : "uint"}#{details[0]}"
161
- when :timestamp, :time then "#{kind} (#{details[0]})"
161
+ when :timestamp, :time then "#{"local_" unless details[1]}#{kind} (#{details[0]})"
162
162
  when :decimal then "decimal(#{details[1]}, #{details[0]})"
163
163
  else kind.to_s
164
164
  end
@@ -303,6 +303,8 @@ module Herringbone
303
303
  # @raise [UnsupportedError] for an unsupported codec, naming the column
304
304
  def decompress(body, size)
305
305
  Compression.decompress(@meta.codec, body, size)
306
+ rescue MissingCodecError => e
307
+ raise e.for_column(@column.dotted_path)
306
308
  rescue UnsupportedError => e
307
309
  raise e, "#{e.message} (column #{@column.dotted_path})"
308
310
  end
@@ -283,7 +283,7 @@ module Herringbone
283
283
  in [:int, 64, true] then Types.physical_attributes(:int64)
284
284
  in [:int, bits, signed] then Types.int_type(bits, signed)
285
285
  in [:float, bits] then Types.physical_attributes({16 => :float16, 32 => :float, 64 => :double}.fetch(bits))
286
- in [:timestamp | :time => kind, unit, utc] then Types.physical_attributes(kind, unit: unit, utc: utc)
286
+ in [:timestamp | :time => kind, unit, utc] then Types.physical_attributes(utc ? kind : :"local_#{kind}", unit: unit)
287
287
  in [:text, :enum] then Types.physical_attributes(:enum, parquet_enum: true)
288
288
  in [:text, kind] then Types.physical_attributes(kind)
289
289
  in [:decimal, precision, scale] then Types.physical_attributes(:decimal, precision: precision, scale: scale)
@@ -470,6 +470,30 @@ module Herringbone
470
470
  end
471
471
  alias_method :&, :intersect
472
472
 
473
+ # The given top-level fields, in the order of this schema.
474
+ #
475
+ # schema.only(:id, :name).fields.map(&:name) # => ["id", "name"]
476
+ #
477
+ # @param names [Array<String, Symbol>] names of top-level fields
478
+ # @return [Schema] a new schema, sharing no nodes with this one
479
+ # @raise [ArgumentError] when a name is not a top-level field, or no name is given
480
+ def only(*names)
481
+ keep = field_names!(names, "only")
482
+ subset { |f| keep.include?(f.name) }
483
+ end
484
+
485
+ # Every top-level field but the given ones, in the order of this schema.
486
+ #
487
+ # schema.except(:ssn).fields.map(&:name) # => ["id", "name"]
488
+ #
489
+ # @param names [Array<String, Symbol>] names of top-level fields
490
+ # @return [Schema] a new schema, sharing no nodes with this one
491
+ # @raise [ArgumentError] when a name is not a top-level field, or all fields would be left out
492
+ def except(*names)
493
+ drop = field_names!(names, "except")
494
+ subset { |f| !drop.include?(f.name) }
495
+ end
496
+
473
497
  # @return [String] one line per node, indented by depth: repetition, physical type, name and
474
498
  # annotation
475
499
  def inspect
@@ -501,6 +525,40 @@ module Herringbone
501
525
 
502
526
  private
503
527
 
528
+ # @param names [Array<String, Symbol, Array>] names given to #only or #except
529
+ # @param method [String] the method, for the error message
530
+ # @return [Array<String>] the names as Strings
531
+ # @raise [ArgumentError] when a name is not a top-level field, or no name is given
532
+ def field_names!(names, method)
533
+ names = names.flatten.map(&:to_s)
534
+ raise ArgumentError, "Schema##{method} needs the names of top-level fields, e.g. schema.#{method}(:id, :name)" if names.empty?
535
+ missing = names - @fields.map(&:name)
536
+ unless missing.empty?
537
+ raise ArgumentError, "Schema##{method}: no top-level field #{missing.join(", ")}. " \
538
+ "The schema has #{@fields.map(&:name).join(", ")}"
539
+ end
540
+ names
541
+ end
542
+
543
+ # @yieldparam field [Field] a top-level field
544
+ # @yieldreturn [Boolean] whether to keep it
545
+ # @return [Schema] a schema of copies of the kept fields' nodes
546
+ # @raise [ArgumentError] when no field is kept
547
+ def subset
548
+ nodes = @fields.select { |f| yield f }.map { |f| copy_node(f.node) }
549
+ raise ArgumentError, "A schema needs at least one field, and this would leave out all of #{@fields.map(&:name).join(", ")}" if nodes.empty?
550
+ Schema.new(Node.new(name: @root.name, repetition: :required, children: nodes))
551
+ end
552
+
553
+ # @param node [Node] node to copy, with its children
554
+ # @return [Node] a copy sharing no nodes with the original
555
+ def copy_node(node)
556
+ Node.new(name: node.name, repetition: node.repetition, type: node.type, type_length: node.type_length,
557
+ converted_type: node.converted_type, logical_type: node.logical_type, scale: node.scale,
558
+ precision: node.precision, field_id: node.field_id, enum_values: node.enum_values,
559
+ children: node.children&.map { |c| copy_node(c) })
560
+ end
561
+
504
562
  # Appends a Column for every leaf under +node+, depth-first. A non-required node adds one
505
563
  # definition level and a repeated node one repetition level.
506
564
  #
@@ -653,28 +711,50 @@ module Herringbone
653
711
  define_method(t) { |name, **opts| column(name, t, **opts) }
654
712
  end
655
713
 
656
- # A TIME column; millis are stored as INT32, micros and nanos as INT64.
714
+ # A TIME column. A Time is converted to UTC when written, and its time of day in UTC is
715
+ # stored. Millis are stored as INT32, micros and nanos as INT64.
657
716
  #
658
717
  # @param name [String, Symbol] field name
659
718
  # @param unit [Symbol] +:millis+, +:micros+ or +:nanos+
660
- # @param utc [Boolean] the isAdjustedToUTC flag of the logical type
661
719
  # @param opts [Hash{Symbol => Object}] options of #column
662
720
  # @option opts [Boolean] :null (true) whether the field is nullable
663
721
  # @option opts [Integer, nil] :field_id (nil) field id to store in the schema
664
722
  # @return [Node] the added node
665
723
  # @raise [ArgumentError] for an unknown unit or a duplicate name
666
- def time(name, unit: :micros, utc: true, **opts) = column(name, :time, unit: unit, utc: utc, **opts)
667
- # A TIMESTAMP column, stored as INT64.
724
+ def time(name, unit: :micros, **opts) = column(name, :time, unit: unit, **opts)
725
+ # A TIME column for wall clock times of day. A Time is written as its time of day in its
726
+ # own zone, without converting it to UTC. Millis are stored as INT32, micros and nanos as INT64.
668
727
  #
669
728
  # @param name [String, Symbol] field name
670
729
  # @param unit [Symbol] +:millis+, +:micros+ or +:nanos+
671
- # @param utc [Boolean] the isAdjustedToUTC flag: true for instants, false for local date-times
672
730
  # @param opts [Hash{Symbol => Object}] options of #column
673
731
  # @option opts [Boolean] :null (true) whether the field is nullable
674
732
  # @option opts [Integer, nil] :field_id (nil) field id to store in the schema
675
733
  # @return [Node] the added node
676
734
  # @raise [ArgumentError] for an unknown unit or a duplicate name
677
- def timestamp(name, unit: :micros, utc: true, **opts) = column(name, :timestamp, unit: unit, utc: utc, **opts)
735
+ def local_time(name, unit: :micros, **opts) = column(name, :local_time, unit: unit, **opts)
736
+ # A TIMESTAMP column, stored as INT64. A Time in any zone is converted to UTC when written,
737
+ # so the column holds instants.
738
+ #
739
+ # @param name [String, Symbol] field name
740
+ # @param unit [Symbol] +:millis+, +:micros+ or +:nanos+
741
+ # @param opts [Hash{Symbol => Object}] options of #column
742
+ # @option opts [Boolean] :null (true) whether the field is nullable
743
+ # @option opts [Integer, nil] :field_id (nil) field id to store in the schema
744
+ # @return [Node] the added node
745
+ # @raise [ArgumentError] for an unknown unit or a duplicate name
746
+ def timestamp(name, unit: :micros, **opts) = column(name, :timestamp, unit: unit, **opts)
747
+ # A TIMESTAMP column for wall clock date-times, stored as INT64. A Time is written as its
748
+ # date and time in its own zone, without converting it to UTC, and no zone is stored.
749
+ #
750
+ # @param name [String, Symbol] field name
751
+ # @param unit [Symbol] +:millis+, +:micros+ or +:nanos+
752
+ # @param opts [Hash{Symbol => Object}] options of #column
753
+ # @option opts [Boolean] :null (true) whether the field is nullable
754
+ # @option opts [Integer, nil] :field_id (nil) field id to store in the schema
755
+ # @return [Node] the added node
756
+ # @raise [ArgumentError] for an unknown unit or a duplicate name
757
+ def local_timestamp(name, unit: :micros, **opts) = column(name, :local_timestamp, unit: unit, **opts)
678
758
  # A DECIMAL column. Up to 9 digits are stored as INT32, up to 18 as INT64, more as a
679
759
  # FIXED_LEN_BYTE_ARRAY of the minimal width, unless +physical:+ says otherwise.
680
760
  #
@@ -734,7 +814,6 @@ module Herringbone
734
814
  # @option type_opts [Integer] :precision decimal precision
735
815
  # @option type_opts [Integer] :scale decimal scale
736
816
  # @option type_opts [Symbol] :unit time or timestamp unit
737
- # @option type_opts [Boolean] :utc time or timestamp UTC adjustment
738
817
  # @option type_opts [Integer] :length FIXED_LEN_BYTE_ARRAY width
739
818
  # @yield [list] declares the struct's fields for a +:struct+ element, or exactly one field
740
819
  # (renamed to "element") when +type+ is nil
@@ -773,7 +852,6 @@ module Herringbone
773
852
  # @option type_opts [Integer] :precision decimal precision
774
853
  # @option type_opts [Integer] :scale decimal scale
775
854
  # @option type_opts [Symbol] :unit time or timestamp unit
776
- # @option type_opts [Boolean] :utc time or timestamp UTC adjustment
777
855
  # @option type_opts [Integer] :length FIXED_LEN_BYTE_ARRAY width
778
856
  # @yield [map] declares the struct's fields for a +:struct+ value, or exactly one field
779
857
  # (renamed to "value") when +value_type+ is nil
@@ -795,8 +873,8 @@ module Herringbone
795
873
  # Generic column declaration: s.column :name, :int32, null: false
796
874
  #
797
875
  # @param name [String, Symbol] field name
798
- # @param type [Symbol, String] DSL type: one of PRIMITIVES, or +:time+, +:timestamp+,
799
- # +:decimal+, +:fixed+, +:enum+ with their options
876
+ # @param type [Symbol, String] DSL type: one of PRIMITIVES, or +:time+, +:local_time+,
877
+ # +:timestamp+, +:local_timestamp+, +:decimal+, +:fixed+, +:enum+ with their options
800
878
  # @param null [Boolean] whether the field is nullable (optional rather than required)
801
879
  # @param field_id [Integer, nil] field id to store in the schema
802
880
  # @param opts [Hash{Symbol => Object}] type options
@@ -804,7 +882,6 @@ module Herringbone
804
882
  # @option opts [Integer] :scale (0) decimal scale
805
883
  # @option opts [Symbol] :physical decimal storage: +:int32+, +:int64+, +:binary+ or +:fixed+
806
884
  # @option opts [Symbol] :unit (:micros) time or timestamp unit
807
- # @option opts [Boolean] :utc (true) time or timestamp UTC adjustment
808
885
  # @option opts [Integer] :length FIXED_LEN_BYTE_ARRAY width (required for +:fixed+)
809
886
  # @option opts [Array<String>, Hash] :values allowed values, see #enum
810
887
  # @option opts [Boolean] :parquet_enum (false) ENUM instead of STRING annotation, see #enum
@@ -908,8 +985,14 @@ module Herringbone
908
985
  # and the rest go to Types.physical_attributes
909
986
  # @option opts [Array<String>, Hash] :values allowed values of a string/enum column
910
987
  # @return [Node] leaf node, not added to #nodes
911
- # @raise [ArgumentError] for an unknown type or invalid type options
988
+ # @raise [ArgumentError] for an unknown type or invalid type options, or for +utc:+, which
989
+ # the local_time and local_timestamp types replace
912
990
  def leaf_node(name, type, repetition, opts)
991
+ if opts.key?(:utc)
992
+ raise ArgumentError, "#{name}: the utc: option is gone. #{type} columns always convert Times to UTC when " \
993
+ "writing. To keep the wall clock time as given, without converting it, declare a local_#{type}, " \
994
+ "e.g. s.local_#{type} :#{name} or s.column :#{name}, :local_#{type}"
995
+ end
913
996
  opts = opts.dup
914
997
  values = opts.delete(:values)
915
998
  attrs = Types.physical_attributes(type.to_sym, **opts)
@@ -83,13 +83,15 @@ module Herringbone
83
83
  end
84
84
 
85
85
  # Physical attributes (type, logical type etc.) for a DSL type name
86
+ #
87
+ # +:time+ and +:timestamp+ hold values converted to UTC and are flagged isAdjustedToUTC;
88
+ # +:local_time+ and +:local_timestamp+ hold wall clock values, are not flagged and get no
89
+ # legacy converted type.
86
90
  # @param type [Symbol, String] column type name, e.g. +:string+, +:int64+, +:decimal+, +:timestamp+
87
91
  # @param opts [Hash{Symbol => Object}] type-specific options
88
92
  # @option opts [Boolean] :parquet_enum for +:enum+, annotate as ENUM instead of STRING
89
93
  # @option opts [Integer] :length byte length for +:fixed+ (required)
90
- # @option opts [Symbol] :unit (:micros) for +:time+ and +:timestamp+, see {time_unit}
91
- # @option opts [Boolean] :utc (true) for +:time+ and +:timestamp+, sets +is_adjusted_to_utc+;
92
- # the legacy converted type is only written when true
94
+ # @option opts [Symbol] :unit (:micros) for the time and timestamp types, see {time_unit}
93
95
  # @option opts [Integer] :precision number of decimal digits for +:decimal+ (required)
94
96
  # @option opts [Integer] :scale (0) digits after the decimal point for +:decimal+
95
97
  # @option opts [Symbol] :physical for +:decimal+, force +:int32+, +:int64+, +:binary+ or
@@ -127,16 +129,18 @@ module Herringbone
127
129
  when :int96 then {type: T::INT96}
128
130
  when :fixed
129
131
  {type: T::FIXED_LEN_BYTE_ARRAY, type_length: Integer(opts.fetch(:length))}
130
- when :time
132
+ when :time, :local_time
133
+ utc = type.to_sym == :time
131
134
  unit = time_unit(opts.fetch(:unit, :micros))
132
135
  converted = {millis: C::TIME_MILLIS, micros: C::TIME_MICROS}[unit.to_sym]
133
- {type: unit.millis ? T::INT32 : T::INT64, converted_type: opts.fetch(:utc, true) ? converted : nil,
134
- logical_type: lt(time: Format::TimeType.new(is_adjusted_to_utc: opts.fetch(:utc, true), unit: unit))}
135
- when :timestamp
136
+ {type: unit.millis ? T::INT32 : T::INT64, converted_type: utc ? converted : nil,
137
+ logical_type: lt(time: Format::TimeType.new(is_adjusted_to_utc: utc, unit: unit))}
138
+ when :timestamp, :local_timestamp
139
+ utc = type.to_sym == :timestamp
136
140
  unit = time_unit(opts.fetch(:unit, :micros))
137
141
  converted = {millis: C::TIMESTAMP_MILLIS, micros: C::TIMESTAMP_MICROS}[unit.to_sym]
138
- {type: T::INT64, converted_type: opts.fetch(:utc, true) ? converted : nil,
139
- logical_type: lt(timestamp: Format::TimestampType.new(is_adjusted_to_utc: opts.fetch(:utc, true), unit: unit))}
142
+ {type: T::INT64, converted_type: utc ? converted : nil,
143
+ logical_type: lt(timestamp: Format::TimestampType.new(is_adjusted_to_utc: utc, unit: unit))}
140
144
  when :decimal
141
145
  precision = Integer(opts.fetch(:precision))
142
146
  scale = Integer(opts.fetch(:scale, 0))
@@ -337,7 +341,8 @@ module Herringbone
337
341
  # date: Date, Time/DateTime (its calendar date), ISO-8601 String, Integer days since epoch
338
342
  # timestamp: Time, DateTime, ActiveSupport::TimeWithZone, Date (midnight UTC), ISO-8601 String,
339
343
  # Integer in the column's unit
340
- # time: Time (its time of day), "HH:MM[:SS[.fraction]]" String, Integer in the column's unit
344
+ # time: Time (its time of day, in UTC unless the column is local), "HH:MM[:SS[.fraction]]"
345
+ # String, Integer in the column's unit
341
346
  # json: String (used as-is) or any other object (serialized with JSON.generate)
342
347
  # string: String, Symbol or anything responding to to_s
343
348
  # boolean: true/false, 1/0, "true"/"false", "t"/"f", "1"/"0", "yes"/"no"
@@ -357,7 +362,7 @@ module Herringbone
357
362
  when :timestamp
358
363
  return timestamp_writer(a, b)
359
364
  when :time
360
- return time_writer(a) if type == T::INT32 || type == T::INT64
365
+ return time_writer(a, b) if type == T::INT32 || type == T::INT64
361
366
  when :json
362
367
  return ->(v) { v.is_a?(String) ? v : JSON.generate(v) }
363
368
  when :string, :enum
@@ -483,9 +488,11 @@ module Herringbone
483
488
  # anything responding to +hour+ and +nsec+ (Time, DateTime), and raises ArgumentError or
484
489
  # RangeError for anything else or a time past 23:59:59.
485
490
  # @param unit [Symbol] +:millis+, +:micros+ or +:nanos+
491
+ # @param utc [Boolean] true to convert a Time to UTC before taking its time of day, false to
492
+ # take the time of day in the Time's own zone
486
493
  # @return [Proc] one-argument lambda
487
494
  # @raise [KeyError] for an unknown unit
488
- def time_writer(unit)
495
+ def time_writer(unit, utc)
489
496
  mult = UNIT_DIVISORS.fetch(unit)
490
497
  lambda do |v|
491
498
  return v if v.is_a?(Integer)
@@ -496,6 +503,7 @@ module Herringbone
496
503
  secs = m[1].to_i * 3600 + m[2].to_i * 60 + m[3].to_i
497
504
  else
498
505
  raise ArgumentError, "expected a Time, got #{v.class}" unless v.respond_to?(:hour) && v.respond_to?(:nsec)
506
+ v = v.getutc if utc && v.respond_to?(:getutc)
499
507
  nanos = v.nsec
500
508
  secs = v.hour * 3600 + v.min * 60 + v.sec
501
509
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Herringbone
4
4
  # Gem version, also written to the +created_by+ field of every file's footer
5
- VERSION = "0.7.0"
5
+ VERSION = "0.9.0"
6
6
  end
data/lib/herringbone.rb CHANGED
@@ -110,6 +110,7 @@ module Herringbone
110
110
  autoload :LZ4, "#{LIB}/codecs/lz4"
111
111
  autoload :LZO, "#{LIB}/codecs/lzo"
112
112
  autoload :Snappy, "#{LIB}/codecs/snappy"
113
+ autoload :Zstd, "#{LIB}/codecs/zstd"
113
114
  end
114
115
 
115
116
  module Encodings
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: herringbone
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julik Tarkhanov
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-09 00:00:00.000000000 Z
10
+ date: 2026-10-17 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bigdecimal
@@ -45,6 +45,7 @@ files:
45
45
  - lib/herringbone/codecs/lz4.rb
46
46
  - lib/herringbone/codecs/lzo.rb
47
47
  - lib/herringbone/codecs/snappy.rb
48
+ - lib/herringbone/codecs/zstd.rb
48
49
  - lib/herringbone/combiner.rb
49
50
  - lib/herringbone/compression.rb
50
51
  - lib/herringbone/encodings/delta.rb