activerecord-yugabytedb-adapter 7.0.4.1

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 (54) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +13 -0
  3. data/CHANGELOG.md +5 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/Gemfile +10 -0
  6. data/Gemfile.lock +67 -0
  7. data/README.md +31 -0
  8. data/Rakefile +8 -0
  9. data/activerecord-yugabytedb-adapter.gemspec +39 -0
  10. data/lib/active_record/connection_adapters/yugabytedb/column.rb +69 -0
  11. data/lib/active_record/connection_adapters/yugabytedb/database_statements.rb +156 -0
  12. data/lib/active_record/connection_adapters/yugabytedb/database_tasks.rb +19 -0
  13. data/lib/active_record/connection_adapters/yugabytedb/explain_pretty_printer.rb +44 -0
  14. data/lib/active_record/connection_adapters/yugabytedb/oid/array.rb +91 -0
  15. data/lib/active_record/connection_adapters/yugabytedb/oid/bit.rb +53 -0
  16. data/lib/active_record/connection_adapters/yugabytedb/oid/bit_varying.rb +15 -0
  17. data/lib/active_record/connection_adapters/yugabytedb/oid/bytea.rb +17 -0
  18. data/lib/active_record/connection_adapters/yugabytedb/oid/cidr.rb +48 -0
  19. data/lib/active_record/connection_adapters/yugabytedb/oid/date.rb +31 -0
  20. data/lib/active_record/connection_adapters/yugabytedb/oid/date_time.rb +36 -0
  21. data/lib/active_record/connection_adapters/yugabytedb/oid/decimal.rb +15 -0
  22. data/lib/active_record/connection_adapters/yugabytedb/oid/enum.rb +20 -0
  23. data/lib/active_record/connection_adapters/yugabytedb/oid/hstore.rb +109 -0
  24. data/lib/active_record/connection_adapters/yugabytedb/oid/inet.rb +15 -0
  25. data/lib/active_record/connection_adapters/yugabytedb/oid/interval.rb +49 -0
  26. data/lib/active_record/connection_adapters/yugabytedb/oid/jsonb.rb +15 -0
  27. data/lib/active_record/connection_adapters/yugabytedb/oid/legacy_point.rb +44 -0
  28. data/lib/active_record/connection_adapters/yugabytedb/oid/macaddr.rb +25 -0
  29. data/lib/active_record/connection_adapters/yugabytedb/oid/money.rb +41 -0
  30. data/lib/active_record/connection_adapters/yugabytedb/oid/oid.rb +15 -0
  31. data/lib/active_record/connection_adapters/yugabytedb/oid/point.rb +64 -0
  32. data/lib/active_record/connection_adapters/yugabytedb/oid/range.rb +115 -0
  33. data/lib/active_record/connection_adapters/yugabytedb/oid/specialized_string.rb +18 -0
  34. data/lib/active_record/connection_adapters/yugabytedb/oid/timestamp.rb +15 -0
  35. data/lib/active_record/connection_adapters/yugabytedb/oid/timestamp_with_time_zone.rb +30 -0
  36. data/lib/active_record/connection_adapters/yugabytedb/oid/type_map_initializer.rb +125 -0
  37. data/lib/active_record/connection_adapters/yugabytedb/oid/uuid.rb +35 -0
  38. data/lib/active_record/connection_adapters/yugabytedb/oid/vector.rb +28 -0
  39. data/lib/active_record/connection_adapters/yugabytedb/oid/xml.rb +30 -0
  40. data/lib/active_record/connection_adapters/yugabytedb/oid.rb +38 -0
  41. data/lib/active_record/connection_adapters/yugabytedb/quoting.rb +205 -0
  42. data/lib/active_record/connection_adapters/yugabytedb/referential_integrity.rb +77 -0
  43. data/lib/active_record/connection_adapters/yugabytedb/schema_creation.rb +100 -0
  44. data/lib/active_record/connection_adapters/yugabytedb/schema_definitions.rb +243 -0
  45. data/lib/active_record/connection_adapters/yugabytedb/schema_dumper.rb +74 -0
  46. data/lib/active_record/connection_adapters/yugabytedb/schema_statements.rb +812 -0
  47. data/lib/active_record/connection_adapters/yugabytedb/type_metadata.rb +44 -0
  48. data/lib/active_record/connection_adapters/yugabytedb/utils.rb +80 -0
  49. data/lib/active_record/connection_adapters/yugabytedb_adapter.rb +1069 -0
  50. data/lib/activerecord-yugabytedb-adapter.rb +11 -0
  51. data/lib/arel/visitors/yugabytedb.rb +99 -0
  52. data/lib/version.rb +5 -0
  53. data/sig/activerecord-yugabytedb-adapter.rbs +4 -0
  54. metadata +124 -0
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module YugabyteDB
6
+ module OID # :nodoc:
7
+ class Range < Type::Value # :nodoc:
8
+ attr_reader :subtype, :type
9
+ delegate :user_input_in_time_zone, to: :subtype
10
+
11
+ def initialize(subtype, type = :range)
12
+ @subtype = subtype
13
+ @type = type
14
+ end
15
+
16
+ def type_cast_for_schema(value)
17
+ value.inspect.gsub("Infinity", "::Float::INFINITY")
18
+ end
19
+
20
+ def cast_value(value)
21
+ return if value == "empty"
22
+ return value unless value.is_a?(::String)
23
+
24
+ extracted = extract_bounds(value)
25
+ from = type_cast_single extracted[:from]
26
+ to = type_cast_single extracted[:to]
27
+
28
+ if !infinity?(from) && extracted[:exclude_start]
29
+ raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')"
30
+ end
31
+ ::Range.new(from, to, extracted[:exclude_end])
32
+ end
33
+
34
+ def serialize(value)
35
+ if value.is_a?(::Range)
36
+ from = type_cast_single_for_database(value.begin)
37
+ to = type_cast_single_for_database(value.end)
38
+ ::Range.new(from, to, value.exclude_end?)
39
+ else
40
+ super
41
+ end
42
+ end
43
+
44
+ def ==(other)
45
+ other.is_a?(Range) &&
46
+ other.subtype == subtype &&
47
+ other.type == type
48
+ end
49
+
50
+ def map(value) # :nodoc:
51
+ new_begin = yield(value.begin)
52
+ new_end = yield(value.end)
53
+ ::Range.new(new_begin, new_end, value.exclude_end?)
54
+ end
55
+
56
+ def force_equality?(value)
57
+ value.is_a?(::Range)
58
+ end
59
+
60
+ private
61
+ def type_cast_single(value)
62
+ infinity?(value) ? value : @subtype.deserialize(value)
63
+ end
64
+
65
+ def type_cast_single_for_database(value)
66
+ infinity?(value) ? value : @subtype.serialize(@subtype.cast(value))
67
+ end
68
+
69
+ def extract_bounds(value)
70
+ from, to = value[1..-2].split(",", 2)
71
+ {
72
+ from: (from == "" || from == "-infinity") ? infinity(negative: true) : unquote(from),
73
+ to: (to == "" || to == "infinity") ? infinity : unquote(to),
74
+ exclude_start: value.start_with?("("),
75
+ exclude_end: value.end_with?(")")
76
+ }
77
+ end
78
+
79
+ # When formatting the bound values of range types, PostgreSQL quotes
80
+ # the bound value using double-quotes in certain conditions. Within
81
+ # a double-quoted string, literal " and \ characters are themselves
82
+ # escaped. In input, PostgreSQL accepts multiple escape styles for "
83
+ # (either \" or "") but in output always uses "".
84
+ # See:
85
+ # * https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-IO
86
+ # * https://www.postgresql.org/docs/current/rowtypes.html#ROWTYPES-IO-SYNTAX
87
+ def unquote(value)
88
+ if value.start_with?('"') && value.end_with?('"')
89
+ unquoted_value = value[1..-2]
90
+ unquoted_value.gsub!('""', '"')
91
+ unquoted_value.gsub!("\\\\", "\\")
92
+ unquoted_value
93
+ else
94
+ value
95
+ end
96
+ end
97
+
98
+ def infinity(negative: false)
99
+ if subtype.respond_to?(:infinity)
100
+ subtype.infinity(negative: negative)
101
+ elsif negative
102
+ -::Float::INFINITY
103
+ else
104
+ ::Float::INFINITY
105
+ end
106
+ end
107
+
108
+ def infinity?(value)
109
+ value.respond_to?(:infinite?) && value.infinite?
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module YugabyteDB
6
+ module OID # :nodoc:
7
+ class SpecializedString < Type::String # :nodoc:
8
+ attr_reader :type
9
+
10
+ def initialize(type, **options)
11
+ @type = type
12
+ super(**options)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module YugabyteDB
6
+ module OID # :nodoc:
7
+ class Timestamp < DateTime # :nodoc:
8
+ def type
9
+ real_type_unless_aliased(:timestamp)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module YugabyteDB
6
+ module OID # :nodoc:
7
+ class TimestampWithTimeZone < DateTime # :nodoc:
8
+ def type
9
+ real_type_unless_aliased(:timestamptz)
10
+ end
11
+
12
+ def cast_value(value)
13
+ return if value.blank?
14
+
15
+ time = super
16
+ return time if time.is_a?(ActiveSupport::TimeWithZone)
17
+
18
+ # While in UTC mode, the PG gem may not return times back in "UTC" even if they were provided to Postgres in UTC.
19
+ # We prefer times always in UTC, so here we convert back.
20
+ if is_utc?
21
+ time.getutc
22
+ else
23
+ time.getlocal
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/array/extract"
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module YugabyteDB
8
+ module OID # :nodoc:
9
+ # This class uses the data from PostgreSQL pg_type table to build
10
+ # the OID -> Type mapping.
11
+ # - OID is an integer representing the type.
12
+ # - Type is an OID::Type object.
13
+ # This class has side effects on the +store+ passed during initialization.
14
+ class TypeMapInitializer # :nodoc:
15
+ def initialize(store)
16
+ @store = store
17
+ end
18
+
19
+ def run(records)
20
+ nodes = records.reject { |row| @store.key? row["oid"].to_i }
21
+ mapped = nodes.extract! { |row| @store.key? row["typname"] }
22
+ ranges = nodes.extract! { |row| row["typtype"] == "r" }
23
+ enums = nodes.extract! { |row| row["typtype"] == "e" }
24
+ domains = nodes.extract! { |row| row["typtype"] == "d" }
25
+ arrays = nodes.extract! { |row| row["typinput"] == "array_in" }
26
+ composites = nodes.extract! { |row| row["typelem"].to_i != 0 }
27
+
28
+ mapped.each { |row| register_mapped_type(row) }
29
+ enums.each { |row| register_enum_type(row) }
30
+ domains.each { |row| register_domain_type(row) }
31
+ arrays.each { |row| register_array_type(row) }
32
+ ranges.each { |row| register_range_type(row) }
33
+ composites.each { |row| register_composite_type(row) }
34
+ end
35
+
36
+ def query_conditions_for_known_type_names
37
+ known_type_names = @store.keys.map { |n| "'#{n}'" }
38
+ <<~SQL % known_type_names.join(", ")
39
+ WHERE
40
+ t.typname IN (%s)
41
+ SQL
42
+ end
43
+
44
+ def query_conditions_for_known_type_types
45
+ known_type_types = %w('r' 'e' 'd')
46
+ <<~SQL % known_type_types.join(", ")
47
+ WHERE
48
+ t.typtype IN (%s)
49
+ SQL
50
+ end
51
+
52
+ def query_conditions_for_array_types
53
+ known_type_oids = @store.keys.reject { |k| k.is_a?(String) }
54
+ <<~SQL % [known_type_oids.join(", ")]
55
+ WHERE
56
+ t.typelem IN (%s)
57
+ SQL
58
+ end
59
+
60
+ private
61
+ def register_mapped_type(row)
62
+ alias_type row["oid"], row["typname"]
63
+ end
64
+
65
+ def register_enum_type(row)
66
+ register row["oid"], OID::Enum.new
67
+ end
68
+
69
+ def register_array_type(row)
70
+ register_with_subtype(row["oid"], row["typelem"].to_i) do |subtype|
71
+ OID::Array.new(subtype, row["typdelim"])
72
+ end
73
+ end
74
+
75
+ def register_range_type(row)
76
+ register_with_subtype(row["oid"], row["rngsubtype"].to_i) do |subtype|
77
+ OID::Range.new(subtype, row["typname"].to_sym)
78
+ end
79
+ end
80
+
81
+ def register_domain_type(row)
82
+ if base_type = @store.lookup(row["typbasetype"].to_i)
83
+ register row["oid"], base_type
84
+ else
85
+ warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}."
86
+ end
87
+ end
88
+
89
+ def register_composite_type(row)
90
+ if subtype = @store.lookup(row["typelem"].to_i)
91
+ register row["oid"], OID::Vector.new(row["typdelim"], subtype)
92
+ end
93
+ end
94
+
95
+ def register(oid, oid_type = nil, &block)
96
+ oid = assert_valid_registration(oid, oid_type || block)
97
+ if block_given?
98
+ @store.register_type(oid, &block)
99
+ else
100
+ @store.register_type(oid, oid_type)
101
+ end
102
+ end
103
+
104
+ def alias_type(oid, target)
105
+ oid = assert_valid_registration(oid, target)
106
+ @store.alias_type(oid, target)
107
+ end
108
+
109
+ def register_with_subtype(oid, target_oid)
110
+ if @store.key?(target_oid)
111
+ register(oid) do |_, *args|
112
+ yield @store.lookup(target_oid, *args)
113
+ end
114
+ end
115
+ end
116
+
117
+ def assert_valid_registration(oid, oid_type)
118
+ raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil?
119
+ oid.to_i
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module YugabyteDB
6
+ module OID # :nodoc:
7
+ class Uuid < Type::Value # :nodoc:
8
+ ACCEPTABLE_UUID = %r{\A(\{)?([a-fA-F0-9]{4}-?){8}(?(1)\}|)\z}
9
+
10
+ alias :serialize :deserialize
11
+
12
+ def type
13
+ :uuid
14
+ end
15
+
16
+ def changed?(old_value, new_value, _new_value_before_type_cast)
17
+ old_value.class != new_value.class ||
18
+ new_value && old_value.casecmp(new_value) != 0
19
+ end
20
+
21
+ def changed_in_place?(raw_old_value, new_value)
22
+ raw_old_value.class != new_value.class ||
23
+ new_value && raw_old_value.casecmp(new_value) != 0
24
+ end
25
+
26
+ private
27
+ def cast_value(value)
28
+ casted = value.to_s
29
+ casted if casted.match?(ACCEPTABLE_UUID)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module YugabyteDB
6
+ module OID # :nodoc:
7
+ class Vector < Type::Value # :nodoc:
8
+ attr_reader :delim, :subtype
9
+
10
+ # +delim+ corresponds to the `typdelim` column in the pg_types
11
+ # table. +subtype+ is derived from the `typelem` column in the
12
+ # pg_types table.
13
+ def initialize(delim, subtype)
14
+ @delim = delim
15
+ @subtype = subtype
16
+ end
17
+
18
+ # FIXME: this should probably split on +delim+ and use +subtype+
19
+ # to cast the values. Unfortunately, the current Rails behavior
20
+ # is to just return the string.
21
+ def cast(value)
22
+ value
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module YugabyteDB
6
+ module OID # :nodoc:
7
+ class Xml < Type::String # :nodoc:
8
+ def type
9
+ :xml
10
+ end
11
+
12
+ def serialize(value)
13
+ return unless value
14
+ Data.new(super)
15
+ end
16
+
17
+ class Data # :nodoc:
18
+ def initialize(value)
19
+ @value = value
20
+ end
21
+
22
+ def to_s
23
+ @value
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/connection_adapters/yugabytedb/oid/array"
4
+ require "active_record/connection_adapters/yugabytedb/oid/bit"
5
+ require "active_record/connection_adapters/yugabytedb/oid/bit_varying"
6
+ require "active_record/connection_adapters/yugabytedb/oid/bytea"
7
+ require "active_record/connection_adapters/yugabytedb/oid/cidr"
8
+ require "active_record/connection_adapters/yugabytedb/oid/date"
9
+ require "active_record/connection_adapters/yugabytedb/oid/date_time"
10
+ require "active_record/connection_adapters/yugabytedb/oid/decimal"
11
+ require "active_record/connection_adapters/yugabytedb/oid/enum"
12
+ require "active_record/connection_adapters/yugabytedb/oid/hstore"
13
+ require "active_record/connection_adapters/yugabytedb/oid/inet"
14
+ require "active_record/connection_adapters/yugabytedb/oid/interval"
15
+ require "active_record/connection_adapters/yugabytedb/oid/jsonb"
16
+ require "active_record/connection_adapters/yugabytedb/oid/macaddr"
17
+ require "active_record/connection_adapters/yugabytedb/oid/money"
18
+ require "active_record/connection_adapters/yugabytedb/oid/oid"
19
+ require "active_record/connection_adapters/yugabytedb/oid/point"
20
+ require "active_record/connection_adapters/yugabytedb/oid/legacy_point"
21
+ require "active_record/connection_adapters/yugabytedb/oid/range"
22
+ require "active_record/connection_adapters/yugabytedb/oid/specialized_string"
23
+ require "active_record/connection_adapters/yugabytedb/oid/timestamp"
24
+ require "active_record/connection_adapters/yugabytedb/oid/timestamp_with_time_zone"
25
+ require "active_record/connection_adapters/yugabytedb/oid/uuid"
26
+ require "active_record/connection_adapters/yugabytedb/oid/vector"
27
+ require "active_record/connection_adapters/yugabytedb/oid/xml"
28
+
29
+ require "active_record/connection_adapters/yugabytedb/oid/type_map_initializer"
30
+
31
+ module ActiveRecord
32
+ module ConnectionAdapters
33
+ module YugabyteDB
34
+ module OID # :nodoc:
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module YugabyteDB
6
+ module Quoting
7
+ # Escapes binary strings for bytea input to the database.
8
+ def escape_bytea(value)
9
+ @connection.escape_bytea(value) if value
10
+ end
11
+
12
+ # Unescapes bytea output from a database to the binary string it represents.
13
+ # NOTE: This is NOT an inverse of escape_bytea! This is only to be used
14
+ # on escaped binary output from database drive.
15
+ def unescape_bytea(value)
16
+ @connection.unescape_bytea(value) if value
17
+ end
18
+
19
+ def quote(value) # :nodoc:
20
+ case value
21
+ when OID::Xml::Data
22
+ "xml '#{quote_string(value.to_s)}'"
23
+ when OID::Bit::Data
24
+ if value.binary?
25
+ "B'#{value}'"
26
+ elsif value.hex?
27
+ "X'#{value}'"
28
+ end
29
+ when Numeric
30
+ if value.finite?
31
+ super
32
+ else
33
+ "'#{value}'"
34
+ end
35
+ when OID::Array::Data
36
+ quote(encode_array(value))
37
+ when Range
38
+ quote(encode_range(value))
39
+ else
40
+ super
41
+ end
42
+ end
43
+
44
+ # Quotes strings for use in SQL input.
45
+ def quote_string(s) # :nodoc:
46
+ @connection.escape(s)
47
+ end
48
+
49
+ # Checks the following cases:
50
+ #
51
+ # - table_name
52
+ # - "table.name"
53
+ # - schema_name.table_name
54
+ # - schema_name."table.name"
55
+ # - "schema.name".table_name
56
+ # - "schema.name"."table.name"
57
+ def quote_table_name(name) # :nodoc:
58
+ self.class.quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
59
+ end
60
+
61
+ # Quotes schema names for use in SQL queries.
62
+ def quote_schema_name(name)
63
+ YugabyteYSQL::Connection.quote_ident(name)
64
+ end
65
+
66
+ def quote_table_name_for_assignment(table, attr)
67
+ quote_column_name(attr)
68
+ end
69
+
70
+ # Quotes column names for use in SQL queries.
71
+ def quote_column_name(name) # :nodoc:
72
+ self.class.quoted_column_names[name] ||= YugabyteYSQL::Connection.quote_ident(super).freeze
73
+ end
74
+
75
+ # Quote date/time values for use in SQL input.
76
+ def quoted_date(value) # :nodoc:
77
+ if value.year <= 0
78
+ bce_year = format("%04d", -value.year + 1)
79
+ super.sub(/^-?\d+/, bce_year) + " BC"
80
+ else
81
+ super
82
+ end
83
+ end
84
+
85
+ def quoted_binary(value) # :nodoc:
86
+ "'#{escape_bytea(value.to_s)}'"
87
+ end
88
+
89
+ def quote_default_expression(value, column) # :nodoc:
90
+ if value.is_a?(Proc)
91
+ value.call
92
+ elsif column.type == :uuid && value.is_a?(String) && /\(\)/.match?(value)
93
+ value # Does not quote function default values for UUID columns
94
+ elsif column.respond_to?(:array?)
95
+ type = lookup_cast_type_from_column(column)
96
+ quote(type.serialize(value))
97
+ else
98
+ super
99
+ end
100
+ end
101
+
102
+ def type_cast(value) # :nodoc:
103
+ case value
104
+ when Type::Binary::Data
105
+ # Return a bind param hash with format as binary.
106
+ # See https://deveiate.org/code/pg/PG/Connection.html#method-i-exec_prepared-doc
107
+ # for more information
108
+ { value: value.to_s, format: 1 }
109
+ when OID::Xml::Data, OID::Bit::Data
110
+ value.to_s
111
+ when OID::Array::Data
112
+ encode_array(value)
113
+ when Range
114
+ encode_range(value)
115
+ else
116
+ super
117
+ end
118
+ end
119
+
120
+ def lookup_cast_type_from_column(column) # :nodoc:
121
+ type_map.lookup(column.oid, column.fmod, column.sql_type)
122
+ end
123
+
124
+ def column_name_matcher
125
+ COLUMN_NAME
126
+ end
127
+
128
+ def column_name_with_order_matcher
129
+ COLUMN_NAME_WITH_ORDER
130
+ end
131
+
132
+ COLUMN_NAME = /
133
+ \A
134
+ (
135
+ (?:
136
+ # "schema_name"."table_name"."column_name"::type_name | function(one or no argument)::type_name
137
+ ((?:\w+\.|"\w+"\.){,2}(?:\w+|"\w+")(?:::\w+)?) | \w+\((?:|\g<2>)\)(?:::\w+)?
138
+ )
139
+ (?:(?:\s+AS)?\s+(?:\w+|"\w+"))?
140
+ )
141
+ (?:\s*,\s*\g<1>)*
142
+ \z
143
+ /ix
144
+
145
+ COLUMN_NAME_WITH_ORDER = /
146
+ \A
147
+ (
148
+ (?:
149
+ # "schema_name"."table_name"."column_name"::type_name | function(one or no argument)::type_name
150
+ ((?:\w+\.|"\w+"\.){,2}(?:\w+|"\w+")(?:::\w+)?) | \w+\((?:|\g<2>)\)(?:::\w+)?
151
+ )
152
+ (?:\s+ASC|\s+DESC)?
153
+ (?:\s+NULLS\s+(?:FIRST|LAST))?
154
+ )
155
+ (?:\s*,\s*\g<1>)*
156
+ \z
157
+ /ix
158
+
159
+ private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
160
+
161
+ private
162
+ def lookup_cast_type(sql_type)
163
+ super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i)
164
+ end
165
+
166
+ def encode_array(array_data)
167
+ encoder = array_data.encoder
168
+ values = type_cast_array(array_data.values)
169
+
170
+ result = encoder.encode(values)
171
+ if encoding = determine_encoding_of_strings_in_array(values)
172
+ result.force_encoding(encoding)
173
+ end
174
+ result
175
+ end
176
+
177
+ def encode_range(range)
178
+ "[#{type_cast_range_value(range.begin)},#{type_cast_range_value(range.end)}#{range.exclude_end? ? ')' : ']'}"
179
+ end
180
+
181
+ def determine_encoding_of_strings_in_array(value)
182
+ case value
183
+ when ::Array then determine_encoding_of_strings_in_array(value.first)
184
+ when ::String then value.encoding
185
+ end
186
+ end
187
+
188
+ def type_cast_array(values)
189
+ case values
190
+ when ::Array then values.map { |item| type_cast_array(item) }
191
+ else type_cast(values)
192
+ end
193
+ end
194
+
195
+ def type_cast_range_value(value)
196
+ infinity?(value) ? "" : type_cast(value)
197
+ end
198
+
199
+ def infinity?(value)
200
+ value.respond_to?(:infinite?) && value.infinite?
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end