activerecord-yugabytedb-adapter 7.0.4.1

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