clickhouse-ruby 0.1.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 +7 -0
- data/CHANGELOG.md +80 -0
- data/LICENSE +21 -0
- data/README.md +251 -0
- data/lib/clickhouse_ruby/active_record/arel_visitor.rb +468 -0
- data/lib/clickhouse_ruby/active_record/connection_adapter.rb +723 -0
- data/lib/clickhouse_ruby/active_record/railtie.rb +192 -0
- data/lib/clickhouse_ruby/active_record/schema_statements.rb +693 -0
- data/lib/clickhouse_ruby/active_record.rb +121 -0
- data/lib/clickhouse_ruby/client.rb +471 -0
- data/lib/clickhouse_ruby/configuration.rb +145 -0
- data/lib/clickhouse_ruby/connection.rb +328 -0
- data/lib/clickhouse_ruby/connection_pool.rb +301 -0
- data/lib/clickhouse_ruby/errors.rb +144 -0
- data/lib/clickhouse_ruby/result.rb +189 -0
- data/lib/clickhouse_ruby/types/array.rb +183 -0
- data/lib/clickhouse_ruby/types/base.rb +77 -0
- data/lib/clickhouse_ruby/types/boolean.rb +68 -0
- data/lib/clickhouse_ruby/types/date_time.rb +163 -0
- data/lib/clickhouse_ruby/types/float.rb +115 -0
- data/lib/clickhouse_ruby/types/integer.rb +157 -0
- data/lib/clickhouse_ruby/types/low_cardinality.rb +58 -0
- data/lib/clickhouse_ruby/types/map.rb +249 -0
- data/lib/clickhouse_ruby/types/nullable.rb +73 -0
- data/lib/clickhouse_ruby/types/parser.rb +244 -0
- data/lib/clickhouse_ruby/types/registry.rb +148 -0
- data/lib/clickhouse_ruby/types/string.rb +83 -0
- data/lib/clickhouse_ruby/types/tuple.rb +206 -0
- data/lib/clickhouse_ruby/types/uuid.rb +84 -0
- data/lib/clickhouse_ruby/types.rb +69 -0
- data/lib/clickhouse_ruby/version.rb +5 -0
- data/lib/clickhouse_ruby.rb +101 -0
- metadata +150 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickhouseRuby
|
|
4
|
+
module Types
|
|
5
|
+
# Type handler for ClickHouse integer types
|
|
6
|
+
#
|
|
7
|
+
# Handles: Int8, Int16, Int32, Int64, Int128, Int256
|
|
8
|
+
# UInt8, UInt16, UInt32, UInt64, UInt128, UInt256
|
|
9
|
+
#
|
|
10
|
+
# ClickHouse integers are exact - no floating point issues.
|
|
11
|
+
# Large integers (128, 256 bit) use Ruby's BigInteger.
|
|
12
|
+
#
|
|
13
|
+
class Integer < Base
|
|
14
|
+
# Size limits for each integer type
|
|
15
|
+
LIMITS = {
|
|
16
|
+
'Int8' => { min: -128, max: 127 },
|
|
17
|
+
'Int16' => { min: -32_768, max: 32_767 },
|
|
18
|
+
'Int32' => { min: -2_147_483_648, max: 2_147_483_647 },
|
|
19
|
+
'Int64' => { min: -9_223_372_036_854_775_808, max: 9_223_372_036_854_775_807 },
|
|
20
|
+
'Int128' => { min: -(2**127), max: (2**127) - 1 },
|
|
21
|
+
'Int256' => { min: -(2**255), max: (2**255) - 1 },
|
|
22
|
+
'UInt8' => { min: 0, max: 255 },
|
|
23
|
+
'UInt16' => { min: 0, max: 65_535 },
|
|
24
|
+
'UInt32' => { min: 0, max: 4_294_967_295 },
|
|
25
|
+
'UInt64' => { min: 0, max: 18_446_744_073_709_551_615 },
|
|
26
|
+
'UInt128' => { min: 0, max: (2**128) - 1 },
|
|
27
|
+
'UInt256' => { min: 0, max: (2**256) - 1 }
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# Converts a Ruby value to an integer
|
|
31
|
+
#
|
|
32
|
+
# @param value [Object] the value to convert
|
|
33
|
+
# @return [Integer, nil] the integer value
|
|
34
|
+
# @raise [TypeCastError] if the value cannot be converted
|
|
35
|
+
def cast(value)
|
|
36
|
+
return nil if value.nil?
|
|
37
|
+
|
|
38
|
+
case value
|
|
39
|
+
when ::Integer
|
|
40
|
+
validate_range!(value)
|
|
41
|
+
value
|
|
42
|
+
when ::Float
|
|
43
|
+
int_value = value.to_i
|
|
44
|
+
validate_range!(int_value)
|
|
45
|
+
int_value
|
|
46
|
+
when ::String
|
|
47
|
+
int_value = parse_string(value)
|
|
48
|
+
validate_range!(int_value)
|
|
49
|
+
int_value
|
|
50
|
+
when true
|
|
51
|
+
1
|
|
52
|
+
when false
|
|
53
|
+
0
|
|
54
|
+
else
|
|
55
|
+
raise TypeCastError.new(
|
|
56
|
+
"Cannot cast #{value.class} to #{name}",
|
|
57
|
+
from_type: value.class.name,
|
|
58
|
+
to_type: name,
|
|
59
|
+
value: value
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Converts a value from ClickHouse to Ruby Integer
|
|
65
|
+
#
|
|
66
|
+
# @param value [Object] the value from ClickHouse
|
|
67
|
+
# @return [Integer, nil] the integer value
|
|
68
|
+
def deserialize(value)
|
|
69
|
+
return nil if value.nil?
|
|
70
|
+
|
|
71
|
+
case value
|
|
72
|
+
when ::Integer
|
|
73
|
+
value
|
|
74
|
+
when ::String
|
|
75
|
+
parse_string(value)
|
|
76
|
+
when ::Float
|
|
77
|
+
value.to_i
|
|
78
|
+
else
|
|
79
|
+
value.to_i
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Converts an integer to SQL literal
|
|
84
|
+
#
|
|
85
|
+
# @param value [Integer, nil] the value to serialize
|
|
86
|
+
# @return [String] the SQL literal
|
|
87
|
+
def serialize(value)
|
|
88
|
+
return 'NULL' if value.nil?
|
|
89
|
+
|
|
90
|
+
value.to_s
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns whether this is an unsigned integer type
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean] true if unsigned
|
|
96
|
+
def unsigned?
|
|
97
|
+
name.start_with?('U')
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns the bit size of this integer type
|
|
101
|
+
#
|
|
102
|
+
# @return [Integer] the bit size (8, 16, 32, 64, 128, or 256)
|
|
103
|
+
def bit_size
|
|
104
|
+
name.gsub(/[^0-9]/, '').to_i
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
# Parses a string to an integer
|
|
110
|
+
#
|
|
111
|
+
# @param value [String] the string to parse
|
|
112
|
+
# @return [Integer] the parsed integer
|
|
113
|
+
# @raise [TypeCastError] if the string is not a valid integer
|
|
114
|
+
def parse_string(value)
|
|
115
|
+
stripped = value.strip
|
|
116
|
+
|
|
117
|
+
# Handle empty strings
|
|
118
|
+
if stripped.empty?
|
|
119
|
+
raise TypeCastError.new(
|
|
120
|
+
"Cannot cast empty string to #{name}",
|
|
121
|
+
from_type: 'String',
|
|
122
|
+
to_type: name,
|
|
123
|
+
value: value
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Use Integer() for strict parsing
|
|
128
|
+
Integer(stripped)
|
|
129
|
+
rescue ArgumentError
|
|
130
|
+
raise TypeCastError.new(
|
|
131
|
+
"Cannot cast '#{value}' to #{name}",
|
|
132
|
+
from_type: 'String',
|
|
133
|
+
to_type: name,
|
|
134
|
+
value: value
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Validates that a value is within the type's range
|
|
139
|
+
#
|
|
140
|
+
# @param value [Integer] the value to validate
|
|
141
|
+
# @raise [TypeCastError] if the value is out of range
|
|
142
|
+
def validate_range!(value)
|
|
143
|
+
limits = LIMITS[name]
|
|
144
|
+
return unless limits # Unknown type, skip validation
|
|
145
|
+
|
|
146
|
+
if value < limits[:min] || value > limits[:max]
|
|
147
|
+
raise TypeCastError.new(
|
|
148
|
+
"Value #{value} is out of range for #{name} (#{limits[:min]}..#{limits[:max]})",
|
|
149
|
+
from_type: value.class.name,
|
|
150
|
+
to_type: name,
|
|
151
|
+
value: value
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickhouseRuby
|
|
4
|
+
module Types
|
|
5
|
+
# Type handler for ClickHouse LowCardinality type
|
|
6
|
+
#
|
|
7
|
+
# LowCardinality is an optimization wrapper that stores string values
|
|
8
|
+
# in a dictionary for better compression and performance.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# type = LowCardinality.new('LowCardinality', element_type: String.new('String'))
|
|
12
|
+
# type.cast('hello')
|
|
13
|
+
# type.serialize('hello') # => "'hello'"
|
|
14
|
+
#
|
|
15
|
+
class LowCardinality < Base
|
|
16
|
+
# @return [Base] the wrapped type
|
|
17
|
+
attr_reader :element_type
|
|
18
|
+
|
|
19
|
+
# @param name [String] the type name
|
|
20
|
+
# @param element_type [Base] the wrapped type
|
|
21
|
+
def initialize(name, element_type: nil)
|
|
22
|
+
super(name)
|
|
23
|
+
@element_type = element_type || Base.new('String')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Converts a Ruby value using the wrapped type
|
|
27
|
+
#
|
|
28
|
+
# @param value [Object] the value to convert
|
|
29
|
+
# @return [Object] the converted value
|
|
30
|
+
def cast(value)
|
|
31
|
+
@element_type.cast(value)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Converts a value from ClickHouse using the wrapped type
|
|
35
|
+
#
|
|
36
|
+
# @param value [Object] the value from ClickHouse
|
|
37
|
+
# @return [Object] the Ruby value
|
|
38
|
+
def deserialize(value)
|
|
39
|
+
@element_type.deserialize(value)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Converts a value to SQL literal using the wrapped type
|
|
43
|
+
#
|
|
44
|
+
# @param value [Object] the value to serialize
|
|
45
|
+
# @return [String] the SQL literal
|
|
46
|
+
def serialize(value)
|
|
47
|
+
@element_type.serialize(value)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns the full type string
|
|
51
|
+
#
|
|
52
|
+
# @return [String] the type string
|
|
53
|
+
def to_s
|
|
54
|
+
"LowCardinality(#{@element_type})"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickhouseRuby
|
|
4
|
+
module Types
|
|
5
|
+
# Type handler for ClickHouse Map type
|
|
6
|
+
#
|
|
7
|
+
# Maps are key-value collections where all keys share one type
|
|
8
|
+
# and all values share another type.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# type = Map.new('Map', arg_types: [String.new('String'), Integer.new('UInt64')])
|
|
12
|
+
# type.cast({'a' => 1, 'b' => 2})
|
|
13
|
+
# type.serialize({'a' => 1}) # => "{'a': 1}"
|
|
14
|
+
#
|
|
15
|
+
class Map < Base
|
|
16
|
+
# @return [Base] the key type
|
|
17
|
+
attr_reader :key_type
|
|
18
|
+
|
|
19
|
+
# @return [Base] the value type
|
|
20
|
+
attr_reader :value_type
|
|
21
|
+
|
|
22
|
+
# @param name [String] the type name
|
|
23
|
+
# @param arg_types [Array<Base>] array of [key_type, value_type]
|
|
24
|
+
def initialize(name, arg_types: nil)
|
|
25
|
+
super(name)
|
|
26
|
+
arg_types ||= [Base.new('String'), Base.new('String')]
|
|
27
|
+
@key_type = arg_types[0]
|
|
28
|
+
@value_type = arg_types[1] || Base.new('String')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Converts a Ruby value to a map (Hash)
|
|
32
|
+
#
|
|
33
|
+
# @param value [Object] the value to convert
|
|
34
|
+
# @return [Hash, nil] the hash value
|
|
35
|
+
# @raise [TypeCastError] if the value cannot be converted
|
|
36
|
+
def cast(value)
|
|
37
|
+
return nil if value.nil?
|
|
38
|
+
|
|
39
|
+
hash = case value
|
|
40
|
+
when ::Hash
|
|
41
|
+
value
|
|
42
|
+
when ::String
|
|
43
|
+
parse_map_string(value)
|
|
44
|
+
else
|
|
45
|
+
raise TypeCastError.new(
|
|
46
|
+
"Cannot cast #{value.class} to Map",
|
|
47
|
+
from_type: value.class.name,
|
|
48
|
+
to_type: to_s,
|
|
49
|
+
value: value
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
hash.transform_keys { |k| @key_type.cast(k) }
|
|
54
|
+
.transform_values { |v| @value_type.cast(v) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Converts a value from ClickHouse to a Ruby Hash
|
|
58
|
+
#
|
|
59
|
+
# @param value [Object] the value from ClickHouse
|
|
60
|
+
# @return [Hash, nil] the hash value
|
|
61
|
+
def deserialize(value)
|
|
62
|
+
return nil if value.nil?
|
|
63
|
+
|
|
64
|
+
hash = case value
|
|
65
|
+
when ::Hash
|
|
66
|
+
value
|
|
67
|
+
when ::String
|
|
68
|
+
parse_map_string(value)
|
|
69
|
+
else
|
|
70
|
+
{ value => nil }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
hash.transform_keys { |k| @key_type.deserialize(k) }
|
|
74
|
+
.transform_values { |v| @value_type.deserialize(v) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Converts a hash to SQL literal
|
|
78
|
+
#
|
|
79
|
+
# @param value [Hash, nil] the value to serialize
|
|
80
|
+
# @return [String] the SQL literal
|
|
81
|
+
def serialize(value)
|
|
82
|
+
return 'NULL' if value.nil?
|
|
83
|
+
|
|
84
|
+
pairs = value.map do |k, v|
|
|
85
|
+
"#{@key_type.serialize(k)}: #{@value_type.serialize(v)}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
"{#{pairs.join(', ')}}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns the full type string including key and value types
|
|
92
|
+
#
|
|
93
|
+
# @return [String] the type string
|
|
94
|
+
def to_s
|
|
95
|
+
"Map(#{@key_type}, #{@value_type})"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# Parses a ClickHouse map string representation
|
|
101
|
+
#
|
|
102
|
+
# @param value [String] the string to parse
|
|
103
|
+
# @return [Hash] the parsed hash
|
|
104
|
+
def parse_map_string(value)
|
|
105
|
+
stripped = value.strip
|
|
106
|
+
|
|
107
|
+
# Handle empty map
|
|
108
|
+
return {} if stripped == '{}'
|
|
109
|
+
|
|
110
|
+
# Remove outer braces
|
|
111
|
+
unless stripped.start_with?('{') && stripped.end_with?('}')
|
|
112
|
+
raise TypeCastError.new(
|
|
113
|
+
"Invalid map format: '#{value}'",
|
|
114
|
+
from_type: 'String',
|
|
115
|
+
to_type: to_s,
|
|
116
|
+
value: value
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
inner = stripped[1...-1]
|
|
121
|
+
return {} if inner.strip.empty?
|
|
122
|
+
|
|
123
|
+
# Parse key-value pairs
|
|
124
|
+
parse_pairs(inner)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Parses comma-separated key:value pairs
|
|
128
|
+
#
|
|
129
|
+
# @param str [String] the inner map string
|
|
130
|
+
# @return [Hash] the parsed pairs
|
|
131
|
+
def parse_pairs(str)
|
|
132
|
+
result = {}
|
|
133
|
+
current = ''
|
|
134
|
+
depth = 0
|
|
135
|
+
in_string = false
|
|
136
|
+
escape_next = false
|
|
137
|
+
|
|
138
|
+
str.each_char do |char|
|
|
139
|
+
if escape_next
|
|
140
|
+
current += char
|
|
141
|
+
escape_next = false
|
|
142
|
+
next
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
case char
|
|
146
|
+
when '\\'
|
|
147
|
+
escape_next = true
|
|
148
|
+
current += char
|
|
149
|
+
when "'"
|
|
150
|
+
in_string = !in_string
|
|
151
|
+
current += char
|
|
152
|
+
when '{', '[', '('
|
|
153
|
+
depth += 1 unless in_string
|
|
154
|
+
current += char
|
|
155
|
+
when '}', ']', ')'
|
|
156
|
+
depth -= 1 unless in_string
|
|
157
|
+
current += char
|
|
158
|
+
when ','
|
|
159
|
+
if depth.zero? && !in_string
|
|
160
|
+
key, value = parse_pair(current.strip)
|
|
161
|
+
result[key] = value
|
|
162
|
+
current = ''
|
|
163
|
+
else
|
|
164
|
+
current += char
|
|
165
|
+
end
|
|
166
|
+
else
|
|
167
|
+
current += char
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Don't forget the last pair
|
|
172
|
+
unless current.strip.empty?
|
|
173
|
+
key, value = parse_pair(current.strip)
|
|
174
|
+
result[key] = value
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
result
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Parses a single key:value pair
|
|
181
|
+
#
|
|
182
|
+
# @param str [String] the pair string
|
|
183
|
+
# @return [Array] [key, value]
|
|
184
|
+
def parse_pair(str)
|
|
185
|
+
# Find the colon separator (not inside quotes or nested structures)
|
|
186
|
+
colon_idx = find_separator(str, ':')
|
|
187
|
+
|
|
188
|
+
if colon_idx.nil?
|
|
189
|
+
raise TypeCastError.new(
|
|
190
|
+
"Invalid map pair format: '#{str}'",
|
|
191
|
+
from_type: 'String',
|
|
192
|
+
to_type: to_s,
|
|
193
|
+
value: str
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
key = parse_value(str[0...colon_idx].strip)
|
|
198
|
+
value = parse_value(str[(colon_idx + 1)..].strip)
|
|
199
|
+
|
|
200
|
+
[key, value]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Finds the index of a separator character, ignoring nested structures
|
|
204
|
+
#
|
|
205
|
+
# @param str [String] the string to search
|
|
206
|
+
# @param sep [String] the separator character
|
|
207
|
+
# @return [Integer, nil] the index or nil if not found
|
|
208
|
+
def find_separator(str, sep)
|
|
209
|
+
depth = 0
|
|
210
|
+
in_string = false
|
|
211
|
+
escape_next = false
|
|
212
|
+
|
|
213
|
+
str.each_char.with_index do |char, idx|
|
|
214
|
+
if escape_next
|
|
215
|
+
escape_next = false
|
|
216
|
+
next
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
case char
|
|
220
|
+
when '\\'
|
|
221
|
+
escape_next = true
|
|
222
|
+
when "'"
|
|
223
|
+
in_string = !in_string
|
|
224
|
+
when '{', '[', '('
|
|
225
|
+
depth += 1 unless in_string
|
|
226
|
+
when '}', ']', ')'
|
|
227
|
+
depth -= 1 unless in_string
|
|
228
|
+
when sep
|
|
229
|
+
return idx if depth.zero? && !in_string
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
nil
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Parses a value, removing quotes if necessary
|
|
237
|
+
#
|
|
238
|
+
# @param str [String] the value string
|
|
239
|
+
# @return [Object] the parsed value
|
|
240
|
+
def parse_value(str)
|
|
241
|
+
if str.start_with?("'") && str.end_with?("'")
|
|
242
|
+
str[1...-1].gsub("\\'", "'")
|
|
243
|
+
else
|
|
244
|
+
str
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickhouseRuby
|
|
4
|
+
module Types
|
|
5
|
+
# Type handler for ClickHouse Nullable type
|
|
6
|
+
#
|
|
7
|
+
# Nullable wraps another type to allow NULL values.
|
|
8
|
+
# Without Nullable, ClickHouse columns cannot contain NULL.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# type = Nullable.new('Nullable', element_type: Integer.new('Int32'))
|
|
12
|
+
# type.cast(nil) # => nil
|
|
13
|
+
# type.cast(42) # => 42
|
|
14
|
+
# type.nullable? # => true
|
|
15
|
+
#
|
|
16
|
+
class Nullable < Base
|
|
17
|
+
# @return [Base] the wrapped type
|
|
18
|
+
attr_reader :element_type
|
|
19
|
+
|
|
20
|
+
# @param name [String] the type name
|
|
21
|
+
# @param element_type [Base] the wrapped type
|
|
22
|
+
def initialize(name, element_type: nil)
|
|
23
|
+
super(name)
|
|
24
|
+
@element_type = element_type || Base.new('String')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Converts a Ruby value, allowing nil
|
|
28
|
+
#
|
|
29
|
+
# @param value [Object] the value to convert
|
|
30
|
+
# @return [Object, nil] the converted value
|
|
31
|
+
def cast(value)
|
|
32
|
+
return nil if value.nil?
|
|
33
|
+
|
|
34
|
+
@element_type.cast(value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Converts a value from ClickHouse, allowing nil
|
|
38
|
+
#
|
|
39
|
+
# @param value [Object] the value from ClickHouse
|
|
40
|
+
# @return [Object, nil] the Ruby value
|
|
41
|
+
def deserialize(value)
|
|
42
|
+
return nil if value.nil?
|
|
43
|
+
return nil if value.is_a?(::String) && value == '\\N'
|
|
44
|
+
|
|
45
|
+
@element_type.deserialize(value)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Converts a value to SQL literal, handling NULL
|
|
49
|
+
#
|
|
50
|
+
# @param value [Object, nil] the value to serialize
|
|
51
|
+
# @return [String] the SQL literal
|
|
52
|
+
def serialize(value)
|
|
53
|
+
return 'NULL' if value.nil?
|
|
54
|
+
|
|
55
|
+
@element_type.serialize(value)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns true - this type allows NULL
|
|
59
|
+
#
|
|
60
|
+
# @return [Boolean] true
|
|
61
|
+
def nullable?
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Returns the full type string
|
|
66
|
+
#
|
|
67
|
+
# @return [String] the type string
|
|
68
|
+
def to_s
|
|
69
|
+
"Nullable(#{@element_type})"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|