activerecord-spanner-adapter 1.6.3 → 2.0.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 +3 -7
- data/.github/workflows/acceptance-tests-on-production.yaml +1 -1
- data/.github/workflows/ci.yaml +3 -7
- data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +4 -33
- data/.github/workflows/nightly-acceptance-tests-on-production.yaml +1 -1
- data/.github/workflows/nightly-unit-tests.yaml +5 -33
- data/.github/workflows/rubocop.yaml +1 -1
- data/.github/workflows/samples.yaml +30 -0
- data/.kokoro/populate-secrets.sh +5 -1
- data/.kokoro/release.cfg +22 -12
- data/.kokoro/release.sh +1 -3
- data/.kokoro/trampoline_v2.sh +19 -11
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +2 -2
- data/.trampolinerc +6 -1
- data/CHANGELOG.md +37 -0
- data/Gemfile +7 -5
- data/README.md +11 -9
- data/Rakefile +2 -2
- data/acceptance/cases/migration/command_recorder_test.rb +7 -38
- data/acceptance/cases/migration/references_index_test.rb +2 -11
- data/acceptance/cases/migration/schema_dumper_test.rb +21 -9
- data/acceptance/cases/models/binary_identifiers.rb +97 -0
- data/acceptance/cases/models/insert_all_test.rb +22 -7
- data/acceptance/cases/sessions/session_not_found_test.rb +2 -0
- data/acceptance/cases/tasks/database_tasks_test.rb +1 -0
- data/acceptance/models/binary_project.rb +20 -0
- data/acceptance/models/string_io.rb +28 -0
- data/acceptance/models/user.rb +20 -0
- data/acceptance/test_helper.rb +6 -1
- data/activerecord-spanner-adapter.gemspec +3 -3
- data/benchmarks/application.rb +3 -7
- data/examples/snippets/Rakefile +27 -5
- data/examples/snippets/array-data-type/application.rb +1 -5
- data/examples/snippets/array-data-type/config/database.yml +1 -0
- data/examples/snippets/bit-reversed-sequence/application.rb +0 -4
- data/examples/snippets/bit-reversed-sequence/config/database.yml +1 -0
- data/examples/snippets/bit-reversed-sequence/db/seeds.rb +2 -2
- data/examples/snippets/bulk-insert/application.rb +1 -5
- data/examples/snippets/bulk-insert/config/database.yml +1 -0
- data/examples/snippets/commit-timestamp/application.rb +0 -4
- data/examples/snippets/commit-timestamp/config/database.yml +1 -0
- data/examples/snippets/config/environment.rb +5 -0
- data/examples/snippets/create-records/application.rb +1 -5
- data/examples/snippets/create-records/config/database.yml +1 -0
- data/examples/snippets/date-data-type/application.rb +1 -5
- data/examples/snippets/date-data-type/config/database.yml +1 -0
- data/examples/snippets/date-data-type/db/seeds.rb +1 -1
- data/examples/snippets/generated-column/application.rb +0 -4
- data/examples/snippets/generated-column/config/database.yml +1 -0
- data/examples/snippets/generated-column/db/seeds.rb +1 -1
- data/examples/snippets/hints/application.rb +0 -4
- data/examples/snippets/hints/config/database.yml +1 -0
- data/examples/snippets/hints/db/seeds.rb +1 -1
- data/examples/snippets/interleaved-tables/application.rb +1 -5
- data/examples/snippets/interleaved-tables/config/database.yml +1 -0
- data/examples/snippets/interleaved-tables/db/seeds.rb +1 -1
- data/examples/snippets/interleaved-tables/models/album.rb +6 -2
- data/examples/snippets/interleaved-tables/models/track.rb +5 -1
- data/examples/snippets/interleaved-tables-before-7.1/application.rb +1 -5
- data/examples/snippets/interleaved-tables-before-7.1/config/database.yml +1 -0
- data/examples/snippets/interleaved-tables-before-7.1/db/seeds.rb +1 -1
- data/examples/snippets/migrations/application.rb +0 -4
- data/examples/snippets/migrations/config/database.yml +1 -0
- data/examples/snippets/mutations/application.rb +1 -5
- data/examples/snippets/mutations/config/database.yml +1 -0
- data/examples/snippets/mutations/db/seeds.rb +1 -1
- data/examples/snippets/optimistic-locking/application.rb +0 -4
- data/examples/snippets/optimistic-locking/config/database.yml +1 -0
- data/examples/snippets/optimistic-locking/db/seeds.rb +1 -1
- data/examples/snippets/partitioned-dml/application.rb +0 -4
- data/examples/snippets/partitioned-dml/config/database.yml +1 -0
- data/examples/snippets/partitioned-dml/db/seeds.rb +1 -1
- data/examples/snippets/query-logs/application.rb +15 -13
- data/examples/snippets/query-logs/config/database.yml +1 -0
- data/examples/snippets/query-logs/db/seeds.rb +1 -1
- data/examples/snippets/quickstart/application.rb +0 -4
- data/examples/snippets/quickstart/config/database.yml +1 -0
- data/examples/snippets/quickstart/db/seeds.rb +1 -1
- data/examples/snippets/read-only-transactions/application.rb +0 -4
- data/examples/snippets/read-only-transactions/config/database.yml +1 -0
- data/examples/snippets/read-only-transactions/db/seeds.rb +1 -1
- data/examples/snippets/read-write-transactions/application.rb +2 -6
- data/examples/snippets/read-write-transactions/config/database.yml +1 -0
- data/examples/snippets/read-write-transactions/db/seeds.rb +1 -1
- data/examples/snippets/stale-reads/application.rb +0 -4
- data/examples/snippets/stale-reads/config/database.yml +1 -0
- data/examples/snippets/stale-reads/db/seeds.rb +1 -1
- data/examples/snippets/tags/application.rb +0 -4
- data/examples/snippets/tags/config/database.yml +1 -0
- data/examples/snippets/tags/db/seeds.rb +1 -1
- data/examples/snippets/timestamp-data-type/application.rb +0 -4
- data/examples/snippets/timestamp-data-type/config/database.yml +1 -0
- data/lib/active_record/connection_adapters/spanner/column.rb +3 -3
- data/lib/active_record/connection_adapters/spanner/database_statements.rb +37 -23
- data/lib/active_record/connection_adapters/spanner/quoting.rb +19 -6
- data/lib/active_record/connection_adapters/spanner/schema_creation.rb +7 -9
- data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +12 -2
- data/lib/active_record/connection_adapters/spanner/schema_statements.rb +28 -46
- data/lib/active_record/connection_adapters/spanner/type_metadata.rb +4 -6
- data/lib/active_record/connection_adapters/spanner_adapter.rb +54 -27
- data/lib/active_record/tasks/spanner_database_tasks.rb +4 -4
- data/lib/active_record/type/spanner/array.rb +4 -0
- data/lib/active_record/type/spanner/bytes.rb +10 -0
- data/lib/activerecord-spanner-adapter.rb +5 -1
- data/lib/activerecord_spanner_adapter/base.rb +58 -30
- data/lib/activerecord_spanner_adapter/connection.rb +9 -5
- data/lib/activerecord_spanner_adapter/foreign_key.rb +9 -2
- data/lib/activerecord_spanner_adapter/index/column.rb +6 -1
- data/lib/activerecord_spanner_adapter/index.rb +10 -2
- data/lib/activerecord_spanner_adapter/information_schema.rb +1 -1
- data/lib/activerecord_spanner_adapter/primary_key.rb +2 -2
- data/lib/activerecord_spanner_adapter/table/column.rb +12 -3
- data/lib/activerecord_spanner_adapter/table.rb +8 -2
- data/lib/activerecord_spanner_adapter/transaction.rb +1 -1
- data/lib/activerecord_spanner_adapter/version.rb +1 -1
- data/lib/arel/visitors/spanner.rb +16 -11
- data/lib/spanner_client_ext.rb +4 -3
- metadata +15 -34
- data/examples/snippets/array-data-type/db/schema.rb +0 -31
- data/examples/snippets/bit-reversed-sequence/db/schema.rb +0 -31
- data/examples/snippets/bulk-insert/db/schema.rb +0 -31
- data/examples/snippets/commit-timestamp/db/schema.rb +0 -34
- data/examples/snippets/create-records/db/schema.rb +0 -31
- data/examples/snippets/date-data-type/db/schema.rb +0 -26
- data/examples/snippets/generated-column/db/schema.rb +0 -26
- data/examples/snippets/hints/db/schema.rb +0 -33
- data/examples/snippets/interleaved-tables/db/schema.rb +0 -39
- data/examples/snippets/interleaved-tables-before-7.1/db/schema.rb +0 -37
- data/examples/snippets/migrations/db/schema.rb +0 -38
- data/examples/snippets/mutations/db/schema.rb +0 -32
- data/examples/snippets/optimistic-locking/db/schema.rb +0 -34
- data/examples/snippets/partitioned-dml/db/schema.rb +0 -31
- data/examples/snippets/query-logs/db/schema.rb +0 -31
- data/examples/snippets/quickstart/db/schema.rb +0 -31
- data/examples/snippets/read-only-transactions/db/schema.rb +0 -31
- data/examples/snippets/read-write-transactions/db/schema.rb +0 -32
- data/examples/snippets/stale-reads/db/schema.rb +0 -31
- data/examples/snippets/tags/db/schema.rb +0 -31
- data/examples/snippets/timestamp-data-type/db/schema.rb +0 -26
|
@@ -17,15 +17,27 @@ module ActiveRecord
|
|
|
17
17
|
ActiveRecord::gem_version >= Gem::Version.create('7.1.0')
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
def is_7_2_or_higher?
|
|
21
|
+
ActiveRecord::gem_version >= Gem::Version.create('7.2.0')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pool_or_connection
|
|
25
|
+
if is_7_2_or_higher?
|
|
26
|
+
ActiveRecord::Base.connection_pool
|
|
27
|
+
else
|
|
28
|
+
ActiveRecord::Base.connection
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
20
32
|
def test_dump_schema_contains_start_batch_ddl
|
|
21
|
-
connection =
|
|
33
|
+
connection = pool_or_connection
|
|
22
34
|
schema = StringIO.new
|
|
23
35
|
ActiveRecord::SchemaDumper.dump connection, schema
|
|
24
36
|
assert schema.string.include?("connection.start_batch_ddl")
|
|
25
37
|
end
|
|
26
38
|
|
|
27
39
|
def test_dump_schema_contains_run_batch
|
|
28
|
-
connection =
|
|
40
|
+
connection = pool_or_connection
|
|
29
41
|
schema = StringIO.new
|
|
30
42
|
ActiveRecord::SchemaDumper.dump connection, schema
|
|
31
43
|
assert schema.string.include?(" connection.run_batch\n"\
|
|
@@ -35,7 +47,7 @@ module ActiveRecord
|
|
|
35
47
|
end
|
|
36
48
|
|
|
37
49
|
def test_dump_schema_contains_albums_table
|
|
38
|
-
connection =
|
|
50
|
+
connection = pool_or_connection
|
|
39
51
|
schema = StringIO.new
|
|
40
52
|
ActiveRecord::SchemaDumper.dump connection, schema
|
|
41
53
|
sql = schema.string
|
|
@@ -47,42 +59,42 @@ module ActiveRecord
|
|
|
47
59
|
end
|
|
48
60
|
|
|
49
61
|
def test_dump_schema_contains_interleaved_index
|
|
50
|
-
connection =
|
|
62
|
+
connection = pool_or_connection
|
|
51
63
|
schema = StringIO.new
|
|
52
64
|
ActiveRecord::SchemaDumper.dump connection, schema
|
|
53
65
|
assert schema.string.include?("t.index [\"singerid\", \"albumid\", \"title\"], name: \"index_tracks_on_singerid_and_albumid_and_title\", order: { singerid: :asc, albumid: :asc, title: :asc }, null_filtered: true, interleave_in: \"albums\""), schema.string
|
|
54
66
|
end
|
|
55
67
|
|
|
56
68
|
def test_dump_schema_should_not_contain_id_limit
|
|
57
|
-
connection =
|
|
69
|
+
connection = pool_or_connection
|
|
58
70
|
schema = StringIO.new
|
|
59
71
|
ActiveRecord::SchemaDumper.dump connection, schema
|
|
60
72
|
assert !schema.string.include?("id: { limit: 8 }")
|
|
61
73
|
end
|
|
62
74
|
|
|
63
75
|
def test_dump_schema_contains_commit_timestamp
|
|
64
|
-
connection =
|
|
76
|
+
connection = pool_or_connection
|
|
65
77
|
schema = StringIO.new
|
|
66
78
|
ActiveRecord::SchemaDumper.dump connection, schema
|
|
67
79
|
assert schema.string.include?("t.time \"last_updated\", allow_commit_timestamp: true"), schema.string
|
|
68
80
|
end
|
|
69
81
|
|
|
70
82
|
def test_dump_schema_contains_virtual_column
|
|
71
|
-
connection =
|
|
83
|
+
connection = pool_or_connection
|
|
72
84
|
schema = StringIO.new
|
|
73
85
|
ActiveRecord::SchemaDumper.dump connection, schema
|
|
74
86
|
assert schema.string.include?("t.virtual \"full_name\", type: :string, as: \"COALESCE(first_name || ' ', '') || last_name\", stored: true"), schema.string
|
|
75
87
|
end
|
|
76
88
|
|
|
77
89
|
def test_dump_schema_contains_string_array
|
|
78
|
-
connection =
|
|
90
|
+
connection = pool_or_connection
|
|
79
91
|
schema = StringIO.new
|
|
80
92
|
ActiveRecord::SchemaDumper.dump connection, schema
|
|
81
93
|
assert schema.string.include?("t.string \"col_array_string\", array: true"), schema.string
|
|
82
94
|
end
|
|
83
95
|
|
|
84
96
|
def test_dump_schema_index_storing
|
|
85
|
-
connection =
|
|
97
|
+
connection = pool_or_connection
|
|
86
98
|
schema = StringIO.new
|
|
87
99
|
ActiveRecord::SchemaDumper.dump connection, schema
|
|
88
100
|
assert schema.string.include?("t.index [\"last_name\"], name: \"index_singers_on_last_name\", order: { last_name: :asc }, storing: [\"first_name\", \"tracks_count\"]"), schema.string
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
# frozen_string_literal: true
|
|
8
|
+
|
|
9
|
+
require "test_helper"
|
|
10
|
+
require "test_helpers/with_separate_database"
|
|
11
|
+
require_relative "../../models/user"
|
|
12
|
+
require_relative "../../models/binary_project"
|
|
13
|
+
|
|
14
|
+
module Models
|
|
15
|
+
class DefaultValueTest < SpannerAdapter::TestCase
|
|
16
|
+
include TestHelpers::WithSeparateDatabase
|
|
17
|
+
|
|
18
|
+
def setup
|
|
19
|
+
super
|
|
20
|
+
|
|
21
|
+
connection.create_table :users, id: :binary do |t|
|
|
22
|
+
t.string :email, null: false
|
|
23
|
+
t.string :full_name, null: false
|
|
24
|
+
end
|
|
25
|
+
connection.create_table :binary_projects, id: :binary do |t|
|
|
26
|
+
t.string :name, null: false
|
|
27
|
+
t.string :description, null: false
|
|
28
|
+
t.binary :owner_id, null: false
|
|
29
|
+
t.foreign_key :users, column: :owner_id
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_includes_works
|
|
34
|
+
user = User.create!(
|
|
35
|
+
email: "test@example.com",
|
|
36
|
+
full_name: "Test User"
|
|
37
|
+
)
|
|
38
|
+
3.times do |i|
|
|
39
|
+
Project.create!(
|
|
40
|
+
name: "Project #{i}",
|
|
41
|
+
description: "Description #{i}",
|
|
42
|
+
owner: user
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# First verify the association works without includes
|
|
47
|
+
projects = Project.all
|
|
48
|
+
assert_equal 3, projects.count
|
|
49
|
+
|
|
50
|
+
# Compare the base64 content instead of the StringIO objects
|
|
51
|
+
first_project = projects.first
|
|
52
|
+
assert_equal to_base64(user.id), to_base64(first_project.owner_id)
|
|
53
|
+
|
|
54
|
+
# Now verify includes is working
|
|
55
|
+
query_count = count_queries do
|
|
56
|
+
loaded_projects = Project.includes(:owner).to_a
|
|
57
|
+
loaded_projects.each do |project|
|
|
58
|
+
# Access the owner to ensure it's preloaded
|
|
59
|
+
assert_equal user.full_name, project.owner.full_name
|
|
60
|
+
end
|
|
61
|
+
assert_equal 3, loaded_projects.count
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Spanner should execute 2 queries: one for projects and one for users
|
|
65
|
+
assert_equal 2, query_count
|
|
66
|
+
|
|
67
|
+
user = User.all.includes(:projects).first
|
|
68
|
+
assert user
|
|
69
|
+
project_count = 0
|
|
70
|
+
user.projects.each do |_|
|
|
71
|
+
project_count += 1
|
|
72
|
+
end
|
|
73
|
+
assert_equal 3, project_count
|
|
74
|
+
assert_equal 3, user.projects.count
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def to_base64 buffer
|
|
80
|
+
buffer.rewind
|
|
81
|
+
value = buffer.read
|
|
82
|
+
Base64.strict_encode64 value.force_encoding("ASCII-8BIT")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def count_queries(&block)
|
|
86
|
+
count = 0
|
|
87
|
+
counter_fn = ->(name, started, finished, unique_id, payload) {
|
|
88
|
+
unless %w[CACHE SCHEMA].include?(payload[:name])
|
|
89
|
+
count += 1
|
|
90
|
+
end
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ActiveSupport::Notifications.subscribed(counter_fn, "sql.active_record", &block)
|
|
94
|
+
count
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -30,13 +30,24 @@ module ActiveRecord
|
|
|
30
30
|
{ id: Author.next_sequence_value, name: "Carol" },
|
|
31
31
|
]
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Author.insert_all(values)
|
|
34
|
+
|
|
35
|
+
authors = Author.all.order(:name)
|
|
36
|
+
|
|
37
|
+
assert_equal "Alice", authors[0].name
|
|
38
|
+
assert_equal "Bob", authors[1].name
|
|
39
|
+
assert_equal "Carol", authors[2].name
|
|
34
40
|
end
|
|
35
41
|
|
|
36
42
|
def test_insert
|
|
37
43
|
value = { id: Author.next_sequence_value, name: "Alice" }
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
Author.insert(value)
|
|
46
|
+
|
|
47
|
+
authors = Author.all.order(:name)
|
|
48
|
+
|
|
49
|
+
assert_equal 1, authors.length
|
|
50
|
+
assert_equal "Alice", authors[0].name
|
|
40
51
|
end
|
|
41
52
|
|
|
42
53
|
def test_insert_all!
|
|
@@ -136,12 +147,16 @@ module ActiveRecord
|
|
|
136
147
|
{ id: Author.next_sequence_value, name: "Carol" },
|
|
137
148
|
]
|
|
138
149
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
Author.upsert_all(values)
|
|
142
|
-
end
|
|
150
|
+
ActiveRecord::Base.transaction do
|
|
151
|
+
Author.upsert_all(values)
|
|
143
152
|
end
|
|
144
|
-
|
|
153
|
+
|
|
154
|
+
authors = Author.all.order(:name)
|
|
155
|
+
|
|
156
|
+
assert_equal 3, authors.length
|
|
157
|
+
assert_equal "Alice", authors[0].name
|
|
158
|
+
assert_equal "Bob", authors[1].name
|
|
159
|
+
assert_equal "Carol", authors[2].name
|
|
145
160
|
end
|
|
146
161
|
|
|
147
162
|
def test_upsert_all_with_buffered_mutation_transaction
|
|
@@ -51,6 +51,8 @@ module ActiveRecord
|
|
|
51
51
|
sessions.each do |session|
|
|
52
52
|
client.delete_session Google::Cloud::Spanner::V1::DeleteSessionRequest.new name: session.name
|
|
53
53
|
end
|
|
54
|
+
# Wait a bit to ensure that the sessions are really deleted.
|
|
55
|
+
sleep 5 unless ENV["SPANNER_EMULATOR_HOST"]
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
def test_single_read
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
# frozen_string_literal: true
|
|
8
|
+
|
|
9
|
+
require_relative 'string_io'
|
|
10
|
+
|
|
11
|
+
class BinaryProject < ActiveRecord::Base
|
|
12
|
+
belongs_to :owner, class_name: 'User'
|
|
13
|
+
|
|
14
|
+
before_create :set_uuid
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def set_uuid
|
|
18
|
+
self.id ||= StringIO.new(SecureRandom.random_bytes(16))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
# frozen_string_literal: true
|
|
8
|
+
|
|
9
|
+
# Add equality and hash functions to StringIO to use it for sets.
|
|
10
|
+
class StringIO
|
|
11
|
+
def ==(o)
|
|
12
|
+
o.class == self.class && self.to_base64 == o.to_base64
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def eql?(o)
|
|
16
|
+
self == o
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def hash
|
|
20
|
+
to_base64.hash
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_base64
|
|
24
|
+
self.rewind
|
|
25
|
+
value = self.read
|
|
26
|
+
Base64.strict_encode64 value.force_encoding("ASCII-8BIT")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
# frozen_string_literal: true
|
|
8
|
+
|
|
9
|
+
require_relative 'string_io'
|
|
10
|
+
|
|
11
|
+
class User < ActiveRecord::Base
|
|
12
|
+
has_many :binary_projects, foreign_key: :owner_id
|
|
13
|
+
|
|
14
|
+
before_create :set_uuid
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def set_uuid
|
|
18
|
+
self.id ||= StringIO.new(SecureRandom.random_bytes(16))
|
|
19
|
+
end
|
|
20
|
+
end
|
data/acceptance/test_helper.rb
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
# https://opensource.org/licenses/MIT.
|
|
6
6
|
|
|
7
7
|
gem "minitest"
|
|
8
|
+
require "logger" # https://github.com/rails/rails/issues/54260
|
|
8
9
|
require "minitest/autorun"
|
|
9
10
|
require "minitest/focus"
|
|
10
11
|
require "minitest/rg"
|
|
12
|
+
require "ostruct"
|
|
11
13
|
require "active_support"
|
|
12
14
|
require "google/cloud/spanner"
|
|
13
15
|
require "active_record"
|
|
@@ -18,7 +20,10 @@ require "securerandom"
|
|
|
18
20
|
require "composite_primary_keys" if ActiveRecord::gem_version < Gem::Version.create('7.1.0')
|
|
19
21
|
|
|
20
22
|
# rubocop:disable Style/GlobalVars
|
|
21
|
-
|
|
23
|
+
#
|
|
24
|
+
if ActiveRecord.gem_version >= Gem::Version.create("7.2.0")
|
|
25
|
+
ActiveRecord::ConnectionAdapters.register("spanner", "ActiveRecord::ConnectionAdapters::SpannerAdapter")
|
|
26
|
+
end
|
|
22
27
|
$spanner_test_database = "ar-test-#{SecureRandom.hex 4}"
|
|
23
28
|
|
|
24
29
|
def connector_config
|
|
@@ -22,16 +22,16 @@ Gem::Specification.new do |spec|
|
|
|
22
22
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename f }
|
|
23
23
|
spec.require_paths = ["lib"]
|
|
24
24
|
|
|
25
|
-
spec.required_ruby_version = ">=
|
|
25
|
+
spec.required_ruby_version = ">= 3.1"
|
|
26
26
|
|
|
27
27
|
spec.add_dependency "google-cloud-spanner", "~> 2.18"
|
|
28
28
|
# Pin gRPC to 1.64.3, as 1.65 and 1.66 cause test runs to hang randomly.
|
|
29
29
|
spec.add_dependency "grpc", "1.64.3"
|
|
30
|
-
spec.add_runtime_dependency "activerecord", [">= 6.
|
|
30
|
+
spec.add_runtime_dependency "activerecord", [">= 6.1", "< 9"]
|
|
31
31
|
|
|
32
32
|
spec.add_development_dependency "autotest-suffix", "~> 1.1"
|
|
33
33
|
spec.add_development_dependency "bundler", "~> 2.0"
|
|
34
|
-
spec.add_development_dependency "google-style", "~> 1.
|
|
34
|
+
spec.add_development_dependency "google-style", "~> 1.30.1"
|
|
35
35
|
spec.add_development_dependency "minitest", "~> 5.10"
|
|
36
36
|
spec.add_development_dependency "minitest-autotest", "~> 1.0"
|
|
37
37
|
spec.add_development_dependency "minitest-focus", "~> 1.1"
|
data/benchmarks/application.rb
CHANGED
|
@@ -63,7 +63,7 @@ class Application
|
|
|
63
63
|
|
|
64
64
|
puts ""
|
|
65
65
|
puts "Press any key to end the application"
|
|
66
|
-
|
|
66
|
+
$stdin.getch
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def self.execute_individual_benchmarks singer, client
|
|
@@ -120,9 +120,7 @@ class Application
|
|
|
120
120
|
sql = "SELECT * FROM Singers WHERE id=@id"
|
|
121
121
|
params = { id: singer[:id] }
|
|
122
122
|
param_types = { id: :INT64 }
|
|
123
|
-
client.execute(sql, params: params, types: param_types).rows.
|
|
124
|
-
return row
|
|
125
|
-
end
|
|
123
|
+
client.execute(sql, params: params, types: param_types).rows.first(1)
|
|
126
124
|
else
|
|
127
125
|
Singer.find singer.id
|
|
128
126
|
end
|
|
@@ -134,9 +132,7 @@ class Application
|
|
|
134
132
|
sql = "SELECT * FROM Singers WHERE id=@id"
|
|
135
133
|
params = { id: singer[:id] }
|
|
136
134
|
param_types = { id: :INT64 }
|
|
137
|
-
client.execute(sql, params: params, types: param_types).rows.
|
|
138
|
-
return row
|
|
139
|
-
end
|
|
135
|
+
client.execute(sql, params: params, types: param_types).rows.first(1)
|
|
140
136
|
else
|
|
141
137
|
singer.reload
|
|
142
138
|
end
|
data/examples/snippets/Rakefile
CHANGED
|
@@ -7,19 +7,37 @@
|
|
|
7
7
|
require_relative "config/environment"
|
|
8
8
|
require "docker"
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
samples = Dir.entries(".").select do |entry|
|
|
10
|
+
def fetch_samples
|
|
11
|
+
Dir.entries(".").select do |entry|
|
|
13
12
|
File.directory?(File.join(".", entry)) \
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
&& !%w[. ..].include?(entry) \
|
|
14
|
+
&& File.exist?(File.join(".", entry, "application.rb"))
|
|
16
15
|
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc "Lists all available samples."
|
|
19
|
+
task :list do
|
|
20
|
+
samples = fetch_samples
|
|
17
21
|
puts "Available samples: "
|
|
18
22
|
samples.sort.each { |dir| puts " #{dir}" }
|
|
19
23
|
puts ""
|
|
20
24
|
puts "Run a sample with the command `bundle exec rake run\\[<sample-name>\\]`"
|
|
21
25
|
end
|
|
22
26
|
|
|
27
|
+
desc "Runs all samples."
|
|
28
|
+
task :all do
|
|
29
|
+
samples = fetch_samples
|
|
30
|
+
ar_version = ENV.fetch "AR_VERSION", "~> 7.1.0"
|
|
31
|
+
less_than_7_1 = ar_version.dup.to_s.sub("~>", "").strip < "7.1.0"
|
|
32
|
+
samples.delete "interleaved-tables" if less_than_7_1
|
|
33
|
+
samples.delete "interleaved-tables-before-7.1" unless less_than_7_1
|
|
34
|
+
samples.delete "bit-reversed-sequence" if less_than_7_1
|
|
35
|
+
samples.delete "query-logs" if less_than_7_1
|
|
36
|
+
samples.each do |sample|
|
|
37
|
+
run_sample sample
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
23
41
|
desc "Runs a simple ActiveRecord tutorial on a Spanner emulator."
|
|
24
42
|
task :run, [:sample] do |_t, args|
|
|
25
43
|
sample = args[:sample]
|
|
@@ -28,7 +46,11 @@ task :run, [:sample] do |_t, args|
|
|
|
28
46
|
puts ""
|
|
29
47
|
sample = "quickstart"
|
|
30
48
|
end
|
|
49
|
+
run_sample sample
|
|
50
|
+
end
|
|
31
51
|
|
|
52
|
+
def run_sample sample
|
|
53
|
+
puts "Running #{sample}"
|
|
32
54
|
puts "Downloading Spanner emulator image..."
|
|
33
55
|
Docker::Image.create "fromImage" => "gcr.io/cloud-spanner-emulator/emulator:latest"
|
|
34
56
|
puts "Creating Spanner emulator container..."
|
|
@@ -9,7 +9,7 @@ require_relative "../config/environment"
|
|
|
9
9
|
require_relative "models/entity_with_array_types"
|
|
10
10
|
|
|
11
11
|
class Application
|
|
12
|
-
def self.run
|
|
12
|
+
def self.run
|
|
13
13
|
# Create a record with all array types.
|
|
14
14
|
record = EntityWithArrayTypes.create \
|
|
15
15
|
col_array_string: ["value1", "value2", "value3"],
|
|
@@ -35,10 +35,6 @@ class Application
|
|
|
35
35
|
puts "Bytes array: #{record.col_array_bytes.map(&:read)}"
|
|
36
36
|
puts "Date array: #{record.col_array_date}"
|
|
37
37
|
puts "Timestamp array: #{record.col_array_timestamp}"
|
|
38
|
-
|
|
39
|
-
puts ""
|
|
40
|
-
puts "Press any key to end the application"
|
|
41
|
-
STDIN.getch
|
|
42
38
|
end
|
|
43
39
|
end
|
|
44
40
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# license that can be found in the LICENSE file or at
|
|
5
5
|
# https://opensource.org/licenses/MIT.
|
|
6
6
|
|
|
7
|
-
require_relative "../../config/environment
|
|
7
|
+
require_relative "../../config/environment"
|
|
8
8
|
require_relative "../models/singer"
|
|
9
9
|
require_relative "../models/album"
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ last_names = %w[Wendelson Allison Peterson Johnson Henderson Ericsson Aronson Te
|
|
|
14
14
|
adjectives = %w[daily happy blue generous cooked bad open]
|
|
15
15
|
nouns = %w[windows potatoes bank street tree glass bottle]
|
|
16
16
|
|
|
17
|
-
#
|
|
17
|
+
# NOTE: We do not use mutations to insert these rows, because letting the database generate the primary key means that
|
|
18
18
|
# we rely on a `THEN RETURN id` clause in the insert statement. This is only supported for DML statements, and not for
|
|
19
19
|
# mutations.
|
|
20
20
|
ActiveRecord::Base.transaction do
|
|
@@ -46,7 +46,7 @@ class Application
|
|
|
46
46
|
]
|
|
47
47
|
end
|
|
48
48
|
puts ""
|
|
49
|
-
puts "Created a batch of #{singers.length} singers and #{albums.length} "\
|
|
49
|
+
puts "Created a batch of #{singers.length} singers and #{albums.length} " \
|
|
50
50
|
"albums using a transaction with buffered mutations:"
|
|
51
51
|
singers.each do |s|
|
|
52
52
|
puts " Created singer #{s.first_name} #{s.last_name} with id #{s.id}"
|
|
@@ -54,10 +54,6 @@ class Application
|
|
|
54
54
|
puts " with album #{a.title} with id #{a.id}"
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
|
-
|
|
58
|
-
puts ""
|
|
59
|
-
puts "Press any key to end the application"
|
|
60
|
-
STDIN.getch
|
|
61
57
|
end
|
|
62
58
|
end
|
|
63
59
|
|
|
@@ -43,10 +43,6 @@ class Application
|
|
|
43
43
|
puts "Singer and album updated:"
|
|
44
44
|
puts "#{singer.first_name} #{singer.last_name} (Last updated: #{singer.last_updated.strftime format})"
|
|
45
45
|
puts " #{album.title} (Last updated: #{album.last_updated.strftime format})"
|
|
46
|
-
|
|
47
|
-
puts ""
|
|
48
|
-
puts "Press any key to end the application"
|
|
49
|
-
STDIN.getch
|
|
50
46
|
end
|
|
51
47
|
end
|
|
52
48
|
|
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
# license that can be found in the LICENSE file or at
|
|
5
5
|
# https://opensource.org/licenses/MIT.
|
|
6
6
|
|
|
7
|
+
require "logger" # https://github.com/rails/rails/issues/54260
|
|
7
8
|
require "active_record"
|
|
8
9
|
require "bundler"
|
|
9
10
|
|
|
10
11
|
Dir["../../lib/*.rb"].each { |file| require file }
|
|
11
12
|
|
|
13
|
+
if ActiveRecord.version >= Gem::Version.create("7.2.0")
|
|
14
|
+
ActiveRecord::ConnectionAdapters.register "spanner", "ActiveRecord::ConnectionAdapters::SpannerAdapter"
|
|
15
|
+
end
|
|
16
|
+
|
|
12
17
|
Bundler.require
|
|
13
18
|
|
|
14
19
|
ActiveRecord::Base.establish_connection(
|
|
@@ -13,7 +13,7 @@ class Application
|
|
|
13
13
|
def self.run
|
|
14
14
|
# Creating a single record without an explicit transaction will automatically save it to the database.
|
|
15
15
|
# It is not recommended to call Entity.create repeatedly to insert multiple records, as each call will
|
|
16
|
-
# use a separate Spanner transaction. Instead multiple records should be created by passing an array of
|
|
16
|
+
# use a separate Spanner transaction. Instead, multiple records should be created by passing an array of
|
|
17
17
|
# entities to the Entity.create method.
|
|
18
18
|
singer = Singer.create first_name: "Dave", last_name: "Allison"
|
|
19
19
|
puts ""
|
|
@@ -32,10 +32,6 @@ class Application
|
|
|
32
32
|
singers.each do |s|
|
|
33
33
|
puts " Created singer #{s.first_name} #{s.last_name} with id #{s.id}"
|
|
34
34
|
end
|
|
35
|
-
|
|
36
|
-
puts ""
|
|
37
|
-
puts "Press any key to end the application"
|
|
38
|
-
STDIN.getch
|
|
39
35
|
end
|
|
40
36
|
end
|
|
41
37
|
|
|
@@ -17,7 +17,7 @@ class Application
|
|
|
17
17
|
puts "#{"#{singer.first_name} #{singer.last_name}".ljust 30}#{singer.birth_date}"
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
# Update the
|
|
20
|
+
# Update the birthdate of a random singer using the current system time. Any time and timezone information will be
|
|
21
21
|
# lost after saving the record as a DATE only contains the year, month and day-of-month information.
|
|
22
22
|
singer = Singer.all.sample
|
|
23
23
|
singer.update birth_date: Time.now
|
|
@@ -25,10 +25,6 @@ class Application
|
|
|
25
25
|
puts ""
|
|
26
26
|
puts "Updated birth date to current system time:"
|
|
27
27
|
puts "#{"#{singer.first_name} #{singer.last_name}".ljust 30}#{singer.birth_date}"
|
|
28
|
-
|
|
29
|
-
puts ""
|
|
30
|
-
puts "Press any key to end the application"
|
|
31
|
-
STDIN.getch
|
|
32
28
|
end
|
|
33
29
|
end
|
|
34
30
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# license that can be found in the LICENSE file or at
|
|
5
5
|
# https://opensource.org/licenses/MIT.
|
|
6
6
|
#
|
|
7
|
-
require_relative "../../config/environment
|
|
7
|
+
require_relative "../../config/environment"
|
|
8
8
|
require_relative "../models/singer"
|
|
9
9
|
|
|
10
10
|
first_names = %w[Nelson Todd William Alex Dominique Adenoid Steve Nathan Beverly Annie Amy Norma Diana Regan Phyllis]
|