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,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickhouseRuby
|
|
4
|
+
module Types
|
|
5
|
+
# Type handler for ClickHouse Array type
|
|
6
|
+
#
|
|
7
|
+
# Arrays in ClickHouse are homogeneous - all elements must be the same type.
|
|
8
|
+
# Supports nested arrays like Array(Array(String)).
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# type = Array.new('Array', element_type: String.new('String'))
|
|
12
|
+
# type.cast(['a', 'b', 'c']) # => ['a', 'b', 'c']
|
|
13
|
+
# type.serialize(['a', 'b']) # => "['a', 'b']"
|
|
14
|
+
#
|
|
15
|
+
class Array < Base
|
|
16
|
+
# @return [Base] the type of array elements
|
|
17
|
+
attr_reader :element_type
|
|
18
|
+
|
|
19
|
+
# @param name [String] the type name
|
|
20
|
+
# @param element_type [Base] the element 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 to an array
|
|
27
|
+
#
|
|
28
|
+
# @param value [Object] the value to convert
|
|
29
|
+
# @return [Array, nil] the array value
|
|
30
|
+
# @raise [TypeCastError] if the value cannot be converted
|
|
31
|
+
def cast(value)
|
|
32
|
+
return nil if value.nil?
|
|
33
|
+
|
|
34
|
+
arr = case value
|
|
35
|
+
when ::Array
|
|
36
|
+
value
|
|
37
|
+
when ::String
|
|
38
|
+
parse_array_string(value)
|
|
39
|
+
else
|
|
40
|
+
raise TypeCastError.new(
|
|
41
|
+
"Cannot cast #{value.class} to Array",
|
|
42
|
+
from_type: value.class.name,
|
|
43
|
+
to_type: to_s,
|
|
44
|
+
value: value
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
arr.map { |v| @element_type.cast(v) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Converts a value from ClickHouse to a Ruby Array
|
|
52
|
+
#
|
|
53
|
+
# @param value [Object] the value from ClickHouse
|
|
54
|
+
# @return [Array, nil] the array value
|
|
55
|
+
def deserialize(value)
|
|
56
|
+
return nil if value.nil?
|
|
57
|
+
|
|
58
|
+
arr = case value
|
|
59
|
+
when ::Array
|
|
60
|
+
value
|
|
61
|
+
when ::String
|
|
62
|
+
parse_array_string(value)
|
|
63
|
+
else
|
|
64
|
+
[value]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
arr.map { |v| @element_type.deserialize(v) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Converts an array to SQL literal
|
|
71
|
+
#
|
|
72
|
+
# @param value [Array, nil] the value to serialize
|
|
73
|
+
# @return [String] the SQL literal
|
|
74
|
+
def serialize(value)
|
|
75
|
+
return 'NULL' if value.nil?
|
|
76
|
+
|
|
77
|
+
elements = value.map { |v| @element_type.serialize(v) }
|
|
78
|
+
"[#{elements.join(', ')}]"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns the full type string including element type
|
|
82
|
+
#
|
|
83
|
+
# @return [String] the type string
|
|
84
|
+
def to_s
|
|
85
|
+
"Array(#{@element_type})"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Parses a ClickHouse array string representation
|
|
91
|
+
#
|
|
92
|
+
# @param value [String] the string to parse
|
|
93
|
+
# @return [Array] the parsed array
|
|
94
|
+
def parse_array_string(value)
|
|
95
|
+
stripped = value.strip
|
|
96
|
+
|
|
97
|
+
# Handle empty array
|
|
98
|
+
return [] if stripped == '[]'
|
|
99
|
+
|
|
100
|
+
# Remove outer brackets
|
|
101
|
+
unless stripped.start_with?('[') && stripped.end_with?(']')
|
|
102
|
+
raise TypeCastError.new(
|
|
103
|
+
"Invalid array format: '#{value}'",
|
|
104
|
+
from_type: 'String',
|
|
105
|
+
to_type: to_s,
|
|
106
|
+
value: value
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
inner = stripped[1...-1]
|
|
111
|
+
return [] if inner.strip.empty?
|
|
112
|
+
|
|
113
|
+
# Parse elements (handles nested arrays and quoted strings)
|
|
114
|
+
parse_elements(inner)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Parses comma-separated elements, handling nesting and quotes
|
|
118
|
+
#
|
|
119
|
+
# @param str [String] the inner array string
|
|
120
|
+
# @return [Array] the parsed elements
|
|
121
|
+
def parse_elements(str)
|
|
122
|
+
elements = []
|
|
123
|
+
current = ''
|
|
124
|
+
depth = 0
|
|
125
|
+
in_string = false
|
|
126
|
+
escape_next = false
|
|
127
|
+
|
|
128
|
+
str.each_char do |char|
|
|
129
|
+
if escape_next
|
|
130
|
+
current += char
|
|
131
|
+
escape_next = false
|
|
132
|
+
next
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
case char
|
|
136
|
+
when '\\'
|
|
137
|
+
escape_next = true
|
|
138
|
+
current += char
|
|
139
|
+
when "'"
|
|
140
|
+
in_string = !in_string
|
|
141
|
+
current += char
|
|
142
|
+
when '[', '('
|
|
143
|
+
depth += 1 unless in_string
|
|
144
|
+
current += char
|
|
145
|
+
when ']', ')'
|
|
146
|
+
depth -= 1 unless in_string
|
|
147
|
+
current += char
|
|
148
|
+
when ','
|
|
149
|
+
if depth.zero? && !in_string
|
|
150
|
+
elements << parse_element(current.strip)
|
|
151
|
+
current = ''
|
|
152
|
+
else
|
|
153
|
+
current += char
|
|
154
|
+
end
|
|
155
|
+
else
|
|
156
|
+
current += char
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Don't forget the last element
|
|
161
|
+
elements << parse_element(current.strip) unless current.strip.empty?
|
|
162
|
+
|
|
163
|
+
elements
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Parses a single element, removing quotes if necessary
|
|
167
|
+
#
|
|
168
|
+
# @param str [String] the element string
|
|
169
|
+
# @return [Object] the parsed element
|
|
170
|
+
def parse_element(str)
|
|
171
|
+
# Remove surrounding quotes if present
|
|
172
|
+
if str.start_with?("'") && str.end_with?("'")
|
|
173
|
+
str[1...-1].gsub("\\'", "'")
|
|
174
|
+
elsif str.start_with?('[')
|
|
175
|
+
# Nested array - let the element type handle it
|
|
176
|
+
str
|
|
177
|
+
else
|
|
178
|
+
str
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickhouseRuby
|
|
4
|
+
module Types
|
|
5
|
+
# Base class for all ClickHouse types
|
|
6
|
+
#
|
|
7
|
+
# Provides the interface for type conversion between Ruby and ClickHouse.
|
|
8
|
+
# Subclasses implement specific conversion logic.
|
|
9
|
+
#
|
|
10
|
+
# @abstract Subclasses should override {#cast}, {#deserialize}, and {#serialize}
|
|
11
|
+
#
|
|
12
|
+
class Base
|
|
13
|
+
# @return [String] the ClickHouse type name
|
|
14
|
+
attr_reader :name
|
|
15
|
+
|
|
16
|
+
# @param name [String] the ClickHouse type name
|
|
17
|
+
def initialize(name)
|
|
18
|
+
@name = name
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Converts a Ruby value to the appropriate type for this ClickHouse column
|
|
22
|
+
#
|
|
23
|
+
# @param value [Object] the value to convert
|
|
24
|
+
# @return [Object] the converted value
|
|
25
|
+
def cast(value)
|
|
26
|
+
value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Converts a value from ClickHouse response format to Ruby
|
|
30
|
+
#
|
|
31
|
+
# @param value [Object] the value from ClickHouse
|
|
32
|
+
# @return [Object] the Ruby value
|
|
33
|
+
def deserialize(value)
|
|
34
|
+
value
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Converts a Ruby value to ClickHouse SQL literal format
|
|
38
|
+
#
|
|
39
|
+
# @param value [Object] the Ruby value
|
|
40
|
+
# @return [String] the SQL literal
|
|
41
|
+
def serialize(value)
|
|
42
|
+
value.to_s
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns whether NULL values are allowed
|
|
46
|
+
#
|
|
47
|
+
# @return [Boolean] true if nullable
|
|
48
|
+
def nullable?
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns the ClickHouse type string
|
|
53
|
+
#
|
|
54
|
+
# @return [String] the type string
|
|
55
|
+
def to_s
|
|
56
|
+
name
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Equality comparison
|
|
60
|
+
#
|
|
61
|
+
# @param other [Base] another type
|
|
62
|
+
# @return [Boolean] true if equal
|
|
63
|
+
def ==(other)
|
|
64
|
+
other.is_a?(Base) && other.name == name
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
alias eql? ==
|
|
68
|
+
|
|
69
|
+
# Hash code for use in hash keys
|
|
70
|
+
#
|
|
71
|
+
# @return [Integer] the hash code
|
|
72
|
+
def hash
|
|
73
|
+
name.hash
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickhouseRuby
|
|
4
|
+
module Types
|
|
5
|
+
# Type handler for ClickHouse Bool type
|
|
6
|
+
#
|
|
7
|
+
# ClickHouse Bool is internally stored as UInt8 (0 or 1)
|
|
8
|
+
# but can accept various truthy/falsy values.
|
|
9
|
+
#
|
|
10
|
+
class Boolean < Base
|
|
11
|
+
# Values that represent true
|
|
12
|
+
TRUE_VALUES = [true, 1, '1', 'true', 'TRUE', 'True', 't', 'T', 'yes', 'YES', 'Yes', 'y', 'Y', 'on', 'ON', 'On'].freeze
|
|
13
|
+
|
|
14
|
+
# Values that represent false
|
|
15
|
+
FALSE_VALUES = [false, 0, '0', 'false', 'FALSE', 'False', 'f', 'F', 'no', 'NO', 'No', 'n', 'N', 'off', 'OFF', 'Off'].freeze
|
|
16
|
+
|
|
17
|
+
# Converts a Ruby value to a boolean
|
|
18
|
+
#
|
|
19
|
+
# @param value [Object] the value to convert
|
|
20
|
+
# @return [Boolean, nil] the boolean value
|
|
21
|
+
# @raise [TypeCastError] if the value cannot be interpreted as boolean
|
|
22
|
+
def cast(value)
|
|
23
|
+
return nil if value.nil?
|
|
24
|
+
|
|
25
|
+
if TRUE_VALUES.include?(value)
|
|
26
|
+
true
|
|
27
|
+
elsif FALSE_VALUES.include?(value)
|
|
28
|
+
false
|
|
29
|
+
else
|
|
30
|
+
raise TypeCastError.new(
|
|
31
|
+
"Cannot cast '#{value}' to Bool",
|
|
32
|
+
from_type: value.class.name,
|
|
33
|
+
to_type: name,
|
|
34
|
+
value: value
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Converts a value from ClickHouse to Ruby boolean
|
|
40
|
+
#
|
|
41
|
+
# @param value [Object] the value from ClickHouse
|
|
42
|
+
# @return [Boolean, nil] the boolean value
|
|
43
|
+
def deserialize(value)
|
|
44
|
+
return nil if value.nil?
|
|
45
|
+
|
|
46
|
+
case value
|
|
47
|
+
when true, 1, '1', 'true'
|
|
48
|
+
true
|
|
49
|
+
when false, 0, '0', 'false'
|
|
50
|
+
false
|
|
51
|
+
else
|
|
52
|
+
# Default to truthy evaluation
|
|
53
|
+
!!value
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Converts a boolean to SQL literal
|
|
58
|
+
#
|
|
59
|
+
# @param value [Boolean, nil] the value to serialize
|
|
60
|
+
# @return [String] the SQL literal (1 or 0)
|
|
61
|
+
def serialize(value)
|
|
62
|
+
return 'NULL' if value.nil?
|
|
63
|
+
|
|
64
|
+
value ? '1' : '0'
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
require 'date'
|
|
5
|
+
|
|
6
|
+
module ClickhouseRuby
|
|
7
|
+
module Types
|
|
8
|
+
# Type handler for ClickHouse date and datetime types
|
|
9
|
+
#
|
|
10
|
+
# Handles: Date, Date32, DateTime, DateTime64
|
|
11
|
+
#
|
|
12
|
+
# Date types:
|
|
13
|
+
# - Date: days since 1970-01-01 (range: 1970-01-01 to 2149-06-06)
|
|
14
|
+
# - Date32: days since 1970-01-01 (range: 1900-01-01 to 2299-12-31)
|
|
15
|
+
#
|
|
16
|
+
# DateTime types:
|
|
17
|
+
# - DateTime: seconds since 1970-01-01 00:00:00 UTC
|
|
18
|
+
# - DateTime64(precision): with sub-second precision (0-9 decimal places)
|
|
19
|
+
#
|
|
20
|
+
class DateTime < Base
|
|
21
|
+
# The precision for DateTime64 (nil for other types)
|
|
22
|
+
# @return [Integer, nil] precision in decimal places
|
|
23
|
+
attr_reader :precision
|
|
24
|
+
|
|
25
|
+
# The timezone for DateTime types
|
|
26
|
+
# @return [String, nil] timezone name
|
|
27
|
+
attr_reader :timezone
|
|
28
|
+
|
|
29
|
+
def initialize(name, precision: nil, timezone: nil)
|
|
30
|
+
super(name)
|
|
31
|
+
@precision = precision
|
|
32
|
+
@timezone = timezone
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Converts a Ruby value to a Time or Date
|
|
36
|
+
#
|
|
37
|
+
# @param value [Object] the value to convert
|
|
38
|
+
# @return [Time, Date, nil] the time/date value
|
|
39
|
+
# @raise [TypeCastError] if the value cannot be converted
|
|
40
|
+
def cast(value)
|
|
41
|
+
return nil if value.nil?
|
|
42
|
+
|
|
43
|
+
case value
|
|
44
|
+
when ::Time
|
|
45
|
+
date_only? ? value.to_date : value
|
|
46
|
+
when ::Date
|
|
47
|
+
date_only? ? value : value.to_time
|
|
48
|
+
when ::String
|
|
49
|
+
parse_string(value)
|
|
50
|
+
when ::Integer
|
|
51
|
+
# Unix timestamp
|
|
52
|
+
date_only? ? Time.at(value).to_date : Time.at(value)
|
|
53
|
+
else
|
|
54
|
+
raise TypeCastError.new(
|
|
55
|
+
"Cannot cast #{value.class} to #{name}",
|
|
56
|
+
from_type: value.class.name,
|
|
57
|
+
to_type: name,
|
|
58
|
+
value: value
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Converts a value from ClickHouse to Ruby Time or Date
|
|
64
|
+
#
|
|
65
|
+
# @param value [Object] the value from ClickHouse
|
|
66
|
+
# @return [Time, Date, nil] the time/date value
|
|
67
|
+
def deserialize(value)
|
|
68
|
+
return nil if value.nil?
|
|
69
|
+
|
|
70
|
+
case value
|
|
71
|
+
when ::Time, ::Date
|
|
72
|
+
date_only? ? value.to_date : value.to_time
|
|
73
|
+
when ::String
|
|
74
|
+
parse_string(value)
|
|
75
|
+
when ::Integer
|
|
76
|
+
date_only? ? Time.at(value).to_date : Time.at(value)
|
|
77
|
+
else
|
|
78
|
+
parse_string(value.to_s)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Converts a time/date to SQL literal
|
|
83
|
+
#
|
|
84
|
+
# @param value [Time, Date, nil] the value to serialize
|
|
85
|
+
# @return [String] the SQL literal
|
|
86
|
+
def serialize(value)
|
|
87
|
+
return 'NULL' if value.nil?
|
|
88
|
+
|
|
89
|
+
if date_only?
|
|
90
|
+
format_date(value)
|
|
91
|
+
else
|
|
92
|
+
format_datetime(value)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns whether this is a date-only type (Date, Date32)
|
|
97
|
+
#
|
|
98
|
+
# @return [Boolean] true if date-only
|
|
99
|
+
def date_only?
|
|
100
|
+
name.start_with?('Date') && !name.start_with?('DateTime')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Parses a string to a Time or Date
|
|
106
|
+
#
|
|
107
|
+
# @param value [String] the string to parse
|
|
108
|
+
# @return [Time, Date] the parsed value
|
|
109
|
+
# @raise [TypeCastError] if the string cannot be parsed
|
|
110
|
+
def parse_string(value)
|
|
111
|
+
stripped = value.strip
|
|
112
|
+
|
|
113
|
+
if stripped.empty?
|
|
114
|
+
raise TypeCastError.new(
|
|
115
|
+
"Cannot cast empty string to #{name}",
|
|
116
|
+
from_type: 'String',
|
|
117
|
+
to_type: name,
|
|
118
|
+
value: value
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if date_only?
|
|
123
|
+
::Date.parse(stripped)
|
|
124
|
+
else
|
|
125
|
+
::Time.parse(stripped)
|
|
126
|
+
end
|
|
127
|
+
rescue ArgumentError => e
|
|
128
|
+
raise TypeCastError.new(
|
|
129
|
+
"Cannot cast '#{value}' to #{name}: #{e.message}",
|
|
130
|
+
from_type: 'String',
|
|
131
|
+
to_type: name,
|
|
132
|
+
value: value
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Formats a date value for SQL
|
|
137
|
+
#
|
|
138
|
+
# @param value [Date, Time] the value to format
|
|
139
|
+
# @return [String] the formatted SQL literal
|
|
140
|
+
def format_date(value)
|
|
141
|
+
date = value.respond_to?(:to_date) ? value.to_date : value
|
|
142
|
+
"'#{date.strftime('%Y-%m-%d')}'"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Formats a datetime value for SQL
|
|
146
|
+
#
|
|
147
|
+
# @param value [Time, Date] the value to format
|
|
148
|
+
# @return [String] the formatted SQL literal
|
|
149
|
+
def format_datetime(value)
|
|
150
|
+
time = value.respond_to?(:to_time) ? value.to_time : value
|
|
151
|
+
|
|
152
|
+
if @precision && @precision > 0
|
|
153
|
+
# DateTime64 with fractional seconds
|
|
154
|
+
format_str = "%Y-%m-%d %H:%M:%S.%#{@precision}N"
|
|
155
|
+
"'#{time.strftime(format_str)}'"
|
|
156
|
+
else
|
|
157
|
+
# Regular DateTime
|
|
158
|
+
"'#{time.strftime('%Y-%m-%d %H:%M:%S')}'"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickhouseRuby
|
|
4
|
+
module Types
|
|
5
|
+
# Type handler for ClickHouse floating point types
|
|
6
|
+
#
|
|
7
|
+
# Handles: Float32, Float64
|
|
8
|
+
#
|
|
9
|
+
# Note: ClickHouse also supports special values: inf, -inf, nan
|
|
10
|
+
#
|
|
11
|
+
class Float < Base
|
|
12
|
+
# Converts a Ruby value to a float
|
|
13
|
+
#
|
|
14
|
+
# @param value [Object] the value to convert
|
|
15
|
+
# @return [Float, nil] the float value
|
|
16
|
+
# @raise [TypeCastError] if the value cannot be converted
|
|
17
|
+
def cast(value)
|
|
18
|
+
return nil if value.nil?
|
|
19
|
+
|
|
20
|
+
case value
|
|
21
|
+
when ::Float
|
|
22
|
+
value
|
|
23
|
+
when ::Integer
|
|
24
|
+
value.to_f
|
|
25
|
+
when ::String
|
|
26
|
+
parse_string(value)
|
|
27
|
+
when ::BigDecimal
|
|
28
|
+
value.to_f
|
|
29
|
+
else
|
|
30
|
+
raise TypeCastError.new(
|
|
31
|
+
"Cannot cast #{value.class} to #{name}",
|
|
32
|
+
from_type: value.class.name,
|
|
33
|
+
to_type: name,
|
|
34
|
+
value: value
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Converts a value from ClickHouse to Ruby Float
|
|
40
|
+
#
|
|
41
|
+
# @param value [Object] the value from ClickHouse
|
|
42
|
+
# @return [Float, nil] the float value
|
|
43
|
+
def deserialize(value)
|
|
44
|
+
return nil if value.nil?
|
|
45
|
+
|
|
46
|
+
case value
|
|
47
|
+
when ::Float
|
|
48
|
+
value
|
|
49
|
+
when ::String
|
|
50
|
+
parse_string(value)
|
|
51
|
+
else
|
|
52
|
+
value.to_f
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Converts a float to SQL literal
|
|
57
|
+
#
|
|
58
|
+
# @param value [Float, nil] the value to serialize
|
|
59
|
+
# @return [String] the SQL literal
|
|
60
|
+
def serialize(value)
|
|
61
|
+
return 'NULL' if value.nil?
|
|
62
|
+
|
|
63
|
+
if value.nan?
|
|
64
|
+
'nan'
|
|
65
|
+
elsif value.infinite? == 1
|
|
66
|
+
'inf'
|
|
67
|
+
elsif value.infinite? == -1
|
|
68
|
+
'-inf'
|
|
69
|
+
else
|
|
70
|
+
value.to_s
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Parses a string to a float
|
|
77
|
+
#
|
|
78
|
+
# @param value [String] the string to parse
|
|
79
|
+
# @return [Float] the parsed float
|
|
80
|
+
# @raise [TypeCastError] if the string is not a valid float
|
|
81
|
+
def parse_string(value)
|
|
82
|
+
stripped = value.strip.downcase
|
|
83
|
+
|
|
84
|
+
# Handle special values
|
|
85
|
+
case stripped
|
|
86
|
+
when 'inf', '+inf', 'infinity', '+infinity'
|
|
87
|
+
::Float::INFINITY
|
|
88
|
+
when '-inf', '-infinity'
|
|
89
|
+
-::Float::INFINITY
|
|
90
|
+
when 'nan'
|
|
91
|
+
::Float::NAN
|
|
92
|
+
else
|
|
93
|
+
# 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
|
|
102
|
+
|
|
103
|
+
Float(stripped)
|
|
104
|
+
end
|
|
105
|
+
rescue ArgumentError
|
|
106
|
+
raise TypeCastError.new(
|
|
107
|
+
"Cannot cast '#{value}' to #{name}",
|
|
108
|
+
from_type: 'String',
|
|
109
|
+
to_type: name,
|
|
110
|
+
value: value
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|