activerecord-spanner-adapter 1.0.0 → 1.2.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -1
  3. data/.github/blunderbuss.yml +2 -0
  4. data/.github/sync-repo-settings.yaml +1 -1
  5. data/.github/workflows/acceptance-tests-on-emulator.yaml +8 -6
  6. data/.github/workflows/acceptance-tests-on-production.yaml +3 -3
  7. data/.github/workflows/ci.yaml +8 -6
  8. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +9 -5
  9. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +2 -2
  10. data/.github/workflows/nightly-unit-tests.yaml +9 -5
  11. data/.github/workflows/release-please-label.yml +4 -4
  12. data/.github/workflows/release-please.yml +12 -11
  13. data/.github/workflows/rubocop.yaml +4 -4
  14. data/.release-please-manifest.json +3 -0
  15. data/CHANGELOG.md +46 -31
  16. data/CONTRIBUTING.md +1 -1
  17. data/Gemfile +6 -2
  18. data/README.md +2 -1
  19. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +12 -8
  20. data/acceptance/cases/models/insert_all_test.rb +150 -0
  21. data/acceptance/cases/transactions/optimistic_locking_test.rb +5 -0
  22. data/acceptance/cases/type/all_types_test.rb +24 -25
  23. data/acceptance/cases/type/json_test.rb +0 -2
  24. data/acceptance/models/album.rb +7 -2
  25. data/acceptance/models/singer.rb +2 -2
  26. data/acceptance/models/track.rb +5 -2
  27. data/acceptance/schema/schema.rb +2 -4
  28. data/acceptance/test_helper.rb +1 -1
  29. data/activerecord-spanner-adapter.gemspec +1 -1
  30. data/examples/rails/README.md +8 -8
  31. data/examples/snippets/interleaved-tables/README.md +164 -0
  32. data/examples/snippets/interleaved-tables/Rakefile +13 -0
  33. data/examples/snippets/interleaved-tables/application.rb +126 -0
  34. data/examples/snippets/interleaved-tables/config/database.yml +8 -0
  35. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +44 -0
  36. data/examples/snippets/interleaved-tables/db/schema.rb +32 -0
  37. data/examples/snippets/interleaved-tables/db/seeds.rb +40 -0
  38. data/examples/snippets/interleaved-tables/models/album.rb +20 -0
  39. data/examples/snippets/interleaved-tables/models/singer.rb +18 -0
  40. data/examples/snippets/interleaved-tables/models/track.rb +28 -0
  41. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +10 -4
  42. data/lib/active_record/connection_adapters/spanner_adapter.rb +64 -31
  43. data/lib/active_record/type/spanner/array.rb +19 -5
  44. data/lib/activerecord_spanner_adapter/base.rb +150 -17
  45. data/lib/activerecord_spanner_adapter/relation.rb +21 -0
  46. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  47. data/lib/arel/visitors/spanner.rb +10 -0
  48. data/release-please-config.json +19 -0
  49. metadata +28 -7
@@ -0,0 +1,126 @@
1
+ # Copyright 2022 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ require "io/console"
8
+ require_relative "../config/environment"
9
+ require_relative "models/singer"
10
+ require_relative "models/album"
11
+ require_relative "models/track"
12
+
13
+ class Application
14
+ def self.run
15
+ # List all singers, albums and tracks.
16
+ list_singers_albums_tracks
17
+
18
+ # Create a new album with some tracks.
19
+ create_new_album
20
+
21
+ # Try to update the singer of an album. This is not possible as albums are interleaved in singers.
22
+ update_singer_of_album
23
+
24
+ # Try to delete a singer that has at least one album. This is NOT possible as albums is NOT marked with
25
+ # ON DELETE CASCADE.
26
+ delete_singer_with_albums
27
+
28
+ # Try to delete an album that has at least one track. This IS possible as tracks IS marked with
29
+ # ON DELETE CASCADE.
30
+ delete_album_with_tracks
31
+
32
+ puts ""
33
+ puts "Press any key to end the application"
34
+ STDIN.getch
35
+ end
36
+
37
+ def self.find_singer
38
+ singerid = Singer.all.sample.singerid
39
+
40
+ singer = Singer.find singerid
41
+ puts "Found singer: #{singer.first_name} #{singer.last_name}"
42
+ end
43
+
44
+ def self.find_album
45
+ singer = Singer.all.sample
46
+ albumid = singer.albums.sample.albumid
47
+
48
+ album = Album.find [singer.singerid, albumid]
49
+ puts "Found album: #{album.title}"
50
+ end
51
+
52
+ def self.list_singers_albums_tracks
53
+ puts ""
54
+ puts "Listing all singers with corresponding albums and tracks"
55
+ Singer.all.order("last_name, first_name").each do |singer|
56
+ puts "#{singer.first_name} #{singer.last_name} has #{singer.albums.count} albums:"
57
+ singer.albums.order("title").each do |album|
58
+ puts " #{album.title} has #{album.tracks.count} tracks:"
59
+ album.tracks.each do |track|
60
+ puts " #{track.title}"
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def self.create_new_album
67
+ # Create a new album with some tracks.
68
+ puts ""
69
+ singer = Singer.all.sample
70
+ puts "Creating a new album for #{singer.first_name} #{singer.last_name}"
71
+ album = singer.albums.build title: "New Title"
72
+ # NOTE: When adding multiple elements to a collection, you *MUST* set the primary key value (i.e. trackid).
73
+ # Otherwise, ActiveRecord thinks that you are adding the same record multiple times and will only add one.
74
+ album.tracks.build title: "Track 1", duration: 3.5, singer: singer, trackid: Track.next_sequence_value
75
+ album.tracks.build title: "Track 2", duration: 3.6, singer: singer, trackid: Track.next_sequence_value
76
+ # This will save the album and corresponding tracks in one transaction.
77
+ album.save!
78
+
79
+ album.reload
80
+ puts "Album #{album.title} has #{album.tracks.count} tracks:"
81
+ album.tracks.order("title").each do |track|
82
+ puts " #{track.title} with duration #{track.duration}"
83
+ end
84
+ end
85
+
86
+ def self.update_singer_of_album
87
+ # It is not possible to change the singer of an album or the album of a track. This is because the associations
88
+ # between these are not traditional foreign keys, but an immutable parent-child relationship.
89
+ album = Album.all.sample
90
+ new_singer = Singer.all.where.not(singerid: album.singer).sample
91
+ # This will fail as we cannot assign a new singer to an album as it is an INTERLEAVE IN PARENT relationship.
92
+ begin
93
+ album.update! singer: new_singer
94
+ raise StandardError, "Unexpected error: Updating the singer of an album should not be possible."
95
+ rescue ActiveRecord::StatementInvalid
96
+ puts ""
97
+ puts "Failed to update the singer of an album. This is expected."
98
+ end
99
+ end
100
+
101
+ def self.delete_singer_with_albums
102
+ # Deleting a singer that has albums is not possible, as the INTERLEAVE IN PARENT of albums is not marked with
103
+ # ON DELETE CASCADE.
104
+ singer = Album.all.sample.singer
105
+ begin
106
+ singer.delete
107
+ raise StandardError, "Unexpected error: Updating the singer of an album should not be possible."
108
+ rescue ActiveRecord::StatementInvalid
109
+ puts ""
110
+ puts "Failed to delete a singer that has #{singer.albums.count} albums. This is expected."
111
+ end
112
+ end
113
+
114
+ def self.delete_album_with_tracks
115
+ # Deleting an album with tracks is supported, as the INTERLEAVE IN PARENT relationship between tracks and albums is
116
+ # marked with ON DELETE CASCADE.
117
+ puts ""
118
+ puts "Total track count: #{Track.count}"
119
+ album = Track.all.sample.album
120
+ puts "Deleting album #{album.title} with #{album.tracks.count} tracks"
121
+ album.delete
122
+ puts "Total track count after deletion: #{Track.count}"
123
+ end
124
+ end
125
+
126
+ Application.run
@@ -0,0 +1,8 @@
1
+ development:
2
+ adapter: spanner
3
+ emulator_host: localhost:9010
4
+ project: test-project
5
+ instance: test-instance
6
+ database: testdb
7
+ pool: 5
8
+ timeout: 5000
@@ -0,0 +1,44 @@
1
+ # Copyright 2022 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ class CreateTables < ActiveRecord::Migration[6.0]
8
+ def change
9
+ # Execute the entire migration as one DDL batch.
10
+ connection.ddl_batch do
11
+ create_table :singers, id: false do |t|
12
+ # Explicitly define the primary key with a custom name to prevent all primary key columns from being named `id`.
13
+ t.primary_key :singerid
14
+ t.string :first_name
15
+ t.string :last_name
16
+ end
17
+
18
+ create_table :albums, id: false do |t|
19
+ # Interleave the `albums` table in the parent table `singers`.
20
+ t.interleave_in :singers
21
+ t.primary_key :albumid
22
+ # `singerid` is defined as a `parent_key` which makes it a part of the primary key in the table definition, but
23
+ # it is not presented to ActiveRecord as part of the primary key, to prevent ActiveRecord from considering this
24
+ # to be an entity with a composite primary key (which is not supported by ActiveRecord).
25
+ t.parent_key :singerid
26
+ t.string :title
27
+ end
28
+
29
+ create_table :tracks, id: false do |t|
30
+ # Interleave the `tracks` table in the parent table `albums` and cascade delete all tracks that belong to an
31
+ # album when an album is deleted.
32
+ t.interleave_in :albums, :cascade
33
+ # `trackid` is considered the only primary key column by ActiveRecord.
34
+ t.primary_key :trackid
35
+ # `singerid` and `albumid` form the parent key of `tracks`. These are part of the primary key definition in the
36
+ # database, but are presented as parent keys to ActiveRecord.
37
+ t.parent_key :singerid
38
+ t.parent_key :albumid
39
+ t.string :title
40
+ t.numeric :duration
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ ActiveRecord::Schema.define(version: 1) do
14
+
15
+ create_table "albums", primary_key: "albumid", id: { limit: 8 }, force: :cascade do |t|
16
+ t.integer "singerid", limit: 8, null: false
17
+ t.string "title"
18
+ end
19
+
20
+ create_table "singers", primary_key: "singerid", id: { limit: 8 }, force: :cascade do |t|
21
+ t.string "first_name"
22
+ t.string "last_name"
23
+ end
24
+
25
+ create_table "tracks", primary_key: "trackid", id: { limit: 8 }, force: :cascade do |t|
26
+ t.integer "singerid", limit: 8, null: false
27
+ t.integer "albumid", limit: 8, null: false
28
+ t.string "title"
29
+ t.decimal "duration"
30
+ end
31
+
32
+ end
@@ -0,0 +1,40 @@
1
+ # Copyright 2022 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ require_relative "../../config/environment.rb"
8
+ require_relative "../models/singer"
9
+ require_relative "../models/album"
10
+ require_relative "../models/track"
11
+
12
+ first_names = %w[Pete Alice John Ethel Trudy Naomi Wendy Ruben Thomas Elly]
13
+ last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Tennet Courtou]
14
+
15
+ adjectives = %w[daily happy blue generous cooked bad open]
16
+ nouns = %w[windows potatoes bank street tree glass bottle]
17
+
18
+ verbs = %w[operate waste package chew yield express polish stress slip want cough campaign cultivate report park refer]
19
+ adverbs = %w[directly right hopefully personally economically privately supposedly consequently fully later urgently]
20
+
21
+ durations = [3.14, 5.4, 3.3, 4.1, 5.0, 3.2, 3.0, 3.5, 4.0, 4.5, 5.5, 6.0]
22
+
23
+ # This ensures all the records are inserted using one read/write transaction that will use mutations instead of DML.
24
+ ActiveRecord::Base.transaction isolation: :buffered_mutations do
25
+ singers = []
26
+ 5.times do
27
+ singers << Singer.create(first_name: first_names.sample, last_name: last_names.sample)
28
+ end
29
+
30
+ albums = []
31
+ 20.times do
32
+ singer = singers.sample
33
+ albums << Album.create(title: "#{adjectives.sample} #{nouns.sample}", singer: singer)
34
+ end
35
+
36
+ 200.times do
37
+ album = albums.sample
38
+ Track.create title: "#{verbs.sample} #{adverbs.sample}", duration: durations.sample, album: album
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ # Copyright 2022 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ require "composite_primary_keys"
8
+
9
+ class Album < ActiveRecord::Base
10
+ # Use the `composite_primary_key` gem to create a composite primary key definition for the model.
11
+ self.primary_keys = :singerid, :albumid
12
+
13
+ # `albums` is defined as INTERLEAVE IN PARENT `singers`.
14
+ # The primary key of `singers` is `singerid`.
15
+ belongs_to :singer, foreign_key: :singerid
16
+
17
+ # `tracks` is defined as INTERLEAVE IN PARENT `albums`.
18
+ # The primary key of `albums` is (`singerid`, `albumid`).
19
+ has_many :tracks, foreign_key: [:singerid, :albumid]
20
+ end
@@ -0,0 +1,18 @@
1
+ # Copyright 2022 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ class Singer < ActiveRecord::Base
8
+ # `albums` is defined as INTERLEAVE IN PARENT `singers`.
9
+ # The primary key of `albums` is (`singerid`, `albumid`).
10
+ has_many :albums, foreign_key: :singerid
11
+
12
+ # `tracks` is defined as INTERLEAVE IN PARENT `albums`.
13
+ # The primary key of `tracks` is [`singerid`, `albumid`, `trackid`].
14
+ # The `singerid` column can be used to associate tracks with a singer without the need to go through albums.
15
+ # Note also that the inclusion of `singerid` as a column in `tracks` is required in order to make `tracks` a child
16
+ # table of `albums` which has primary key (`singerid`, `albumid`).
17
+ has_many :tracks, foreign_key: :singerid
18
+ end
@@ -0,0 +1,28 @@
1
+ # Copyright 2022 Google LLC
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ class Track < ActiveRecord::Base
8
+ # Use the `composite_primary_key` gem to create a composite primary key definition for the model.
9
+ self.primary_keys = :singerid, :albumid, :trackid
10
+
11
+ # `tracks` is defined as INTERLEAVE IN PARENT `albums`. The primary key of `albums` is ()`singerid`, `albumid`).
12
+ belongs_to :album, foreign_key: [:singerid, :albumid]
13
+
14
+ # `tracks` also has a `singerid` column should be used to associate a Track with a Singer.
15
+ belongs_to :singer, foreign_key: :singerid
16
+
17
+ # Override the default initialize method to automatically set the singer attribute when an album is given.
18
+ def initialize attributes = nil
19
+ super
20
+ self.singer ||= album&.singer
21
+ end
22
+
23
+ def album=value
24
+ super
25
+ # Ensure the singer of this track is equal to the singer of the album that is set.
26
+ self.singer = value&.singer
27
+ end
28
+ end
@@ -10,14 +10,20 @@ module ActiveRecord
10
10
  class SchemaCreation < SchemaCreation
11
11
  private
12
12
 
13
- # rubocop:disable Naming/MethodName, Metrics/AbcSize
13
+ # rubocop:disable Naming/MethodName, Metrics/AbcSize, Metrics/PerceivedComplexity
14
14
 
15
15
  def visit_TableDefinition o
16
16
  create_sql = +"CREATE TABLE #{quote_table_name o.name} "
17
17
  statements = o.columns.map { |c| accept c }
18
18
 
19
- o.foreign_keys.each do |to_table, options|
20
- statements << foreign_key_in_create(o.name, to_table, options)
19
+ if ActiveRecord::VERSION::MAJOR >= 7
20
+ o.foreign_keys.each do |fk|
21
+ statements << accept(fk)
22
+ end
23
+ else
24
+ o.foreign_keys.each do |to_table, options|
25
+ statements << foreign_key_in_create(o.name, to_table, options)
26
+ end
21
27
  end
22
28
 
23
29
  create_sql << "(#{statements.join ', '}) " if statements.any?
@@ -106,7 +112,7 @@ module ActiveRecord
106
112
  sql
107
113
  end
108
114
 
109
- # rubocop:enable Naming/MethodName, Metrics/AbcSize
115
+ # rubocop:enable Naming/MethodName, Metrics/AbcSize, Metrics/PerceivedComplexity
110
116
 
111
117
  def add_column_options! sql, options
112
118
  if options[:null] == false || options[:primary_key] == true
@@ -8,6 +8,7 @@ require "securerandom"
8
8
  require "google/cloud/spanner"
9
9
  require "spanner_client_ext"
10
10
  require "active_record/connection_adapters/abstract_adapter"
11
+ require "active_record/connection_adapters/abstract/connection_pool"
11
12
  require "active_record/connection_adapters/spanner/database_statements"
12
13
  require "active_record/connection_adapters/spanner/schema_statements"
13
14
  require "active_record/connection_adapters/spanner/schema_cache"
@@ -43,9 +44,9 @@ module ActiveRecord
43
44
  module ConnectionAdapters
44
45
  module AbstractPool
45
46
  def get_schema_cache connection
46
- @schema_cache ||= SpannerSchemaCache.new connection
47
- @schema_cache.connection = connection
48
- @schema_cache
47
+ self.schema_cache ||= SpannerSchemaCache.new connection
48
+ schema_cache.connection = connection
49
+ schema_cache
49
50
  end
50
51
  end
51
52
 
@@ -178,41 +179,73 @@ module ActiveRecord
178
179
  Arel::Visitors::Spanner.new self
179
180
  end
180
181
 
181
- private
182
+ def build_insert_sql insert
183
+ if current_spanner_transaction&.isolation == :buffered_mutations
184
+ raise "ActiveRecordSpannerAdapter does not support insert_sql with buffered_mutations transaction."
185
+ end
182
186
 
183
- def initialize_type_map m = type_map
184
- m.register_type "BOOL", Type::Boolean.new
185
- register_class_with_limit(
186
- m, %r{^BYTES}i, ActiveRecord::Type::Spanner::Bytes
187
- )
188
- m.register_type "DATE", Type::Date.new
189
- m.register_type "FLOAT64", Type::Float.new
190
- m.register_type "NUMERIC", Type::Decimal.new
191
- m.register_type "INT64", Type::Integer.new(limit: 8)
192
- register_class_with_limit m, %r{^STRING}i, Type::String
193
- m.register_type "TIMESTAMP", ActiveRecord::Type::Spanner::Time.new
194
- m.register_type "JSON", ActiveRecord::Type::Json.new
187
+ if insert.skip_duplicates? || insert.update_duplicates?
188
+ raise NotImplementedError, "CloudSpanner does not support skip_duplicates and update_duplicates."
189
+ end
195
190
 
196
- register_array_types m
191
+ values_list, = insert.values_list
192
+ "INSERT #{insert.into} #{values_list}"
197
193
  end
198
194
 
199
- def register_array_types m
200
- m.register_type %r{^ARRAY<BOOL>}i, Type::Spanner::Array.new(Type::Boolean.new)
201
- m.register_type %r{^ARRAY<BYTES\((MAX|d+)\)>}i, Type::Spanner::Array.new(Type::Binary.new)
202
- m.register_type %r{^ARRAY<DATE>}i, Type::Spanner::Array.new(Type::Date.new)
203
- m.register_type %r{^ARRAY<FLOAT64>}i, Type::Spanner::Array.new(Type::Float.new)
204
- m.register_type %r{^ARRAY<NUMERIC>}i, Type::Spanner::Array.new(Type::Decimal.new)
205
- m.register_type %r{^ARRAY<INT64>}i, Type::Spanner::Array.new(Type::Integer.new(limit: 8))
206
- m.register_type %r{^ARRAY<STRING\((MAX|d+)\)>}i, Type::Spanner::Array.new(Type::String.new)
207
- m.register_type %r{^ARRAY<TIMESTAMP>}i, Type::Spanner::Array.new(ActiveRecord::Type::Spanner::Time.new)
208
- m.register_type %r{^ARRAY<JSON>}i, Type::Spanner::Array.new(ActiveRecord::Type::Json.new)
195
+ module TypeMapBuilder
196
+ private
197
+
198
+ def initialize_type_map m = type_map
199
+ m.register_type "BOOL", Type::Boolean.new
200
+ register_class_with_limit(
201
+ m, %r{^BYTES}i, ActiveRecord::Type::Spanner::Bytes
202
+ )
203
+ m.register_type "DATE", Type::Date.new
204
+ m.register_type "FLOAT64", Type::Float.new
205
+ m.register_type "NUMERIC", Type::Decimal.new
206
+ m.register_type "INT64", Type::Integer.new(limit: 8)
207
+ register_class_with_limit m, %r{^STRING}i, Type::String
208
+ m.register_type "TIMESTAMP", ActiveRecord::Type::Spanner::Time.new
209
+ m.register_type "JSON", ActiveRecord::Type::Json.new
210
+
211
+ register_array_types m
212
+ end
213
+
214
+ def register_array_types m
215
+ m.register_type %r{^ARRAY<BOOL>}i, Type::Spanner::Array.new(Type::Boolean.new)
216
+ m.register_type %r{^ARRAY<BYTES\((MAX|d+)\)>}i,
217
+ Type::Spanner::Array.new(ActiveRecord::Type::Spanner::Bytes.new)
218
+ m.register_type %r{^ARRAY<DATE>}i, Type::Spanner::Array.new(Type::Date.new)
219
+ m.register_type %r{^ARRAY<FLOAT64>}i, Type::Spanner::Array.new(Type::Float.new)
220
+ m.register_type %r{^ARRAY<NUMERIC>}i, Type::Spanner::Array.new(Type::Decimal.new)
221
+ m.register_type %r{^ARRAY<INT64>}i, Type::Spanner::Array.new(Type::Integer.new(limit: 8))
222
+ m.register_type %r{^ARRAY<STRING\((MAX|d+)\)>}i, Type::Spanner::Array.new(Type::String.new)
223
+ m.register_type %r{^ARRAY<TIMESTAMP>}i, Type::Spanner::Array.new(ActiveRecord::Type::Spanner::Time.new)
224
+ m.register_type %r{^ARRAY<JSON>}i, Type::Spanner::Array.new(ActiveRecord::Type::Json.new)
225
+ end
226
+
227
+ def extract_limit sql_type
228
+ value = /\((.*)\)/.match sql_type
229
+ return unless value
230
+
231
+ value[1] == "MAX" ? "MAX" : value[1].to_i
232
+ end
209
233
  end
210
234
 
211
- def extract_limit sql_type
212
- value = /\((.*)\)/.match sql_type
213
- return unless value
235
+ if ActiveRecord::VERSION::MAJOR >= 7
236
+ class << self
237
+ include TypeMapBuilder
238
+ end
239
+
240
+ TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map m }
241
+
242
+ private
214
243
 
215
- value[1] == "MAX" ? "MAX" : value[1].to_i
244
+ def type_map
245
+ TYPE_MAP
246
+ end
247
+ else
248
+ include TypeMapBuilder
216
249
  end
217
250
 
218
251
  def translate_exception exception, message:, sql:, binds:
@@ -15,15 +15,29 @@ module ActiveRecord
15
15
  @element_type = element_type
16
16
  end
17
17
 
18
- def serialize value
18
+ def cast value
19
19
  return super if value.nil?
20
- return super unless @element_type.is_a? Type::Decimal
21
20
  return super unless value.respond_to? :map
22
21
 
23
- # Convert a decimal (NUMERIC) array to a String array to prevent it from being encoded as a FLOAT64 array.
24
22
  value.map do |v|
25
- next if v.nil?
26
- v.to_s
23
+ @element_type.cast v
24
+ end
25
+ end
26
+
27
+ def serialize value
28
+ return super if value.nil?
29
+ return super unless value.respond_to? :map
30
+
31
+ if @element_type.is_a? ActiveRecord::Type::Decimal
32
+ # Convert a decimal (NUMERIC) array to a String array to prevent it from being encoded as a FLOAT64 array.
33
+ value.map do |v|
34
+ next if v.nil?
35
+ v.to_s
36
+ end
37
+ else
38
+ value.map do |v|
39
+ @element_type.serialize v
40
+ end
27
41
  end
28
42
  end
29
43
  end