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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/MANUAL.md +36 -6
- data/README.md +7 -5
- data/lib/herringbone/active_record.rb +1 -1
- data/lib/herringbone/codecs/zstd.rb +774 -0
- data/lib/herringbone/compression.rb +35 -6
- data/lib/herringbone/inferring_writer.rb +1 -1
- data/lib/herringbone/reader/column_chunk_reader.rb +2 -0
- data/lib/herringbone/schema/merge.rb +1 -1
- data/lib/herringbone/schema.rb +95 -12
- data/lib/herringbone/types.rb +20 -12
- data/lib/herringbone/version.rb +1 -1
- data/lib/herringbone.rb +1 -0
- metadata +3 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
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)
|
data/lib/herringbone/schema.rb
CHANGED
|
@@ -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
|
|
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,
|
|
667
|
-
# A
|
|
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
|
|
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+, +:
|
|
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)
|
data/lib/herringbone/types.rb
CHANGED
|
@@ -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
|
|
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:
|
|
134
|
-
logical_type: lt(time: Format::TimeType.new(is_adjusted_to_utc:
|
|
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:
|
|
139
|
-
logical_type: lt(timestamp: Format::TimestampType.new(is_adjusted_to_utc:
|
|
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]]"
|
|
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
|
data/lib/herringbone/version.rb
CHANGED
data/lib/herringbone.rb
CHANGED
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.
|
|
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-
|
|
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
|