activerecord-spanner-adapter 1.5.0 → 1.6.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/acceptance-tests-on-emulator.yaml +1 -1
  3. data/.github/workflows/acceptance-tests-on-production.yaml +5 -3
  4. data/.github/workflows/ci.yaml +1 -1
  5. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +1 -1
  6. data/.github/workflows/nightly-acceptance-tests-on-production.yaml +5 -3
  7. data/.github/workflows/nightly-unit-tests.yaml +1 -1
  8. data/.github/workflows/release-please-label.yml +1 -1
  9. data/.release-please-manifest.json +1 -1
  10. data/CHANGELOG.md +14 -0
  11. data/Gemfile +5 -2
  12. data/README.md +10 -10
  13. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +6 -0
  14. data/acceptance/cases/migration/change_schema_test.rb +19 -3
  15. data/acceptance/cases/migration/schema_dumper_test.rb +10 -1
  16. data/acceptance/cases/models/insert_all_test.rb +22 -0
  17. data/acceptance/cases/models/interleave_test.rb +6 -0
  18. data/acceptance/cases/tasks/database_tasks_test.rb +340 -2
  19. data/acceptance/cases/transactions/optimistic_locking_test.rb +6 -0
  20. data/acceptance/cases/transactions/read_write_transactions_test.rb +24 -0
  21. data/acceptance/models/table_with_sequence.rb +10 -0
  22. data/acceptance/schema/schema.rb +65 -19
  23. data/acceptance/test_helper.rb +1 -1
  24. data/activerecord-spanner-adapter.gemspec +1 -1
  25. data/examples/snippets/bit-reversed-sequence/README.md +103 -0
  26. data/examples/snippets/bit-reversed-sequence/Rakefile +13 -0
  27. data/examples/snippets/bit-reversed-sequence/application.rb +68 -0
  28. data/examples/snippets/bit-reversed-sequence/config/database.yml +8 -0
  29. data/examples/snippets/bit-reversed-sequence/db/migrate/01_create_tables.rb +33 -0
  30. data/examples/snippets/bit-reversed-sequence/db/schema.rb +31 -0
  31. data/examples/snippets/bit-reversed-sequence/db/seeds.rb +31 -0
  32. data/examples/snippets/bit-reversed-sequence/models/album.rb +11 -0
  33. data/examples/snippets/bit-reversed-sequence/models/singer.rb +15 -0
  34. data/examples/snippets/interleaved-tables/README.md +44 -53
  35. data/examples/snippets/interleaved-tables/Rakefile +2 -2
  36. data/examples/snippets/interleaved-tables/application.rb +2 -2
  37. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +12 -18
  38. data/examples/snippets/interleaved-tables/db/schema.rb +9 -7
  39. data/examples/snippets/interleaved-tables/db/seeds.rb +1 -1
  40. data/examples/snippets/interleaved-tables/models/album.rb +3 -7
  41. data/examples/snippets/interleaved-tables/models/singer.rb +1 -1
  42. data/examples/snippets/interleaved-tables/models/track.rb +6 -7
  43. data/examples/snippets/interleaved-tables-before-7.1/README.md +167 -0
  44. data/examples/snippets/interleaved-tables-before-7.1/Rakefile +13 -0
  45. data/examples/snippets/interleaved-tables-before-7.1/application.rb +126 -0
  46. data/examples/snippets/interleaved-tables-before-7.1/config/database.yml +8 -0
  47. data/examples/snippets/interleaved-tables-before-7.1/db/migrate/01_create_tables.rb +44 -0
  48. data/examples/snippets/interleaved-tables-before-7.1/db/schema.rb +37 -0
  49. data/examples/snippets/interleaved-tables-before-7.1/db/seeds.rb +40 -0
  50. data/examples/snippets/interleaved-tables-before-7.1/models/album.rb +20 -0
  51. data/examples/snippets/interleaved-tables-before-7.1/models/singer.rb +18 -0
  52. data/examples/snippets/interleaved-tables-before-7.1/models/track.rb +28 -0
  53. data/examples/snippets/query-logs/README.md +43 -0
  54. data/examples/snippets/query-logs/Rakefile +13 -0
  55. data/examples/snippets/query-logs/application.rb +63 -0
  56. data/examples/snippets/query-logs/config/database.yml +8 -0
  57. data/examples/snippets/query-logs/db/migrate/01_create_tables.rb +21 -0
  58. data/examples/snippets/query-logs/db/schema.rb +31 -0
  59. data/examples/snippets/query-logs/db/seeds.rb +24 -0
  60. data/examples/snippets/query-logs/models/album.rb +9 -0
  61. data/examples/snippets/query-logs/models/singer.rb +9 -0
  62. data/lib/active_record/connection_adapters/spanner/column.rb +13 -0
  63. data/lib/active_record/connection_adapters/spanner/database_statements.rb +144 -35
  64. data/lib/active_record/connection_adapters/spanner/schema_cache.rb +3 -21
  65. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +11 -0
  66. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +4 -0
  67. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +3 -2
  68. data/lib/active_record/connection_adapters/spanner_adapter.rb +28 -9
  69. data/lib/activerecord_spanner_adapter/base.rb +58 -21
  70. data/lib/activerecord_spanner_adapter/information_schema.rb +33 -24
  71. data/lib/activerecord_spanner_adapter/primary_key.rb +1 -1
  72. data/lib/activerecord_spanner_adapter/table/column.rb +4 -9
  73. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  74. data/lib/arel/visitors/spanner.rb +3 -1
  75. metadata +33 -4
@@ -14,6 +14,10 @@ module ActiveRecord
14
14
  class DatabaseTasksTest < SpannerAdapter::TestCase
15
15
  attr_reader :connector_config, :connection
16
16
 
17
+ def is_7_1_or_higher?
18
+ ActiveRecord::gem_version >= Gem::Version.create('7.1.0')
19
+ end
20
+
17
21
  def setup
18
22
  @database_id = "ar-tasks-test-#{SecureRandom.hex 4}"
19
23
  @connector_config = {
@@ -87,8 +91,12 @@ module ActiveRecord
87
91
  end
88
92
  ActiveRecord::Tasks::DatabaseTasks.dump_schema db_config, :sql
89
93
  sql = File.read(filename)
90
- if ENV["SPANNER_EMULATOR_HOST"]
94
+ if ENV["SPANNER_EMULATOR_HOST"] && is_7_1_or_higher?
95
+ assert_equal expected_schema_sql_on_emulator_7_1, sql, msg = sql
96
+ elsif ENV["SPANNER_EMULATOR_HOST"]
91
97
  assert_equal expected_schema_sql_on_emulator, sql, msg = sql
98
+ elsif is_7_1_or_higher?
99
+ assert_equal expected_schema_sql_on_production_7_1, sql, msg = sql
92
100
  else
93
101
  assert_equal expected_schema_sql_on_production, sql, msg = sql
94
102
  end
@@ -235,6 +243,168 @@ CREATE TABLE tracks (
235
243
  ) PRIMARY KEY(singerid, albumid, trackid),
236
244
  INTERLEAVE IN PARENT albums ON DELETE CASCADE;
237
245
  CREATE NULL_FILTERED INDEX index_tracks_on_singerid_and_albumid_and_title ON tracks(singerid, albumid, title), INTERLEAVE IN albums;
246
+ CREATE TABLE table_with_sequence (
247
+ id INT64 NOT NULL DEFAULT (FARM_FINGERPRINT(GENERATE_UUID())),
248
+ name STRING(MAX) NOT NULL,
249
+ age INT64 NOT NULL,
250
+ ) PRIMARY KEY(id);
251
+ CREATE TABLE schema_migrations (
252
+ version STRING(MAX) NOT NULL,
253
+ ) PRIMARY KEY(version);
254
+ CREATE TABLE ar_internal_metadata (
255
+ key STRING(MAX) NOT NULL,
256
+ value STRING(MAX),
257
+ created_at TIMESTAMP NOT NULL,
258
+ updated_at TIMESTAMP NOT NULL,
259
+ ) PRIMARY KEY(key);
260
+ INSERT INTO `schema_migrations` (version) VALUES
261
+ ('1');
262
+
263
+ "
264
+ end
265
+
266
+ def expected_schema_sql_on_emulator_7_1
267
+ "CREATE TABLE all_types (
268
+ id INT64 NOT NULL,
269
+ col_string STRING(MAX),
270
+ col_int64 INT64,
271
+ col_float64 FLOAT64,
272
+ col_numeric NUMERIC,
273
+ col_bool BOOL,
274
+ col_bytes BYTES(MAX),
275
+ col_date DATE,
276
+ col_timestamp TIMESTAMP,
277
+ col_json JSON,
278
+ col_array_string ARRAY<STRING(MAX)>,
279
+ col_array_int64 ARRAY<INT64>,
280
+ col_array_float64 ARRAY<FLOAT64>,
281
+ col_array_numeric ARRAY<NUMERIC>,
282
+ col_array_bool ARRAY<BOOL>,
283
+ col_array_bytes ARRAY<BYTES(MAX)>,
284
+ col_array_date ARRAY<DATE>,
285
+ col_array_timestamp ARRAY<TIMESTAMP>,
286
+ col_array_json ARRAY<JSON>,
287
+ ) PRIMARY KEY(id);
288
+ CREATE TABLE firms (
289
+ id INT64 NOT NULL,
290
+ name STRING(MAX),
291
+ rating INT64,
292
+ description STRING(MAX),
293
+ account_id INT64,
294
+ ) PRIMARY KEY(id);
295
+ CREATE INDEX index_firms_on_account_id ON firms(account_id);
296
+ CREATE TABLE customers (
297
+ id INT64 NOT NULL,
298
+ name STRING(MAX),
299
+ ) PRIMARY KEY(id);
300
+ CREATE TABLE accounts (
301
+ id INT64 NOT NULL,
302
+ customer_id INT64,
303
+ firm_id INT64,
304
+ name STRING(MAX),
305
+ credit_limit INT64,
306
+ transactions_count INT64,
307
+ ) PRIMARY KEY(id);
308
+ CREATE TABLE transactions (
309
+ id INT64 NOT NULL,
310
+ amount FLOAT64,
311
+ account_id INT64,
312
+ ) PRIMARY KEY(id);
313
+ CREATE TABLE departments (
314
+ id INT64 NOT NULL,
315
+ name STRING(MAX),
316
+ resource_type STRING(255),
317
+ resource_id INT64,
318
+ ) PRIMARY KEY(id);
319
+ CREATE INDEX index_departments_on_resource ON departments(resource_type, resource_id);
320
+ CREATE TABLE member_types (
321
+ id INT64 NOT NULL,
322
+ name STRING(MAX),
323
+ ) PRIMARY KEY(id);
324
+ CREATE TABLE members (
325
+ id INT64 NOT NULL,
326
+ name STRING(MAX),
327
+ member_type_id INT64,
328
+ admittable_type STRING(255),
329
+ admittable_id INT64,
330
+ ) PRIMARY KEY(id);
331
+ CREATE TABLE memberships (
332
+ id INT64 NOT NULL,
333
+ joined_on TIMESTAMP,
334
+ club_id INT64,
335
+ member_id INT64,
336
+ favourite BOOL,
337
+ ) PRIMARY KEY(id);
338
+ CREATE TABLE clubs (
339
+ id INT64 NOT NULL,
340
+ name STRING(MAX),
341
+ ) PRIMARY KEY(id);
342
+ CREATE TABLE authors (
343
+ id INT64 NOT NULL,
344
+ name STRING(MAX) NOT NULL,
345
+ registered_date DATE,
346
+ organization_id INT64,
347
+ ) PRIMARY KEY(id);
348
+ CREATE TABLE posts (
349
+ id INT64 NOT NULL,
350
+ title STRING(MAX),
351
+ content STRING(MAX),
352
+ author_id INT64,
353
+ comments_count INT64,
354
+ post_date DATE,
355
+ published_time TIMESTAMP,
356
+ ) PRIMARY KEY(id);
357
+ CREATE INDEX index_posts_on_author_id ON posts(author_id);
358
+ CREATE TABLE comments (
359
+ id INT64 NOT NULL,
360
+ comment STRING(MAX),
361
+ post_id INT64,
362
+ CONSTRAINT fk_rails_2fd19c0db7 FOREIGN KEY(post_id) REFERENCES posts(id),
363
+ ) PRIMARY KEY(id);
364
+ CREATE TABLE addresses (
365
+ id INT64 NOT NULL,
366
+ line1 STRING(MAX),
367
+ postal_code STRING(MAX),
368
+ city STRING(MAX),
369
+ author_id INT64,
370
+ ) PRIMARY KEY(id);
371
+ CREATE TABLE organizations (
372
+ id INT64 NOT NULL,
373
+ name STRING(MAX),
374
+ last_updated TIMESTAMP OPTIONS (
375
+ allow_commit_timestamp = true
376
+ ),
377
+ ) PRIMARY KEY(id);
378
+ CREATE TABLE singers (
379
+ singerid INT64 NOT NULL,
380
+ first_name STRING(200),
381
+ last_name STRING(MAX),
382
+ tracks_count INT64,
383
+ lock_version INT64,
384
+ full_name STRING(MAX) AS (COALESCE(first_name || ' ', '') || last_name) STORED,
385
+ ) PRIMARY KEY(singerid);
386
+ CREATE TABLE albums (
387
+ singerid INT64 NOT NULL,
388
+ albumid INT64 NOT NULL,
389
+ title STRING(MAX),
390
+ lock_version INT64,
391
+ ) PRIMARY KEY(singerid, albumid),
392
+ INTERLEAVE IN PARENT singers ON DELETE NO ACTION;
393
+ CREATE TABLE tracks (
394
+ singerid INT64 NOT NULL,
395
+ albumid INT64 NOT NULL,
396
+ trackid INT64 NOT NULL,
397
+ title STRING(MAX),
398
+ duration NUMERIC,
399
+ lock_version INT64,
400
+ ) PRIMARY KEY(singerid, albumid, trackid),
401
+ INTERLEAVE IN PARENT albums ON DELETE CASCADE;
402
+ CREATE NULL_FILTERED INDEX index_tracks_on_singerid_and_albumid_and_title ON tracks(singerid, albumid, title), INTERLEAVE IN albums;
403
+ CREATE TABLE table_with_sequence (
404
+ id INT64 NOT NULL DEFAULT (FARM_FINGERPRINT(GENERATE_UUID())),
405
+ name STRING(MAX) NOT NULL,
406
+ age INT64 NOT NULL,
407
+ ) PRIMARY KEY(id);
238
408
  CREATE TABLE schema_migrations (
239
409
  version STRING(MAX) NOT NULL,
240
410
  ) PRIMARY KEY(version);
@@ -251,7 +421,10 @@ INSERT INTO `schema_migrations` (version) VALUES
251
421
  end
252
422
 
253
423
  def expected_schema_sql_on_production
254
- "CREATE TABLE accounts (
424
+ "CREATE SEQUENCE test_sequence OPTIONS (
425
+ sequence_kind = 'bit_reversed_positive'
426
+ );
427
+ CREATE TABLE accounts (
255
428
  id INT64 NOT NULL,
256
429
  customer_id INT64,
257
430
  firm_id INT64,
@@ -391,6 +564,171 @@ CREATE TABLE tracks (
391
564
  ) PRIMARY KEY(singerid, albumid, trackid),
392
565
  INTERLEAVE IN PARENT albums ON DELETE CASCADE;
393
566
  CREATE NULL_FILTERED INDEX index_tracks_on_singerid_and_albumid_and_title ON tracks(singerid, albumid, title), INTERLEAVE IN albums;
567
+ CREATE TABLE table_with_sequence (
568
+ id INT64 NOT NULL DEFAULT (GET_NEXT_SEQUENCE_VALUE(SEQUENCE test_sequence)),
569
+ name STRING(MAX) NOT NULL,
570
+ age INT64 NOT NULL,
571
+ ) PRIMARY KEY(id);
572
+ CREATE TABLE transactions (
573
+ id INT64 NOT NULL,
574
+ amount FLOAT64,
575
+ account_id INT64,
576
+ ) PRIMARY KEY(id);
577
+ INSERT INTO `schema_migrations` (version) VALUES
578
+ ('1');
579
+
580
+ "
581
+ end
582
+
583
+ def expected_schema_sql_on_production_7_1
584
+ "CREATE SEQUENCE test_sequence OPTIONS (
585
+ sequence_kind = 'bit_reversed_positive'
586
+ );
587
+ CREATE TABLE accounts (
588
+ id INT64 NOT NULL,
589
+ customer_id INT64,
590
+ firm_id INT64,
591
+ name STRING(MAX),
592
+ credit_limit INT64,
593
+ transactions_count INT64,
594
+ ) PRIMARY KEY(id);
595
+ CREATE TABLE addresses (
596
+ id INT64 NOT NULL,
597
+ line1 STRING(MAX),
598
+ postal_code STRING(MAX),
599
+ city STRING(MAX),
600
+ author_id INT64,
601
+ ) PRIMARY KEY(id);
602
+ CREATE TABLE all_types (
603
+ id INT64 NOT NULL,
604
+ col_string STRING(MAX),
605
+ col_int64 INT64,
606
+ col_float64 FLOAT64,
607
+ col_numeric NUMERIC,
608
+ col_bool BOOL,
609
+ col_bytes BYTES(MAX),
610
+ col_date DATE,
611
+ col_timestamp TIMESTAMP,
612
+ col_json JSON,
613
+ col_array_string ARRAY<STRING(MAX)>,
614
+ col_array_int64 ARRAY<INT64>,
615
+ col_array_float64 ARRAY<FLOAT64>,
616
+ col_array_numeric ARRAY<NUMERIC>,
617
+ col_array_bool ARRAY<BOOL>,
618
+ col_array_bytes ARRAY<BYTES(MAX)>,
619
+ col_array_date ARRAY<DATE>,
620
+ col_array_timestamp ARRAY<TIMESTAMP>,
621
+ col_array_json ARRAY<JSON>,
622
+ ) PRIMARY KEY(id);
623
+ CREATE TABLE ar_internal_metadata (
624
+ key STRING(MAX) NOT NULL,
625
+ value STRING(MAX),
626
+ created_at TIMESTAMP NOT NULL,
627
+ updated_at TIMESTAMP NOT NULL,
628
+ ) PRIMARY KEY(key);
629
+ CREATE TABLE authors (
630
+ id INT64 NOT NULL,
631
+ name STRING(MAX) NOT NULL,
632
+ registered_date DATE,
633
+ organization_id INT64,
634
+ ) PRIMARY KEY(id);
635
+ CREATE TABLE clubs (
636
+ id INT64 NOT NULL,
637
+ name STRING(MAX),
638
+ ) PRIMARY KEY(id);
639
+ CREATE TABLE comments (
640
+ id INT64 NOT NULL,
641
+ comment STRING(MAX),
642
+ post_id INT64,
643
+ ) PRIMARY KEY(id);
644
+ CREATE TABLE customers (
645
+ id INT64 NOT NULL,
646
+ name STRING(MAX),
647
+ ) PRIMARY KEY(id);
648
+ CREATE TABLE departments (
649
+ id INT64 NOT NULL,
650
+ name STRING(MAX),
651
+ resource_type STRING(255),
652
+ resource_id INT64,
653
+ ) PRIMARY KEY(id);
654
+ CREATE INDEX index_departments_on_resource ON departments(resource_type, resource_id);
655
+ CREATE TABLE firms (
656
+ id INT64 NOT NULL,
657
+ name STRING(MAX),
658
+ rating INT64,
659
+ description STRING(MAX),
660
+ account_id INT64,
661
+ ) PRIMARY KEY(id);
662
+ CREATE INDEX index_firms_on_account_id ON firms(account_id);
663
+ CREATE TABLE member_types (
664
+ id INT64 NOT NULL,
665
+ name STRING(MAX),
666
+ ) PRIMARY KEY(id);
667
+ CREATE TABLE members (
668
+ id INT64 NOT NULL,
669
+ name STRING(MAX),
670
+ member_type_id INT64,
671
+ admittable_type STRING(255),
672
+ admittable_id INT64,
673
+ ) PRIMARY KEY(id);
674
+ CREATE TABLE memberships (
675
+ id INT64 NOT NULL,
676
+ joined_on TIMESTAMP,
677
+ club_id INT64,
678
+ member_id INT64,
679
+ favourite BOOL,
680
+ ) PRIMARY KEY(id);
681
+ CREATE TABLE organizations (
682
+ id INT64 NOT NULL,
683
+ name STRING(MAX),
684
+ last_updated TIMESTAMP OPTIONS (
685
+ allow_commit_timestamp = true
686
+ ),
687
+ ) PRIMARY KEY(id);
688
+ CREATE TABLE posts (
689
+ id INT64 NOT NULL,
690
+ title STRING(MAX),
691
+ content STRING(MAX),
692
+ author_id INT64,
693
+ comments_count INT64,
694
+ post_date DATE,
695
+ published_time TIMESTAMP,
696
+ ) PRIMARY KEY(id);
697
+ ALTER TABLE comments ADD CONSTRAINT fk_rails_2fd19c0db7 FOREIGN KEY(post_id) REFERENCES posts(id);
698
+ CREATE INDEX index_posts_on_author_id ON posts(author_id);
699
+ CREATE TABLE schema_migrations (
700
+ version STRING(MAX) NOT NULL,
701
+ ) PRIMARY KEY(version);
702
+ CREATE TABLE singers (
703
+ singerid INT64 NOT NULL,
704
+ first_name STRING(200),
705
+ last_name STRING(MAX),
706
+ tracks_count INT64,
707
+ lock_version INT64,
708
+ full_name STRING(MAX) AS (COALESCE(first_name || ' ', '') || last_name) STORED,
709
+ ) PRIMARY KEY(singerid);
710
+ CREATE TABLE albums (
711
+ singerid INT64 NOT NULL,
712
+ albumid INT64 NOT NULL,
713
+ title STRING(MAX),
714
+ lock_version INT64,
715
+ ) PRIMARY KEY(singerid, albumid),
716
+ INTERLEAVE IN PARENT singers ON DELETE NO ACTION;
717
+ CREATE TABLE tracks (
718
+ singerid INT64 NOT NULL,
719
+ albumid INT64 NOT NULL,
720
+ trackid INT64 NOT NULL,
721
+ title STRING(MAX),
722
+ duration NUMERIC,
723
+ lock_version INT64,
724
+ ) PRIMARY KEY(singerid, albumid, trackid),
725
+ INTERLEAVE IN PARENT albums ON DELETE CASCADE;
726
+ CREATE NULL_FILTERED INDEX index_tracks_on_singerid_and_albumid_and_title ON tracks(singerid, albumid, title), INTERLEAVE IN albums;
727
+ CREATE TABLE table_with_sequence (
728
+ id INT64 NOT NULL DEFAULT (GET_NEXT_SEQUENCE_VALUE(SEQUENCE test_sequence)),
729
+ name STRING(MAX) NOT NULL,
730
+ age INT64 NOT NULL,
731
+ ) PRIMARY KEY(id);
394
732
  CREATE TABLE transactions (
395
733
  id INT64 NOT NULL,
396
734
  amount FLOAT64,
@@ -6,6 +6,12 @@
6
6
 
7
7
  # frozen_string_literal: true
8
8
 
9
+ # ActiveRecord 7.1 introduced native support for composite primary keys.
10
+ # This deprecates the https://github.com/composite-primary-keys/composite_primary_keys gem that was previously used in
11
+ # this library to support composite primary keys, which again are needed for interleaved tables. These tests use the
12
+ # third-party composite primary key gem and are therefore not executed for Rails 7.1 and higher.
13
+ return if ActiveRecord::gem_version >= Gem::Version.create('7.1.0')
14
+
9
15
  require "test_helper"
10
16
  require "models/singer"
11
17
  require "models/album"
@@ -11,6 +11,7 @@ require "models/author"
11
11
  require "models/post"
12
12
  require "models/comment"
13
13
  require "models/organization"
14
+ require "models/table_with_sequence"
14
15
 
15
16
  module ActiveRecord
16
17
  module Transactions
@@ -243,6 +244,29 @@ module ActiveRecord
243
244
 
244
245
  assert_equal 0, Comment.count
245
246
  end
247
+
248
+ def test_create_record_with_sequence
249
+ record = TableWithSequence.create name: "Some name", age: 40
250
+ assert record.id, "ID should be generated and returned by the database"
251
+ assert record.id > 0, "ID should be positive" unless ENV["SPANNER_EMULATOR_HOST"]
252
+ end
253
+
254
+ def test_create_record_with_sequence_in_transaction
255
+ record = TableWithSequence.transaction do
256
+ TableWithSequence.create name: "Some name", age: 40
257
+ end
258
+ assert record.id, "ID should be generated and returned by the database"
259
+ assert record.id > 0, "ID should be positive" unless ENV["SPANNER_EMULATOR_HOST"]
260
+ end
261
+
262
+ def test_create_record_with_sequence_using_mutations
263
+ err = assert_raises ActiveRecord::StatementInvalid do
264
+ TableWithSequence.transaction isolation: :buffered_mutations do
265
+ TableWithSequence.create name: "Foo", age: 50
266
+ end
267
+ end
268
+ assert_equal "Mutations cannot be used to create records that use a sequence to generate the primary key. TableWithSequence uses test_sequence.", err.message
269
+ end
246
270
  end
247
271
  end
248
272
  end
@@ -0,0 +1,10 @@
1
+ # Copyright 2023 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 TableWithSequence < ActiveRecord::Base
8
+ self.table_name = :table_with_sequence
9
+ self.sequence_name = :test_sequence
10
+ end
@@ -6,6 +6,11 @@
6
6
 
7
7
  # frozen_string_literal: true
8
8
 
9
+
10
+ def is_7_1_or_higher?
11
+ ActiveRecord::gem_version >= Gem::Version.create('7.1.0')
12
+ end
13
+
9
14
  def create_tables_in_test_schema
10
15
  ActiveRecord::Schema.define(version: 1) do
11
16
  ActiveRecord::Base.connection.ddl_batch do
@@ -122,29 +127,70 @@ def create_tables_in_test_schema
122
127
  t.virtual :full_name, type: :string, as: "COALESCE(first_name || ' ', '') || last_name", stored: true
123
128
  end
124
129
 
125
- create_table :albums, id: false do |t|
126
- t.interleave_in :singers
127
- t.primary_key :albumid
128
- # `singerid` is part of the primary key in the table definition, but it is not visible to ActiveRecord as part of
129
- # the primary key, to prevent ActiveRecord from considering this to be an entity with a composite primary key.
130
- t.parent_key :singerid
131
- t.string :title
132
- t.integer :lock_version
133
- end
134
-
135
- create_table :tracks, id: false do |t|
136
- # `:cascade` causes all tracks that belong to an album to automatically be deleted when an album is deleted.
137
- t.interleave_in :albums, :cascade
138
- t.primary_key :trackid
139
- t.parent_key :singerid
140
- t.parent_key :albumid
141
- t.string :title
142
- t.numeric :duration
143
- t.integer :lock_version
130
+ if is_7_1_or_higher?
131
+ create_table :albums, primary_key: [:singerid, :albumid] do |t|
132
+ t.interleave_in :singers
133
+ t.integer :singerid, null: false
134
+ t.integer :albumid, null: false
135
+ t.string :title
136
+ t.integer :lock_version
137
+ end
138
+ else
139
+ create_table :albums, id: false do |t|
140
+ t.interleave_in :singers
141
+ t.primary_key :albumid
142
+ # `singerid` is part of the primary key in the table definition, but it is not visible to ActiveRecord as part of
143
+ # the primary key, to prevent ActiveRecord from considering this to be an entity with a composite primary key.
144
+ t.parent_key :singerid
145
+ t.string :title
146
+ t.integer :lock_version
147
+ end
148
+ end
149
+
150
+ if is_7_1_or_higher?
151
+ create_table :tracks, primary_key: [:singerid, :albumid, :trackid] do |t|
152
+ # `:cascade` causes all tracks that belong to an album to automatically be deleted when an album is deleted.
153
+ t.interleave_in :albums, :cascade
154
+ t.integer :singerid, null: false
155
+ t.integer :albumid, null: false
156
+ t.integer :trackid, null: false
157
+ t.string :title
158
+ t.numeric :duration
159
+ t.integer :lock_version
160
+ end
161
+ else
162
+ create_table :tracks, id: false do |t|
163
+ # `:cascade` causes all tracks that belong to an album to automatically be deleted when an album is deleted.
164
+ t.interleave_in :albums, :cascade
165
+ t.primary_key :trackid
166
+ t.parent_key :singerid
167
+ t.parent_key :albumid
168
+ t.string :title
169
+ t.numeric :duration
170
+ t.integer :lock_version
171
+ end
144
172
  end
145
173
 
146
174
  add_index :tracks, [:singerid, :albumid, :title], interleave_in: :albums, null_filtered: true, unique: false
147
175
 
176
+ if ENV["SPANNER_EMULATOR_HOST"]
177
+ create_table :table_with_sequence, id: false do |t|
178
+ # The emulator does not yet support bit-reversed sequences, so we emulate a sequence value
179
+ # by hashing a UUID instead.
180
+ t.integer :id, primary_key: true, null: false, default: -> { "FARM_FINGERPRINT(GENERATE_UUID())" }
181
+ t.string :name, null: false
182
+ t.integer :age, null: false
183
+ end
184
+ else
185
+ connection.execute "create sequence test_sequence OPTIONS (sequence_kind = 'bit_reversed_positive')"
186
+
187
+ create_table :table_with_sequence, id: false do |t|
188
+ t.integer :id, primary_key: true, null: false, default: -> { "GET_NEXT_SEQUENCE_VALUE(SEQUENCE test_sequence)" }
189
+ t.string :name, null: false
190
+ t.integer :age, null: false
191
+ end
192
+ end
193
+
148
194
  end
149
195
  end
150
196
  end
@@ -15,7 +15,7 @@ require "active_support/testing/stream"
15
15
  require "activerecord-spanner-adapter"
16
16
  require "active_record/connection_adapters/spanner_adapter"
17
17
  require "securerandom"
18
- require "composite_primary_keys"
18
+ require "composite_primary_keys" if ActiveRecord::gem_version < Gem::Version.create('7.1.0')
19
19
 
20
20
  # rubocop:disable Style/GlobalVars
21
21
 
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
25
25
  spec.required_ruby_version = ">= 2.7"
26
26
 
27
27
  spec.add_dependency "google-cloud-spanner", "~> 2.18"
28
- spec.add_runtime_dependency "activerecord", [">= 6.0.0", "< 7.1"]
28
+ spec.add_runtime_dependency "activerecord", [">= 6.0.0", "< 7.2"]
29
29
 
30
30
  spec.add_development_dependency "autotest-suffix", "~> 1.1"
31
31
  spec.add_development_dependency "bundler", "~> 2.0"
@@ -0,0 +1,103 @@
1
+ # Sample - Bit-reversed Sequence
2
+
3
+ This example shows how to use a bit-reversed sequence to generate the primary key of a model.
4
+
5
+ See https://cloud.google.com/spanner/docs/primary-key-default-value#bit-reversed-sequence for more information
6
+ about bit-reversed sequences in Cloud Spanner.
7
+
8
+ ## Creating Tables with Bit-Reversed Sequences in ActiveRecord
9
+ You can create bit-reversed sequences using migrations in ActiveRecord by executing a SQL statement using the underlying
10
+ connection.
11
+
12
+ ```ruby
13
+ connection.execute "create sequence singer_sequence OPTIONS (sequence_kind = 'bit_reversed_positive')"
14
+ ```
15
+
16
+ The sequence can be used to generate a default value for the primary key column of a table:
17
+
18
+ ```ruby
19
+ create_table :singers, id: false do |t|
20
+ t.integer :singerid, primary_key: true, null: false, default: -> { "GET_NEXT_SEQUENCE_VALUE(SEQUENCE singer_sequence)" }
21
+ t.string :first_name
22
+ t.string :last_name
23
+ end
24
+ ```
25
+
26
+ ## Example Data Model
27
+ This example uses the following table schema:
28
+
29
+ ```sql
30
+ CREATE SEQUENCE singer_sequence (OPTIONS sequence_kind="bit_reversed_positive")
31
+
32
+ CREATE TABLE singers (
33
+ singerid INT64 NOT NULL DEFAULT GET_NEXT_SEQUENCE_VALUE(SEQUENCE singer_sequence),
34
+ first_name STRING(MAX),
35
+ last_name STRING(MAX)
36
+ ) PRIMARY KEY (singerid);
37
+
38
+ CREATE TABLE albums (
39
+ singerid INT64 NOT NULL,
40
+ albumid INT64 NOT NULL,
41
+ title STRING(MAX)
42
+ ) PRIMARY KEY (singerid, albumid), INTERLEAVE IN PARENT singers;
43
+ ```
44
+
45
+ This schema can be created in ActiveRecord 7.1 and later as follows:
46
+
47
+ ```ruby
48
+ # Execute the entire migration as one DDL batch.
49
+ connection.ddl_batch do
50
+ connection.execute "create sequence singer_sequence OPTIONS (sequence_kind = 'bit_reversed_positive')"
51
+
52
+ # Explicitly define the primary key.
53
+ create_table :singers, id: false, primary_key: :singerid do |t|
54
+ t.integer :singerid, primary_key: true, null: false, default: -> { "GET_NEXT_SEQUENCE_VALUE(SEQUENCE singer_sequence)" }
55
+ t.string :first_name
56
+ t.string :last_name
57
+ end
58
+
59
+ create_table :albums, primary_key: [:singerid, :albumid], id: false do |t|
60
+ # Interleave the `albums` table in the parent table `singers`.
61
+ t.interleave_in :singers
62
+ t.integer :singerid
63
+ t.integer :albumid
64
+ t.string :title
65
+ end
66
+ end
67
+ ```
68
+
69
+ ## Models for Tables with a Sequence
70
+ The models for tables that use a sequence to generate the primary key must include the sequence name. This instructs
71
+ the Cloud Spanner ActiveRecord provider to let the database generate the primary key value, instead of generating one
72
+ in memory.
73
+
74
+ ### Example Models
75
+
76
+ ```ruby
77
+ class Singer < ActiveRecord::Base
78
+ self.sequence_name = :singer_sequence
79
+
80
+ # `albums` is defined as INTERLEAVE IN PARENT `singers`.
81
+ # The primary key of `albums` is (`singerid`, `albumid`).
82
+ has_many :albums, foreign_key: :singerid
83
+ end
84
+
85
+ class Album < ActiveRecord::Base
86
+ # `albums` is defined as INTERLEAVE IN PARENT `singers`.
87
+ # The primary key of `singers` is `singerid`.
88
+ belongs_to :singer, foreign_key: :singerid
89
+ end
90
+ ```
91
+
92
+ ## Running the Sample
93
+
94
+ The sample will automatically start a Spanner Emulator in a docker container and execute the sample
95
+ against that emulator. The emulator will automatically be stopped when the application finishes.
96
+
97
+ Run the application with the following commands:
98
+
99
+ ```bash
100
+ export AR_VERSION="~> 7.1.2"
101
+ bundle install
102
+ bundle exec rake run
103
+ ```
@@ -0,0 +1,13 @@
1
+ # Copyright 2023 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"
8
+ require "sinatra/activerecord/rake"
9
+
10
+ desc "Sample showing how to work with bit-reversed sequences."
11
+ task :run do
12
+ Dir.chdir("..") { sh "bundle exec rake run[bit-reversed-sequence]" }
13
+ end