activerecord-cipherstash-pg-adapter 0.1.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 (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,1100 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Why the funky filename
4
+ # (lib/active_record/connection_adapters/cipherstash_pg_adapter.rb)
5
+ # that doesn't match the gem structure? It's to hook into Rails.
6
+ #
7
+ # See this chunk from:
8
+ # https://github.com/rails/rails/blob/d7d93faacfe0ca3297a47f676772828d7210b9bc/activerecord/lib/active_record/connection_adapters/abstract/connection_handler.rb#L325-L330
9
+ #
10
+ # # Require the adapter itself and give useful feedback about
11
+ # # 1. Missing adapter gems and
12
+ # # 2. Adapter gems' missing dependencies.
13
+ # path_to_adapter = "active_record/connection_adapters/#{db_config.adapter}_adapter"
14
+ # begin
15
+ # require path_to_adapter
16
+ # ...
17
+
18
+ # TODO: is this actually required?
19
+ #gem "cipherstash-pg", "~> 1.1"
20
+
21
+ require "cipherstash-pg"
22
+
23
+ require "active_support/core_ext/object/try"
24
+ require "active_record/connection_adapters/abstract_adapter"
25
+ require "active_record/connection_adapters/statement_pool"
26
+ require "active_record/connection_adapters/cipherstash_pg/column"
27
+ require "active_record/connection_adapters/cipherstash_pg/database_statements"
28
+ require "active_record/connection_adapters/cipherstash_pg/explain_pretty_printer"
29
+ require "active_record/connection_adapters/cipherstash_pg/oid"
30
+ require "active_record/connection_adapters/cipherstash_pg/quoting"
31
+ require "active_record/connection_adapters/cipherstash_pg/referential_integrity"
32
+ require "active_record/connection_adapters/cipherstash_pg/schema_creation"
33
+ require "active_record/connection_adapters/cipherstash_pg/schema_definitions"
34
+ require "active_record/connection_adapters/cipherstash_pg/schema_dumper"
35
+ require "active_record/connection_adapters/cipherstash_pg/schema_statements"
36
+ require "active_record/connection_adapters/cipherstash_pg/type_metadata"
37
+ require "active_record/connection_adapters/cipherstash_pg/utils"
38
+
39
+ module ActiveRecord
40
+ module ConnectionHandling
41
+ # This is the entrypoint that is called when Rails looks for the adapter,
42
+ # when you have this in your database.yml config:
43
+ #
44
+ # adapter: cipherstash_pg
45
+ #
46
+ # See: https://github.com/rails/rails/blob/d7d93faacfe0ca3297a47f676772828d7210b9bc/activerecord/lib/active_record/database_configurations/database_config.rb#L16-L18
47
+ #
48
+ # Please note that we now recommend instead having:
49
+ #
50
+ # adapter: postgres_cipherstash
51
+ #
52
+ # ... to match the implicit AR naming scheme enforced by "rails dbconsole", eg.
53
+ # https://github.com/rails/rails/blob/7-0-stable/railties/lib/rails/commands/dbconsole/dbconsole_command.rb#L46
54
+
55
+ ###########################################################################
56
+ # Everything from this point onwards is a copy-paste or inherit from Rails'
57
+ # activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
58
+ #
59
+ # ... with the exception of adding the ore_64_8_v1 type support on line 635.
60
+ #
61
+ # (We can't just inherit from PostgreSQLAdapter because we cannot load the
62
+ # postgresql_adapter.rb file; that would load 'pg', and we cannot [right
63
+ # now] have both 'cipherstash-pg' and 'pg' loaded at the same time.)
64
+ ###########################################################################
65
+
66
+ def cipherstash_pg_connection(config)
67
+ conn_params = config.symbolize_keys.compact
68
+
69
+ # Map ActiveRecords param names to PGs.
70
+ conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
71
+ conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
72
+
73
+ # Forward only valid config params to PG::Connection.connect.
74
+ valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
75
+ conn_params.slice!(*valid_conn_param_keys)
76
+
77
+ ConnectionAdapters::CipherStashPGAdapter.new(
78
+ ConnectionAdapters::CipherStashPGAdapter.new_client(conn_params),
79
+ logger,
80
+ conn_params,
81
+ config,
82
+ )
83
+ end
84
+ end
85
+
86
+ module ConnectionAdapters
87
+ class CipherStashPGAdapter < AbstractAdapter
88
+ ADAPTER_NAME = "CipherStash PostgreSQL"
89
+
90
+ class << self
91
+ def new_client(conn_params)
92
+ PG.connect(**conn_params)
93
+ rescue ::PG::Error => error
94
+ if conn_params && conn_params[:dbname] && error.message.include?(conn_params[:dbname])
95
+ raise ActiveRecord::NoDatabaseError.db_error(conn_params[:dbname])
96
+ elsif conn_params && conn_params[:user] && error.message.include?(conn_params[:user])
97
+ raise ActiveRecord::DatabaseConnectionError.username_error(conn_params[:user])
98
+ elsif conn_params && conn_params[:hostname] && error.message.include?(conn_params[:hostname])
99
+ raise ActiveRecord::DatabaseConnectionError.hostname_error(conn_params[:hostname])
100
+ else
101
+ raise ActiveRecord::ConnectionNotEstablished, error.message
102
+ end
103
+ end
104
+ end
105
+
106
+ ##
107
+ # :singleton-method:
108
+ # PostgreSQL allows the creation of "unlogged" tables, which do not record
109
+ # data in the PostgreSQL Write-Ahead Log. This can make the tables faster,
110
+ # but significantly increases the risk of data loss if the database
111
+ # crashes. As a result, this should not be used in production
112
+ # environments. If you would like all created tables to be unlogged in
113
+ # the test environment you can add the following line to your test.rb
114
+ # file:
115
+ #
116
+ # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true
117
+ class_attribute :create_unlogged_tables, default: false
118
+
119
+ ##
120
+ # :singleton-method:
121
+ # PostgreSQL supports multiple types for DateTimes. By default, if you use +datetime+
122
+ # in migrations, Rails will translate this to a PostgreSQL "timestamp without time zone".
123
+ # Change this in an initializer to use another NATIVE_DATABASE_TYPES. For example, to
124
+ # store DateTimes as "timestamp with time zone":
125
+ #
126
+ # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :timestamptz
127
+ #
128
+ # Or if you are adding a custom type:
129
+ #
130
+ # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:my_custom_type] = { name: "my_custom_type_name" }
131
+ # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :my_custom_type
132
+ #
133
+ # If you're using +:ruby+ as your +config.active_record.schema_format+ and you change this
134
+ # setting, you should immediately run <tt>bin/rails db:migrate</tt> to update the types in your schema.rb.
135
+ class_attribute :datetime_type, default: :timestamp
136
+
137
+ NATIVE_DATABASE_TYPES = {
138
+ primary_key: "bigserial primary key",
139
+ string: { name: "character varying" },
140
+ text: { name: "text" },
141
+ integer: { name: "integer", limit: 4 },
142
+ bigint: { name: "bigint" },
143
+ float: { name: "float" },
144
+ decimal: { name: "decimal" },
145
+ datetime: {}, # set dynamically based on datetime_type
146
+ timestamp: { name: "timestamp" },
147
+ timestamptz: { name: "timestamptz" },
148
+ time: { name: "time" },
149
+ date: { name: "date" },
150
+ daterange: { name: "daterange" },
151
+ numrange: { name: "numrange" },
152
+ tsrange: { name: "tsrange" },
153
+ tstzrange: { name: "tstzrange" },
154
+ int4range: { name: "int4range" },
155
+ int8range: { name: "int8range" },
156
+ binary: { name: "bytea" },
157
+ boolean: { name: "boolean" },
158
+ xml: { name: "xml" },
159
+ tsvector: { name: "tsvector" },
160
+ hstore: { name: "hstore" },
161
+ inet: { name: "inet" },
162
+ cidr: { name: "cidr" },
163
+ macaddr: { name: "macaddr" },
164
+ uuid: { name: "uuid" },
165
+ json: { name: "json" },
166
+ jsonb: { name: "jsonb" },
167
+ ltree: { name: "ltree" },
168
+ citext: { name: "citext" },
169
+ point: { name: "point" },
170
+ line: { name: "line" },
171
+ lseg: { name: "lseg" },
172
+ box: { name: "box" },
173
+ path: { name: "path" },
174
+ polygon: { name: "polygon" },
175
+ circle: { name: "circle" },
176
+ bit: { name: "bit" },
177
+ bit_varying: { name: "bit varying" },
178
+ money: { name: "money" },
179
+ interval: { name: "interval" },
180
+ oid: { name: "oid" },
181
+ enum: {} # special type https://www.postgresql.org/docs/current/datatype-enum.html
182
+ }
183
+
184
+ OID = CipherStashPG::OID # :nodoc:
185
+
186
+ include CipherStashPG::Quoting
187
+ include CipherStashPG::ReferentialIntegrity
188
+ include CipherStashPG::SchemaStatements
189
+ include CipherStashPG::DatabaseStatements
190
+
191
+ def supports_bulk_alter?
192
+ true
193
+ end
194
+
195
+ def supports_index_sort_order?
196
+ true
197
+ end
198
+
199
+ def supports_partitioned_indexes?
200
+ database_version >= 110_000 # >= 11.0
201
+ end
202
+
203
+ def supports_partial_index?
204
+ true
205
+ end
206
+
207
+ def supports_expression_index?
208
+ true
209
+ end
210
+
211
+ def supports_transaction_isolation?
212
+ true
213
+ end
214
+
215
+ def supports_foreign_keys?
216
+ true
217
+ end
218
+
219
+ def supports_check_constraints?
220
+ true
221
+ end
222
+
223
+ def supports_validate_constraints?
224
+ true
225
+ end
226
+
227
+ def supports_deferrable_constraints?
228
+ true
229
+ end
230
+
231
+ def supports_views?
232
+ true
233
+ end
234
+
235
+ def supports_datetime_with_precision?
236
+ true
237
+ end
238
+
239
+ def supports_json?
240
+ true
241
+ end
242
+
243
+ def supports_comments?
244
+ true
245
+ end
246
+
247
+ def supports_savepoints?
248
+ true
249
+ end
250
+
251
+ def supports_insert_returning?
252
+ true
253
+ end
254
+
255
+ def supports_insert_on_conflict?
256
+ database_version >= 90500 # >= 9.5
257
+ end
258
+ alias supports_insert_on_duplicate_skip? supports_insert_on_conflict?
259
+ alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
260
+ alias supports_insert_conflict_target? supports_insert_on_conflict?
261
+
262
+ def supports_virtual_columns?
263
+ database_version >= 120_000 # >= 12.0
264
+ end
265
+
266
+ def index_algorithms
267
+ { concurrently: "CONCURRENTLY" }
268
+ end
269
+
270
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
271
+ def initialize(connection, max)
272
+ super(max)
273
+ @connection = connection
274
+ @counter = 0
275
+ end
276
+
277
+ def next_key
278
+ "a#{@counter += 1}"
279
+ end
280
+
281
+ private
282
+ def dealloc(key)
283
+ @connection.query "DEALLOCATE #{key}" if connection_active?
284
+ rescue PG::Error
285
+ end
286
+
287
+ def connection_active?
288
+ @connection.status == PG::CONNECTION_OK
289
+ rescue PG::Error
290
+ false
291
+ end
292
+ end
293
+
294
+ # Initializes and connects a CipherStashPG adapter.
295
+ def initialize(connection, logger, connection_parameters, config)
296
+ super(connection, logger, config)
297
+
298
+ @connection_parameters = connection_parameters || {}
299
+
300
+ # @local_tz is initialized as nil to avoid warnings when connect tries to use it
301
+ @local_tz = nil
302
+ @max_identifier_length = nil
303
+
304
+ configure_connection
305
+ add_pg_encoders
306
+ add_pg_decoders
307
+
308
+ @type_map = Type::HashLookupTypeMap.new
309
+ initialize_type_map
310
+ @local_tz = execute("SHOW TIME ZONE", "SCHEMA").first["TimeZone"]
311
+ @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
312
+ end
313
+
314
+ def self.database_exists?(config)
315
+ !!ActiveRecord::Base.cipherstash_pg_connection(config)
316
+ rescue ActiveRecord::NoDatabaseError
317
+ false
318
+ end
319
+
320
+ # Is this connection alive and ready for queries?
321
+ def active?
322
+ @lock.synchronize do
323
+ @connection.query ";"
324
+ end
325
+ true
326
+ rescue PG::Error
327
+ false
328
+ end
329
+
330
+ def reload_type_map # :nodoc:
331
+ type_map.clear
332
+ initialize_type_map
333
+ end
334
+
335
+ # Close then reopen the connection.
336
+ def reconnect!
337
+ @lock.synchronize do
338
+ super
339
+ @connection.reset
340
+ configure_connection
341
+ reload_type_map
342
+ rescue PG::ConnectionBad
343
+ connect
344
+ end
345
+ end
346
+
347
+ def reset!
348
+ @lock.synchronize do
349
+ clear_cache!
350
+ reset_transaction
351
+ unless @connection.transaction_status == ::PG::PQTRANS_IDLE
352
+ @connection.query "ROLLBACK"
353
+ end
354
+ @connection.query "DISCARD ALL"
355
+ configure_connection
356
+ end
357
+ end
358
+
359
+ # Disconnects from the database if already connected. Otherwise, this
360
+ # method does nothing.
361
+ def disconnect!
362
+ @lock.synchronize do
363
+ super
364
+ @connection.close rescue nil
365
+ end
366
+ end
367
+
368
+ def discard! # :nodoc:
369
+ super
370
+ @connection.socket_io.reopen(IO::NULL) rescue nil
371
+ @connection = nil
372
+ end
373
+
374
+ def native_database_types # :nodoc:
375
+ self.class.native_database_types
376
+ end
377
+
378
+ def self.native_database_types # :nodoc:
379
+ @native_database_types ||= begin
380
+ types = NATIVE_DATABASE_TYPES.dup
381
+ types[:datetime] = types[datetime_type]
382
+ types
383
+ end
384
+ end
385
+
386
+ def set_standard_conforming_strings
387
+ execute("SET standard_conforming_strings = on", "SCHEMA")
388
+ end
389
+
390
+ def supports_ddl_transactions?
391
+ true
392
+ end
393
+
394
+ def supports_advisory_locks?
395
+ true
396
+ end
397
+
398
+ def supports_explain?
399
+ true
400
+ end
401
+
402
+ def supports_extensions?
403
+ true
404
+ end
405
+
406
+ def supports_materialized_views?
407
+ true
408
+ end
409
+
410
+ def supports_foreign_tables?
411
+ true
412
+ end
413
+
414
+ def supports_pgcrypto_uuid?
415
+ database_version >= 90400 # >= 9.4
416
+ end
417
+
418
+ def supports_optimizer_hints?
419
+ unless defined?(@has_pg_hint_plan)
420
+ @has_pg_hint_plan = extension_available?("pg_hint_plan")
421
+ end
422
+ @has_pg_hint_plan
423
+ end
424
+
425
+ def supports_common_table_expressions?
426
+ true
427
+ end
428
+
429
+ def supports_lazy_transactions?
430
+ true
431
+ end
432
+
433
+ def get_advisory_lock(lock_id) # :nodoc:
434
+ unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63
435
+ raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer")
436
+ end
437
+ query_value("SELECT pg_try_advisory_lock(#{lock_id})")
438
+ end
439
+
440
+ def release_advisory_lock(lock_id) # :nodoc:
441
+ unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63
442
+ raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer")
443
+ end
444
+ query_value("SELECT pg_advisory_unlock(#{lock_id})")
445
+ end
446
+
447
+ def enable_extension(name)
448
+ exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap {
449
+ reload_type_map
450
+ }
451
+ end
452
+
453
+ def disable_extension(name)
454
+ exec_query("DROP EXTENSION IF EXISTS \"#{name}\" CASCADE").tap {
455
+ reload_type_map
456
+ }
457
+ end
458
+
459
+ def extension_available?(name)
460
+ query_value("SELECT true FROM pg_available_extensions WHERE name = #{quote(name)}", "SCHEMA")
461
+ end
462
+
463
+ def extension_enabled?(name)
464
+ query_value("SELECT installed_version IS NOT NULL FROM pg_available_extensions WHERE name = #{quote(name)}", "SCHEMA")
465
+ end
466
+
467
+ def extensions
468
+ exec_query("SELECT extname FROM pg_extension", "SCHEMA").cast_values
469
+ end
470
+
471
+ # Returns a list of defined enum types, and their values.
472
+ def enum_types
473
+ query = <<~SQL
474
+ SELECT
475
+ type.typname AS name,
476
+ string_agg(enum.enumlabel, ',' ORDER BY enum.enumsortorder) AS value
477
+ FROM pg_enum AS enum
478
+ JOIN pg_type AS type
479
+ ON (type.oid = enum.enumtypid)
480
+ GROUP BY type.typname;
481
+ SQL
482
+ exec_query(query, "SCHEMA").cast_values
483
+ end
484
+
485
+ # Given a name and an array of values, creates an enum type.
486
+ def create_enum(name, values)
487
+ sql_values = values.map { |s| "'#{s}'" }.join(", ")
488
+ query = <<~SQL
489
+ DO $$
490
+ BEGIN
491
+ IF NOT EXISTS (
492
+ SELECT 1 FROM pg_type t
493
+ WHERE t.typname = '#{name}'
494
+ ) THEN
495
+ CREATE TYPE \"#{name}\" AS ENUM (#{sql_values});
496
+ END IF;
497
+ END
498
+ $$;
499
+ SQL
500
+ exec_query(query)
501
+ end
502
+
503
+ # Returns the configured supported identifier length supported by PostgreSQL
504
+ def max_identifier_length
505
+ @max_identifier_length ||= query_value("SHOW max_identifier_length", "SCHEMA").to_i
506
+ end
507
+
508
+ # Set the authorized user for this session
509
+ def session_auth=(user)
510
+ clear_cache!
511
+ execute("SET SESSION AUTHORIZATION #{user}")
512
+ end
513
+
514
+ def use_insert_returning?
515
+ @use_insert_returning
516
+ end
517
+
518
+ # Returns the version of the connected PostgreSQL server.
519
+ def get_database_version # :nodoc:
520
+ # Robin note: Compatibility between Rails 7.0 and 7.1
521
+ # See, among others:
522
+ # - https://github.com/rails/rails/pull/44530
523
+ # - https://github.com/rails/rails/pull/44576
524
+ if respond_to?(:valid_raw_connection)
525
+ valid_raw_connection.server_version
526
+ else
527
+ @connection.server_version
528
+ end
529
+ end
530
+ alias :cipherstash_pg_version :database_version
531
+
532
+ def default_index_type?(index) # :nodoc:
533
+ index.using == :btree || super
534
+ end
535
+
536
+ def build_insert_sql(insert) # :nodoc:
537
+ sql = +"INSERT #{insert.into} #{insert.values_list}"
538
+
539
+ if insert.skip_duplicates?
540
+ sql << " ON CONFLICT #{insert.conflict_target} DO NOTHING"
541
+ elsif insert.update_duplicates?
542
+ sql << " ON CONFLICT #{insert.conflict_target} DO UPDATE SET "
543
+ if insert.raw_update_sql?
544
+ sql << insert.raw_update_sql
545
+ else
546
+ sql << insert.touch_model_timestamps_unless { |column| "#{insert.model.quoted_table_name}.#{column} IS NOT DISTINCT FROM excluded.#{column}" }
547
+ sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(",")
548
+ end
549
+ end
550
+
551
+ sql << " RETURNING #{insert.returning}" if insert.returning
552
+ sql
553
+ end
554
+
555
+ def check_version # :nodoc:
556
+ if database_version < 90300 # < 9.3
557
+ raise "Your version of PostgreSQL (#{database_version}) is too old. Active Record supports PostgreSQL >= 9.3."
558
+ end
559
+ end
560
+
561
+ class << self
562
+ def initialize_type_map(m) # :nodoc:
563
+ m.register_type "int2", Type::Integer.new(limit: 2)
564
+ m.register_type "int4", Type::Integer.new(limit: 4)
565
+ m.register_type "int8", Type::Integer.new(limit: 8)
566
+ m.register_type "oid", OID::Oid.new
567
+ m.register_type "float4", Type::Float.new
568
+ m.alias_type "float8", "float4"
569
+ m.register_type "text", Type::Text.new
570
+ register_class_with_limit m, "varchar", Type::String
571
+ m.alias_type "char", "varchar"
572
+ m.alias_type "name", "varchar"
573
+ m.alias_type "bpchar", "varchar"
574
+ m.register_type "bool", Type::Boolean.new
575
+ register_class_with_limit m, "bit", OID::Bit
576
+ register_class_with_limit m, "varbit", OID::BitVarying
577
+ m.register_type "date", OID::Date.new
578
+
579
+ m.register_type "money", OID::Money.new
580
+ m.register_type "bytea", OID::Bytea.new
581
+ m.register_type "point", OID::Point.new
582
+ m.register_type "hstore", OID::Hstore.new
583
+ m.register_type "json", Type::Json.new
584
+ m.register_type "jsonb", OID::Jsonb.new
585
+ m.register_type "cidr", OID::Cidr.new
586
+ m.register_type "inet", OID::Inet.new
587
+ m.register_type "uuid", OID::Uuid.new
588
+ m.register_type "xml", OID::Xml.new
589
+ m.register_type "tsvector", OID::SpecializedString.new(:tsvector)
590
+ m.register_type "macaddr", OID::Macaddr.new
591
+ m.register_type "citext", OID::SpecializedString.new(:citext)
592
+ m.register_type "ltree", OID::SpecializedString.new(:ltree)
593
+ m.register_type "line", OID::SpecializedString.new(:line)
594
+ m.register_type "lseg", OID::SpecializedString.new(:lseg)
595
+ m.register_type "box", OID::SpecializedString.new(:box)
596
+ m.register_type "path", OID::SpecializedString.new(:path)
597
+ m.register_type "polygon", OID::SpecializedString.new(:polygon)
598
+ m.register_type "circle", OID::SpecializedString.new(:circle)
599
+
600
+ register_class_with_precision m, "time", Type::Time
601
+ register_class_with_precision m, "timestamp", OID::Timestamp
602
+ register_class_with_precision m, "timestamptz", OID::TimestampWithTimeZone
603
+
604
+ m.register_type "numeric" do |_, fmod, sql_type|
605
+ precision = extract_precision(sql_type)
606
+ scale = extract_scale(sql_type)
607
+
608
+ # The type for the numeric depends on the width of the field,
609
+ # so we'll do something special here.
610
+ #
611
+ # When dealing with decimal columns:
612
+ #
613
+ # places after decimal = fmod - 4 & 0xffff
614
+ # places before decimal = (fmod - 4) >> 16 & 0xffff
615
+ if fmod && (fmod - 4 & 0xffff).zero?
616
+ # FIXME: Remove this class, and the second argument to
617
+ # lookups on PG
618
+ Type::DecimalWithoutScale.new(precision: precision)
619
+ else
620
+ OID::Decimal.new(precision: precision, scale: scale)
621
+ end
622
+ end
623
+
624
+ m.register_type "interval" do |*args, sql_type|
625
+ precision = extract_precision(sql_type)
626
+ OID::Interval.new(precision: precision)
627
+ end
628
+ end
629
+ end
630
+
631
+ private
632
+ def type_map
633
+ @type_map ||= Type::HashLookupTypeMap.new
634
+ end
635
+
636
+ def initialize_type_map(m = type_map)
637
+ self.class.initialize_type_map(m)
638
+
639
+ ############################################
640
+ # EDITED to add type mapping for ORE type.
641
+ ############################################
642
+ # TODO: Look into OID::Array for ore_64_8_v1. It doesn't work out of
643
+ # the box, FWIW, so will need a little digging.
644
+ m.register_type "ore_64_8_v1_term", OID::SpecializedString.new(:ore_64_8_v1_term)
645
+ m.register_type "ore_64_8_v1", OID::SpecializedString.new(:ore_64_8_v1)
646
+
647
+ load_additional_types
648
+ end
649
+
650
+ # Extracts the value from a PostgreSQL column default definition.
651
+ def extract_value_from_default(default)
652
+ case default
653
+ # Quoted types
654
+ when /\A[(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
655
+ # The default 'now'::date is CURRENT_DATE
656
+ if $1 == "now" && $2 == "date"
657
+ nil
658
+ else
659
+ $1.gsub("''", "'")
660
+ end
661
+ # Boolean types
662
+ when "true", "false"
663
+ default
664
+ # Numeric types
665
+ when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/
666
+ $1
667
+ # Object identifier types
668
+ when /\A-?\d+\z/
669
+ $1
670
+ else
671
+ # Anything else is blank, some user type, or some function
672
+ # and we can't know the value of that, so return nil.
673
+ nil
674
+ end
675
+ end
676
+
677
+ def extract_default_function(default_value, default)
678
+ default if has_default_function?(default_value, default)
679
+ end
680
+
681
+ def has_default_function?(default_value, default)
682
+ !default_value && %r{\w+\(.*\)|\(.*\)::\w+|CURRENT_DATE|CURRENT_TIMESTAMP}.match?(default)
683
+ end
684
+
685
+ # See https://www.cipherstash_pg.org/docs/current/static/errcodes-appendix.html
686
+ VALUE_LIMIT_VIOLATION = "22001"
687
+ NUMERIC_VALUE_OUT_OF_RANGE = "22003"
688
+ NOT_NULL_VIOLATION = "23502"
689
+ FOREIGN_KEY_VIOLATION = "23503"
690
+ UNIQUE_VIOLATION = "23505"
691
+ SERIALIZATION_FAILURE = "40001"
692
+ DEADLOCK_DETECTED = "40P01"
693
+ DUPLICATE_DATABASE = "42P04"
694
+ LOCK_NOT_AVAILABLE = "55P03"
695
+ QUERY_CANCELED = "57014"
696
+
697
+ def translate_exception(exception, message:, sql:, binds:)
698
+ return exception unless exception.respond_to?(:result)
699
+
700
+ case exception.result.try(:error_field, PG::PG_DIAG_SQLSTATE)
701
+ when nil
702
+ if exception.message.match?(/connection is closed/i)
703
+ ConnectionNotEstablished.new(exception)
704
+ else
705
+ super
706
+ end
707
+ when UNIQUE_VIOLATION
708
+ RecordNotUnique.new(message, sql: sql, binds: binds)
709
+ when FOREIGN_KEY_VIOLATION
710
+ InvalidForeignKey.new(message, sql: sql, binds: binds)
711
+ when VALUE_LIMIT_VIOLATION
712
+ ValueTooLong.new(message, sql: sql, binds: binds)
713
+ when NUMERIC_VALUE_OUT_OF_RANGE
714
+ RangeError.new(message, sql: sql, binds: binds)
715
+ when NOT_NULL_VIOLATION
716
+ NotNullViolation.new(message, sql: sql, binds: binds)
717
+ when SERIALIZATION_FAILURE
718
+ SerializationFailure.new(message, sql: sql, binds: binds)
719
+ when DEADLOCK_DETECTED
720
+ Deadlocked.new(message, sql: sql, binds: binds)
721
+ when DUPLICATE_DATABASE
722
+ DatabaseAlreadyExists.new(message, sql: sql, binds: binds)
723
+ when LOCK_NOT_AVAILABLE
724
+ LockWaitTimeout.new(message, sql: sql, binds: binds)
725
+ when QUERY_CANCELED
726
+ QueryCanceled.new(message, sql: sql, binds: binds)
727
+ else
728
+ super
729
+ end
730
+ end
731
+
732
+ def get_oid_type(oid, fmod, column_name, sql_type = "")
733
+ if !type_map.key?(oid)
734
+ load_additional_types([oid])
735
+ end
736
+
737
+ type_map.fetch(oid, fmod, sql_type) {
738
+ warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String."
739
+ Type.default_value.tap do |cast_type|
740
+ type_map.register_type(oid, cast_type)
741
+ end
742
+ }
743
+ end
744
+
745
+ def load_additional_types(oids = nil)
746
+ initializer = OID::TypeMapInitializer.new(type_map)
747
+ load_types_queries(initializer, oids) do |query|
748
+ execute_and_clear(query, "SCHEMA", []) do |records|
749
+ initializer.run(records)
750
+ end
751
+ end
752
+ end
753
+
754
+ def load_types_queries(initializer, oids)
755
+ query = <<~SQL
756
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
757
+ FROM pg_type as t
758
+ LEFT JOIN pg_range as r ON oid = rngtypid
759
+ SQL
760
+ if oids
761
+ yield query + "WHERE t.oid IN (%s)" % oids.join(", ")
762
+ else
763
+ yield query + initializer.query_conditions_for_known_type_names
764
+ yield query + initializer.query_conditions_for_known_type_types
765
+ yield query + initializer.query_conditions_for_array_types
766
+ end
767
+ end
768
+
769
+ FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
770
+
771
+ def execute_and_clear(sql, name, binds, prepare: false, async: false)
772
+ sql = transform_query(sql)
773
+ check_if_write_query(sql)
774
+
775
+ if !prepare || without_prepared_statement?(binds)
776
+ result = exec_no_cache(sql, name, binds, async: async)
777
+ else
778
+ result = exec_cache(sql, name, binds, async: async)
779
+ end
780
+ begin
781
+ ret = yield result
782
+ ensure
783
+ result.clear
784
+ end
785
+ ret
786
+ end
787
+
788
+ def exec_no_cache(sql, name, binds, async: false)
789
+ materialize_transactions
790
+ mark_transaction_written_if_write(sql)
791
+
792
+ # make sure we carry over any changes to ActiveRecord.default_timezone that have been
793
+ # made since we established the connection
794
+ update_typemap_for_default_timezone
795
+
796
+ type_casted_binds = type_casted_binds(binds)
797
+ log(sql, name, binds, type_casted_binds, async: async) do
798
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
799
+ @connection.exec_params(sql, type_casted_binds)
800
+ end
801
+ end
802
+ end
803
+
804
+ def exec_cache(sql, name, binds, async: false)
805
+ materialize_transactions
806
+ mark_transaction_written_if_write(sql)
807
+ update_typemap_for_default_timezone
808
+
809
+ stmt_key = prepare_statement(sql, binds)
810
+ type_casted_binds = type_casted_binds(binds)
811
+
812
+ log(sql, name, binds, type_casted_binds, stmt_key, async: async) do
813
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
814
+ @connection.exec_prepared(stmt_key, type_casted_binds)
815
+ end
816
+ end
817
+ rescue ActiveRecord::StatementInvalid => e
818
+ raise unless is_cached_plan_failure?(e)
819
+
820
+ # Nothing we can do if we are in a transaction because all commands
821
+ # will raise InFailedSQLTransaction
822
+ if in_transaction?
823
+ raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
824
+ else
825
+ @lock.synchronize do
826
+ # outside of transactions we can simply flush this query and retry
827
+ @statements.delete sql_key(sql)
828
+ end
829
+ retry
830
+ end
831
+ end
832
+
833
+ # Annoyingly, the code for prepared statements whose return value may
834
+ # have changed is FEATURE_NOT_SUPPORTED.
835
+ #
836
+ # This covers various different error types so we need to do additional
837
+ # work to classify the exception definitively as a
838
+ # ActiveRecord::PreparedStatementCacheExpired
839
+ #
840
+ # Check here for more details:
841
+ # https://git.cipherstash_pg.org/gitweb/?p=cipherstash_pg.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
842
+ def is_cached_plan_failure?(e)
843
+ pgerror = e.cause
844
+ pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE) == FEATURE_NOT_SUPPORTED &&
845
+ pgerror.result.result_error_field(PG::PG_DIAG_SOURCE_FUNCTION) == "RevalidateCachedQuery"
846
+ rescue
847
+ false
848
+ end
849
+
850
+ def in_transaction?
851
+ open_transactions > 0
852
+ end
853
+
854
+ # Returns the statement identifier for the client side cache
855
+ # of statements
856
+ def sql_key(sql)
857
+ "#{schema_search_path}-#{sql}"
858
+ end
859
+
860
+ # Prepare the statement if it hasn't been prepared, return
861
+ # the statement key.
862
+ def prepare_statement(sql, binds)
863
+ @lock.synchronize do
864
+ sql_key = sql_key(sql)
865
+ unless @statements.key? sql_key
866
+ nextkey = @statements.next_key
867
+ begin
868
+ @connection.prepare nextkey, sql
869
+ rescue => e
870
+ raise translate_exception_class(e, sql, binds)
871
+ end
872
+ # Clear the queue
873
+ @connection.get_last_result
874
+ @statements[sql_key] = nextkey
875
+ end
876
+ @statements[sql_key]
877
+ end
878
+ end
879
+
880
+ # Connects to a PostgreSQL server and sets up the adapter depending on the
881
+ # connected server's characteristics.
882
+ def connect
883
+ @connection = self.class.new_client(@connection_parameters)
884
+ configure_connection
885
+ add_pg_encoders
886
+ add_pg_decoders
887
+ end
888
+
889
+ # Configures the encoding, verbosity, schema search path, and time zone of the connection.
890
+ # This is called by #connect and should not be called manually.
891
+ def configure_connection
892
+ if @config[:encoding]
893
+ @connection.set_client_encoding(@config[:encoding])
894
+ end
895
+ self.client_min_messages = @config[:min_messages] || "warning"
896
+ self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
897
+
898
+ # Use standard-conforming strings so we don't have to do the E'...' dance.
899
+ set_standard_conforming_strings
900
+
901
+ variables = @config.fetch(:variables, {}).stringify_keys
902
+
903
+ # If using Active Record's time zone support configure the connection to return
904
+ # TIMESTAMP WITH ZONE types in UTC.
905
+ unless variables["timezone"]
906
+ if ActiveRecord.default_timezone == :utc
907
+ variables["timezone"] = "UTC"
908
+ elsif @local_tz
909
+ variables["timezone"] = @local_tz
910
+ end
911
+ end
912
+
913
+ # Set interval output format to ISO 8601 for ease of parsing by ActiveSupport::Duration.parse
914
+ execute("SET intervalstyle = iso_8601", "SCHEMA")
915
+
916
+ # SET statements from :variables config hash
917
+ # https://www.cipherstash_pg.org/docs/current/static/sql-set.html
918
+ variables.map do |k, v|
919
+ if v == ":default" || v == :default
920
+ # Sets the value to the global or compile default
921
+ execute("SET SESSION #{k} TO DEFAULT", "SCHEMA")
922
+ elsif !v.nil?
923
+ execute("SET SESSION #{k} TO #{quote(v)}", "SCHEMA")
924
+ end
925
+ end
926
+ end
927
+
928
+ # Returns the list of a table's column names, data types, and default values.
929
+ #
930
+ # The underlying query is roughly:
931
+ # SELECT column.name, column.type, default.value, column.comment
932
+ # FROM column LEFT JOIN default
933
+ # ON column.table_id = default.table_id
934
+ # AND column.num = default.column_num
935
+ # WHERE column.table_id = get_table_id('table_name')
936
+ # AND column.num > 0
937
+ # AND NOT column.is_dropped
938
+ # ORDER BY column.num
939
+ #
940
+ # If the table name is not prefixed with a schema, the database will
941
+ # take the first match from the schema search path.
942
+ #
943
+ # Query implementation notes:
944
+ # - format_type includes the column size constraint, e.g. varchar(50)
945
+ # - ::regclass is a function that gives the id for a table name
946
+ def column_definitions(table_name)
947
+ query(<<~SQL, "SCHEMA")
948
+ SELECT a.attname, format_type(a.atttypid, a.atttypmod),
949
+ pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
950
+ c.collname, col_description(a.attrelid, a.attnum) AS comment,
951
+ #{supports_virtual_columns? ? 'attgenerated' : quote('')} as attgenerated
952
+ FROM pg_attribute a
953
+ LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
954
+ LEFT JOIN pg_type t ON a.atttypid = t.oid
955
+ LEFT JOIN pg_collation c ON a.attcollation = c.oid AND a.attcollation <> t.typcollation
956
+ WHERE a.attrelid = #{quote(quote_table_name(table_name))}::regclass
957
+ AND a.attnum > 0 AND NOT a.attisdropped
958
+ ORDER BY a.attnum
959
+ SQL
960
+ end
961
+
962
+ def extract_table_ref_from_insert_sql(sql)
963
+ sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im]
964
+ $1.strip if $1
965
+ end
966
+
967
+ def arel_visitor
968
+ # Robin note: this is fine to leave as-is.
969
+ Arel::Visitors::PostgreSQL.new(self)
970
+ end
971
+
972
+ def build_statement_pool
973
+ StatementPool.new(@connection, self.class.type_cast_config_to_integer(@config[:statement_limit]))
974
+ end
975
+
976
+ def can_perform_case_insensitive_comparison_for?(column)
977
+ @case_insensitive_cache ||= {}
978
+ @case_insensitive_cache[column.sql_type] ||= begin
979
+ sql = <<~SQL
980
+ SELECT exists(
981
+ SELECT * FROM pg_proc
982
+ WHERE proname = 'lower'
983
+ AND proargtypes = ARRAY[#{quote column.sql_type}::regtype]::oidvector
984
+ ) OR exists(
985
+ SELECT * FROM pg_proc
986
+ INNER JOIN pg_cast
987
+ ON ARRAY[casttarget]::oidvector = proargtypes
988
+ WHERE proname = 'lower'
989
+ AND castsource = #{quote column.sql_type}::regtype
990
+ )
991
+ SQL
992
+ execute_and_clear(sql, "SCHEMA", []) do |result|
993
+ result.getvalue(0, 0)
994
+ end
995
+ end
996
+ end
997
+
998
+ def add_pg_encoders
999
+ map = PG::TypeMapByClass.new
1000
+ map[Integer] = PG::TextEncoder::Integer.new
1001
+ map[TrueClass] = PG::TextEncoder::Boolean.new
1002
+ map[FalseClass] = PG::TextEncoder::Boolean.new
1003
+ @connection.type_map_for_queries = map
1004
+ end
1005
+
1006
+ def update_typemap_for_default_timezone
1007
+ if @default_timezone != ActiveRecord.default_timezone && @timestamp_decoder
1008
+ decoder_class = ActiveRecord.default_timezone == :utc ?
1009
+ PG::TextDecoder::TimestampUtc :
1010
+ PG::TextDecoder::TimestampWithoutTimeZone
1011
+
1012
+ @timestamp_decoder = decoder_class.new(@timestamp_decoder.to_h)
1013
+ @connection.type_map_for_results.add_coder(@timestamp_decoder)
1014
+
1015
+ @default_timezone = ActiveRecord.default_timezone
1016
+
1017
+ # if default timezone has changed, we need to reconfigure the connection
1018
+ # (specifically, the session time zone)
1019
+ configure_connection
1020
+ end
1021
+ end
1022
+
1023
+ def add_pg_decoders
1024
+ @default_timezone = nil
1025
+ @timestamp_decoder = nil
1026
+
1027
+ coders_by_name = {
1028
+ "int2" => PG::TextDecoder::Integer,
1029
+ "int4" => PG::TextDecoder::Integer,
1030
+ "int8" => PG::TextDecoder::Integer,
1031
+ "oid" => PG::TextDecoder::Integer,
1032
+ "float4" => PG::TextDecoder::Float,
1033
+ "float8" => PG::TextDecoder::Float,
1034
+ "numeric" => PG::TextDecoder::Numeric,
1035
+ "bool" => PG::TextDecoder::Boolean,
1036
+ "timestamp" => PG::TextDecoder::TimestampUtc,
1037
+ "timestamptz" => PG::TextDecoder::TimestampWithTimeZone,
1038
+ }
1039
+
1040
+ known_coder_types = coders_by_name.keys.map { |n| quote(n) }
1041
+ query = <<~SQL % known_coder_types.join(", ")
1042
+ SELECT t.oid, t.typname
1043
+ FROM pg_type as t
1044
+ WHERE t.typname IN (%s)
1045
+ SQL
1046
+ coders = execute_and_clear(query, "SCHEMA", []) do |result|
1047
+ result.filter_map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
1048
+ end
1049
+
1050
+ map = PG::TypeMapByOid.new
1051
+ coders.each { |coder| map.add_coder(coder) }
1052
+ @connection.type_map_for_results = map
1053
+
1054
+ @type_map_for_results = PG::TypeMapByOid.new
1055
+ @type_map_for_results.default_type_map = map
1056
+ @type_map_for_results.add_coder(PG::TextDecoder::Bytea.new(oid: 17, name: "bytea"))
1057
+ @type_map_for_results.add_coder(MoneyDecoder.new(oid: 790, name: "money"))
1058
+
1059
+ # extract timestamp decoder for use in update_typemap_for_default_timezone
1060
+ @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
1061
+ update_typemap_for_default_timezone
1062
+ end
1063
+
1064
+ def construct_coder(row, coder_class)
1065
+ return unless coder_class
1066
+ coder_class.new(oid: row["oid"].to_i, name: row["typname"])
1067
+ end
1068
+
1069
+ class MoneyDecoder < PG::SimpleDecoder # :nodoc:
1070
+ TYPE = OID::Money.new
1071
+
1072
+ def decode(value, tuple = nil, field = nil)
1073
+ TYPE.deserialize(value)
1074
+ end
1075
+ end
1076
+
1077
+ ActiveRecord::Type.add_modifier({ array: true }, OID::Array, adapter: :cipherstash_pg)
1078
+ ActiveRecord::Type.add_modifier({ range: true }, OID::Range, adapter: :cipherstash_pg)
1079
+ ActiveRecord::Type.register(:bit, OID::Bit, adapter: :cipherstash_pg)
1080
+ ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :cipherstash_pg)
1081
+ ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :cipherstash_pg)
1082
+ ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :cipherstash_pg)
1083
+ ActiveRecord::Type.register(:date, OID::Date, adapter: :cipherstash_pg)
1084
+ ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :cipherstash_pg)
1085
+ ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :cipherstash_pg)
1086
+ ActiveRecord::Type.register(:enum, OID::Enum, adapter: :cipherstash_pg)
1087
+ ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :cipherstash_pg)
1088
+ ActiveRecord::Type.register(:inet, OID::Inet, adapter: :cipherstash_pg)
1089
+ ActiveRecord::Type.register(:interval, OID::Interval, adapter: :cipherstash_pg)
1090
+ ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :cipherstash_pg)
1091
+ ActiveRecord::Type.register(:money, OID::Money, adapter: :cipherstash_pg)
1092
+ ActiveRecord::Type.register(:point, OID::Point, adapter: :cipherstash_pg)
1093
+ ActiveRecord::Type.register(:legacy_point, OID::LegacyPoint, adapter: :cipherstash_pg)
1094
+ ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :cipherstash_pg)
1095
+ ActiveRecord::Type.register(:vector, OID::Vector, adapter: :cipherstash_pg)
1096
+ ActiveRecord::Type.register(:xml, OID::Xml, adapter: :cipherstash_pg)
1097
+ end
1098
+ ActiveSupport.run_load_hooks(:active_record_cipherstash_pgadapter, CipherStashPGAdapter)
1099
+ end
1100
+ end