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.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/LICENSE +21 -0
  4. data/README.md +251 -0
  5. data/lib/clickhouse_ruby/active_record/arel_visitor.rb +468 -0
  6. data/lib/clickhouse_ruby/active_record/connection_adapter.rb +723 -0
  7. data/lib/clickhouse_ruby/active_record/railtie.rb +192 -0
  8. data/lib/clickhouse_ruby/active_record/schema_statements.rb +693 -0
  9. data/lib/clickhouse_ruby/active_record.rb +121 -0
  10. data/lib/clickhouse_ruby/client.rb +471 -0
  11. data/lib/clickhouse_ruby/configuration.rb +145 -0
  12. data/lib/clickhouse_ruby/connection.rb +328 -0
  13. data/lib/clickhouse_ruby/connection_pool.rb +301 -0
  14. data/lib/clickhouse_ruby/errors.rb +144 -0
  15. data/lib/clickhouse_ruby/result.rb +189 -0
  16. data/lib/clickhouse_ruby/types/array.rb +183 -0
  17. data/lib/clickhouse_ruby/types/base.rb +77 -0
  18. data/lib/clickhouse_ruby/types/boolean.rb +68 -0
  19. data/lib/clickhouse_ruby/types/date_time.rb +163 -0
  20. data/lib/clickhouse_ruby/types/float.rb +115 -0
  21. data/lib/clickhouse_ruby/types/integer.rb +157 -0
  22. data/lib/clickhouse_ruby/types/low_cardinality.rb +58 -0
  23. data/lib/clickhouse_ruby/types/map.rb +249 -0
  24. data/lib/clickhouse_ruby/types/nullable.rb +73 -0
  25. data/lib/clickhouse_ruby/types/parser.rb +244 -0
  26. data/lib/clickhouse_ruby/types/registry.rb +148 -0
  27. data/lib/clickhouse_ruby/types/string.rb +83 -0
  28. data/lib/clickhouse_ruby/types/tuple.rb +206 -0
  29. data/lib/clickhouse_ruby/types/uuid.rb +84 -0
  30. data/lib/clickhouse_ruby/types.rb +69 -0
  31. data/lib/clickhouse_ruby/version.rb +5 -0
  32. data/lib/clickhouse_ruby.rb +101 -0
  33. 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