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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -1
  3. data/README.md +165 -79
  4. data/lib/clickhouse_ruby/active_record/arel_visitor.rb +205 -76
  5. data/lib/clickhouse_ruby/active_record/connection_adapter.rb +103 -98
  6. data/lib/clickhouse_ruby/active_record/railtie.rb +20 -15
  7. data/lib/clickhouse_ruby/active_record/relation_extensions.rb +398 -0
  8. data/lib/clickhouse_ruby/active_record/schema_statements.rb +90 -104
  9. data/lib/clickhouse_ruby/active_record.rb +24 -10
  10. data/lib/clickhouse_ruby/client.rb +181 -74
  11. data/lib/clickhouse_ruby/configuration.rb +51 -10
  12. data/lib/clickhouse_ruby/connection.rb +180 -64
  13. data/lib/clickhouse_ruby/connection_pool.rb +25 -19
  14. data/lib/clickhouse_ruby/errors.rb +13 -1
  15. data/lib/clickhouse_ruby/result.rb +11 -16
  16. data/lib/clickhouse_ruby/retry_handler.rb +172 -0
  17. data/lib/clickhouse_ruby/streaming_result.rb +309 -0
  18. data/lib/clickhouse_ruby/types/array.rb +11 -64
  19. data/lib/clickhouse_ruby/types/base.rb +59 -0
  20. data/lib/clickhouse_ruby/types/boolean.rb +28 -25
  21. data/lib/clickhouse_ruby/types/date_time.rb +10 -27
  22. data/lib/clickhouse_ruby/types/decimal.rb +173 -0
  23. data/lib/clickhouse_ruby/types/enum.rb +262 -0
  24. data/lib/clickhouse_ruby/types/float.rb +14 -28
  25. data/lib/clickhouse_ruby/types/integer.rb +21 -43
  26. data/lib/clickhouse_ruby/types/low_cardinality.rb +1 -1
  27. data/lib/clickhouse_ruby/types/map.rb +21 -36
  28. data/lib/clickhouse_ruby/types/null_safe.rb +81 -0
  29. data/lib/clickhouse_ruby/types/nullable.rb +2 -2
  30. data/lib/clickhouse_ruby/types/parser.rb +28 -18
  31. data/lib/clickhouse_ruby/types/registry.rb +40 -29
  32. data/lib/clickhouse_ruby/types/string.rb +9 -13
  33. data/lib/clickhouse_ruby/types/string_parser.rb +135 -0
  34. data/lib/clickhouse_ruby/types/tuple.rb +11 -68
  35. data/lib/clickhouse_ruby/types/uuid.rb +15 -22
  36. data/lib/clickhouse_ruby/types.rb +19 -15
  37. data/lib/clickhouse_ruby/version.rb +1 -1
  38. data/lib/clickhouse_ruby.rb +11 -11
  39. 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, '1', 'true', 'TRUE', 'True', 't', 'T', 'yes', 'YES', 'Yes', 'y', 'Y', 'on', 'ON', 'On'].freeze
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, '0', 'false', 'FALSE', 'False', 'f', 'F', 'no', 'NO', 'No', 'n', 'N', 'off', 'OFF', 'Off'].freeze
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, nil] the boolean value
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 cast(value)
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
- raise TypeCastError.new(
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, nil] the boolean value
43
- def deserialize(value)
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, '1', 'true'
44
+ when true, 1, "1", "true"
48
45
  true
49
- when false, 0, '0', 'false'
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, nil] the value to serialize
56
+ # @param value [Boolean] the value to serialize (guaranteed non-nil)
60
57
  # @return [String] the SQL literal (1 or 0)
61
- def serialize(value)
62
- return 'NULL' if value.nil?
63
-
64
- value ? '1' : '0'
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 'time'
4
- require 'date'
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
- raise TypeCastError.new(
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 'NULL' if value.nil?
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?('Date') && !name.start_with?('DateTime')
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
- raise TypeCastError.new(
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('%Y-%m-%d')}'"
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 && @precision > 0
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('%Y-%m-%d %H:%M:%S')}'"
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