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
|
@@ -8,45 +8,42 @@ module ClickhouseRuby
|
|
|
8
8
|
# but can accept various truthy/falsy values.
|
|
9
9
|
#
|
|
10
10
|
class Boolean < Base
|
|
11
|
+
include NullSafe
|
|
12
|
+
|
|
11
13
|
# Values that represent true
|
|
12
|
-
TRUE_VALUES = [true, 1,
|
|
14
|
+
TRUE_VALUES = [true, 1, "1", "true", "TRUE", "True", "t", "T", "yes", "YES", "Yes", "y", "Y", "on", "ON",
|
|
15
|
+
"On",].freeze
|
|
13
16
|
|
|
14
17
|
# Values that represent false
|
|
15
|
-
FALSE_VALUES = [false, 0,
|
|
18
|
+
FALSE_VALUES = [false, 0, "0", "false", "FALSE", "False", "f", "F", "no", "NO", "No", "n", "N", "off", "OFF",
|
|
19
|
+
"Off",].freeze
|
|
20
|
+
|
|
21
|
+
protected
|
|
16
22
|
|
|
17
23
|
# Converts a Ruby value to a boolean
|
|
18
24
|
#
|
|
19
|
-
# @param value [Object] the value to convert
|
|
20
|
-
# @return [Boolean
|
|
25
|
+
# @param value [Object] the value to convert (guaranteed non-nil)
|
|
26
|
+
# @return [Boolean] the boolean value
|
|
21
27
|
# @raise [TypeCastError] if the value cannot be interpreted as boolean
|
|
22
|
-
def
|
|
23
|
-
return nil if value.nil?
|
|
24
|
-
|
|
28
|
+
def cast_value(value)
|
|
25
29
|
if TRUE_VALUES.include?(value)
|
|
26
30
|
true
|
|
27
31
|
elsif FALSE_VALUES.include?(value)
|
|
28
32
|
false
|
|
29
33
|
else
|
|
30
|
-
|
|
31
|
-
"Cannot cast '#{value}' to Bool",
|
|
32
|
-
from_type: value.class.name,
|
|
33
|
-
to_type: name,
|
|
34
|
-
value: value
|
|
35
|
-
)
|
|
34
|
+
raise_cast_error(value, "Cannot cast '#{value}' to Bool")
|
|
36
35
|
end
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
# Converts a value from ClickHouse to Ruby boolean
|
|
40
39
|
#
|
|
41
|
-
# @param value [Object] the value from ClickHouse
|
|
42
|
-
# @return [Boolean
|
|
43
|
-
def
|
|
44
|
-
return nil if value.nil?
|
|
45
|
-
|
|
40
|
+
# @param value [Object] the value from ClickHouse (guaranteed non-nil)
|
|
41
|
+
# @return [Boolean] the boolean value
|
|
42
|
+
def deserialize_value(value)
|
|
46
43
|
case value
|
|
47
|
-
when true, 1,
|
|
44
|
+
when true, 1, "1", "true"
|
|
48
45
|
true
|
|
49
|
-
when false, 0,
|
|
46
|
+
when false, 0, "0", "false"
|
|
50
47
|
false
|
|
51
48
|
else
|
|
52
49
|
# Default to truthy evaluation
|
|
@@ -56,12 +53,18 @@ module ClickhouseRuby
|
|
|
56
53
|
|
|
57
54
|
# Converts a boolean to SQL literal
|
|
58
55
|
#
|
|
59
|
-
# @param value [Boolean
|
|
56
|
+
# @param value [Boolean] the value to serialize (guaranteed non-nil)
|
|
60
57
|
# @return [String] the SQL literal (1 or 0)
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
def serialize_value(value)
|
|
59
|
+
# Check explicit FALSE_VALUES first since Ruby's 0 is truthy
|
|
60
|
+
if FALSE_VALUES.include?(value)
|
|
61
|
+
"0"
|
|
62
|
+
elsif TRUE_VALUES.include?(value)
|
|
63
|
+
"1"
|
|
64
|
+
else
|
|
65
|
+
# Default to truthy evaluation for other values
|
|
66
|
+
value ? "1" : "0"
|
|
67
|
+
end
|
|
65
68
|
end
|
|
66
69
|
end
|
|
67
70
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "time"
|
|
4
|
+
require "date"
|
|
5
5
|
|
|
6
6
|
module ClickhouseRuby
|
|
7
7
|
module Types
|
|
@@ -51,12 +51,7 @@ module ClickhouseRuby
|
|
|
51
51
|
# Unix timestamp
|
|
52
52
|
date_only? ? Time.at(value).to_date : Time.at(value)
|
|
53
53
|
else
|
|
54
|
-
|
|
55
|
-
"Cannot cast #{value.class} to #{name}",
|
|
56
|
-
from_type: value.class.name,
|
|
57
|
-
to_type: name,
|
|
58
|
-
value: value
|
|
59
|
-
)
|
|
54
|
+
raise_cast_error(value)
|
|
60
55
|
end
|
|
61
56
|
end
|
|
62
57
|
|
|
@@ -84,7 +79,7 @@ module ClickhouseRuby
|
|
|
84
79
|
# @param value [Time, Date, nil] the value to serialize
|
|
85
80
|
# @return [String] the SQL literal
|
|
86
81
|
def serialize(value)
|
|
87
|
-
return
|
|
82
|
+
return "NULL" if value.nil?
|
|
88
83
|
|
|
89
84
|
if date_only?
|
|
90
85
|
format_date(value)
|
|
@@ -97,7 +92,7 @@ module ClickhouseRuby
|
|
|
97
92
|
#
|
|
98
93
|
# @return [Boolean] true if date-only
|
|
99
94
|
def date_only?
|
|
100
|
-
name.start_with?(
|
|
95
|
+
name.start_with?("Date") && !name.start_with?("DateTime")
|
|
101
96
|
end
|
|
102
97
|
|
|
103
98
|
private
|
|
@@ -110,14 +105,7 @@ module ClickhouseRuby
|
|
|
110
105
|
def parse_string(value)
|
|
111
106
|
stripped = value.strip
|
|
112
107
|
|
|
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
|
|
108
|
+
raise_empty_string_error(value) if stripped.empty?
|
|
121
109
|
|
|
122
110
|
if date_only?
|
|
123
111
|
::Date.parse(stripped)
|
|
@@ -125,12 +113,7 @@ module ClickhouseRuby
|
|
|
125
113
|
::Time.parse(stripped)
|
|
126
114
|
end
|
|
127
115
|
rescue ArgumentError => e
|
|
128
|
-
|
|
129
|
-
"Cannot cast '#{value}' to #{name}: #{e.message}",
|
|
130
|
-
from_type: 'String',
|
|
131
|
-
to_type: name,
|
|
132
|
-
value: value
|
|
133
|
-
)
|
|
116
|
+
raise_cast_error(value, "Cannot cast '#{value}' to #{name}: #{e.message}")
|
|
134
117
|
end
|
|
135
118
|
|
|
136
119
|
# Formats a date value for SQL
|
|
@@ -139,7 +122,7 @@ module ClickhouseRuby
|
|
|
139
122
|
# @return [String] the formatted SQL literal
|
|
140
123
|
def format_date(value)
|
|
141
124
|
date = value.respond_to?(:to_date) ? value.to_date : value
|
|
142
|
-
"'#{date.strftime(
|
|
125
|
+
"'#{date.strftime("%Y-%m-%d")}'"
|
|
143
126
|
end
|
|
144
127
|
|
|
145
128
|
# Formats a datetime value for SQL
|
|
@@ -149,13 +132,13 @@ module ClickhouseRuby
|
|
|
149
132
|
def format_datetime(value)
|
|
150
133
|
time = value.respond_to?(:to_time) ? value.to_time : value
|
|
151
134
|
|
|
152
|
-
if @precision
|
|
135
|
+
if @precision&.positive?
|
|
153
136
|
# DateTime64 with fractional seconds
|
|
154
137
|
format_str = "%Y-%m-%d %H:%M:%S.%#{@precision}N"
|
|
155
138
|
"'#{time.strftime(format_str)}'"
|
|
156
139
|
else
|
|
157
140
|
# Regular DateTime
|
|
158
|
-
"'#{time.strftime(
|
|
141
|
+
"'#{time.strftime("%Y-%m-%d %H:%M:%S")}'"
|
|
159
142
|
end
|
|
160
143
|
end
|
|
161
144
|
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickhouseRuby
|
|
4
|
+
module Types
|
|
5
|
+
# Type handler for ClickHouse Decimal types
|
|
6
|
+
#
|
|
7
|
+
# Handles: Decimal(P, S), Decimal32(S), Decimal64(S), Decimal128(S), Decimal256(S)
|
|
8
|
+
#
|
|
9
|
+
# Uses BigDecimal for arbitrary precision arithmetic to avoid floating point errors.
|
|
10
|
+
# Validates precision and scale according to ClickHouse constraints.
|
|
11
|
+
#
|
|
12
|
+
class Decimal < Base
|
|
13
|
+
# Precision limits for each Decimal variant
|
|
14
|
+
PRECISION_LIMITS = {
|
|
15
|
+
32 => 9,
|
|
16
|
+
64 => 18,
|
|
17
|
+
128 => 38,
|
|
18
|
+
256 => 76,
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# @return [Integer] the precision (total number of significant digits)
|
|
22
|
+
attr_reader :precision
|
|
23
|
+
|
|
24
|
+
# @return [Integer] the scale (number of digits after decimal point)
|
|
25
|
+
attr_reader :scale
|
|
26
|
+
|
|
27
|
+
# Initializes a Decimal type
|
|
28
|
+
#
|
|
29
|
+
# @param name [String] the ClickHouse type name (e.g., 'Decimal(18, 4)')
|
|
30
|
+
# @param precision [Integer, nil] optional precision override
|
|
31
|
+
# @param scale [Integer, nil] optional scale override
|
|
32
|
+
# @param arg_types [Array, nil] optional parsed argument types (ignored for Decimal)
|
|
33
|
+
# @raise [ConfigurationError] if precision/scale are invalid
|
|
34
|
+
def initialize(name, precision: nil, scale: nil, arg_types: nil)
|
|
35
|
+
super(name)
|
|
36
|
+
@precision, @scale = parse_decimal_params(name, precision, scale)
|
|
37
|
+
validate_params!
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Converts a Ruby value to BigDecimal
|
|
41
|
+
#
|
|
42
|
+
# @param value [Object] the value to convert
|
|
43
|
+
# @return [BigDecimal, nil] the BigDecimal value
|
|
44
|
+
# @raise [TypeCastError] if the value cannot be converted
|
|
45
|
+
def cast(value)
|
|
46
|
+
return nil if value.nil?
|
|
47
|
+
|
|
48
|
+
bd = case value
|
|
49
|
+
when ::BigDecimal
|
|
50
|
+
value
|
|
51
|
+
when ::Integer
|
|
52
|
+
BigDecimal(value)
|
|
53
|
+
when ::Float
|
|
54
|
+
BigDecimal(value.to_s)
|
|
55
|
+
when ::String
|
|
56
|
+
BigDecimal(value)
|
|
57
|
+
else
|
|
58
|
+
raise_cast_error(value)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
validate_value!(bd)
|
|
62
|
+
bd
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Converts a value from ClickHouse response to Ruby BigDecimal
|
|
66
|
+
#
|
|
67
|
+
# @param value [Object] the value from ClickHouse
|
|
68
|
+
# @return [BigDecimal, nil] the BigDecimal value
|
|
69
|
+
def deserialize(value)
|
|
70
|
+
return nil if value.nil?
|
|
71
|
+
|
|
72
|
+
BigDecimal(value.to_s)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Converts a BigDecimal to ClickHouse SQL literal
|
|
76
|
+
#
|
|
77
|
+
# @param value [BigDecimal, nil] the value to serialize
|
|
78
|
+
# @return [String] the SQL literal
|
|
79
|
+
def serialize(value)
|
|
80
|
+
return "NULL" if value.nil?
|
|
81
|
+
|
|
82
|
+
bd = cast(value)
|
|
83
|
+
# Use 'F' format to preserve precision (fixed-point notation)
|
|
84
|
+
bd.to_s("F")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns the internal ClickHouse type based on precision
|
|
88
|
+
#
|
|
89
|
+
# @return [Symbol] the internal type (:Decimal32, :Decimal64, :Decimal128, or :Decimal256)
|
|
90
|
+
def internal_type
|
|
91
|
+
case precision
|
|
92
|
+
when 1..9
|
|
93
|
+
:Decimal32
|
|
94
|
+
when 10..18
|
|
95
|
+
:Decimal64
|
|
96
|
+
when 19..38
|
|
97
|
+
:Decimal128
|
|
98
|
+
when 39..76
|
|
99
|
+
:Decimal256
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Parses precision and scale from type string
|
|
106
|
+
#
|
|
107
|
+
# Handles:
|
|
108
|
+
# - Decimal(18, 4) → precision=18, scale=4
|
|
109
|
+
# - Decimal64(4) → precision=max_for_variant, scale=4
|
|
110
|
+
#
|
|
111
|
+
# @param name [String] the type string
|
|
112
|
+
# @param precision [Integer, nil] optional override
|
|
113
|
+
# @param scale [Integer, nil] optional override
|
|
114
|
+
# @return [Array<Integer>] [precision, scale]
|
|
115
|
+
def parse_decimal_params(name, precision, scale)
|
|
116
|
+
# Match: Decimal(P, S) or Decimal32(S), Decimal64(S), etc.
|
|
117
|
+
# Allow negative numbers to be parsed (for proper error reporting)
|
|
118
|
+
if name =~ /^Decimal(\d{2,3})?\((-?\d+)(?:,\s*(-?\d+))?\)$/
|
|
119
|
+
variant = Regexp.last_match(1)&.to_i
|
|
120
|
+
first_arg = Regexp.last_match(2).to_i
|
|
121
|
+
second_arg = Regexp.last_match(3)&.to_i
|
|
122
|
+
|
|
123
|
+
if variant
|
|
124
|
+
# Decimal32(4) → variant determines precision, first_arg is scale
|
|
125
|
+
max_p = PRECISION_LIMITS[variant]
|
|
126
|
+
[max_p, first_arg]
|
|
127
|
+
else
|
|
128
|
+
# Decimal(18, 4) → first_arg is precision, second_arg is scale
|
|
129
|
+
second_arg ||= 0
|
|
130
|
+
[first_arg, second_arg]
|
|
131
|
+
end
|
|
132
|
+
else
|
|
133
|
+
# Fallback to parameters or defaults
|
|
134
|
+
[precision || 10, scale || 0]
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Validates that precision and scale parameters are valid
|
|
139
|
+
#
|
|
140
|
+
# @raise [ConfigurationError] if validation fails
|
|
141
|
+
def validate_params!
|
|
142
|
+
unless (1..76).include?(@precision)
|
|
143
|
+
raise ConfigurationError, "Decimal precision must be 1-76, got #{@precision}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
return if (0..@precision).include?(@scale)
|
|
147
|
+
|
|
148
|
+
raise ConfigurationError, "Decimal scale must be 0-#{@precision}, got #{@scale}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Validates that a BigDecimal value doesn't exceed type limits
|
|
152
|
+
#
|
|
153
|
+
# @param bd [BigDecimal] the value to validate
|
|
154
|
+
# @raise [TypeCastError] if the value exceeds limits
|
|
155
|
+
def validate_value!(bd)
|
|
156
|
+
# Check integer part doesn't exceed allowed digits
|
|
157
|
+
max_integer_digits = @precision - @scale
|
|
158
|
+
|
|
159
|
+
# Use 'F' format to get fixed-point notation
|
|
160
|
+
bd_str = bd.to_s("F")
|
|
161
|
+
# Split on decimal point to get integer and fractional parts
|
|
162
|
+
parts = bd_str.split(".")
|
|
163
|
+
integer_part = parts[0].gsub(/[^0-9]/, "") # Remove sign
|
|
164
|
+
# Remove leading zeros
|
|
165
|
+
integer_digits = integer_part.sub(/^0+/, "") || "0"
|
|
166
|
+
|
|
167
|
+
return unless integer_digits.length > max_integer_digits
|
|
168
|
+
|
|
169
|
+
raise_cast_error(bd, "Value #{bd} exceeds maximum integer digits (#{max_integer_digits}) for #{name}")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClickhouseRuby
|
|
4
|
+
module Types
|
|
5
|
+
# Type handler for ClickHouse Enum types
|
|
6
|
+
#
|
|
7
|
+
# Handles: Enum, Enum8, Enum16
|
|
8
|
+
#
|
|
9
|
+
# Enum types store predefined string values as integers for efficient storage.
|
|
10
|
+
# Supports both explicit value assignment ('name' = value) and auto-increment syntax.
|
|
11
|
+
#
|
|
12
|
+
class Enum < Base
|
|
13
|
+
# @return [Array<String>] the list of possible enum values
|
|
14
|
+
attr_reader :possible_values
|
|
15
|
+
|
|
16
|
+
# @return [Hash{String => Integer}] mapping of string values to integers
|
|
17
|
+
attr_reader :value_to_int
|
|
18
|
+
|
|
19
|
+
# @return [Hash{Integer => String}] mapping of integers to string values
|
|
20
|
+
attr_reader :int_to_value
|
|
21
|
+
|
|
22
|
+
def initialize(name, arg_types: nil)
|
|
23
|
+
super(name)
|
|
24
|
+
|
|
25
|
+
# When called from registry, we get arg_types with parsed enum entries.
|
|
26
|
+
# When called directly (for testing), we get the full name like "Enum8('active' = 1, 'inactive' = 2)"
|
|
27
|
+
if arg_types && !arg_types.empty?
|
|
28
|
+
@possible_values, @value_to_int, @int_to_value = parse_enum_from_args(name, arg_types)
|
|
29
|
+
else
|
|
30
|
+
@possible_values, @value_to_int, @int_to_value = parse_enum_definition(name)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Converts a Ruby value to a valid enum value
|
|
35
|
+
#
|
|
36
|
+
# @param value [String, Integer, nil] the value to convert
|
|
37
|
+
# @return [String, nil] the enum value (string)
|
|
38
|
+
# @raise [TypeCastError] if value is not a valid enum value
|
|
39
|
+
def cast(value)
|
|
40
|
+
return nil if value.nil?
|
|
41
|
+
|
|
42
|
+
case value
|
|
43
|
+
when ::String
|
|
44
|
+
validate_string_value!(value)
|
|
45
|
+
value
|
|
46
|
+
when ::Integer
|
|
47
|
+
validate_int_value!(value)
|
|
48
|
+
@int_to_value[value]
|
|
49
|
+
else
|
|
50
|
+
raise_cast_error(value, "Cannot cast #{value.class} to #{self}")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Converts a value from ClickHouse response format to Ruby
|
|
55
|
+
#
|
|
56
|
+
# @param value [Object] the value from ClickHouse
|
|
57
|
+
# @return [String] the string value
|
|
58
|
+
def deserialize(value)
|
|
59
|
+
value.to_s
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Converts a Ruby value to ClickHouse SQL literal format
|
|
63
|
+
#
|
|
64
|
+
# @param value [String, nil] the value to serialize
|
|
65
|
+
# @return [String] the SQL literal
|
|
66
|
+
def serialize(value)
|
|
67
|
+
return "NULL" if value.nil?
|
|
68
|
+
|
|
69
|
+
escaped = value.to_s.gsub("'", "\\\\'")
|
|
70
|
+
"'#{escaped}'"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Parses an Enum from registry arg_types
|
|
76
|
+
#
|
|
77
|
+
# The arg_types are Base type objects where the :type is the enum entry string
|
|
78
|
+
#
|
|
79
|
+
# @param name [String] the type name (e.g., "Enum8")
|
|
80
|
+
# @param arg_types [Array<Base>] the parsed enum entries
|
|
81
|
+
# @return [Array] [possible_values, value_to_int, int_to_value]
|
|
82
|
+
def parse_enum_from_args(_name, arg_types)
|
|
83
|
+
possible_values = []
|
|
84
|
+
value_to_int = {}
|
|
85
|
+
int_to_value = {}
|
|
86
|
+
auto_index = 1
|
|
87
|
+
|
|
88
|
+
arg_types.each do |arg_type|
|
|
89
|
+
# The arg_type.name contains the enum entry string
|
|
90
|
+
entry = arg_type.name
|
|
91
|
+
enum_name, int_val = parse_enum_entry(entry)
|
|
92
|
+
|
|
93
|
+
possible_values << enum_name
|
|
94
|
+
|
|
95
|
+
# If no explicit value, use auto-increment
|
|
96
|
+
if int_val.nil?
|
|
97
|
+
int_val = auto_index
|
|
98
|
+
auto_index += 1
|
|
99
|
+
elsif int_val >= auto_index
|
|
100
|
+
auto_index = int_val + 1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
value_to_int[enum_name] = int_val
|
|
104
|
+
int_to_value[int_val] = enum_name
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
[possible_values, value_to_int, int_to_value]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Parses an Enum type definition to extract values and mappings
|
|
111
|
+
#
|
|
112
|
+
# Handles both syntaxes:
|
|
113
|
+
# - Explicit: Enum8('active' = 1, 'inactive' = 2)
|
|
114
|
+
# - Auto-increment: Enum('hello', 'world') # hello=1, world=2
|
|
115
|
+
#
|
|
116
|
+
# @param type_string [String] the full type string (e.g., "Enum8('active' = 1)")
|
|
117
|
+
# @return [Array] [possible_values, value_to_int, int_to_value]
|
|
118
|
+
def parse_enum_definition(type_string)
|
|
119
|
+
# Extract the part inside parentheses
|
|
120
|
+
# e.g., Enum8('active' = 1, 'inactive' = 2) -> 'active' = 1, 'inactive' = 2
|
|
121
|
+
match = type_string.match(/\((.*)\)\z/m)
|
|
122
|
+
raise TypeCastError, "Invalid Enum definition: #{type_string}" unless match
|
|
123
|
+
|
|
124
|
+
enum_args = match[1]
|
|
125
|
+
|
|
126
|
+
possible_values = []
|
|
127
|
+
value_to_int = {}
|
|
128
|
+
int_to_value = {}
|
|
129
|
+
|
|
130
|
+
# Split by comma, but need to handle escaped quotes
|
|
131
|
+
# Parse each enum entry: 'name' = value or 'name'
|
|
132
|
+
entries = parse_enum_entries(enum_args)
|
|
133
|
+
|
|
134
|
+
auto_index = 1 # For auto-increment values
|
|
135
|
+
|
|
136
|
+
entries.each do |entry|
|
|
137
|
+
name, int_val = parse_enum_entry(entry)
|
|
138
|
+
possible_values << name
|
|
139
|
+
|
|
140
|
+
# If no explicit value, use auto-increment
|
|
141
|
+
if int_val.nil?
|
|
142
|
+
int_val = auto_index
|
|
143
|
+
auto_index += 1
|
|
144
|
+
elsif int_val >= auto_index
|
|
145
|
+
auto_index = int_val + 1
|
|
146
|
+
end
|
|
147
|
+
# Update auto_index to be the next number after the highest seen
|
|
148
|
+
|
|
149
|
+
value_to_int[name] = int_val
|
|
150
|
+
int_to_value[int_val] = name
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
[possible_values, value_to_int, int_to_value]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Splits enum entries by comma, handling escaped quotes
|
|
157
|
+
#
|
|
158
|
+
# @param enum_args [String] the enum arguments string
|
|
159
|
+
# @return [Array<String>] list of enum entries
|
|
160
|
+
def parse_enum_entries(enum_args)
|
|
161
|
+
entries = []
|
|
162
|
+
current_entry = +""
|
|
163
|
+
in_quotes = false
|
|
164
|
+
i = 0
|
|
165
|
+
|
|
166
|
+
while i < enum_args.length
|
|
167
|
+
char = enum_args[i]
|
|
168
|
+
|
|
169
|
+
if char == "'" && (i.zero? || enum_args[i - 1] != "\\")
|
|
170
|
+
in_quotes = !in_quotes
|
|
171
|
+
current_entry += char
|
|
172
|
+
elsif char == "," && !in_quotes
|
|
173
|
+
entries << current_entry.strip
|
|
174
|
+
current_entry = +""
|
|
175
|
+
else
|
|
176
|
+
current_entry += char
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
i += 1
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
entries << current_entry.strip if current_entry.strip.length.positive?
|
|
183
|
+
entries
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Parses a single enum entry
|
|
187
|
+
#
|
|
188
|
+
# @param entry [String] e.g., "'active' = 1", "'value'", or "active" (from parser)
|
|
189
|
+
# @return [Array<String, Integer>] [name, integer_value]
|
|
190
|
+
def parse_enum_entry(entry)
|
|
191
|
+
entry = entry.strip
|
|
192
|
+
|
|
193
|
+
# Check if it has explicit assignment: 'name' = value or name = value (from parser)
|
|
194
|
+
if entry.include?("=")
|
|
195
|
+
parts = entry.split("=", 2)
|
|
196
|
+
name_part = parts[0].strip
|
|
197
|
+
value_part = parts[1].strip
|
|
198
|
+
|
|
199
|
+
# Extract name - might be quoted or unquoted
|
|
200
|
+
name = if name_part.start_with?("'") && name_part.end_with?("'")
|
|
201
|
+
extract_quoted_string(name_part)
|
|
202
|
+
else
|
|
203
|
+
# Already unquoted (from parser)
|
|
204
|
+
name_part
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Parse integer value
|
|
208
|
+
int_val = value_part.to_i
|
|
209
|
+
|
|
210
|
+
[name, int_val]
|
|
211
|
+
else
|
|
212
|
+
# Auto-increment: just 'name' or name (from parser)
|
|
213
|
+
name = if entry.start_with?("'") && entry.end_with?("'")
|
|
214
|
+
extract_quoted_string(entry)
|
|
215
|
+
else
|
|
216
|
+
# Already unquoted (from parser)
|
|
217
|
+
entry
|
|
218
|
+
end
|
|
219
|
+
# Will assign index + 1 later
|
|
220
|
+
[name, nil]
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Extracts a string value from quotes, handling escapes
|
|
225
|
+
#
|
|
226
|
+
# @param quoted_str [String] e.g., "'hello'" or "'it\\'s'"
|
|
227
|
+
# @return [String] the unquoted string with escapes processed
|
|
228
|
+
def extract_quoted_string(quoted_str)
|
|
229
|
+
quoted_str = quoted_str.strip
|
|
230
|
+
# Remove surrounding quotes
|
|
231
|
+
unless quoted_str.start_with?("'") && quoted_str.end_with?("'")
|
|
232
|
+
raise TypeCastError, "Invalid enum value format: #{quoted_str}"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Remove quotes
|
|
236
|
+
unquoted = quoted_str[1...-1]
|
|
237
|
+
# Unescape single quotes
|
|
238
|
+
unquoted.gsub("\\'", "'")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Validates that a string value is a valid enum value
|
|
242
|
+
#
|
|
243
|
+
# @param value [String] the value to validate
|
|
244
|
+
# @raise [TypeCastError] if value is not valid
|
|
245
|
+
def validate_string_value!(value)
|
|
246
|
+
return if @possible_values.include?(value)
|
|
247
|
+
|
|
248
|
+
raise_cast_error(value, "Unknown enum value '#{value}'. Valid values: #{@possible_values.join(", ")}")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Validates that an integer value maps to a valid enum value
|
|
252
|
+
#
|
|
253
|
+
# @param value [Integer] the value to validate
|
|
254
|
+
# @raise [TypeCastError] if value is not valid
|
|
255
|
+
def validate_int_value!(value)
|
|
256
|
+
return if @int_to_value.key?(value)
|
|
257
|
+
|
|
258
|
+
raise_cast_error(value, "Unknown enum integer #{value}. Valid integers: #{@int_to_value.keys.join(", ")}")
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|