activerecord-cipherstash-pg-adapter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.tool-versions +2 -0
  4. data/CHANGELOG.md +5 -0
  5. data/Gemfile +15 -0
  6. data/LICENSE +124 -0
  7. data/README.md +29 -0
  8. data/Rakefile +8 -0
  9. data/activerecord-cipherstash-pg-adapter.gemspec +35 -0
  10. data/lib/active_record/connection_adapters/cipherstash_pg/column.rb +69 -0
  11. data/lib/active_record/connection_adapters/cipherstash_pg/database_extensions.rb +31 -0
  12. data/lib/active_record/connection_adapters/cipherstash_pg/database_statements.rb +152 -0
  13. data/lib/active_record/connection_adapters/cipherstash_pg/database_tasks.rb +15 -0
  14. data/lib/active_record/connection_adapters/cipherstash_pg/explain_pretty_printer.rb +44 -0
  15. data/lib/active_record/connection_adapters/cipherstash_pg/oid/array.rb +91 -0
  16. data/lib/active_record/connection_adapters/cipherstash_pg/oid/bit.rb +53 -0
  17. data/lib/active_record/connection_adapters/cipherstash_pg/oid/bit_varying.rb +15 -0
  18. data/lib/active_record/connection_adapters/cipherstash_pg/oid/bytea.rb +17 -0
  19. data/lib/active_record/connection_adapters/cipherstash_pg/oid/cidr.rb +48 -0
  20. data/lib/active_record/connection_adapters/cipherstash_pg/oid/date.rb +31 -0
  21. data/lib/active_record/connection_adapters/cipherstash_pg/oid/date_time.rb +36 -0
  22. data/lib/active_record/connection_adapters/cipherstash_pg/oid/decimal.rb +15 -0
  23. data/lib/active_record/connection_adapters/cipherstash_pg/oid/enum.rb +20 -0
  24. data/lib/active_record/connection_adapters/cipherstash_pg/oid/hstore.rb +109 -0
  25. data/lib/active_record/connection_adapters/cipherstash_pg/oid/inet.rb +15 -0
  26. data/lib/active_record/connection_adapters/cipherstash_pg/oid/interval.rb +49 -0
  27. data/lib/active_record/connection_adapters/cipherstash_pg/oid/jsonb.rb +15 -0
  28. data/lib/active_record/connection_adapters/cipherstash_pg/oid/legacy_point.rb +44 -0
  29. data/lib/active_record/connection_adapters/cipherstash_pg/oid/macaddr.rb +25 -0
  30. data/lib/active_record/connection_adapters/cipherstash_pg/oid/money.rb +41 -0
  31. data/lib/active_record/connection_adapters/cipherstash_pg/oid/oid.rb +15 -0
  32. data/lib/active_record/connection_adapters/cipherstash_pg/oid/point.rb +64 -0
  33. data/lib/active_record/connection_adapters/cipherstash_pg/oid/range.rb +115 -0
  34. data/lib/active_record/connection_adapters/cipherstash_pg/oid/specialized_string.rb +18 -0
  35. data/lib/active_record/connection_adapters/cipherstash_pg/oid/timestamp.rb +15 -0
  36. data/lib/active_record/connection_adapters/cipherstash_pg/oid/timestamp_with_time_zone.rb +30 -0
  37. data/lib/active_record/connection_adapters/cipherstash_pg/oid/type_map_initializer.rb +125 -0
  38. data/lib/active_record/connection_adapters/cipherstash_pg/oid/uuid.rb +35 -0
  39. data/lib/active_record/connection_adapters/cipherstash_pg/oid/vector.rb +28 -0
  40. data/lib/active_record/connection_adapters/cipherstash_pg/oid/xml.rb +30 -0
  41. data/lib/active_record/connection_adapters/cipherstash_pg/oid.rb +38 -0
  42. data/lib/active_record/connection_adapters/cipherstash_pg/quoting.rb +231 -0
  43. data/lib/active_record/connection_adapters/cipherstash_pg/referential_integrity.rb +77 -0
  44. data/lib/active_record/connection_adapters/cipherstash_pg/schema_creation.rb +100 -0
  45. data/lib/active_record/connection_adapters/cipherstash_pg/schema_definitions.rb +243 -0
  46. data/lib/active_record/connection_adapters/cipherstash_pg/schema_dumper.rb +74 -0
  47. data/lib/active_record/connection_adapters/cipherstash_pg/schema_statements.rb +812 -0
  48. data/lib/active_record/connection_adapters/cipherstash_pg/type_metadata.rb +44 -0
  49. data/lib/active_record/connection_adapters/cipherstash_pg/utils.rb +80 -0
  50. data/lib/active_record/connection_adapters/cipherstash_pg_adapter.rb +1100 -0
  51. data/lib/active_record/connection_adapters/postgres_cipherstash_adapter.rb +13 -0
  52. data/lib/activerecord-cipherstash-pg-adapter.rb +33 -0
  53. data/lib/version.rb +3 -0
  54. metadata +126 -0
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module CipherStashPG
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.cipherstash_pg.org/docs/current/rangetypes.html#RANGETYPES-IO
86
+ # * https://www.cipherstash_pg.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 CipherStashPG
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 CipherStashPG
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 CipherStashPG
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 unless time.acts_like?(:time)
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 CipherStashPG
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 CipherStashPG
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 CipherStashPG
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 CipherStashPG
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/cipherstash_pg/oid/array"
4
+ require "active_record/connection_adapters/cipherstash_pg/oid/bit"
5
+ require "active_record/connection_adapters/cipherstash_pg/oid/bit_varying"
6
+ require "active_record/connection_adapters/cipherstash_pg/oid/bytea"
7
+ require "active_record/connection_adapters/cipherstash_pg/oid/cidr"
8
+ require "active_record/connection_adapters/cipherstash_pg/oid/date"
9
+ require "active_record/connection_adapters/cipherstash_pg/oid/date_time"
10
+ require "active_record/connection_adapters/cipherstash_pg/oid/decimal"
11
+ require "active_record/connection_adapters/cipherstash_pg/oid/enum"
12
+ require "active_record/connection_adapters/cipherstash_pg/oid/hstore"
13
+ require "active_record/connection_adapters/cipherstash_pg/oid/inet"
14
+ require "active_record/connection_adapters/cipherstash_pg/oid/interval"
15
+ require "active_record/connection_adapters/cipherstash_pg/oid/jsonb"
16
+ require "active_record/connection_adapters/cipherstash_pg/oid/macaddr"
17
+ require "active_record/connection_adapters/cipherstash_pg/oid/money"
18
+ require "active_record/connection_adapters/cipherstash_pg/oid/oid"
19
+ require "active_record/connection_adapters/cipherstash_pg/oid/point"
20
+ require "active_record/connection_adapters/cipherstash_pg/oid/legacy_point"
21
+ require "active_record/connection_adapters/cipherstash_pg/oid/range"
22
+ require "active_record/connection_adapters/cipherstash_pg/oid/specialized_string"
23
+ require "active_record/connection_adapters/cipherstash_pg/oid/timestamp"
24
+ require "active_record/connection_adapters/cipherstash_pg/oid/timestamp_with_time_zone"
25
+ require "active_record/connection_adapters/cipherstash_pg/oid/uuid"
26
+ require "active_record/connection_adapters/cipherstash_pg/oid/vector"
27
+ require "active_record/connection_adapters/cipherstash_pg/oid/xml"
28
+
29
+ require "active_record/connection_adapters/cipherstash_pg/oid/type_map_initializer"
30
+
31
+ module ActiveRecord
32
+ module ConnectionAdapters
33
+ module CipherStashPG
34
+ module OID # :nodoc:
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module CipherStashPG
6
+ module Quoting
7
+ class IntegerOutOf64BitRange < StandardError
8
+ def initialize(msg)
9
+ super(msg)
10
+ end
11
+ end
12
+
13
+ # Escapes binary strings for bytea input to the database.
14
+ def escape_bytea(value)
15
+ @connection.escape_bytea(value) if value
16
+ end
17
+
18
+ # Unescapes bytea output from a database to the binary string it represents.
19
+ # NOTE: This is NOT an inverse of escape_bytea! This is only to be used
20
+ # on escaped binary output from database drive.
21
+ def unescape_bytea(value)
22
+ @connection.unescape_bytea(value) if value
23
+ end
24
+
25
+ def check_int_in_range(value)
26
+ if value.to_int > 9223372036854775807 || value.to_int < -9223372036854775808
27
+ exception = <<~ERROR
28
+ Provided value outside of the range of a signed 64bit integer.
29
+
30
+ PostgreSQL will treat the column type in question as a numeric.
31
+ This may result in a slow sequential scan due to a comparison
32
+ being performed between an integer or bigint value and a numeric value.
33
+
34
+ To allow for this potentially unwanted behavior, set
35
+ ActiveRecord.raise_int_wider_than_64bit to false.
36
+ ERROR
37
+ raise IntegerOutOf64BitRange.new exception
38
+ end
39
+ end
40
+
41
+ def quote(value) # :nodoc:
42
+ if ActiveRecord.raise_int_wider_than_64bit && value.is_a?(Integer)
43
+ check_int_in_range(value)
44
+ end
45
+
46
+ case value
47
+ when OID::Xml::Data
48
+ "xml '#{quote_string(value.to_s)}'"
49
+ when OID::Bit::Data
50
+ if value.binary?
51
+ "B'#{value}'"
52
+ elsif value.hex?
53
+ "X'#{value}'"
54
+ end
55
+ when Numeric
56
+ if value.finite?
57
+ super
58
+ else
59
+ "'#{value}'"
60
+ end
61
+ when OID::Array::Data
62
+ quote(encode_array(value))
63
+ when Range
64
+ quote(encode_range(value))
65
+ else
66
+ super
67
+ end
68
+ end
69
+
70
+ # Quotes strings for use in SQL input.
71
+ def quote_string(s) # :nodoc:
72
+ @connection.escape(s)
73
+ end
74
+
75
+ # Checks the following cases:
76
+ #
77
+ # - table_name
78
+ # - "table.name"
79
+ # - schema_name.table_name
80
+ # - schema_name."table.name"
81
+ # - "schema.name".table_name
82
+ # - "schema.name"."table.name"
83
+ def quote_table_name(name) # :nodoc:
84
+ self.class.quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
85
+ end
86
+
87
+ # Quotes schema names for use in SQL queries.
88
+ def quote_schema_name(name)
89
+ PG::Connection.quote_ident(name)
90
+ end
91
+
92
+ def quote_table_name_for_assignment(table, attr)
93
+ quote_column_name(attr)
94
+ end
95
+
96
+ # Quotes column names for use in SQL queries.
97
+ def quote_column_name(name) # :nodoc:
98
+ self.class.quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze
99
+ end
100
+
101
+ # Quote date/time values for use in SQL input.
102
+ def quoted_date(value) # :nodoc:
103
+ if value.year <= 0
104
+ bce_year = format("%04d", -value.year + 1)
105
+ super.sub(/^-?\d+/, bce_year) + " BC"
106
+ else
107
+ super
108
+ end
109
+ end
110
+
111
+ def quoted_binary(value) # :nodoc:
112
+ "'#{escape_bytea(value.to_s)}'"
113
+ end
114
+
115
+ def quote_default_expression(value, column) # :nodoc:
116
+ if value.is_a?(Proc)
117
+ value.call
118
+ elsif column.type == :uuid && value.is_a?(String) && /\(\)/.match?(value)
119
+ value # Does not quote function default values for UUID columns
120
+ elsif column.respond_to?(:array?)
121
+ type = lookup_cast_type_from_column(column)
122
+ quote(type.serialize(value))
123
+ else
124
+ super
125
+ end
126
+ end
127
+
128
+ def type_cast(value) # :nodoc:
129
+ case value
130
+ when Type::Binary::Data
131
+ # Return a bind param hash with format as binary.
132
+ # See https://deveiate.org/code/pg/PG/Connection.html#method-i-exec_prepared-doc
133
+ # for more information
134
+ { value: value.to_s, format: 1 }
135
+ when OID::Xml::Data, OID::Bit::Data
136
+ value.to_s
137
+ when OID::Array::Data
138
+ encode_array(value)
139
+ when Range
140
+ encode_range(value)
141
+ else
142
+ super
143
+ end
144
+ end
145
+
146
+ def lookup_cast_type_from_column(column) # :nodoc:
147
+ type_map.lookup(column.oid, column.fmod, column.sql_type)
148
+ end
149
+
150
+ def column_name_matcher
151
+ COLUMN_NAME
152
+ end
153
+
154
+ def column_name_with_order_matcher
155
+ COLUMN_NAME_WITH_ORDER
156
+ end
157
+
158
+ COLUMN_NAME = /
159
+ \A
160
+ (
161
+ (?:
162
+ # "schema_name"."table_name"."column_name"::type_name | function(one or no argument)::type_name
163
+ ((?:\w+\.|"\w+"\.){,2}(?:\w+|"\w+")(?:::\w+)?) | \w+\((?:|\g<2>)\)(?:::\w+)?
164
+ )
165
+ (?:(?:\s+AS)?\s+(?:\w+|"\w+"))?
166
+ )
167
+ (?:\s*,\s*\g<1>)*
168
+ \z
169
+ /ix
170
+
171
+ COLUMN_NAME_WITH_ORDER = /
172
+ \A
173
+ (
174
+ (?:
175
+ # "schema_name"."table_name"."column_name"::type_name | function(one or no argument)::type_name
176
+ ((?:\w+\.|"\w+"\.){,2}(?:\w+|"\w+")(?:::\w+)?) | \w+\((?:|\g<2>)\)(?:::\w+)?
177
+ )
178
+ (?:\s+ASC|\s+DESC)?
179
+ (?:\s+NULLS\s+(?:FIRST|LAST))?
180
+ )
181
+ (?:\s*,\s*\g<1>)*
182
+ \z
183
+ /ix
184
+
185
+ private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
186
+
187
+ private
188
+ def lookup_cast_type(sql_type)
189
+ super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i)
190
+ end
191
+
192
+ def encode_array(array_data)
193
+ encoder = array_data.encoder
194
+ values = type_cast_array(array_data.values)
195
+
196
+ result = encoder.encode(values)
197
+ if encoding = determine_encoding_of_strings_in_array(values)
198
+ result.force_encoding(encoding)
199
+ end
200
+ result
201
+ end
202
+
203
+ def encode_range(range)
204
+ "[#{type_cast_range_value(range.begin)},#{type_cast_range_value(range.end)}#{range.exclude_end? ? ')' : ']'}"
205
+ end
206
+
207
+ def determine_encoding_of_strings_in_array(value)
208
+ case value
209
+ when ::Array then determine_encoding_of_strings_in_array(value.first)
210
+ when ::String then value.encoding
211
+ end
212
+ end
213
+
214
+ def type_cast_array(values)
215
+ case values
216
+ when ::Array then values.map { |item| type_cast_array(item) }
217
+ else type_cast(values)
218
+ end
219
+ end
220
+
221
+ def type_cast_range_value(value)
222
+ infinity?(value) ? "" : type_cast(value)
223
+ end
224
+
225
+ def infinity?(value)
226
+ value.respond_to?(:infinite?) && value.infinite?
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end