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.
- 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
|