activerecord-spanner-adapter 1.5.0 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/acceptance-tests-on-emulator.yaml +1 -1
- data/.github/workflows/acceptance-tests-on-production.yaml +5 -3
- data/.github/workflows/ci.yaml +1 -1
- data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +1 -1
- data/.github/workflows/nightly-acceptance-tests-on-production.yaml +5 -3
- data/.github/workflows/nightly-unit-tests.yaml +1 -1
- data/.github/workflows/release-please-label.yml +1 -1
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +14 -0
- data/Gemfile +5 -2
- data/README.md +10 -10
- data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +6 -0
- data/acceptance/cases/migration/change_schema_test.rb +19 -3
- data/acceptance/cases/migration/schema_dumper_test.rb +10 -1
- data/acceptance/cases/models/insert_all_test.rb +22 -0
- data/acceptance/cases/models/interleave_test.rb +6 -0
- data/acceptance/cases/tasks/database_tasks_test.rb +340 -2
- data/acceptance/cases/transactions/optimistic_locking_test.rb +6 -0
- data/acceptance/cases/transactions/read_write_transactions_test.rb +24 -0
- data/acceptance/models/table_with_sequence.rb +10 -0
- data/acceptance/schema/schema.rb +65 -19
- data/acceptance/test_helper.rb +1 -1
- data/activerecord-spanner-adapter.gemspec +1 -1
- data/examples/snippets/bit-reversed-sequence/README.md +103 -0
- data/examples/snippets/bit-reversed-sequence/Rakefile +13 -0
- data/examples/snippets/bit-reversed-sequence/application.rb +68 -0
- data/examples/snippets/bit-reversed-sequence/config/database.yml +8 -0
- data/examples/snippets/bit-reversed-sequence/db/migrate/01_create_tables.rb +33 -0
- data/examples/snippets/bit-reversed-sequence/db/schema.rb +31 -0
- data/examples/snippets/bit-reversed-sequence/db/seeds.rb +31 -0
- data/examples/snippets/bit-reversed-sequence/models/album.rb +11 -0
- data/examples/snippets/bit-reversed-sequence/models/singer.rb +15 -0
- data/examples/snippets/interleaved-tables/README.md +44 -53
- data/examples/snippets/interleaved-tables/Rakefile +2 -2
- data/examples/snippets/interleaved-tables/application.rb +2 -2
- data/examples/snippets/interleaved-tables/db/migrate/01_create_tables.rb +12 -18
- data/examples/snippets/interleaved-tables/db/schema.rb +9 -7
- data/examples/snippets/interleaved-tables/db/seeds.rb +1 -1
- data/examples/snippets/interleaved-tables/models/album.rb +3 -7
- data/examples/snippets/interleaved-tables/models/singer.rb +1 -1
- data/examples/snippets/interleaved-tables/models/track.rb +6 -7
- data/examples/snippets/interleaved-tables-before-7.1/README.md +167 -0
- data/examples/snippets/interleaved-tables-before-7.1/Rakefile +13 -0
- data/examples/snippets/interleaved-tables-before-7.1/application.rb +126 -0
- data/examples/snippets/interleaved-tables-before-7.1/config/database.yml +8 -0
- data/examples/snippets/interleaved-tables-before-7.1/db/migrate/01_create_tables.rb +44 -0
- data/examples/snippets/interleaved-tables-before-7.1/db/schema.rb +37 -0
- data/examples/snippets/interleaved-tables-before-7.1/db/seeds.rb +40 -0
- data/examples/snippets/interleaved-tables-before-7.1/models/album.rb +20 -0
- data/examples/snippets/interleaved-tables-before-7.1/models/singer.rb +18 -0
- data/examples/snippets/interleaved-tables-before-7.1/models/track.rb +28 -0
- data/examples/snippets/query-logs/README.md +43 -0
- data/examples/snippets/query-logs/Rakefile +13 -0
- data/examples/snippets/query-logs/application.rb +63 -0
- data/examples/snippets/query-logs/config/database.yml +8 -0
- data/examples/snippets/query-logs/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/query-logs/db/schema.rb +31 -0
- data/examples/snippets/query-logs/db/seeds.rb +24 -0
- data/examples/snippets/query-logs/models/album.rb +9 -0
- data/examples/snippets/query-logs/models/singer.rb +9 -0
- data/lib/active_record/connection_adapters/spanner/column.rb +13 -0
- data/lib/active_record/connection_adapters/spanner/database_statements.rb +144 -35
- data/lib/active_record/connection_adapters/spanner/schema_cache.rb +3 -21
- data/lib/active_record/connection_adapters/spanner/schema_creation.rb +11 -0
- data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +4 -0
- data/lib/active_record/connection_adapters/spanner/schema_statements.rb +3 -2
- data/lib/active_record/connection_adapters/spanner_adapter.rb +28 -9
- data/lib/activerecord_spanner_adapter/base.rb +58 -21
- data/lib/activerecord_spanner_adapter/information_schema.rb +33 -24
- data/lib/activerecord_spanner_adapter/primary_key.rb +1 -1
- data/lib/activerecord_spanner_adapter/table/column.rb +4 -9
- data/lib/activerecord_spanner_adapter/version.rb +1 -1
- data/lib/arel/visitors/spanner.rb +3 -1
- 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
|
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
|
data/acceptance/schema/schema.rb
CHANGED
@@ -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
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
data/acceptance/test_helper.rb
CHANGED
@@ -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.
|
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
|