activerecord-spanner-adapter 1.5.1 → 1.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) 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/.github/workflows/rubocop.yaml +1 -1
  10. data/.release-please-manifest.json +1 -1
  11. data/.rubocop.yml +1 -1
  12. data/CHANGELOG.md +14 -0
  13. data/Gemfile +7 -2
  14. data/README.md +10 -10
  15. data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +6 -0
  16. data/acceptance/cases/migration/change_schema_test.rb +19 -3
  17. data/acceptance/cases/migration/schema_dumper_test.rb +10 -1
  18. data/acceptance/cases/models/interleave_test.rb +6 -0
  19. data/acceptance/cases/tasks/database_tasks_test.rb +340 -2
  20. data/acceptance/cases/transactions/optimistic_locking_test.rb +6 -0
  21. data/acceptance/cases/transactions/read_write_transactions_test.rb +24 -0
  22. data/acceptance/models/table_with_sequence.rb +10 -0
  23. data/acceptance/schema/schema.rb +65 -19
  24. data/acceptance/test_helper.rb +1 -1
  25. data/activerecord-spanner-adapter.gemspec +1 -1
  26. data/benchmarks/application.rb +1 -1
  27. data/examples/snippets/bit-reversed-sequence/README.md +103 -0
  28. data/examples/snippets/bit-reversed-sequence/Rakefile +13 -0
  29. data/examples/snippets/bit-reversed-sequence/application.rb +68 -0
  30. data/examples/snippets/bit-reversed-sequence/config/database.yml +8 -0
  31. data/examples/snippets/bit-reversed-sequence/db/migrate/01_create_tables.rb +33 -0
  32. data/examples/snippets/bit-reversed-sequence/db/schema.rb +31 -0
  33. data/examples/snippets/bit-reversed-sequence/db/seeds.rb +31 -0
  34. data/examples/snippets/bit-reversed-sequence/models/album.rb +11 -0
  35. data/examples/snippets/bit-reversed-sequence/models/singer.rb +15 -0
  36. data/examples/snippets/interleaved-tables/README.md +44 -53
  37. data/examples/snippets/interleaved-tables/Rakefile +2 -2
  38. data/examples/snippets/interleaved-tables/application.rb +2 -2
  39. data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +12 -18
  40. data/examples/snippets/interleaved-tables/db/schema.rb +9 -7
  41. data/examples/snippets/interleaved-tables/db/seeds.rb +1 -1
  42. data/examples/snippets/interleaved-tables/models/album.rb +3 -7
  43. data/examples/snippets/interleaved-tables/models/singer.rb +1 -1
  44. data/examples/snippets/interleaved-tables/models/track.rb +6 -7
  45. data/examples/snippets/interleaved-tables-before-7.1/README.md +167 -0
  46. data/examples/snippets/interleaved-tables-before-7.1/Rakefile +13 -0
  47. data/examples/snippets/interleaved-tables-before-7.1/application.rb +126 -0
  48. data/examples/snippets/interleaved-tables-before-7.1/config/database.yml +8 -0
  49. data/examples/snippets/interleaved-tables-before-7.1/db/migrate/01_create_tables.rb +44 -0
  50. data/examples/snippets/interleaved-tables-before-7.1/db/schema.rb +37 -0
  51. data/examples/snippets/interleaved-tables-before-7.1/db/seeds.rb +40 -0
  52. data/examples/snippets/interleaved-tables-before-7.1/models/album.rb +20 -0
  53. data/examples/snippets/interleaved-tables-before-7.1/models/singer.rb +18 -0
  54. data/examples/snippets/interleaved-tables-before-7.1/models/track.rb +28 -0
  55. data/examples/snippets/query-logs/README.md +43 -0
  56. data/examples/snippets/query-logs/Rakefile +13 -0
  57. data/examples/snippets/query-logs/application.rb +63 -0
  58. data/examples/snippets/query-logs/config/database.yml +8 -0
  59. data/examples/snippets/query-logs/db/migrate/01_create_tables.rb +21 -0
  60. data/examples/snippets/query-logs/db/schema.rb +31 -0
  61. data/examples/snippets/query-logs/db/seeds.rb +24 -0
  62. data/examples/snippets/query-logs/models/album.rb +9 -0
  63. data/examples/snippets/query-logs/models/singer.rb +9 -0
  64. data/examples/snippets/read-only-transactions/application.rb +1 -1
  65. data/lib/active_record/connection_adapters/spanner/column.rb +13 -0
  66. data/lib/active_record/connection_adapters/spanner/database_statements.rb +144 -35
  67. data/lib/active_record/connection_adapters/spanner/schema_cache.rb +3 -21
  68. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +11 -2
  69. data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +4 -0
  70. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +12 -2
  71. data/lib/active_record/connection_adapters/spanner_adapter.rb +28 -9
  72. data/lib/activerecord_spanner_adapter/base.rb +64 -20
  73. data/lib/activerecord_spanner_adapter/foreign_key.rb +6 -2
  74. data/lib/activerecord_spanner_adapter/index/column.rb +3 -1
  75. data/lib/activerecord_spanner_adapter/index.rb +4 -2
  76. data/lib/activerecord_spanner_adapter/information_schema.rb +124 -72
  77. data/lib/activerecord_spanner_adapter/primary_key.rb +1 -1
  78. data/lib/activerecord_spanner_adapter/table/column.rb +7 -10
  79. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  80. data/lib/arel/visitors/spanner.rb +3 -1
  81. data/lib/spanner_client_ext.rb +6 -1
  82. metadata +34 -5
@@ -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"
@@ -12,7 +12,7 @@ require_relative "models/singer"
12
12
  require_relative "models/album"
13
13
 
14
14
  class Application
15
- def self.run # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
15
+ def self.run # rubocop:disable Metrics/AbcSize
16
16
  ActiveRecord::Base.logger.level = Logger::WARN
17
17
  config = ActiveRecord::Base.connection_config
18
18
  spanner = Google::Cloud::Spanner.new project: config[:project], credentials: config[:credentials]
@@ -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