activerecord-spanner-adapter 0.3.0 → 1.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 +5 -5
- data/.github/CODEOWNERS +7 -0
- data/.github/sync-repo-settings.yaml +16 -0
- data/.github/workflows/acceptance-tests-on-emulator.yaml +45 -0
- data/.github/workflows/acceptance-tests-on-production.yaml +49 -0
- data/.github/workflows/ci.yaml +33 -0
- data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +52 -0
- data/.github/workflows/nightly-acceptance-tests-on-production.yaml +35 -0
- data/.github/workflows/nightly-unit-tests.yaml +40 -0
- data/.github/workflows/release-please-label.yml +25 -0
- data/.github/workflows/release-please.yml +39 -0
- data/.github/workflows/rubocop.yaml +31 -0
- data/.gitignore +67 -5
- data/.kokoro/populate-secrets.sh +77 -0
- data/.kokoro/release.cfg +33 -0
- data/.kokoro/release.sh +15 -0
- data/.kokoro/trampoline_v2.sh +489 -0
- data/.rubocop.yml +46 -0
- data/.toys/release.rb +18 -0
- data/.trampolinerc +48 -0
- data/.yardopts +11 -0
- data/CHANGELOG.md +55 -0
- data/CODE_OF_CONDUCT.md +40 -0
- data/CONTRIBUTING.md +79 -0
- data/Gemfile +9 -4
- data/LICENSE +6 -6
- data/README.md +66 -30
- data/Rakefile +79 -3
- data/SECURITY.md +7 -0
- data/acceptance/cases/associations/has_many_associations_test.rb +119 -0
- data/acceptance/cases/associations/has_many_through_associations_test.rb +63 -0
- data/acceptance/cases/associations/has_one_associations_test.rb +79 -0
- data/acceptance/cases/associations/has_one_through_associations_test.rb +98 -0
- data/acceptance/cases/interleaved_associations/has_many_associations_using_interleaved_test.rb +211 -0
- data/acceptance/cases/migration/change_schema_test.rb +433 -0
- data/acceptance/cases/migration/change_table_test.rb +115 -0
- data/acceptance/cases/migration/column_attributes_test.rb +122 -0
- data/acceptance/cases/migration/column_positioning_test.rb +48 -0
- data/acceptance/cases/migration/columns_test.rb +201 -0
- data/acceptance/cases/migration/command_recorder_test.rb +406 -0
- data/acceptance/cases/migration/create_join_table_test.rb +216 -0
- data/acceptance/cases/migration/ddl_batching_test.rb +80 -0
- data/acceptance/cases/migration/foreign_key_test.rb +297 -0
- data/acceptance/cases/migration/index_test.rb +211 -0
- data/acceptance/cases/migration/references_foreign_key_test.rb +259 -0
- data/acceptance/cases/migration/references_index_test.rb +135 -0
- data/acceptance/cases/migration/references_statements_test.rb +166 -0
- data/acceptance/cases/migration/rename_column_test.rb +96 -0
- data/acceptance/cases/models/calculation_query_test.rb +128 -0
- data/acceptance/cases/models/generated_column_test.rb +126 -0
- data/acceptance/cases/models/mutation_test.rb +122 -0
- data/acceptance/cases/models/query_test.rb +171 -0
- data/acceptance/cases/sessions/session_not_found_test.rb +121 -0
- data/acceptance/cases/transactions/optimistic_locking_test.rb +141 -0
- data/acceptance/cases/transactions/read_only_transactions_test.rb +130 -0
- data/acceptance/cases/transactions/read_write_transactions_test.rb +248 -0
- data/acceptance/cases/type/all_types_test.rb +172 -0
- data/acceptance/cases/type/binary_test.rb +59 -0
- data/acceptance/cases/type/boolean_test.rb +31 -0
- data/acceptance/cases/type/date_test.rb +32 -0
- data/acceptance/cases/type/date_time_test.rb +30 -0
- data/acceptance/cases/type/float_test.rb +27 -0
- data/acceptance/cases/type/integer_test.rb +44 -0
- data/acceptance/cases/type/json_test.rb +34 -0
- data/acceptance/cases/type/numeric_test.rb +27 -0
- data/acceptance/cases/type/string_test.rb +79 -0
- data/acceptance/cases/type/text_test.rb +30 -0
- data/acceptance/cases/type/time_test.rb +87 -0
- data/acceptance/models/account.rb +13 -0
- data/acceptance/models/address.rb +9 -0
- data/acceptance/models/album.rb +12 -0
- data/acceptance/models/all_types.rb +8 -0
- data/acceptance/models/author.rb +11 -0
- data/acceptance/models/club.rb +12 -0
- data/acceptance/models/comment.rb +9 -0
- data/acceptance/models/customer.rb +9 -0
- data/acceptance/models/department.rb +9 -0
- data/acceptance/models/firm.rb +10 -0
- data/acceptance/models/member.rb +13 -0
- data/acceptance/models/member_type.rb +9 -0
- data/acceptance/models/membership.rb +10 -0
- data/acceptance/models/organization.rb +9 -0
- data/acceptance/models/post.rb +10 -0
- data/acceptance/models/singer.rb +10 -0
- data/acceptance/models/track.rb +20 -0
- data/acceptance/models/transaction.rb +9 -0
- data/acceptance/schema/schema.rb +147 -0
- data/acceptance/test_helper.rb +261 -0
- data/activerecord-spanner-adapter.gemspec +32 -17
- data/assets/solidus-db.png +0 -0
- data/benchmarks/README.md +17 -0
- data/benchmarks/Rakefile +14 -0
- data/benchmarks/application.rb +308 -0
- data/benchmarks/config/database.yml +8 -0
- data/benchmarks/config/environment.rb +12 -0
- data/benchmarks/db/migrate/01_create_tables.rb +25 -0
- data/benchmarks/db/schema.rb +29 -0
- data/benchmarks/models/album.rb +9 -0
- data/benchmarks/models/singer.rb +9 -0
- data/bin/console +6 -7
- data/examples/rails/README.md +262 -0
- data/examples/snippets/README.md +29 -0
- data/examples/snippets/Rakefile +57 -0
- data/examples/snippets/array-data-type/README.md +45 -0
- data/examples/snippets/array-data-type/Rakefile +13 -0
- data/examples/snippets/array-data-type/application.rb +45 -0
- data/examples/snippets/array-data-type/config/database.yml +8 -0
- data/examples/snippets/array-data-type/db/migrate/01_create_tables.rb +24 -0
- data/examples/snippets/array-data-type/db/schema.rb +26 -0
- data/examples/snippets/array-data-type/db/seeds.rb +5 -0
- data/examples/snippets/array-data-type/models/entity_with_array_types.rb +18 -0
- data/examples/snippets/bin/create_emulator_instance.rb +18 -0
- data/examples/snippets/bulk-insert/README.md +21 -0
- data/examples/snippets/bulk-insert/Rakefile +13 -0
- data/examples/snippets/bulk-insert/application.rb +64 -0
- data/examples/snippets/bulk-insert/config/database.yml +8 -0
- data/examples/snippets/bulk-insert/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/bulk-insert/db/schema.rb +26 -0
- data/examples/snippets/bulk-insert/db/seeds.rb +5 -0
- data/examples/snippets/bulk-insert/models/album.rb +9 -0
- data/examples/snippets/bulk-insert/models/singer.rb +9 -0
- data/examples/snippets/commit-timestamp/README.md +18 -0
- data/examples/snippets/commit-timestamp/Rakefile +13 -0
- data/examples/snippets/commit-timestamp/application.rb +53 -0
- data/examples/snippets/commit-timestamp/config/database.yml +8 -0
- data/examples/snippets/commit-timestamp/db/migrate/01_create_tables.rb +26 -0
- data/examples/snippets/commit-timestamp/db/schema.rb +29 -0
- data/examples/snippets/commit-timestamp/db/seeds.rb +5 -0
- data/examples/snippets/commit-timestamp/models/album.rb +9 -0
- data/examples/snippets/commit-timestamp/models/singer.rb +9 -0
- data/examples/snippets/config/environment.rb +21 -0
- data/examples/snippets/create-records/README.md +12 -0
- data/examples/snippets/create-records/Rakefile +13 -0
- data/examples/snippets/create-records/application.rb +42 -0
- data/examples/snippets/create-records/config/database.yml +8 -0
- data/examples/snippets/create-records/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/create-records/db/schema.rb +26 -0
- data/examples/snippets/create-records/db/seeds.rb +5 -0
- data/examples/snippets/create-records/models/album.rb +9 -0
- data/examples/snippets/create-records/models/singer.rb +9 -0
- data/examples/snippets/date-data-type/README.md +19 -0
- data/examples/snippets/date-data-type/Rakefile +13 -0
- data/examples/snippets/date-data-type/application.rb +35 -0
- data/examples/snippets/date-data-type/config/database.yml +8 -0
- data/examples/snippets/date-data-type/db/migrate/01_create_tables.rb +20 -0
- data/examples/snippets/date-data-type/db/schema.rb +21 -0
- data/examples/snippets/date-data-type/db/seeds.rb +16 -0
- data/examples/snippets/date-data-type/models/singer.rb +8 -0
- data/examples/snippets/generated-column/README.md +41 -0
- data/examples/snippets/generated-column/Rakefile +13 -0
- data/examples/snippets/generated-column/application.rb +37 -0
- data/examples/snippets/generated-column/config/database.yml +8 -0
- data/examples/snippets/generated-column/db/migrate/01_create_tables.rb +23 -0
- data/examples/snippets/generated-column/db/schema.rb +21 -0
- data/examples/snippets/generated-column/db/seeds.rb +18 -0
- data/examples/snippets/generated-column/models/singer.rb +8 -0
- data/examples/snippets/hints/README.md +19 -0
- data/examples/snippets/hints/Rakefile +13 -0
- data/examples/snippets/hints/application.rb +47 -0
- data/examples/snippets/hints/config/database.yml +8 -0
- data/examples/snippets/hints/db/migrate/01_create_tables.rb +23 -0
- data/examples/snippets/hints/db/schema.rb +28 -0
- data/examples/snippets/hints/db/seeds.rb +29 -0
- data/examples/snippets/hints/models/album.rb +9 -0
- data/examples/snippets/hints/models/singer.rb +9 -0
- data/examples/snippets/migrations/README.md +43 -0
- data/examples/snippets/migrations/Rakefile +13 -0
- data/examples/snippets/migrations/application.rb +26 -0
- data/examples/snippets/migrations/config/database.yml +8 -0
- data/examples/snippets/migrations/db/migrate/01_create_tables.rb +28 -0
- data/examples/snippets/migrations/db/schema.rb +33 -0
- data/examples/snippets/migrations/db/seeds.rb +5 -0
- data/examples/snippets/migrations/models/album.rb +10 -0
- data/examples/snippets/migrations/models/singer.rb +10 -0
- data/examples/snippets/migrations/models/track.rb +9 -0
- data/examples/snippets/mutations/README.md +34 -0
- data/examples/snippets/mutations/Rakefile +13 -0
- data/examples/snippets/mutations/application.rb +47 -0
- data/examples/snippets/mutations/config/database.yml +8 -0
- data/examples/snippets/mutations/db/migrate/01_create_tables.rb +22 -0
- data/examples/snippets/mutations/db/schema.rb +27 -0
- data/examples/snippets/mutations/db/seeds.rb +25 -0
- data/examples/snippets/mutations/models/album.rb +9 -0
- data/examples/snippets/mutations/models/singer.rb +9 -0
- data/examples/snippets/optimistic-locking/README.md +12 -0
- data/examples/snippets/optimistic-locking/Rakefile +13 -0
- data/examples/snippets/optimistic-locking/application.rb +48 -0
- data/examples/snippets/optimistic-locking/config/database.yml +8 -0
- data/examples/snippets/optimistic-locking/db/migrate/01_create_tables.rb +26 -0
- data/examples/snippets/optimistic-locking/db/schema.rb +29 -0
- data/examples/snippets/optimistic-locking/db/seeds.rb +25 -0
- data/examples/snippets/optimistic-locking/models/album.rb +9 -0
- data/examples/snippets/optimistic-locking/models/singer.rb +9 -0
- data/examples/snippets/partitioned-dml/README.md +16 -0
- data/examples/snippets/partitioned-dml/Rakefile +13 -0
- data/examples/snippets/partitioned-dml/application.rb +48 -0
- data/examples/snippets/partitioned-dml/config/database.yml +8 -0
- data/examples/snippets/partitioned-dml/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/partitioned-dml/db/schema.rb +26 -0
- data/examples/snippets/partitioned-dml/db/seeds.rb +29 -0
- data/examples/snippets/partitioned-dml/models/album.rb +9 -0
- data/examples/snippets/partitioned-dml/models/singer.rb +9 -0
- data/examples/snippets/quickstart/README.md +26 -0
- data/examples/snippets/quickstart/Rakefile +13 -0
- data/examples/snippets/quickstart/application.rb +51 -0
- data/examples/snippets/quickstart/config/database.yml +8 -0
- data/examples/snippets/quickstart/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/quickstart/db/schema.rb +26 -0
- data/examples/snippets/quickstart/db/seeds.rb +24 -0
- data/examples/snippets/quickstart/models/album.rb +9 -0
- data/examples/snippets/quickstart/models/singer.rb +9 -0
- data/examples/snippets/read-only-transactions/README.md +13 -0
- data/examples/snippets/read-only-transactions/Rakefile +13 -0
- data/examples/snippets/read-only-transactions/application.rb +77 -0
- data/examples/snippets/read-only-transactions/config/database.yml +8 -0
- data/examples/snippets/read-only-transactions/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/read-only-transactions/db/schema.rb +26 -0
- data/examples/snippets/read-only-transactions/db/seeds.rb +24 -0
- data/examples/snippets/read-only-transactions/models/album.rb +9 -0
- data/examples/snippets/read-only-transactions/models/singer.rb +9 -0
- data/examples/snippets/read-write-transactions/README.md +12 -0
- data/examples/snippets/read-write-transactions/Rakefile +13 -0
- data/examples/snippets/read-write-transactions/application.rb +39 -0
- data/examples/snippets/read-write-transactions/config/database.yml +8 -0
- data/examples/snippets/read-write-transactions/db/migrate/01_create_tables.rb +22 -0
- data/examples/snippets/read-write-transactions/db/schema.rb +27 -0
- data/examples/snippets/read-write-transactions/db/seeds.rb +25 -0
- data/examples/snippets/read-write-transactions/models/album.rb +9 -0
- data/examples/snippets/read-write-transactions/models/singer.rb +9 -0
- data/examples/snippets/stale-reads/README.md +27 -0
- data/examples/snippets/stale-reads/Rakefile +13 -0
- data/examples/snippets/stale-reads/application.rb +63 -0
- data/examples/snippets/stale-reads/config/database.yml +8 -0
- data/examples/snippets/stale-reads/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/stale-reads/db/schema.rb +26 -0
- data/examples/snippets/stale-reads/db/seeds.rb +24 -0
- data/examples/snippets/stale-reads/models/album.rb +9 -0
- data/examples/snippets/stale-reads/models/singer.rb +9 -0
- data/examples/snippets/timestamp-data-type/README.md +17 -0
- data/examples/snippets/timestamp-data-type/Rakefile +13 -0
- data/examples/snippets/timestamp-data-type/application.rb +42 -0
- data/examples/snippets/timestamp-data-type/config/database.yml +8 -0
- data/examples/snippets/timestamp-data-type/db/migrate/01_create_tables.rb +21 -0
- data/examples/snippets/timestamp-data-type/db/schema.rb +21 -0
- data/examples/snippets/timestamp-data-type/db/seeds.rb +6 -0
- data/examples/snippets/timestamp-data-type/models/meeting.rb +19 -0
- data/examples/solidus/README.md +172 -0
- data/lib/active_record/connection_adapters/spanner/database_statements.rb +244 -266
- data/lib/active_record/connection_adapters/spanner/quoting.rb +42 -50
- data/lib/active_record/connection_adapters/spanner/schema_cache.rb +43 -0
- data/lib/active_record/connection_adapters/spanner/schema_creation.rb +125 -9
- data/lib/active_record/connection_adapters/spanner/schema_definitions.rb +122 -0
- data/lib/active_record/connection_adapters/spanner/schema_dumper.rb +19 -0
- data/lib/active_record/connection_adapters/spanner/schema_statements.rb +553 -139
- data/lib/active_record/connection_adapters/spanner/type_metadata.rb +37 -0
- data/lib/active_record/connection_adapters/spanner_adapter.rb +185 -78
- data/lib/active_record/tasks/spanner_database_tasks.rb +74 -0
- data/lib/active_record/type/spanner/array.rb +32 -0
- data/lib/active_record/type/spanner/bytes.rb +26 -0
- data/lib/active_record/type/spanner/spanner_active_record_converter.rb +33 -0
- data/lib/active_record/type/spanner/time.rb +37 -0
- data/lib/activerecord-spanner-adapter.rb +23 -0
- data/lib/activerecord_spanner_adapter/base.rb +238 -0
- data/lib/activerecord_spanner_adapter/connection.rb +350 -0
- data/lib/activerecord_spanner_adapter/errors.rb +13 -0
- data/lib/activerecord_spanner_adapter/foreign_key.rb +29 -0
- data/lib/activerecord_spanner_adapter/index/column.rb +38 -0
- data/lib/activerecord_spanner_adapter/index.rb +80 -0
- data/lib/activerecord_spanner_adapter/information_schema.rb +262 -0
- data/lib/activerecord_spanner_adapter/primary_key.rb +31 -0
- data/lib/activerecord_spanner_adapter/table/column.rb +59 -0
- data/lib/activerecord_spanner_adapter/table.rb +61 -0
- data/lib/activerecord_spanner_adapter/transaction.rb +154 -0
- data/lib/activerecord_spanner_adapter/version.rb +9 -0
- data/lib/arel/visitors/spanner.rb +111 -0
- data/lib/spanner_client_ext.rb +107 -0
- data/renovate.json +5 -0
- metadata +405 -34
- data/.travis.yml +0 -5
- data/lib/active_record/connection_adapters/spanner/client.rb +0 -190
- data/lib/active_record/connection_adapters/spanner.rb +0 -10
- data/lib/activerecord-spanner-adapter/version.rb +0 -3
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Copyright 2020 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 "models/author"
|
|
11
|
+
require "models/post"
|
|
12
|
+
require "models/comment"
|
|
13
|
+
require "models/address"
|
|
14
|
+
|
|
15
|
+
module ActiveRecord
|
|
16
|
+
module Model
|
|
17
|
+
class QueryTest < SpannerAdapter::TestCase
|
|
18
|
+
include SpannerAdapter::Associations::TestHelper
|
|
19
|
+
|
|
20
|
+
attr_accessor :author, :post, :comment
|
|
21
|
+
|
|
22
|
+
def setup
|
|
23
|
+
super
|
|
24
|
+
|
|
25
|
+
@author = Author.create name: "David"
|
|
26
|
+
@post = Post.create title: "Title - 1", author: author
|
|
27
|
+
@comment = Comment.create comment: "Comment - 1", post: post
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def teardown
|
|
31
|
+
super
|
|
32
|
+
Comment.destroy_all
|
|
33
|
+
Post.destroy_all
|
|
34
|
+
Author.destroy_all
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_in_clause_is_correctly_sliced
|
|
38
|
+
author2 = Author.create name: "John"
|
|
39
|
+
|
|
40
|
+
assert_equal 2, Author.count
|
|
41
|
+
assert_equal [author], Author.where(name: "David", id: [author.id, author2.id])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_type_casting_nested_joins
|
|
45
|
+
assert_equal [comment], Comment.joins(post: :author).where(authors: { id: author.id })
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_where_copies_bind_params
|
|
49
|
+
posts = author.posts.where("posts.id = #{post.id}")
|
|
50
|
+
joined = Post.where(id: posts)
|
|
51
|
+
|
|
52
|
+
assert_operator joined.length, :>, 0
|
|
53
|
+
|
|
54
|
+
joined.each { |j_post|
|
|
55
|
+
assert_equal author, j_post.author
|
|
56
|
+
assert_equal post.id, j_post.id
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_where_or_with_relation
|
|
61
|
+
post2 = Post.create title: "Title - 2", author: author
|
|
62
|
+
expected = Post.where("id = #{post.id} or id = #{post2.id}").to_a
|
|
63
|
+
assert_equal expected, Post.where("id = #{post.id}").or(Post.where("id = #{post2.id}")).to_a
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_joins_and_preload
|
|
67
|
+
assert_nothing_raised do
|
|
68
|
+
Post.includes(:author).or(Post.includes(:author))
|
|
69
|
+
Post.eager_load(:author).or(Post.eager_load(:author))
|
|
70
|
+
Post.preload(:author).or(Post.preload(:author))
|
|
71
|
+
Post.group(:author_id).or(Post.group(:author_id))
|
|
72
|
+
Post.joins(:author).or(Post.joins(:author))
|
|
73
|
+
Post.left_outer_joins(:author).or(Post.left_outer_joins(:author))
|
|
74
|
+
Post.from("posts")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_not_inverts_where_clause
|
|
79
|
+
relation = Post.where.not(title: "hello")
|
|
80
|
+
expected_where_clause = Post.where(title: "hello").where_clause.invert
|
|
81
|
+
|
|
82
|
+
assert_equal expected_where_clause, relation.where_clause
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def test_range
|
|
86
|
+
post2 = Post.create title: "Title - 1", author: author
|
|
87
|
+
comment2 = Comment.create comment: "Comment - 2", post: post2
|
|
88
|
+
|
|
89
|
+
assert_equal 2, Post.where(comments_count: 1..3).count
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_with_infinite_upper_bound_range
|
|
93
|
+
assert_equal 1, Post.where(comments_count: 1..Float::INFINITY).count
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_offset_and_limit
|
|
97
|
+
post = Post.create title: "Title - 1", author: author
|
|
98
|
+
5.times.each do |i|
|
|
99
|
+
Comment.create comment: "Comment - #{i+1}", post: post
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
assert_equal 5, post.comments.count
|
|
103
|
+
assert_equal 3, Comment.offset(1).limit(3).count
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_select
|
|
107
|
+
post = Post.select(:title).to_a.first
|
|
108
|
+
|
|
109
|
+
assert_nil post.id
|
|
110
|
+
assert_equal "Title - 1", post.title
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def test_order_and_pluck
|
|
114
|
+
post = Post.create title: "Title - 2", author: author
|
|
115
|
+
titles = Post.order("title").pluck("posts.title")
|
|
116
|
+
|
|
117
|
+
assert_equal ["Title - 1", "Title - 2"], titles
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_time_value
|
|
121
|
+
time_value = Time.new(2016, 05, 11, 19, 0, 0)
|
|
122
|
+
post = Post.create(published_time: time_value)
|
|
123
|
+
assert_equal post, Post.find_by(published_time: time_value)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_timestamp_value
|
|
127
|
+
timestamp_value = Time.now
|
|
128
|
+
post = Post.create(published_time: timestamp_value)
|
|
129
|
+
assert_equal post, Post.find_by(published_time: timestamp_value)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def test_date_value
|
|
133
|
+
date = Date.new(2016, 05, 11)
|
|
134
|
+
post = Post.create(post_date: date)
|
|
135
|
+
assert_equal post, Post.find_by(post_date: date)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_relation_merging
|
|
139
|
+
post.comments << Comment.new(comment: "Comment - 2")
|
|
140
|
+
|
|
141
|
+
posts = Post.where("comments_count >= 0").merge(Post.limit(2)).merge(Post.order("id ASC"))
|
|
142
|
+
|
|
143
|
+
assert_equal [post], posts.to_a
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def test_statement_hint
|
|
147
|
+
post = Post.optimizer_hints("statement_hint: @{USE_ADDITIONAL_PARALLELISM=TRUE}")
|
|
148
|
+
.select(:title).to_a.first
|
|
149
|
+
|
|
150
|
+
assert_nil post.id
|
|
151
|
+
assert_equal "Title - 1", post.title
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def test_table_hint
|
|
155
|
+
post = Post.optimizer_hints("table_hint: posts@{FORCE_INDEX=_BASE_TABLE}")
|
|
156
|
+
.select(:title).to_a.first
|
|
157
|
+
|
|
158
|
+
assert_nil post.id
|
|
159
|
+
assert_equal "Title - 1", post.title
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def test_join_hint
|
|
163
|
+
post = Post.joins("inner join @{JOIN_TYPE=HASH_JOIN} comments on posts.id=comments.post_id")
|
|
164
|
+
.select(:title).to_a.first
|
|
165
|
+
|
|
166
|
+
assert_nil post.id
|
|
167
|
+
assert_equal "Title - 1", post.title
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Copyright 2021 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 "models/author"
|
|
11
|
+
require "models/post"
|
|
12
|
+
require "models/comment"
|
|
13
|
+
require "models/organization"
|
|
14
|
+
|
|
15
|
+
module ActiveRecord
|
|
16
|
+
module Sessions
|
|
17
|
+
|
|
18
|
+
# Verifies that the adapter can handle a Session not found error in all (common) scenarios.
|
|
19
|
+
class SessionNotFoundTest < SpannerAdapter::TestCase
|
|
20
|
+
include SpannerAdapter::Associations::TestHelper
|
|
21
|
+
|
|
22
|
+
attr_accessor :organization
|
|
23
|
+
|
|
24
|
+
def setup
|
|
25
|
+
super
|
|
26
|
+
|
|
27
|
+
@organization = Organization.create name: "Organization 1"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def teardown
|
|
31
|
+
super
|
|
32
|
+
|
|
33
|
+
Organization.destroy_all
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def client
|
|
37
|
+
@client ||= Google::Cloud::Spanner::V1::Spanner::Client.new do |config|
|
|
38
|
+
config.credentials = ENV["SPANNER_EMULATOR_HOST"] \
|
|
39
|
+
? :this_channel_is_insecure \
|
|
40
|
+
: ENV["SPANNER_TEST_KEYFILE"] || ENV["GCLOUD_TEST_KEYFILE"]
|
|
41
|
+
config.endpoint = ENV["SPANNER_EMULATOR_HOST"] if ENV["SPANNER_EMULATOR_HOST"]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def delete_all_sessions
|
|
46
|
+
sessions = client.list_sessions(
|
|
47
|
+
Google::Cloud::Spanner::V1::ListSessionsRequest.new(
|
|
48
|
+
database: "projects/#{ENV["SPANNER_TEST_PROJECT"]}/instances/#{ENV["SPANNER_TEST_INSTANCE"]}/databases/#{$spanner_test_database}"
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
sessions.each do |session|
|
|
52
|
+
client.delete_session Google::Cloud::Spanner::V1::DeleteSessionRequest.new name: session.name
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_single_read
|
|
57
|
+
delete_all_sessions
|
|
58
|
+
organization = Organization.find_by id: @organization.id
|
|
59
|
+
refute_nil organization
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_single_mutation
|
|
63
|
+
delete_all_sessions
|
|
64
|
+
id = Organization.create name: "Organization 2"
|
|
65
|
+
assert_equal "Organization 2", Organization.find_by(id: id).name
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_batch_mutation
|
|
69
|
+
delete_all_sessions
|
|
70
|
+
Organization.create([{name: "Organization 2"}, {name: "Organization 3"}])
|
|
71
|
+
assert_equal 3, Organization.count
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_begin_transaction
|
|
75
|
+
delete_all_sessions
|
|
76
|
+
Organization.transaction do
|
|
77
|
+
organization = Organization.find_by id: @organization.id
|
|
78
|
+
refute_nil organization
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_read_in_transaction
|
|
83
|
+
attempts = 0
|
|
84
|
+
Organization.transaction do
|
|
85
|
+
attempts += 1
|
|
86
|
+
delete_all_sessions if attempts == 1
|
|
87
|
+
organization = Organization.find_by id: @organization.id
|
|
88
|
+
refute_nil organization
|
|
89
|
+
end
|
|
90
|
+
# The transaction could also be aborted by the backend, hence the > 1.
|
|
91
|
+
assert attempts > 1, "Should retry at least once"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_dml_in_transaction
|
|
95
|
+
id = nil
|
|
96
|
+
attempts = 0
|
|
97
|
+
Organization.transaction do
|
|
98
|
+
attempts += 1
|
|
99
|
+
delete_all_sessions if attempts == 1
|
|
100
|
+
id = Organization.create name: "Organization 2"
|
|
101
|
+
end
|
|
102
|
+
assert attempts > 1, "Should retry at least once"
|
|
103
|
+
assert_equal "Organization 2", Organization.find_by(id: id).name
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_commit
|
|
107
|
+
attempts = 0
|
|
108
|
+
Organization.transaction do
|
|
109
|
+
Organization.find_by id: @organization.id
|
|
110
|
+
attempts += 1
|
|
111
|
+
# The following is a trick for the emulator only. If a session on the emulator has an active transaction,
|
|
112
|
+
# and that session is deleted, the emulator still thinks that the transaction is active.
|
|
113
|
+
# See https://github.com/GoogleCloudPlatform/cloud-spanner-emulator/issues/30
|
|
114
|
+
Base.connection.current_spanner_transaction.shoot_and_forget_rollback if ENV["SPANNER_EMULATOR_HOST"] && attempts == 1
|
|
115
|
+
delete_all_sessions if attempts == 1
|
|
116
|
+
end
|
|
117
|
+
assert attempts > 1, "Should retry at least once"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Copyright 2021 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 "models/singer"
|
|
11
|
+
require "models/album"
|
|
12
|
+
require "models/track"
|
|
13
|
+
|
|
14
|
+
module ActiveRecord
|
|
15
|
+
module Transactions
|
|
16
|
+
class OptimisticLockingTest < SpannerAdapter::TestCase
|
|
17
|
+
include SpannerAdapter::Associations::TestHelper
|
|
18
|
+
|
|
19
|
+
def setup
|
|
20
|
+
super
|
|
21
|
+
|
|
22
|
+
singer = Singer.create first_name: "Pete", last_name: "Allison"
|
|
23
|
+
album = Album.create title: "Musical Jeans", singer: singer
|
|
24
|
+
Track.create title: "Increased Headline", album: album, singer: singer
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def teardown
|
|
28
|
+
super
|
|
29
|
+
|
|
30
|
+
Track.delete_all
|
|
31
|
+
Album.delete_all
|
|
32
|
+
Singer.delete_all
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Runs the given block in a transaction with the given isolation level, or without a transaction if isolation is
|
|
36
|
+
# nil.
|
|
37
|
+
def run_in_transaction isolation
|
|
38
|
+
if isolation
|
|
39
|
+
Base.transaction isolation: isolation do
|
|
40
|
+
yield
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
yield
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_update_single_record_increases_version_number
|
|
48
|
+
[nil, :serializable, :buffered_mutations].each do |isolation|
|
|
49
|
+
singer = Singer.all.sample
|
|
50
|
+
original_version = singer.lock_version
|
|
51
|
+
|
|
52
|
+
run_in_transaction isolation do
|
|
53
|
+
singer.update last_name: "Peterson-#{singer.last_name}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
singer.reload
|
|
57
|
+
assert_equal original_version + 1, singer.lock_version
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_update_multiple_records_increases_version_numbers
|
|
62
|
+
singer = Singer.all.sample
|
|
63
|
+
album = Album.all.sample
|
|
64
|
+
track = Track.all.sample
|
|
65
|
+
[nil, :serializable, :buffered_mutations].each do |isolation|
|
|
66
|
+
original_singer_version = singer.reload.lock_version
|
|
67
|
+
original_album_version = album.reload.lock_version
|
|
68
|
+
original_track_version = track.reload.lock_version
|
|
69
|
+
|
|
70
|
+
run_in_transaction isolation do
|
|
71
|
+
singer.update last_name: "Peterson-#{singer.last_name}"
|
|
72
|
+
singer.albums.each { |album| album.update title: "Updated: #{album.title}" }
|
|
73
|
+
singer.tracks.each { |track| track.update title: "Updated: #{track.title}" }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
singer.reload
|
|
77
|
+
assert_equal original_singer_version + 1, singer.lock_version
|
|
78
|
+
singer.albums.each { |album| assert_equal original_album_version + 1, album.lock_version }
|
|
79
|
+
singer.tracks.each { |track| assert_equal original_track_version + 1, track.lock_version }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_concurrent_update_single_record_fails
|
|
84
|
+
[nil, :serializable, :buffered_mutations].each do |isolation|
|
|
85
|
+
singer = Singer.all.sample
|
|
86
|
+
original_version = singer.lock_version
|
|
87
|
+
|
|
88
|
+
# Update the singer in a separate thread to simulate a concurrent update.
|
|
89
|
+
t = Thread.new do
|
|
90
|
+
singer2 = Singer.find singer.id
|
|
91
|
+
singer2.update last_name: "Henderson-#{singer2.last_name}"
|
|
92
|
+
end
|
|
93
|
+
t.join
|
|
94
|
+
|
|
95
|
+
run_in_transaction isolation do
|
|
96
|
+
assert_raises ActiveRecord::StaleObjectError do
|
|
97
|
+
singer.update last_name: "Peterson-#{singer.last_name}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
singer.reload
|
|
102
|
+
assert_equal original_version + 1, singer.lock_version
|
|
103
|
+
assert singer.last_name.start_with?("Henderson-")
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def test_concurrent_update_multiple_records_fails
|
|
108
|
+
singer = Singer.all.sample
|
|
109
|
+
album = Album.all.sample
|
|
110
|
+
track = Track.all.sample
|
|
111
|
+
[nil, :serializable, :buffered_mutations].each do |isolation|
|
|
112
|
+
original_singer_version = singer.reload.lock_version
|
|
113
|
+
original_album_version = album.reload.lock_version
|
|
114
|
+
original_track_version = track.reload.lock_version
|
|
115
|
+
|
|
116
|
+
# Update the singer in a separate thread to simulate a concurrent update.
|
|
117
|
+
t = Thread.new do
|
|
118
|
+
singer2 = Singer.find singer.id
|
|
119
|
+
singer2.update last_name: "Henderson-#{singer2.last_name}"
|
|
120
|
+
end
|
|
121
|
+
t.join
|
|
122
|
+
|
|
123
|
+
run_in_transaction isolation do
|
|
124
|
+
assert_raises ActiveRecord::StaleObjectError do
|
|
125
|
+
singer.update last_name: "Peterson-#{singer.last_name}"
|
|
126
|
+
end
|
|
127
|
+
singer.albums.each { |album| album.update title: "Updated: #{album.title}" }
|
|
128
|
+
singer.tracks.each { |track| track.update title: "Updated: #{track.title}" }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
singer.reload
|
|
132
|
+
# The singer should be updated, but only by the separate thread.
|
|
133
|
+
assert_equal original_singer_version + 1, singer.lock_version
|
|
134
|
+
assert singer.last_name.start_with? "Henderson-"
|
|
135
|
+
singer.albums.each { |album| assert_equal original_album_version + 1, album.lock_version }
|
|
136
|
+
singer.tracks.each { |track| assert_equal original_track_version + 1, track.lock_version }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Copyright 2021 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 "models/organization"
|
|
11
|
+
|
|
12
|
+
module ActiveRecord
|
|
13
|
+
module Transactions
|
|
14
|
+
class ReadOnlyTransactionsTest < SpannerAdapter::TestCase
|
|
15
|
+
include SpannerAdapter::Associations::TestHelper
|
|
16
|
+
|
|
17
|
+
attr_accessor :organization
|
|
18
|
+
|
|
19
|
+
def setup
|
|
20
|
+
super
|
|
21
|
+
|
|
22
|
+
@organization = Organization.create name: "Organization 1"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def teardown
|
|
26
|
+
super
|
|
27
|
+
|
|
28
|
+
Organization.destroy_all
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_read_in_snapshot
|
|
32
|
+
Base.transaction isolation: :read_only do
|
|
33
|
+
org = Organization.find @organization.id
|
|
34
|
+
assert_equal "Organization 1", org.name
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_read_in_snapshot_at_timestamp
|
|
39
|
+
# Get a valid timestamp from the server to use for the transaction.
|
|
40
|
+
timestamp = ActiveRecord::Base.connection.select_all("SELECT CURRENT_TIMESTAMP AS ts")[0]["ts"]
|
|
41
|
+
Base.transaction isolation: { timestamp: timestamp } do
|
|
42
|
+
org = Organization.find @organization.id
|
|
43
|
+
assert_equal "Organization 1", org.name
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_read_in_snapshot_with_staleness
|
|
48
|
+
Base.transaction isolation: { staleness: 1 } do
|
|
49
|
+
begin
|
|
50
|
+
# It could be that the record or even the table cannot be found, as the read timestamp could be
|
|
51
|
+
# before either of them were created, but the record could also be found, all depending on the execution
|
|
52
|
+
# speed of the test. All those scenarios are valid.
|
|
53
|
+
org = Organization.find @organization.id
|
|
54
|
+
assert_equal "Organization 1", org.name
|
|
55
|
+
rescue => e
|
|
56
|
+
assert e.message.include?("Table not found") || e.message.include?("Couldn't find Organization"), e.message
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_single_read_at_timestamp
|
|
62
|
+
# Get a valid timestamp from the server to use for the transaction.
|
|
63
|
+
timestamp = ActiveRecord::Base.connection.select_all("SELECT CURRENT_TIMESTAMP AS ts")[0]["ts"]
|
|
64
|
+
|
|
65
|
+
org = Organization.optimizer_hints("read_timestamp:#{timestamp.xmlschema(9)}").find @organization.id
|
|
66
|
+
assert_equal "Organization 1", org.name
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_single_read_at_min_read_timestamp
|
|
70
|
+
# Get a valid timestamp from the server to use for the transaction.
|
|
71
|
+
timestamp = ActiveRecord::Base.connection.select_all("SELECT CURRENT_TIMESTAMP AS ts")[0]["ts"]
|
|
72
|
+
|
|
73
|
+
org = Organization.optimizer_hints("min_read_timestamp:#{timestamp.xmlschema(9)}").find @organization.id
|
|
74
|
+
assert_equal "Organization 1", org.name
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_single_read_with_max_staleness
|
|
78
|
+
begin
|
|
79
|
+
# It could be that the record or even the table cannot be found, as the read timestamp could be
|
|
80
|
+
# before either of them were created, but the record could also be found, all depending on the execution
|
|
81
|
+
# speed of the test. All those scenarios are valid.
|
|
82
|
+
org = Organization.optimizer_hints("max_staleness: 1").find @organization.id
|
|
83
|
+
assert_equal "Organization 1", org.name
|
|
84
|
+
rescue => e
|
|
85
|
+
assert e.message.include?("Table not found") || e.message.include?("Couldn't find Organization"), e.message
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_single_read_with_exact_staleness
|
|
90
|
+
begin
|
|
91
|
+
# It could be that the record or even the table cannot be found, as the read timestamp could be
|
|
92
|
+
# before either of them were created, but the record could also be found, all depending on the execution
|
|
93
|
+
# speed of the test. All those scenarios are valid.
|
|
94
|
+
org = Organization.optimizer_hints("exact_staleness: 1").find @organization.id
|
|
95
|
+
assert_equal "Organization 1", org.name
|
|
96
|
+
rescue => e
|
|
97
|
+
assert e.message.include?("Table not found") || e.message.include?("Couldn't find Organization"), e.message
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_snapshot_does_not_see_new_changes
|
|
102
|
+
Base.transaction isolation: :read_only do
|
|
103
|
+
org = Organization.find @organization.id
|
|
104
|
+
assert_equal "Organization 1", org.name
|
|
105
|
+
|
|
106
|
+
# Update the name of the organization using a separate thread (and separate transaction).
|
|
107
|
+
t = Thread.new { organization.update(name: "New name") }
|
|
108
|
+
t.join
|
|
109
|
+
|
|
110
|
+
# Reload the record using the current snapshot. The change will not be visible.
|
|
111
|
+
org.reload
|
|
112
|
+
assert_equal "Organization 1", org.name
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Now read the record outside of the snapshot. The new value should be visible.
|
|
116
|
+
org = Organization.find @organization.id
|
|
117
|
+
assert_equal "New name", org.name
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_write_in_snapshot
|
|
121
|
+
Base.transaction isolation: :read_only do
|
|
122
|
+
err = assert_raises ActiveRecord::StatementInvalid do
|
|
123
|
+
Organization.create(name: "Created in read-only transaction")
|
|
124
|
+
end
|
|
125
|
+
assert err.cause.is_a?(Google::Cloud::InvalidArgumentError)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|