clickhouse-ruby 0.1.0 → 0.2.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 +74 -1
- data/README.md +165 -79
- data/lib/clickhouse_ruby/active_record/arel_visitor.rb +205 -76
- data/lib/clickhouse_ruby/active_record/connection_adapter.rb +103 -98
- data/lib/clickhouse_ruby/active_record/railtie.rb +20 -15
- data/lib/clickhouse_ruby/active_record/relation_extensions.rb +398 -0
- data/lib/clickhouse_ruby/active_record/schema_statements.rb +90 -104
- data/lib/clickhouse_ruby/active_record.rb +24 -10
- data/lib/clickhouse_ruby/client.rb +181 -74
- data/lib/clickhouse_ruby/configuration.rb +51 -10
- data/lib/clickhouse_ruby/connection.rb +180 -64
- data/lib/clickhouse_ruby/connection_pool.rb +25 -19
- data/lib/clickhouse_ruby/errors.rb +13 -1
- data/lib/clickhouse_ruby/result.rb +11 -16
- data/lib/clickhouse_ruby/retry_handler.rb +172 -0
- data/lib/clickhouse_ruby/streaming_result.rb +309 -0
- data/lib/clickhouse_ruby/types/array.rb +11 -64
- data/lib/clickhouse_ruby/types/base.rb +59 -0
- data/lib/clickhouse_ruby/types/boolean.rb +28 -25
- data/lib/clickhouse_ruby/types/date_time.rb +10 -27
- data/lib/clickhouse_ruby/types/decimal.rb +173 -0
- data/lib/clickhouse_ruby/types/enum.rb +262 -0
- data/lib/clickhouse_ruby/types/float.rb +14 -28
- data/lib/clickhouse_ruby/types/integer.rb +21 -43
- data/lib/clickhouse_ruby/types/low_cardinality.rb +1 -1
- data/lib/clickhouse_ruby/types/map.rb +21 -36
- data/lib/clickhouse_ruby/types/null_safe.rb +81 -0
- data/lib/clickhouse_ruby/types/nullable.rb +2 -2
- data/lib/clickhouse_ruby/types/parser.rb +28 -18
- data/lib/clickhouse_ruby/types/registry.rb +40 -29
- data/lib/clickhouse_ruby/types/string.rb +9 -13
- data/lib/clickhouse_ruby/types/string_parser.rb +135 -0
- data/lib/clickhouse_ruby/types/tuple.rb +11 -68
- data/lib/clickhouse_ruby/types/uuid.rb +15 -22
- data/lib/clickhouse_ruby/types.rb +19 -15
- data/lib/clickhouse_ruby/version.rb +1 -1
- data/lib/clickhouse_ruby.rb +11 -11
- metadata +41 -6
|
@@ -27,12 +27,7 @@ module ClickhouseRuby
|
|
|
27
27
|
when ::BigDecimal
|
|
28
28
|
value.to_f
|
|
29
29
|
else
|
|
30
|
-
|
|
31
|
-
"Cannot cast #{value.class} to #{name}",
|
|
32
|
-
from_type: value.class.name,
|
|
33
|
-
to_type: name,
|
|
34
|
-
value: value
|
|
35
|
-
)
|
|
30
|
+
raise_cast_error(value)
|
|
36
31
|
end
|
|
37
32
|
end
|
|
38
33
|
|
|
@@ -40,6 +35,7 @@ module ClickhouseRuby
|
|
|
40
35
|
#
|
|
41
36
|
# @param value [Object] the value from ClickHouse
|
|
42
37
|
# @return [Float, nil] the float value
|
|
38
|
+
# @raise [TypeCastError] if the value cannot be converted
|
|
43
39
|
def deserialize(value)
|
|
44
40
|
return nil if value.nil?
|
|
45
41
|
|
|
@@ -48,8 +44,10 @@ module ClickhouseRuby
|
|
|
48
44
|
value
|
|
49
45
|
when ::String
|
|
50
46
|
parse_string(value)
|
|
51
|
-
|
|
47
|
+
when ::Integer, ::BigDecimal, ::Rational
|
|
52
48
|
value.to_f
|
|
49
|
+
else
|
|
50
|
+
raise_cast_error(value, "Cannot deserialize #{value.class} to #{name}")
|
|
53
51
|
end
|
|
54
52
|
end
|
|
55
53
|
|
|
@@ -58,14 +56,14 @@ module ClickhouseRuby
|
|
|
58
56
|
# @param value [Float, nil] the value to serialize
|
|
59
57
|
# @return [String] the SQL literal
|
|
60
58
|
def serialize(value)
|
|
61
|
-
return
|
|
59
|
+
return "NULL" if value.nil?
|
|
62
60
|
|
|
63
61
|
if value.nan?
|
|
64
|
-
|
|
62
|
+
"nan"
|
|
65
63
|
elsif value.infinite? == 1
|
|
66
|
-
|
|
64
|
+
"inf"
|
|
67
65
|
elsif value.infinite? == -1
|
|
68
|
-
|
|
66
|
+
"-inf"
|
|
69
67
|
else
|
|
70
68
|
value.to_s
|
|
71
69
|
end
|
|
@@ -83,32 +81,20 @@ module ClickhouseRuby
|
|
|
83
81
|
|
|
84
82
|
# Handle special values
|
|
85
83
|
case stripped
|
|
86
|
-
when
|
|
84
|
+
when "inf", "+inf", "infinity", "+infinity"
|
|
87
85
|
::Float::INFINITY
|
|
88
|
-
when
|
|
86
|
+
when "-inf", "-infinity"
|
|
89
87
|
-::Float::INFINITY
|
|
90
|
-
when
|
|
88
|
+
when "nan"
|
|
91
89
|
::Float::NAN
|
|
92
90
|
else
|
|
93
91
|
# Handle empty strings
|
|
94
|
-
if stripped.empty?
|
|
95
|
-
raise TypeCastError.new(
|
|
96
|
-
"Cannot cast empty string to #{name}",
|
|
97
|
-
from_type: 'String',
|
|
98
|
-
to_type: name,
|
|
99
|
-
value: value
|
|
100
|
-
)
|
|
101
|
-
end
|
|
92
|
+
raise_empty_string_error(value) if stripped.empty?
|
|
102
93
|
|
|
103
94
|
Float(stripped)
|
|
104
95
|
end
|
|
105
96
|
rescue ArgumentError
|
|
106
|
-
|
|
107
|
-
"Cannot cast '#{value}' to #{name}",
|
|
108
|
-
from_type: 'String',
|
|
109
|
-
to_type: name,
|
|
110
|
-
value: value
|
|
111
|
-
)
|
|
97
|
+
raise_cast_error(value, "Cannot cast '#{value}' to #{name}")
|
|
112
98
|
end
|
|
113
99
|
end
|
|
114
100
|
end
|
|
@@ -13,18 +13,18 @@ module ClickhouseRuby
|
|
|
13
13
|
class Integer < Base
|
|
14
14
|
# Size limits for each integer type
|
|
15
15
|
LIMITS = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
28
|
}.freeze
|
|
29
29
|
|
|
30
30
|
# Converts a Ruby value to an integer
|
|
@@ -52,12 +52,7 @@ module ClickhouseRuby
|
|
|
52
52
|
when false
|
|
53
53
|
0
|
|
54
54
|
else
|
|
55
|
-
|
|
56
|
-
"Cannot cast #{value.class} to #{name}",
|
|
57
|
-
from_type: value.class.name,
|
|
58
|
-
to_type: name,
|
|
59
|
-
value: value
|
|
60
|
-
)
|
|
55
|
+
raise_cast_error(value)
|
|
61
56
|
end
|
|
62
57
|
end
|
|
63
58
|
|
|
@@ -85,7 +80,7 @@ module ClickhouseRuby
|
|
|
85
80
|
# @param value [Integer, nil] the value to serialize
|
|
86
81
|
# @return [String] the SQL literal
|
|
87
82
|
def serialize(value)
|
|
88
|
-
return
|
|
83
|
+
return "NULL" if value.nil?
|
|
89
84
|
|
|
90
85
|
value.to_s
|
|
91
86
|
end
|
|
@@ -94,14 +89,14 @@ module ClickhouseRuby
|
|
|
94
89
|
#
|
|
95
90
|
# @return [Boolean] true if unsigned
|
|
96
91
|
def unsigned?
|
|
97
|
-
name.start_with?(
|
|
92
|
+
name.start_with?("U")
|
|
98
93
|
end
|
|
99
94
|
|
|
100
95
|
# Returns the bit size of this integer type
|
|
101
96
|
#
|
|
102
97
|
# @return [Integer] the bit size (8, 16, 32, 64, 128, or 256)
|
|
103
98
|
def bit_size
|
|
104
|
-
name.gsub(/[^0-9]/,
|
|
99
|
+
name.gsub(/[^0-9]/, "").to_i
|
|
105
100
|
end
|
|
106
101
|
|
|
107
102
|
private
|
|
@@ -115,24 +110,12 @@ module ClickhouseRuby
|
|
|
115
110
|
stripped = value.strip
|
|
116
111
|
|
|
117
112
|
# 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
|
|
113
|
+
raise_empty_string_error(value) if stripped.empty?
|
|
126
114
|
|
|
127
115
|
# Use Integer() for strict parsing
|
|
128
116
|
Integer(stripped)
|
|
129
117
|
rescue ArgumentError
|
|
130
|
-
|
|
131
|
-
"Cannot cast '#{value}' to #{name}",
|
|
132
|
-
from_type: 'String',
|
|
133
|
-
to_type: name,
|
|
134
|
-
value: value
|
|
135
|
-
)
|
|
118
|
+
raise_cast_error(value, "Cannot cast '#{value}' to #{name}")
|
|
136
119
|
end
|
|
137
120
|
|
|
138
121
|
# Validates that a value is within the type's range
|
|
@@ -143,14 +126,9 @@ module ClickhouseRuby
|
|
|
143
126
|
limits = LIMITS[name]
|
|
144
127
|
return unless limits # Unknown type, skip validation
|
|
145
128
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
from_type: value.class.name,
|
|
150
|
-
to_type: name,
|
|
151
|
-
value: value
|
|
152
|
-
)
|
|
153
|
-
end
|
|
129
|
+
return unless value < limits[:min] || value > limits[:max]
|
|
130
|
+
|
|
131
|
+
raise_range_error(value, limits[:min], limits[:max])
|
|
154
132
|
end
|
|
155
133
|
end
|
|
156
134
|
end
|
|
@@ -20,7 +20,7 @@ module ClickhouseRuby
|
|
|
20
20
|
# @param element_type [Base] the wrapped type
|
|
21
21
|
def initialize(name, element_type: nil)
|
|
22
22
|
super(name)
|
|
23
|
-
@element_type = element_type || Base.new(
|
|
23
|
+
@element_type = element_type || Base.new("String")
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
# Converts a Ruby value using the wrapped type
|
|
@@ -23,9 +23,9 @@ module ClickhouseRuby
|
|
|
23
23
|
# @param arg_types [Array<Base>] array of [key_type, value_type]
|
|
24
24
|
def initialize(name, arg_types: nil)
|
|
25
25
|
super(name)
|
|
26
|
-
arg_types ||= [Base.new(
|
|
26
|
+
arg_types ||= [Base.new("String"), Base.new("String")]
|
|
27
27
|
@key_type = arg_types[0]
|
|
28
|
-
@value_type = arg_types[1] || Base.new(
|
|
28
|
+
@value_type = arg_types[1] || Base.new("String")
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
# Converts a Ruby value to a map (Hash)
|
|
@@ -42,16 +42,11 @@ module ClickhouseRuby
|
|
|
42
42
|
when ::String
|
|
43
43
|
parse_map_string(value)
|
|
44
44
|
else
|
|
45
|
-
|
|
46
|
-
"Cannot cast #{value.class} to Map",
|
|
47
|
-
from_type: value.class.name,
|
|
48
|
-
to_type: to_s,
|
|
49
|
-
value: value
|
|
50
|
-
)
|
|
45
|
+
raise_cast_error(value, "Cannot cast #{value.class} to Map")
|
|
51
46
|
end
|
|
52
47
|
|
|
53
48
|
hash.transform_keys { |k| @key_type.cast(k) }
|
|
54
|
-
|
|
49
|
+
.transform_values { |v| @value_type.cast(v) }
|
|
55
50
|
end
|
|
56
51
|
|
|
57
52
|
# Converts a value from ClickHouse to a Ruby Hash
|
|
@@ -71,7 +66,7 @@ module ClickhouseRuby
|
|
|
71
66
|
end
|
|
72
67
|
|
|
73
68
|
hash.transform_keys { |k| @key_type.deserialize(k) }
|
|
74
|
-
|
|
69
|
+
.transform_values { |v| @value_type.deserialize(v) }
|
|
75
70
|
end
|
|
76
71
|
|
|
77
72
|
# Converts a hash to SQL literal
|
|
@@ -79,13 +74,13 @@ module ClickhouseRuby
|
|
|
79
74
|
# @param value [Hash, nil] the value to serialize
|
|
80
75
|
# @return [String] the SQL literal
|
|
81
76
|
def serialize(value)
|
|
82
|
-
return
|
|
77
|
+
return "NULL" if value.nil?
|
|
83
78
|
|
|
84
79
|
pairs = value.map do |k, v|
|
|
85
80
|
"#{@key_type.serialize(k)}: #{@value_type.serialize(v)}"
|
|
86
81
|
end
|
|
87
82
|
|
|
88
|
-
"{#{pairs.join(
|
|
83
|
+
"{#{pairs.join(", ")}}"
|
|
89
84
|
end
|
|
90
85
|
|
|
91
86
|
# Returns the full type string including key and value types
|
|
@@ -105,16 +100,11 @@ module ClickhouseRuby
|
|
|
105
100
|
stripped = value.strip
|
|
106
101
|
|
|
107
102
|
# Handle empty map
|
|
108
|
-
return {} if stripped ==
|
|
103
|
+
return {} if stripped == "{}"
|
|
109
104
|
|
|
110
105
|
# Remove outer braces
|
|
111
|
-
unless stripped.start_with?(
|
|
112
|
-
|
|
113
|
-
"Invalid map format: '#{value}'",
|
|
114
|
-
from_type: 'String',
|
|
115
|
-
to_type: to_s,
|
|
116
|
-
value: value
|
|
117
|
-
)
|
|
106
|
+
unless stripped.start_with?("{") && stripped.end_with?("}")
|
|
107
|
+
raise_format_error(value, "map")
|
|
118
108
|
end
|
|
119
109
|
|
|
120
110
|
inner = stripped[1...-1]
|
|
@@ -130,7 +120,7 @@ module ClickhouseRuby
|
|
|
130
120
|
# @return [Hash] the parsed pairs
|
|
131
121
|
def parse_pairs(str)
|
|
132
122
|
result = {}
|
|
133
|
-
current =
|
|
123
|
+
current = ""
|
|
134
124
|
depth = 0
|
|
135
125
|
in_string = false
|
|
136
126
|
escape_next = false
|
|
@@ -143,23 +133,23 @@ module ClickhouseRuby
|
|
|
143
133
|
end
|
|
144
134
|
|
|
145
135
|
case char
|
|
146
|
-
when
|
|
136
|
+
when "\\"
|
|
147
137
|
escape_next = true
|
|
148
138
|
current += char
|
|
149
139
|
when "'"
|
|
150
140
|
in_string = !in_string
|
|
151
141
|
current += char
|
|
152
|
-
when
|
|
142
|
+
when "{", "[", "("
|
|
153
143
|
depth += 1 unless in_string
|
|
154
144
|
current += char
|
|
155
|
-
when
|
|
145
|
+
when "}", "]", ")"
|
|
156
146
|
depth -= 1 unless in_string
|
|
157
147
|
current += char
|
|
158
|
-
when
|
|
148
|
+
when ","
|
|
159
149
|
if depth.zero? && !in_string
|
|
160
150
|
key, value = parse_pair(current.strip)
|
|
161
151
|
result[key] = value
|
|
162
|
-
current =
|
|
152
|
+
current = ""
|
|
163
153
|
else
|
|
164
154
|
current += char
|
|
165
155
|
end
|
|
@@ -183,15 +173,10 @@ module ClickhouseRuby
|
|
|
183
173
|
# @return [Array] [key, value]
|
|
184
174
|
def parse_pair(str)
|
|
185
175
|
# Find the colon separator (not inside quotes or nested structures)
|
|
186
|
-
colon_idx = find_separator(str,
|
|
176
|
+
colon_idx = find_separator(str, ":")
|
|
187
177
|
|
|
188
178
|
if colon_idx.nil?
|
|
189
|
-
|
|
190
|
-
"Invalid map pair format: '#{str}'",
|
|
191
|
-
from_type: 'String',
|
|
192
|
-
to_type: to_s,
|
|
193
|
-
value: str
|
|
194
|
-
)
|
|
179
|
+
raise_format_error(str, "map pair")
|
|
195
180
|
end
|
|
196
181
|
|
|
197
182
|
key = parse_value(str[0...colon_idx].strip)
|
|
@@ -217,13 +202,13 @@ module ClickhouseRuby
|
|
|
217
202
|
end
|
|
218
203
|
|
|
219
204
|
case char
|
|
220
|
-
when
|
|
205
|
+
when "\\"
|
|
221
206
|
escape_next = true
|
|
222
207
|
when "'"
|
|
223
208
|
in_string = !in_string
|
|
224
|
-
when
|
|
209
|
+
when "{", "[", "("
|
|
225
210
|
depth += 1 unless in_string
|
|
226
|
-
when
|
|
211
|
+
when "}", "]", ")"
|
|
227
212
|
depth -= 1 unless in_string
|
|
228
213
|
when sep
|
|
229
214
|
return idx if depth.zero? && !in_string
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickhouseRuby
|
|
4
|
+
module Types
|
|
5
|
+
# Provides automatic null handling for type classes
|
|
6
|
+
#
|
|
7
|
+
# When included, wraps cast, deserialize, and serialize methods
|
|
8
|
+
# to handle nil values automatically. Subclasses implement
|
|
9
|
+
# cast_value, deserialize_value, and serialize_value instead.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# class Integer < Base
|
|
13
|
+
# include NullSafe
|
|
14
|
+
#
|
|
15
|
+
# protected
|
|
16
|
+
#
|
|
17
|
+
# def cast_value(value)
|
|
18
|
+
# # value is guaranteed non-nil here
|
|
19
|
+
# value.to_i
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
module NullSafe
|
|
24
|
+
# Converts a Ruby value, returning nil for nil input
|
|
25
|
+
#
|
|
26
|
+
# @param value [Object] the value to cast
|
|
27
|
+
# @return [Object, nil] the cast value or nil
|
|
28
|
+
def cast(value)
|
|
29
|
+
return nil if value.nil?
|
|
30
|
+
|
|
31
|
+
cast_value(value)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Deserializes a value from ClickHouse, returning nil for nil input
|
|
35
|
+
#
|
|
36
|
+
# @param value [Object] the value from ClickHouse
|
|
37
|
+
# @return [Object, nil] the deserialized value or nil
|
|
38
|
+
def deserialize(value)
|
|
39
|
+
return nil if value.nil?
|
|
40
|
+
|
|
41
|
+
deserialize_value(value)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Serializes a value for ClickHouse, returning "NULL" for nil input
|
|
45
|
+
#
|
|
46
|
+
# @param value [Object] the value to serialize
|
|
47
|
+
# @return [String] the SQL literal
|
|
48
|
+
def serialize(value)
|
|
49
|
+
return "NULL" if value.nil?
|
|
50
|
+
|
|
51
|
+
serialize_value(value)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
protected
|
|
55
|
+
|
|
56
|
+
# Override in subclass - actual casting logic (value guaranteed non-nil)
|
|
57
|
+
#
|
|
58
|
+
# @param value [Object] the non-nil value to cast
|
|
59
|
+
# @return [Object] the cast value
|
|
60
|
+
def cast_value(value)
|
|
61
|
+
value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Override in subclass - actual deserialization logic (value guaranteed non-nil)
|
|
65
|
+
#
|
|
66
|
+
# @param value [Object] the non-nil value to deserialize
|
|
67
|
+
# @return [Object] the deserialized value
|
|
68
|
+
def deserialize_value(value)
|
|
69
|
+
value
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Override in subclass - actual serialization logic (value guaranteed non-nil)
|
|
73
|
+
#
|
|
74
|
+
# @param value [Object] the non-nil value to serialize
|
|
75
|
+
# @return [String] the SQL literal
|
|
76
|
+
def serialize_value(value)
|
|
77
|
+
value.to_s
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -21,7 +21,7 @@ module ClickhouseRuby
|
|
|
21
21
|
# @param element_type [Base] the wrapped type
|
|
22
22
|
def initialize(name, element_type: nil)
|
|
23
23
|
super(name)
|
|
24
|
-
@element_type = element_type || Base.new(
|
|
24
|
+
@element_type = element_type || Base.new("String")
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# Converts a Ruby value, allowing nil
|
|
@@ -50,7 +50,7 @@ module ClickhouseRuby
|
|
|
50
50
|
# @param value [Object, nil] the value to serialize
|
|
51
51
|
# @return [String] the SQL literal
|
|
52
52
|
def serialize(value)
|
|
53
|
-
return
|
|
53
|
+
return "NULL" if value.nil?
|
|
54
54
|
|
|
55
55
|
@element_type.serialize(value)
|
|
56
56
|
end
|
|
@@ -58,12 +58,12 @@ module ClickhouseRuby
|
|
|
58
58
|
# @return [Hash] the parsed AST with :type and optional :args keys
|
|
59
59
|
# @raise [ParseError] if the type string is invalid
|
|
60
60
|
def parse(type_string)
|
|
61
|
-
raise ParseError
|
|
61
|
+
raise ParseError, "Type string cannot be nil" if type_string.nil?
|
|
62
62
|
|
|
63
63
|
@input = type_string.strip
|
|
64
64
|
@pos = 0
|
|
65
65
|
|
|
66
|
-
raise ParseError.new(
|
|
66
|
+
raise ParseError.new("Type string cannot be empty", input: type_string) if @input.empty?
|
|
67
67
|
|
|
68
68
|
result = parse_type
|
|
69
69
|
skip_whitespace
|
|
@@ -84,6 +84,7 @@ module ClickhouseRuby
|
|
|
84
84
|
# - Type names: String, UInt64
|
|
85
85
|
# - Numeric literals: 3 (precision), 9 (scale)
|
|
86
86
|
# - String literals: 'UTC' (timezone)
|
|
87
|
+
# - Enum entries: 'active' = 1 (for Enum8/Enum16)
|
|
87
88
|
#
|
|
88
89
|
# @return [Hash] the parsed type/value
|
|
89
90
|
def parse_type
|
|
@@ -95,19 +96,32 @@ module ClickhouseRuby
|
|
|
95
96
|
return { type: value }
|
|
96
97
|
end
|
|
97
98
|
|
|
98
|
-
# Handle string literals (e.g., DateTime64(3, 'UTC'))
|
|
99
|
+
# Handle string literals (e.g., DateTime64(3, 'UTC') or Enum8('active' = 1))
|
|
99
100
|
if peek == "'"
|
|
100
101
|
value = parse_string_literal
|
|
102
|
+
skip_whitespace
|
|
103
|
+
# Handle Enum value assignment: 'name' = value (or 'name' = -1)
|
|
104
|
+
if peek == "="
|
|
105
|
+
consume("=")
|
|
106
|
+
skip_whitespace
|
|
107
|
+
# Skip optional negative sign
|
|
108
|
+
if peek == "-"
|
|
109
|
+
@pos += 1
|
|
110
|
+
skip_whitespace
|
|
111
|
+
end
|
|
112
|
+
# Skip the numeric value
|
|
113
|
+
parse_numeric if numeric_char?(peek)
|
|
114
|
+
end
|
|
101
115
|
return { type: value }
|
|
102
116
|
end
|
|
103
117
|
|
|
104
118
|
name = parse_identifier
|
|
105
119
|
|
|
106
120
|
skip_whitespace
|
|
107
|
-
if peek ==
|
|
108
|
-
consume(
|
|
121
|
+
if peek == "("
|
|
122
|
+
consume("(")
|
|
109
123
|
args = parse_type_list
|
|
110
|
-
consume(
|
|
124
|
+
consume(")")
|
|
111
125
|
{ type: name, args: args }
|
|
112
126
|
else
|
|
113
127
|
{ type: name }
|
|
@@ -122,12 +136,12 @@ module ClickhouseRuby
|
|
|
122
136
|
skip_whitespace
|
|
123
137
|
|
|
124
138
|
# Handle empty argument list
|
|
125
|
-
return types if peek ==
|
|
139
|
+
return types if peek == ")"
|
|
126
140
|
|
|
127
141
|
types << parse_type
|
|
128
142
|
|
|
129
|
-
while peek ==
|
|
130
|
-
consume(
|
|
143
|
+
while peek == ","
|
|
144
|
+
consume(",")
|
|
131
145
|
types << parse_type
|
|
132
146
|
end
|
|
133
147
|
|
|
@@ -144,15 +158,13 @@ module ClickhouseRuby
|
|
|
144
158
|
|
|
145
159
|
# First character must be letter or underscore
|
|
146
160
|
unless @pos < @input.length && identifier_start_char?(@input[@pos])
|
|
147
|
-
raise ParseError.new(
|
|
161
|
+
raise ParseError.new("Expected type name", position: @pos, input: @input)
|
|
148
162
|
end
|
|
149
163
|
|
|
150
164
|
@pos += 1
|
|
151
165
|
|
|
152
166
|
# Subsequent characters can be letters, digits, or underscores
|
|
153
|
-
while @pos < @input.length && identifier_char?(@input[@pos])
|
|
154
|
-
@pos += 1
|
|
155
|
-
end
|
|
167
|
+
@pos += 1 while @pos < @input.length && identifier_char?(@input[@pos])
|
|
156
168
|
|
|
157
169
|
@input[start_pos...@pos]
|
|
158
170
|
end
|
|
@@ -187,9 +199,7 @@ module ClickhouseRuby
|
|
|
187
199
|
def parse_numeric
|
|
188
200
|
start_pos = @pos
|
|
189
201
|
|
|
190
|
-
while @pos < @input.length && numeric_char?(@input[@pos])
|
|
191
|
-
@pos += 1
|
|
192
|
-
end
|
|
202
|
+
@pos += 1 while @pos < @input.length && numeric_char?(@input[@pos])
|
|
193
203
|
|
|
194
204
|
@input[start_pos...@pos]
|
|
195
205
|
end
|
|
@@ -203,7 +213,7 @@ module ClickhouseRuby
|
|
|
203
213
|
|
|
204
214
|
while @pos < @input.length && @input[@pos] != "'"
|
|
205
215
|
# Handle escaped quotes
|
|
206
|
-
@pos += 1 if @input[@pos] ==
|
|
216
|
+
@pos += 1 if @input[@pos] == "\\" && @pos + 1 < @input.length
|
|
207
217
|
@pos += 1
|
|
208
218
|
end
|
|
209
219
|
|
|
@@ -226,7 +236,7 @@ module ClickhouseRuby
|
|
|
226
236
|
# @raise [ParseError] if the character doesn't match
|
|
227
237
|
def consume(expected)
|
|
228
238
|
skip_whitespace
|
|
229
|
-
actual = @pos < @input.length ? @input[@pos] :
|
|
239
|
+
actual = @pos < @input.length ? @input[@pos] : "end of input"
|
|
230
240
|
|
|
231
241
|
unless actual == expected
|
|
232
242
|
raise ParseError.new("Expected '#{expected}', got '#{actual}'", position: @pos, input: @input)
|