quack-ruby 0.1.0-x86_64-darwin

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.
@@ -0,0 +1,72 @@
1
+ module Quack
2
+ # Preserve the SQL timestamp kind as well as Ruby's nanosecond-precision instant.
3
+ # Time equality/arithmetic continue to use Ruby's standard behavior.
4
+ class Timestamp < Time
5
+ attr_reader :duckdb_type
6
+
7
+ def self.from(value, type)
8
+ seconds = value.is_a?(DateTime) ? (value.new_offset(0) - DateTime.new(1970, 1, 1)) * 86_400 : value.to_r
9
+ at(seconds).utc.tap { |time| time.instance_variable_set(:@duckdb_type, String(type).freeze) }
10
+ end
11
+
12
+ def getutc
13
+ super.tap { |time| time.instance_variable_set(:@duckdb_type, duckdb_type) }
14
+ end
15
+
16
+ def getlocal(*args)
17
+ super.tap { |time| time.instance_variable_set(:@duckdb_type, duckdb_type) }
18
+ end
19
+
20
+ def +(seconds) = self.class.from(super, duckdb_type)
21
+ def -(other)
22
+ result = super
23
+ result.is_a?(Time) ? self.class.from(result, duckdb_type) : result
24
+ end
25
+ end
26
+
27
+ BitString = Data.define(:bits) do
28
+ def initialize(bits:)
29
+ bits = String(bits)
30
+ raise ArgumentError, "BIT values need at least one 0 or 1 bit" unless bits.match?(/\A[01]+\z/)
31
+ super(bits: bits.dup.freeze)
32
+ end
33
+ def to_s = bits
34
+ end
35
+
36
+ # Standard WKB is a lossless geometry value, including empty/Z/M geometries.
37
+ Geometry = Data.define(:wkb, :crs) do
38
+ def initialize(wkb:, crs: nil)
39
+ super(wkb: String(wkb).b.freeze, crs: crs&.dup&.freeze)
40
+ end
41
+ end
42
+
43
+ Map = Data.define(:entries, :key_type, :value_type) do
44
+ include Enumerable
45
+ def initialize(entries:, key_type: nil, value_type: nil)
46
+ super(entries: entries.to_h.freeze, key_type: key_type, value_type: value_type)
47
+ end
48
+ def each(&block) = entries.each(&block)
49
+ def [](key) = entries[key]
50
+ def to_h = entries.dup
51
+ def ==(other) = other.is_a?(Map) && entries == other.entries
52
+ alias eql? ==
53
+ def hash = [self.class, entries].hash
54
+ end
55
+
56
+ Union = Data.define(:tag, :value, :members) do
57
+ def initialize(tag:, value:, members: nil)
58
+ super(tag: String(tag).freeze, value: value, members: members)
59
+ end
60
+ def ==(other) = other.is_a?(Union) && tag == other.tag && value == other.value
61
+ alias eql? ==
62
+ def hash = [self.class, tag, value].hash
63
+ end
64
+
65
+ Tuple = Data.define(:values) do
66
+ def initialize(values:)
67
+ super(values: values.to_a.freeze)
68
+ end
69
+ def [](index) = values[index]
70
+ def to_a = values.dup
71
+ end
72
+ end
@@ -0,0 +1,64 @@
1
+ module Quack
2
+ class Result
3
+ include Enumerable
4
+ attr_reader :columns, :rows, :column_metadata
5
+
6
+ def initialize(metadata, rows)
7
+ @column_metadata = metadata.freeze
8
+ @columns = metadata.map { |col| col.fetch("name") }.freeze
9
+ @rows = rows
10
+ end
11
+ def each
12
+ return enum_for(__method__) unless block_given?
13
+ rows.each { |row| yield columns.zip(row).to_h }
14
+ self
15
+ end
16
+ def affected_rows
17
+ columns == ["Count"] && rows.first&.first.is_a?(Integer) ? rows.first.first : 0
18
+ end
19
+ end
20
+
21
+ class Stream
22
+ include Enumerable
23
+ attr_reader :column_metadata, :columns
24
+
25
+ def initialize(native)
26
+ @native = native
27
+ @column_metadata = native.columns.freeze
28
+ @columns = column_metadata.map { |col| col.fetch("name") }.freeze
29
+ @closed = false
30
+ end
31
+ def each_batch
32
+ return enum_for(__method__) unless block_given?
33
+ begin
34
+ while (batch = @native.next_batch)
35
+ batch.each do |row|
36
+ row.each_index { |index| row[index] = Codec.decode(row[index], column_metadata[index].fetch("duckdb_type")) }
37
+ end
38
+ yield batch
39
+ end
40
+ ensure
41
+ close
42
+ end
43
+ self
44
+ end
45
+ def each_row
46
+ return enum_for(__method__) unless block_given?
47
+ each_batch { |batch| batch.each { |row| yield row } }
48
+ self
49
+ end
50
+ def each
51
+ return enum_for(__method__) unless block_given?
52
+ each_row { |row| yield columns.zip(row).to_h }
53
+ self
54
+ end
55
+ def close
56
+ return if @closed
57
+ @native.close
58
+ @closed = true
59
+ nil
60
+ end
61
+ def closed? = @closed
62
+ def to_result = Result.new(column_metadata, each_row.to_a)
63
+ end
64
+ end
data/lib/quack/sql.rb ADDED
@@ -0,0 +1,183 @@
1
+ require "strscan"
2
+
3
+ module Quack
4
+ module SQL
5
+ module_function
6
+
7
+ def identifier(name)
8
+ '"' + String(name).gsub('"', '""') + '"'
9
+ end
10
+
11
+ def identifier_parts(name)
12
+ scanner = StringScanner.new(String(name))
13
+ parts = []
14
+ until scanner.eos?
15
+ if scanner.scan(/"/)
16
+ part = +""
17
+ closed = false
18
+ until scanner.eos?
19
+ char = scanner.getch
20
+ if char == '"'
21
+ if scanner.scan(/"/) then part << '"'
22
+ else closed = true; break
23
+ end
24
+ else part << char
25
+ end
26
+ end
27
+ raise ArgumentError, "unterminated quoted identifier" unless closed
28
+ else
29
+ part = scanner.scan(/[^."]+/)
30
+ end
31
+ raise ArgumentError, "invalid qualified identifier" if part.nil? || part.empty?
32
+ parts << part
33
+ break if scanner.eos?
34
+ raise ArgumentError, "invalid qualified identifier" unless scanner.scan(/\./) && !scanner.eos?
35
+ end
36
+ raise ArgumentError, "expected one to three identifier parts" unless (1..3).cover?(parts.length)
37
+ parts
38
+ end
39
+
40
+ def literal(value)
41
+ case value
42
+ when nil then "NULL"
43
+ when true then "TRUE"
44
+ when false then "FALSE"
45
+ when Integer
46
+ return "'#{value}'::BIGNUM" unless (-(2**127)...2**128).cover?(value)
47
+ value.negative? ? "(#{value})" : value.to_s
48
+ when BigDecimal
49
+ raise ArgumentError, "non-finite decimal parameter" unless value.finite?
50
+ text = value.to_s("F").sub(/\.0\z/, "")
51
+ integer, fraction = text.delete_prefix("-").split(".")
52
+ scale = fraction.to_s.length
53
+ width = [integer.sub(/\A0+/, "").length + scale, 1].max
54
+ raise RangeError, "DuckDB decimals support at most 38 digits" if width > 38
55
+ "'#{text}'::DECIMAL(#{width},#{scale})"
56
+ when Float
57
+ return "'NaN'::DOUBLE" if value.nan?
58
+ return value.positive? ? "'Infinity'::DOUBLE" : "'-Infinity'::DOUBLE" if value.infinite?
59
+ "'#{value}'::DOUBLE"
60
+ when String
61
+ if value.encoding == Encoding::BINARY
62
+ "from_hex('#{value.unpack1('H*')}')"
63
+ else
64
+ text = value.encode(Encoding::UTF_8)
65
+ text.include?("\0") ? "decode(from_hex('#{text.unpack1('H*')}'))" : "'#{text.gsub("'", "''")}'"
66
+ end
67
+ when Timestamp then timestamp_literal(value, value.duckdb_type)
68
+ when Time, DateTime
69
+ fraction = value.is_a?(DateTime) ? value.sec_fraction : value.to_r % 1
70
+ timestamp_literal(value, ((fraction * 1_000_000) % 1).zero? ? "TIMESTAMPTZ" : "TIMESTAMP_NS")
71
+ when Date then "DATE '#{value.iso8601}'"
72
+ when TimeOfDay
73
+ type = if value.utc_offset
74
+ "TIMETZ"
75
+ elsif (value.nanoseconds % 1000).zero?
76
+ "TIME"
77
+ else
78
+ "TIME_NS"
79
+ end
80
+ "#{type} '#{value}'"
81
+ when Interval then "INTERVAL '#{Integer(value.months)} months #{Integer(value.days)} days #{Integer(value.microseconds)} microseconds'"
82
+ when InfiniteTemporal
83
+ type = (%w[DATE] + TIMESTAMP_TYPES).include?(value.type) ? value.type : raise(ArgumentError, "invalid temporal type")
84
+ "#{type} '#{value.sign.negative? ? '-' : ''}infinity'"
85
+ when BitString then "#{literal(value.bits)}::BIT"
86
+ when Geometry
87
+ geometry = "ST_GeomFromWKB(#{literal(value.wkb)})"
88
+ value.crs ? "#{geometry}::GEOMETRY(#{literal(value.crs)})" : geometry
89
+ when Map
90
+ expression = "map([#{value.entries.keys.map { |key| literal(key) }.join(', ')}], [#{value.entries.values.map { |item| literal(item) }.join(', ')}])"
91
+ if value.key_type && value.value_type
92
+ seed = "map([#{Codec.type_expression(value.key_type)}][0:0], [#{Codec.type_expression(value.value_type)}][0:0])"
93
+ "cast_to_type(#{expression}, #{seed})"
94
+ else expression
95
+ end
96
+ when Union
97
+ active = literal(value.value)
98
+ active = "(#{active})::#{Codec.sql_type(value.members.fetch(value.tag))}" if value.members
99
+ expression = "union_value(#{identifier(value.tag)} := #{active})"
100
+ if value.members
101
+ "#{expression}::UNION(#{value.members.map { |name, type| "#{identifier(name)} #{Codec.sql_type(type)}" }.join(', ')})"
102
+ else expression
103
+ end
104
+ when Tuple then "row(#{value.values.map { |item| literal(item) }.join(', ')})"
105
+ when Variant then value.to_sql
106
+ when Array then "[#{value.map { |v| literal(v) }.join(', ')}]"
107
+ when Hash then "{#{value.map { |key, v| "#{literal(String(key))}: #{literal(v)}" }.join(', ')}}"
108
+ else raise TypeError, "unsupported Quack parameter type: #{value.class}"
109
+ end
110
+ end
111
+
112
+ TIMESTAMP_TYPES = %w[TIMESTAMP_S TIMESTAMP_MS TIMESTAMP TIMESTAMP_NS TIMESTAMPTZ TIMESTAMPTZ_NS].freeze
113
+
114
+ def timestamp_literal(value, type)
115
+ raise ArgumentError, "invalid timestamp type" unless TIMESTAMP_TYPES.include?(type)
116
+ return "#{type} '#{value.sign.negative? ? '-' : ''}infinity'" if value.is_a?(InfiniteTemporal)
117
+ Codec.pack(value, {"id" => Codec::SQL_NAMES.key(type) || "Timestamp"})
118
+ utc = value.is_a?(DateTime) ? value.new_offset(0).new_start(Date::GREGORIAN) : value.getutc
119
+ suffix = type.start_with?("TIMESTAMPTZ") ? "+00" : ""
120
+ "#{type} '#{utc.strftime('%Y-%m-%d %H:%M:%S.%N')}#{suffix}'"
121
+ end
122
+
123
+ # Parameter substitution is client-side. The lexer preserves UTF-8, quoted
124
+ # identifiers/strings, dollar strings, and nested comments verbatim.
125
+ def bind(sql, parameters = nil)
126
+ return String(sql) if parameters.nil?
127
+ unless parameters.is_a?(Array) || parameters.is_a?(Hash)
128
+ raise ArgumentError, "parameters must be an Array or Hash"
129
+ end
130
+ named = parameters.is_a?(Hash)
131
+ parameters = parameters.transform_keys(&:to_s) if named
132
+ scanner = StringScanner.new(String(sql))
133
+ output = +""
134
+ index = 0
135
+ until scanner.eos?
136
+ if (token = scanner.scan(/--[^\n]*(?:\n|\z)/))
137
+ output << token
138
+ elsif scanner.scan(%r{/\*})
139
+ output << "/*"
140
+ depth = 1
141
+ until scanner.eos? || depth.zero?
142
+ if scanner.scan(%r{/\*}) then output << "/*"; depth += 1
143
+ elsif scanner.scan(%r{\*/}) then output << "*/"; depth -= 1
144
+ else output << scanner.getch
145
+ end
146
+ end
147
+ elsif (start = scanner.scan(/(?:\b[Ee])?'|"/))
148
+ output << start
149
+ quote = start[-1]
150
+ escaped = start.length > 1
151
+ until scanner.eos?
152
+ char = scanner.getch
153
+ output << char
154
+ if escaped && char == "\\"
155
+ output << scanner.getch unless scanner.eos?
156
+ elsif char == quote
157
+ if scanner.peek(1) == quote then output << scanner.getch
158
+ else break
159
+ end
160
+ end
161
+ end
162
+ elsif (delimiter = scanner.scan(/\$(?:[A-Za-z_][A-Za-z_0-9]*)?\$/))
163
+ output << delimiter
164
+ output << (scanner.scan_until(/#{Regexp.escape(delimiter)}/) || scanner.rest.tap { scanner.terminate })
165
+ elsif (cast = scanner.scan(/::/))
166
+ output << cast
167
+ elsif named && (placeholder = scanner.scan(/:[A-Za-z_][A-Za-z_0-9]*/))
168
+ name = placeholder.delete_prefix(":")
169
+ raise ArgumentError, "missing parameter :#{name}" unless parameters.key?(name)
170
+ output << literal(parameters.fetch(name))
171
+ elsif !named && scanner.scan(/\?/)
172
+ raise ArgumentError, "more placeholders than parameters" if index >= parameters.length
173
+ output << literal(parameters[index])
174
+ index += 1
175
+ else
176
+ output << scanner.getch
177
+ end
178
+ end
179
+ raise ArgumentError, "#{index} placeholders for #{parameters.length} parameters" if !named && index != parameters.length
180
+ output
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,40 @@
1
+ module Quack
2
+ # Time-of-day can represent 24:00:00 and nanoseconds without inventing a date.
3
+ TimeOfDay = Data.define(:nanoseconds, :utc_offset) do
4
+ def to_s
5
+ seconds, fraction = nanoseconds.divmod(1_000_000_000)
6
+ hours, seconds = seconds.divmod(3600)
7
+ minutes, seconds = seconds.divmod(60)
8
+ text = format("%02d:%02d:%02d", hours, minutes, seconds)
9
+ text += ".#{format('%09d', fraction).sub(/0+\z/, '')}" unless fraction.zero?
10
+ unless utc_offset.nil?
11
+ offset = utc_offset.abs
12
+ text += format("%s%02d:%02d:%02d", utc_offset.negative? ? "-" : "+", offset / 3600, offset / 60 % 60, offset % 60)
13
+ end
14
+ text
15
+ end
16
+ end
17
+ Interval = Data.define(:months, :days, :microseconds)
18
+ InfiniteTemporal = Data.define(:type, :sign)
19
+
20
+ def self.__decimal(text) = BigDecimal(text)
21
+ def self.__date(days)
22
+ return InfiniteTemporal.new("DATE", days.negative? ? -1 : 1) if [2**31 - 1, -(2**31 - 1)].include?(days)
23
+ Date.new(1970, 1, 1, Date::GREGORIAN) + days
24
+ end
25
+ def self.__time(value, unit) = TimeOfDay.new(unit == "Micros" ? value * 1000 : value, nil)
26
+ def self.__time_tz(bits)
27
+ unsigned = bits & ((1 << 64) - 1)
28
+ # DuckDB packs microseconds into the high 40 bits and a biased UTC offset
29
+ # (16 hours minus one second minus offset seconds) into the low 24 bits.
30
+ TimeOfDay.new((unsigned >> 24) * 1000, 57_599 - (unsigned & 0xffffff))
31
+ end
32
+ def self.__timestamp(value, unit, timezone_utc)
33
+ return InfiniteTemporal.new(timezone_utc ? "TIMESTAMPTZ" : "TIMESTAMP", value.negative? ? -1 : 1) if [2**63 - 1, -(2**63 - 1)].include?(value)
34
+ divisor = {"Seconds" => 1, "Millis" => 1000, "Micros" => 1_000_000, "Nanos" => 1_000_000_000}.fetch(unit)
35
+ Time.at(Rational(value, divisor)).utc
36
+ end
37
+ def self.__interval(months, days, micros) = Interval.new(months, days, micros)
38
+
39
+ def self.__pack(value, type) = Codec.pack(value, type)
40
+ end
@@ -0,0 +1,298 @@
1
+ module Quack
2
+ # A logical, typed tree. Node types preserve distinctions such as UBIGINT,
3
+ # BLOB versus VARCHAR, and timestamp units inside heterogeneous containers.
4
+ class Variant
5
+ Node = Data.define(:type, :value)
6
+ attr_reader :node
7
+
8
+ INTEGER_TYPES = {3 => ["TINYINT", 1], 4 => ["SMALLINT", 2], 5 => ["INTEGER", 4], 6 => ["BIGINT", 8], 7 => ["HUGEINT", 16],
9
+ 8 => ["UTINYINT", 1], 9 => ["USMALLINT", 2], 10 => ["UINTEGER", 4], 11 => ["UBIGINT", 8], 12 => ["UHUGEINT", 16]}.freeze
10
+ TEMPORAL_TYPES = {20 => ["TIME", "Micros"], 21 => ["TIME_NS", "Nanos"],
11
+ 22 => ["TIMESTAMP_S", "Seconds"], 23 => ["TIMESTAMP_MS", "Millis"],
12
+ 24 => ["TIMESTAMP", "Micros"], 25 => ["TIMESTAMP_NS", "Nanos"],
13
+ 27 => ["TIMESTAMPTZ", "Micros"], 34 => ["TIMESTAMPTZ_NS", "Nanos"]}.freeze
14
+ STRING_TYPES = {16 => "VARCHAR", 17 => "BLOB", 31 => "BIGNUM", 32 => "BIT", 33 => "GEOMETRY"}.freeze
15
+
16
+ def initialize(value, type: nil)
17
+ @node = value.is_a?(Node) ? value : self.class.make_node(value, type)
18
+ self.class.validate_node(@node)
19
+ freeze
20
+ end
21
+
22
+ def type = node.type
23
+ def value = self.class.unwrap(node)
24
+ def ==(other) = other.is_a?(Variant) && node == other.node
25
+ alias eql? ==
26
+ def hash = [self.class, node].hash
27
+
28
+ def self.unwrap(node)
29
+ case node.type
30
+ when "OBJECT" then node.value.transform_values { |child| unwrap(child) }
31
+ when "ARRAY" then node.value.map { |child| unwrap(child) }
32
+ else node.value
33
+ end
34
+ end
35
+
36
+ def self.make_node(value, type = nil)
37
+ return value.node if value.is_a?(Variant)
38
+ return value if value.is_a?(Node)
39
+ type ||= case value
40
+ when nil then "NULL"
41
+ when true, false then "BOOLEAN"
42
+ when Integer then (-(2**63)...2**63).cover?(value) ? "BIGINT" : "BIGNUM"
43
+ when Float then "DOUBLE"
44
+ when BigDecimal
45
+ text = value.to_s("F").sub(/\.0\z/, "")
46
+ integer, fraction = text.delete_prefix("-").split(".")
47
+ scale = fraction.to_s.length
48
+ "DECIMAL(#{[integer.sub(/\A0+/, '').length + scale, 1].max},#{scale})"
49
+ when String then value.encoding == Encoding::BINARY ? "BLOB" : "VARCHAR"
50
+ when Timestamp then value.duckdb_type
51
+ when Time, DateTime then "TIMESTAMP_NS"
52
+ when Date then "DATE"
53
+ when TimeOfDay then value.utc_offset.nil? ? "TIME_NS" : "TIMETZ"
54
+ when Interval then "INTERVAL"
55
+ when InfiniteTemporal then value.type
56
+ when BitString then "BIT"
57
+ when Geometry then "GEOMETRY"
58
+ when Array, Tuple then "ARRAY"
59
+ when Hash then "OBJECT"
60
+ else raise TypeError, "unsupported VARIANT value #{value.class}"
61
+ end
62
+ type = String(type).upcase.freeze
63
+ children = case type
64
+ when "ARRAY" then (value.is_a?(Tuple) ? value.values : value).map { |child| make_node(child) }.freeze
65
+ when "OBJECT" then value.to_h.to_h { |key, child| [String(key).freeze, make_node(child)] }.freeze
66
+ else value
67
+ end
68
+ Node.new(type, children)
69
+ end
70
+
71
+ def self.validate_node(node, depth = 0)
72
+ raise ArgumentError, "VARIANT nesting exceeds 256 levels" if depth > 256
73
+ type = node.type
74
+ # Node is public; validate its names even when constructed directly.
75
+ valid = ["NULL", "BOOLEAN", "FLOAT", "DOUBLE", "UUID", "DATE", "TIMETZ", "INTERVAL", "OBJECT", "ARRAY"] + INTEGER_TYPES.values.map(&:first) + TEMPORAL_TYPES.values.map(&:first) + STRING_TYPES.values
76
+ unless valid.include?(type) || type.match?(/\ADECIMAL\((?:[1-9]|[12][0-9]|3[0-8]),(?:[0-9]|[12][0-9]|3[0-8])\)\z/)
77
+ raise ArgumentError, "unsupported VARIANT type #{type}"
78
+ end
79
+ case type
80
+ when "ARRAY" then node.value.each { |child| validate_node(child, depth + 1) }
81
+ when "OBJECT" then node.value.each_value { |child| validate_node(child, depth + 1) }
82
+ when "BOOLEAN"
83
+ raise TypeError, "VARIANT BOOLEAN must be true or false" unless node.value == true || node.value == false
84
+ end
85
+ end
86
+
87
+ class Reader
88
+ def initialize(data, offset)
89
+ @data, @offset = data, Integer(offset)
90
+ raise ProtocolError, "invalid VARIANT byte offset" unless (0..data.bytesize).cover?(@offset)
91
+ end
92
+ def bytes(count)
93
+ raise ProtocolError, "truncated VARIANT payload" if count.negative? || @offset + count > @data.bytesize
94
+ result = @data.byteslice(@offset, count)
95
+ @offset += count
96
+ result
97
+ end
98
+ def uint(count) = bytes(count).bytes.reverse.reduce(0) { |value, byte| (value << 8) | byte }
99
+ def int(count)
100
+ value = uint(count)
101
+ value >= 2**(count * 8 - 1) ? value - 2**(count * 8) : value
102
+ end
103
+ def varint
104
+ result = 0
105
+ 5.times do |index|
106
+ byte = uint(1)
107
+ result |= (byte & 127) << (index * 7)
108
+ return result if byte < 128 && result < 2**32
109
+ end
110
+ raise ProtocolError, "invalid VARIANT varint"
111
+ end
112
+ end
113
+
114
+ def self.from_physical(physical)
115
+ decode_node = lambda do |index, ancestors|
116
+ raise ProtocolError, "cyclic or excessively nested VARIANT" if ancestors.include?(index) || ancestors.length > 256
117
+ item = physical.fetch("values").fetch(index)
118
+ id = item.fetch("type_id")
119
+ reader = Reader.new(physical.fetch("data"), item.fetch("byte_offset"))
120
+ type, value = if INTEGER_TYPES.key?(id)
121
+ name, size = INTEGER_TYPES.fetch(id)
122
+ [name, name.start_with?("U") ? reader.uint(size) : reader.int(size)]
123
+ elsif TEMPORAL_TYPES.key?(id)
124
+ name, unit = TEMPORAL_TYPES.fetch(id)
125
+ raw = reader.int(8)
126
+ converted = if name.start_with?("TIME") && !name.start_with?("TIMESTAMP") && !name.start_with?("TIMESTAMPTZ")
127
+ Quack.__time(raw, unit)
128
+ else
129
+ timestamp = Quack.__timestamp(raw, unit, name.start_with?("TIMESTAMPTZ"))
130
+ timestamp.is_a?(InfiniteTemporal) ? timestamp : Timestamp.from(timestamp, name)
131
+ end
132
+ [name, converted]
133
+ elsif STRING_TYPES.key?(id)
134
+ name = STRING_TYPES.fetch(id)
135
+ data = reader.bytes(reader.varint)
136
+ converted = case name
137
+ when "VARCHAR"
138
+ text = data.dup.force_encoding(Encoding::UTF_8)
139
+ raise ProtocolError, "invalid VARIANT UTF-8" unless text.valid_encoding?
140
+ text
141
+ when "BIGNUM" then Codec.decode_bignum(data)
142
+ when "BIT" then Codec.decode_bit(data)
143
+ when "GEOMETRY" then Geometry.new(data)
144
+ else data.b
145
+ end
146
+ [name, converted]
147
+ else
148
+ case id
149
+ when 0 then ["NULL", nil]
150
+ when 1, 2 then ["BOOLEAN", id == 1]
151
+ when 13 then ["FLOAT", reader.bytes(4).unpack1("e")]
152
+ when 14 then ["DOUBLE", reader.bytes(8).unpack1("E")]
153
+ when 15
154
+ width, scale = reader.varint, reader.varint
155
+ raise ProtocolError, "invalid VARIANT decimal metadata" unless (1..38).cover?(width) && scale <= width
156
+ size = width <= 4 ? 2 : width <= 9 ? 4 : width <= 18 ? 8 : 16
157
+ integer = reader.int(size)
158
+ digits = integer.abs.to_s.rjust(scale + 1, "0")
159
+ text = scale.zero? ? digits : "#{digits[0...-scale]}.#{digits[-scale..]}"
160
+ ["DECIMAL(#{width},#{scale})", BigDecimal(integer.negative? ? "-#{text}" : text)]
161
+ when 18
162
+ hex = (reader.uint(16) ^ (1 << 127)).to_s(16).rjust(32, "0")
163
+ ["UUID", [hex[0, 8], hex[8, 4], hex[12, 4], hex[16, 4], hex[20, 12]].join("-")]
164
+ when 19 then ["DATE", Quack.__date(reader.int(4))]
165
+ when 26 then ["TIMETZ", Quack.__time_tz(reader.int(8))]
166
+ when 28 then ["INTERVAL", Interval.new(reader.int(4), reader.int(4), reader.int(8))]
167
+ when 29, 30
168
+ count = reader.varint
169
+ first = count.zero? ? 0 : reader.varint
170
+ children = physical.fetch("children")
171
+ raise ProtocolError, "invalid VARIANT child range" if first + count > children.length
172
+ items = children.slice(first, count).map do |child|
173
+ node = decode_node.call(child.fetch("values_index"), ancestors + [index])
174
+ id == 29 ? [physical.fetch("keys").fetch(child.fetch("keys_index")), node] : node
175
+ end
176
+ [id == 29 ? "OBJECT" : "ARRAY", id == 29 ? items.to_h.freeze : items.freeze]
177
+ else raise UnsupportedTypeError, "unsupported VARIANT logical type #{id}"
178
+ end
179
+ end
180
+ Node.new(type, value)
181
+ end
182
+ node = decode_node.call(0, [])
183
+ node.type == "NULL" ? nil : new(node)
184
+ rescue KeyError, IndexError, TypeError => error
185
+ raise ProtocolError, "invalid VARIANT structure: #{error.message}"
186
+ end
187
+
188
+ def to_sql
189
+ self.class.validate_node(node)
190
+ self.class.node_sql(node)
191
+ end
192
+
193
+ def self.node_sql(node)
194
+ case node.type
195
+ when "NULL" then "('null'::JSON)::VARIANT"
196
+ when "ARRAY" then "[#{node.value.map { |child| node_sql(child) }.join(', ')}]::VARIANT"
197
+ when "OBJECT"
198
+ return "('{}'::JSON)::VARIANT" if node.value.empty?
199
+ "{#{node.value.map { |key, child| "#{SQL.literal(key)}: #{node_sql(child)}" }.join(', ')}}::VARIANT"
200
+ else
201
+ literal = node.type.start_with?("TIMESTAMP") || node.type.start_with?("TIMESTAMPTZ") ? SQL.timestamp_literal(node.value, node.type) : SQL.literal(node.value)
202
+ "((#{literal})::#{node.type})::VARIANT"
203
+ end
204
+ end
205
+
206
+ def self.varint(value)
207
+ raise RangeError, "VARIANT length out of range" unless (0...2**32).cover?(value)
208
+ bytes = []
209
+ loop do
210
+ byte = value & 127
211
+ value >>= 7
212
+ bytes << (value.zero? ? byte : byte | 128)
213
+ break if value.zero?
214
+ end
215
+ bytes.pack("C*")
216
+ end
217
+
218
+ def self.integer_bytes(value, size, signed: true)
219
+ range = signed ? (-(2**(size * 8 - 1))...2**(size * 8 - 1)) : (0...2**(size * 8))
220
+ raise RangeError, "VARIANT integer out of range" unless value.is_a?(Integer) && range.cover?(value)
221
+ Array.new(size) { |index| (value >> (index * 8)) & 255 }.pack("C*")
222
+ end
223
+
224
+ def to_physical
225
+ keys = []
226
+ visit_keys = lambda do |node|
227
+ case node.type
228
+ when "OBJECT"
229
+ keys.concat(node.value.keys)
230
+ node.value.each_value { |child| visit_keys.call(child) }
231
+ when "ARRAY" then node.value.each { |child| visit_keys.call(child) }
232
+ end
233
+ end
234
+ visit_keys.call(node)
235
+ keys = keys.uniq.sort
236
+ key_indices = keys.each_with_index.to_h
237
+ physical = {"keys" => keys, "children" => [], "values" => [], "data" => +"".b}
238
+ encode = lambda do |node|
239
+ index = physical.fetch("values").length
240
+ physical.fetch("values") << nil
241
+ type, value = node.type, node.value
242
+ id, bytes = if (entry = INTEGER_TYPES.find { |_id, (name, _size)| name == type })
243
+ [entry.first, self.class.integer_bytes(value, entry.last.last, signed: !type.start_with?("U"))]
244
+ elsif (entry = TEMPORAL_TYPES.find { |_id, (name, _unit)| name == type })
245
+ spec = {"id" => Codec::SQL_NAMES.key(type) || type.capitalize}
246
+ spec["id"] = "Timestamp" if type == "TIMESTAMP"
247
+ packed = Codec.pack(value, spec)
248
+ [entry.first, self.class.integer_bytes(packed[1], 8)]
249
+ elsif (entry = STRING_TYPES.find { |_id, name| name == type })
250
+ payload = case type
251
+ when "BIGNUM" then Codec.encode_bignum(value)
252
+ when "BIT" then Codec.encode_bit(value)
253
+ when "GEOMETRY" then value.wkb
254
+ else String(value).b
255
+ end
256
+ [entry.first, self.class.varint(payload.bytesize) + payload]
257
+ else
258
+ case type
259
+ when "NULL" then [0, "".b]
260
+ when "BOOLEAN" then [value ? 1 : 2, "".b]
261
+ when "FLOAT" then [13, [value].pack("e")]
262
+ when "DOUBLE" then [14, [value].pack("E")]
263
+ when /\ADECIMAL\((\d+),(\d+)\)\z/
264
+ width, scale = Regexp.last_match.captures.map(&:to_i)
265
+ packed = Codec.pack(value, {"id" => "Decimal", "width" => width, "scale" => scale})
266
+ integer = (BigDecimal(packed[1]) * 10**scale).to_i
267
+ size = width <= 4 ? 2 : width <= 9 ? 4 : width <= 18 ? 8 : 16
268
+ [15, self.class.varint(width) + self.class.varint(scale) + self.class.integer_bytes(integer, size)]
269
+ when "UUID"
270
+ hex = String(value).delete("-")
271
+ raise ArgumentError, "invalid VARIANT UUID" unless hex.match?(/\A[0-9a-fA-F]{32}\z/)
272
+ [18, self.class.integer_bytes(hex.to_i(16) ^ (1 << 127), 16, signed: false)]
273
+ when "DATE"
274
+ [19, self.class.integer_bytes(Codec.pack(value, {"id" => "Date"})[1], 4)]
275
+ when "TIMETZ"
276
+ [26, self.class.integer_bytes(Codec.pack(value, {"id" => "TimeTz"})[1], 8)]
277
+ when "INTERVAL"
278
+ [28, self.class.integer_bytes(value.months, 4) + self.class.integer_bytes(value.days, 4) + self.class.integer_bytes(value.microseconds, 8)]
279
+ when "OBJECT", "ARRAY"
280
+ children = type == "OBJECT" ? value.to_a : value.map { |child| [nil, child] }
281
+ first = physical.fetch("children").length
282
+ physical.fetch("children").concat(Array.new(children.length))
283
+ children.each_with_index do |(key, child), offset|
284
+ physical.fetch("children")[first + offset] = {"keys_index" => key && key_indices.fetch(key), "values_index" => encode.call(child)}
285
+ end
286
+ [type == "OBJECT" ? 29 : 30, self.class.varint(children.length) + (children.empty? ? "".b : self.class.varint(first))]
287
+ else raise ArgumentError, "unsupported VARIANT type #{type}"
288
+ end
289
+ end
290
+ physical.fetch("values")[index] = {"type_id" => id, "byte_offset" => physical.fetch("data").bytesize}
291
+ physical.fetch("data") << bytes
292
+ index
293
+ end
294
+ encode.call(node)
295
+ physical
296
+ end
297
+ end
298
+ end