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,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