activerecord-cipherstash-pg-adapter 0.8.0 → 0.8.2

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