activerecord-spanner-adapter 1.6.3 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c2a9a0a36a471ac93bdb0d59e50cc9b8e5c6647655487dc55a1d9e398abcf46
4
- data.tar.gz: de680bcac6cf0f491bd0007d80d48496cf5e74b5eddf17d13b58353f06b818da
3
+ metadata.gz: e521009bd7ee056c90e4147edd3e6dfe012e7808abf8dcf2b9a0145f380d669d
4
+ data.tar.gz: 9be31f9f418a9ca5f80531924bc299a75c64826d09989c7318aff88eaccf8a41
5
5
  SHA512:
6
- metadata.gz: '0658d456b4d6377bf5f95f44296869404ac9ac1c65aaa25360080bab1df7c122c4b7aba28992f5030e6465e51fefa1b6630a4e79547c65446edd29c40954ca61'
7
- data.tar.gz: 470e026052a753f08d59f2824044894afe84d5601735270250c09368c7541ef98d5356a6ca6810c10755033a81601afb40826656967bdf8cda132563a2e9b267
6
+ metadata.gz: a529d5691fdcecc54e6a090a807c5b5ff561aa2a355d268a605c91b54705a9a093544129da76df2bb5341953a55dfac792be77079aa1b63121260eb8ed9559c3
7
+ data.tar.gz: 0f6be5f7de6113d16ce2f5e0474924ee4af6bdf3599c84050382f5c47a64f24876b656522e57ebb1e35742e6154221174b0a1e7487fa82034a5a4a5acfa527c2
@@ -18,16 +18,14 @@ jobs:
18
18
  strategy:
19
19
  max-parallel: 4
20
20
  matrix:
21
- ruby: ["2.7", "3.0", "3.1", "3.2"]
22
- ar: ["~> 6.0.6", "~> 6.1.7", "~> 7.0.4", "~> 7.1.0"]
21
+ ruby: ["2.7", "3.0", "3.1", "3.2", "3.3"]
22
+ ar: ["~> 6.1.0", "~> 7.0.0", "~> 7.1.0", "~> 7.2.0"]
23
23
  # Exclude combinations that are not supported.
24
24
  exclude:
25
+ - ruby: "2.7"
26
+ ar: "~> 7.2.0"
25
27
  - ruby: "3.0"
26
- ar: "~> 6.0.6"
27
- - ruby: "3.1"
28
- ar: "~> 6.0.6"
29
- - ruby: "3.2"
30
- ar: "~> 6.0.6"
28
+ ar: "~> 7.2.0"
31
29
  env:
32
30
  AR_VERSION: ${{ matrix.ar }}
33
31
  steps:
@@ -24,7 +24,7 @@ jobs:
24
24
  strategy:
25
25
  max-parallel: 4
26
26
  matrix:
27
- ruby: [3.0]
27
+ ruby: [3.3]
28
28
  steps:
29
29
  - uses: actions/checkout@v4
30
30
  - name: Set up Ruby
@@ -10,16 +10,14 @@ jobs:
10
10
  strategy:
11
11
  max-parallel: 4
12
12
  matrix:
13
- ruby: ["2.7", "3.0", "3.1", "3.2"]
14
- ar: ["~> 6.0.6", "~> 6.1.7", "~> 7.0.4", "~> 7.1.0"]
13
+ ruby: ["2.7", "3.0", "3.1", "3.2", "3.3"]
14
+ ar: ["~> 6.1.0", "~> 7.0.0", "~> 7.1.0", "~> 7.2.0"]
15
15
  # Exclude combinations that are not supported.
16
16
  exclude:
17
+ - ruby: "2.7"
18
+ ar: "~> 7.2.0"
17
19
  - ruby: "3.0"
18
- ar: "~> 6.0.6"
19
- - ruby: "3.1"
20
- ar: "~> 6.0.6"
21
- - ruby: "3.2"
22
- ar: "~> 6.0.6"
20
+ ar: "~> 7.2.0"
23
21
  env:
24
22
  AR_VERSION: ${{ matrix.ar }}
25
23
  steps:
@@ -18,41 +18,14 @@ jobs:
18
18
  strategy:
19
19
  max-parallel: 4
20
20
  matrix:
21
- # Run acceptance tests all supported combinations of Ruby and ActiveRecord.
22
- ruby: [2.7, 3.0, 3.1, 3.2]
23
- ar: [6.0.0, 6.0.1, 6.0.2.2, 6.0.3.7, 6.0.4, 6.1.3.2, 6.1.4.7, 6.1.5.1, 6.1.6.1, 7.0.2.4, 7.0.3.1, 7.0.4, 7.0.5, 7.0.6, 7.0.7, 7.1.0, 7.1.1, 7.1.2]
21
+ ruby: ["2.7", "3.0", "3.1", "3.2", "3.3"]
22
+ ar: ["~> 6.1.0", "~> 7.0.0", "~> 7.1.0", "~> 7.2.0"]
24
23
  # Exclude combinations that are not supported.
25
24
  exclude:
26
- - ruby: 3.0
27
- ar: 6.0.0
28
- - ruby: 3.0
29
- ar: 6.0.1
30
- - ruby: 3.0
31
- ar: 6.0.2.2
32
- - ruby: 3.0
33
- ar: 6.0.3.7
34
- - ruby: 3.0
35
- ar: 6.0.4
36
- - ruby: 3.1
37
- ar: 6.0.0
38
- - ruby: 3.1
39
- ar: 6.0.1
40
- - ruby: 3.1
41
- ar: 6.0.2.2
42
- - ruby: 3.1
43
- ar: 6.0.3.7
44
- - ruby: 3.1
45
- ar: 6.0.4
46
- - ruby: 3.2
47
- ar: 6.0.0
48
- - ruby: 3.2
49
- ar: 6.0.1
50
- - ruby: 3.2
51
- ar: 6.0.2.2
52
- - ruby: 3.2
53
- ar: 6.0.3.7
54
- - ruby: 3.2
55
- ar: 6.0.4
25
+ - ruby: "2.7"
26
+ ar: "~> 7.2.0"
27
+ - ruby: "3.0"
28
+ ar: "~> 7.2.0"
56
29
  env:
57
30
  AR_VERSION: ${{ matrix.ar }}
58
31
  steps:
@@ -10,7 +10,7 @@ jobs:
10
10
  strategy:
11
11
  max-parallel: 4
12
12
  matrix:
13
- ruby: [3.0]
13
+ ruby: [3.3]
14
14
  steps:
15
15
  - uses: actions/checkout@v4
16
16
  - name: Set up Ruby
@@ -10,41 +10,15 @@ jobs:
10
10
  strategy:
11
11
  max-parallel: 4
12
12
  matrix:
13
- # Run unit tests all supported combinations of Ruby and ActiveRecord.
14
- ruby: [2.7, 3.0, 3.1, 3.2]
15
- ar: [6.0.0, 6.0.1, 6.0.2.2, 6.0.3.7, 6.0.4, 6.1.3.2, 6.1.4.7, 6.1.5.1, 6.1.6.1, 7.0.2.4, 7.0.3.1, 7.0.4, 7.0.5, 7.1.0, 7.1.1, 7.1.2]
13
+ # Run acceptance tests all supported combinations of Ruby and ActiveRecord.
14
+ ruby: ["2.7", "3.0", "3.1", "3.2", "3.3"]
15
+ ar: ["~> 6.1.0", "~> 7.0.0", "~> 7.1.0", "~> 7.2.0"]
16
16
  # Exclude combinations that are not supported.
17
17
  exclude:
18
- - ruby: 3.0
19
- ar: 6.0.0
20
- - ruby: 3.0
21
- ar: 6.0.1
22
- - ruby: 3.0
23
- ar: 6.0.2.2
24
- - ruby: 3.0
25
- ar: 6.0.3.7
26
- - ruby: 3.0
27
- ar: 6.0.4
28
- - ruby: 3.1
29
- ar: 6.0.0
30
- - ruby: 3.1
31
- ar: 6.0.1
32
- - ruby: 3.1
33
- ar: 6.0.2.2
34
- - ruby: 3.1
35
- ar: 6.0.3.7
36
- - ruby: 3.1
37
- ar: 6.0.4
38
- - ruby: 3.2
39
- ar: 6.0.0
40
- - ruby: 3.2
41
- ar: 6.0.1
42
- - ruby: 3.2
43
- ar: 6.0.2.2
44
- - ruby: 3.2
45
- ar: 6.0.3.7
46
- - ruby: 3.2
47
- ar: 6.0.4
18
+ - ruby: "2.7"
19
+ ar: "~> 7.2.0"
20
+ - ruby: "3.0"
21
+ ar: "~> 7.2.0"
48
22
  env:
49
23
  AR_VERSION: ${{ matrix.ar }}
50
24
  steps:
@@ -24,6 +24,10 @@ function msg { println "$*" >&2 ;}
24
24
  function println { printf '%s\n' "$(now) $*" ;}
25
25
 
26
26
  # Populates requested secrets set in SECRET_MANAGER_KEYS
27
+ if [[ -z "${SECRET_MANAGER_PROJECT_ID-}" ]]; then
28
+ msg "SECRET_MANAGER_PROJECT_ID is not set in environment variables, using default"
29
+ SECRET_MANAGER_PROJECT_ID="cloud-devrel-kokoro-resources"
30
+ fi
27
31
 
28
32
  # In Kokoro CI builds, we use the service account attached to the
29
33
  # Kokoro VM. This means we need to setup auth on other CI systems.
@@ -65,7 +69,7 @@ do
65
69
  msg "Retrieving secret ${key}"
66
70
  "${GCLOUD_COMMANDS[@]}" \
67
71
  secrets versions access latest \
68
- --project cloud-devrel-kokoro-resources \
72
+ --project "${SECRET_MANAGER_PROJECT_ID}" \
69
73
  --secret $key > \
70
74
  "$SECRET_LOCATION/$key"
71
75
  if [[ $? == 0 ]]; then
data/.kokoro/release.cfg CHANGED
@@ -7,19 +7,13 @@ action {
7
7
  }
8
8
  }
9
9
 
10
- # Download resources for system tests (service account key, etc.)
11
- gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/google-cloud-ruby"
12
-
13
- # Download trampoline resources.
14
- gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline"
15
-
16
10
  # Use the trampoline script to run in docker.
17
11
  build_file: "ruby-spanner-activerecord/.kokoro/trampoline_v2.sh"
18
12
 
19
13
  # Configure the docker image for kokoro-trampoline.
20
14
  env_vars: {
21
15
  key: "TRAMPOLINE_IMAGE"
22
- value: "gcr.io/cloud-devrel-kokoro-resources/yoshi-ruby/release"
16
+ value: "us-central1-docker.pkg.dev/cloud-sdk-release-custom-pool/release-images/ruby-multi"
23
17
  }
24
18
 
25
19
  env_vars: {
@@ -27,16 +21,32 @@ env_vars: {
27
21
  value: ".kokoro/release.sh"
28
22
  }
29
23
 
24
+ env_vars: {
25
+ key: "SECRET_MANAGER_PROJECT_ID"
26
+ value: "cloud-sdk-release-custom-pool"
27
+ }
28
+
30
29
  env_vars: {
31
30
  key: "SECRET_MANAGER_KEYS"
32
31
  value: "releasetool-publish-reporter-app,releasetool-publish-reporter-googleapis-installation,releasetool-publish-reporter-pem,docuploader_service_account"
33
32
  }
34
33
 
34
+ # Pick up Rubygems key from internal keystore
35
+ before_action {
36
+ fetch_keystore {
37
+ keystore_resource {
38
+ keystore_config_id: 73713
39
+ keyname: "rubygems-publish-key"
40
+ backend: "blade:keystore-fastconfigpush"
41
+ }
42
+ }
43
+ }
44
+
35
45
  # Store the packages uploaded to rubygems.org, which
36
46
  # we can later use to generate SBOMs and attestations.
37
47
  action {
38
- define_artifacts {
39
- regex: "github/ruby-spanner-activerecord/pkg/*.gem"
40
- strip_prefix: "github"
41
- }
42
- }
48
+ define_artifacts {
49
+ regex: "github/ruby-spanner-activerecord/pkg/*.gem"
50
+ strip_prefix: "github"
51
+ }
52
+ }
@@ -138,18 +138,26 @@ if [[ -n "${KOKORO_BUILD_ID:-}" ]]; then
138
138
  RUNNING_IN_CI="true"
139
139
  TRAMPOLINE_CI="kokoro"
140
140
  if [[ "${TRAMPOLINE_USE_LEGACY_SERVICE_ACCOUNT:-}" == "true" ]]; then
141
- if [[ ! -f "${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json" ]]; then
142
- log_red "${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json does not exist. Did you forget to mount cloud-devrel-kokoro-resources/trampoline? Aborting."
143
- exit 1
144
- fi
145
- # This service account will be activated later.
146
- TRAMPOLINE_SERVICE_ACCOUNT="${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json"
141
+ if [[ ! -f "${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json" ]]; then
142
+ log_red "${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json does not exist. Did you forget to mount cloud-devrel-kokoro-resources/trampoline? Aborting."
143
+ exit 1
144
+ fi
145
+ # This service account will be activated later.
146
+ TRAMPOLINE_SERVICE_ACCOUNT="${KOKORO_GFILE_DIR}/kokoro-trampoline.service-account.json"
147
147
  else
148
- if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then
149
- gcloud auth list
150
- fi
151
- log_yellow "Configuring Container Registry access"
152
- gcloud auth configure-docker --quiet
148
+ if [[ "${TRAMPOLINE_VERBOSE:-}" == "true" ]]; then
149
+ gcloud auth list
150
+ fi
151
+ log_yellow "Configuring Container Registry access"
152
+ TRAMPOLINE_HOST=$(echo "${TRAMPOLINE_IMAGE}" | cut -d/ -f1)
153
+ if [[ ! "${TRAMPOLINE_HOST}" =~ "gcr.io" ]]; then
154
+ # If you need to specificy a host other than gcr.io, you have to run on an update version of gcloud.
155
+ echo "TRAMPOLINE_HOST: ${TRAMPOLINE_HOST}"
156
+ gcloud components update
157
+ gcloud auth configure-docker "${TRAMPOLINE_HOST}"
158
+ else
159
+ gcloud auth configure-docker --quiet
160
+ fi
153
161
  fi
154
162
  pass_down_envvars+=(
155
163
  # KOKORO dynamic variables.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.6.3"
2
+ ".": "1.8.0"
3
3
  }
data/.trampolinerc CHANGED
@@ -18,7 +18,12 @@ required_envvars+=(
18
18
 
19
19
  # Add env vars which are passed down into the container here.
20
20
  pass_down_envvars+=(
21
- "AUTORELEASE_PR" "RELEASE_DRY_RUN" "RELEASE_PACKAGE" "KOKORO_GIT_COMMIT" "RUBY_VERSIONS" "EXTRA_CI_ARGS"
21
+ "AUTORELEASE_PR"
22
+ "EXTRA_CI_ARGS"
23
+ "KOKORO_GIT_COMMIT"
24
+ "RELEASE_DRY_RUN"
25
+ "RELEASE_PACKAGE"
26
+ "RUBY_VERSIONS"
22
27
  )
23
28
 
24
29
  # Prevent unintentional override on the default image.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ### 1.8.0 (2024-12-12)
4
+
5
+ #### Features
6
+
7
+ * INSERT OR [IGNORE|UPDATE] ([#332](https://github.com/googleapis/ruby-spanner-activerecord/issues/332))
8
+ #### Bug Fixes
9
+
10
+ * Fixed incorrect argument handling. ([#333](https://github.com/googleapis/ruby-spanner-activerecord/issues/333))
11
+
12
+ ### 1.7.0 (2024-12-11)
13
+
14
+ #### Features
15
+
16
+ * support Rails 7.2.0 ([#328](https://github.com/googleapis/ruby-spanner-activerecord/issues/328))
17
+ #### Bug Fixes
18
+
19
+ * `SpannerAdapter` requires prepared statements to be enabled ([#323](https://github.com/googleapis/ruby-spanner-activerecord/issues/323))
20
+ * local emulator test ([#320](https://github.com/googleapis/ruby-spanner-activerecord/issues/320))
21
+
3
22
  ### 1.6.3 (2024-08-31)
4
23
 
5
24
  #### Bug Fixes
data/Gemfile CHANGED
@@ -3,7 +3,8 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in activerecord-spanner.gemspec
4
4
  gemspec
5
5
 
6
- gem "activerecord", ENV.fetch("AR_VERSION", "~> 6.1.6.1")
6
+ gem "activerecord", ENV.fetch("AR_VERSION", "~> 7.1.0")
7
+ gem "ostruct"
7
8
  gem "minitest", "~> 5.25.0"
8
9
  gem "minitest-rg", "~> 5.3.0"
9
10
  gem "pry", "~> 0.13.0"
@@ -12,7 +13,7 @@ gem "pry-byebug", "~> 3.9.0"
12
13
  gem 'sqlite3', '~> 1.4'
13
14
 
14
15
  # Required for samples and testing.
15
- install_if -> { ENV.fetch("AR_VERSION", "~> 6.1.6.1").dup.to_s.sub("~>", "").strip < "7.1.0" && !ENV["SKIP_COMPOSITE_PK"] } do
16
+ install_if -> { ENV.fetch("AR_VERSION", "~> 7.1.0").dup.to_s.sub("~>", "").strip < "7.1.0" && !ENV["SKIP_COMPOSITE_PK"] } do
16
17
  gem "composite_primary_keys"
17
18
  end
18
19
 
data/Rakefile CHANGED
@@ -56,8 +56,8 @@ task :acceptance, [:project, :keyfile, :instance, :tests] do |t, args|
56
56
  tests ||= "**"
57
57
 
58
58
  # always overwrite when running tests
59
- ENV["SPANNER_PROJECT"] = project
60
- ENV["SPANNER_KEYFILE_JSON"] = keyfile
59
+ ENV["SPANNER_TEST_PROJECT"] = project
60
+ ENV["SPANNER_TEST_KEYFILE_JSON"] = keyfile
61
61
  ENV["SPANNER_TEST_INSTANCE"] = instance
62
62
  ENV["SPANNER_EMULATOR_HOST"] = emulator_host
63
63
 
@@ -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 = ActiveRecord::Base.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 = ActiveRecord::Base.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 = ActiveRecord::Base.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 = ActiveRecord::Base.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 = ActiveRecord::Base.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 = ActiveRecord::Base.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 = ActiveRecord::Base.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 = ActiveRecord::Base.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 = ActiveRecord::Base.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
@@ -30,13 +30,24 @@ module ActiveRecord
30
30
  { id: Author.next_sequence_value, name: "Carol" },
31
31
  ]
32
32
 
33
- assert_raise(NotImplementedError) { Author.insert_all(values) }
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
- assert_raise(NotImplementedError) { Author.insert(value) }
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
- err = assert_raise(NotImplementedError) do
140
- ActiveRecord::Base.transaction do
141
- Author.upsert_all(values)
142
- end
150
+ ActiveRecord::Base.transaction do
151
+ Author.upsert_all(values)
143
152
  end
144
- assert_match "Use upsert outside a transaction block", err.message
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
@@ -48,6 +48,7 @@ module ActiveRecord
48
48
  end
49
49
 
50
50
  def teardown
51
+ drop_database
51
52
  ActiveRecord::Base.connection_pool.disconnect!
52
53
  FileUtils.rm_rf ActiveRecord::Tasks::DatabaseTasks.db_dir
53
54
  ActiveRecord::Tasks::DatabaseTasks.db_dir = @original_db_dir
@@ -8,6 +8,7 @@ gem "minitest"
8
8
  require "minitest/autorun"
9
9
  require "minitest/focus"
10
10
  require "minitest/rg"
11
+ require "ostruct"
11
12
  require "active_support"
12
13
  require "google/cloud/spanner"
13
14
  require "active_record"
@@ -18,7 +19,10 @@ require "securerandom"
18
19
  require "composite_primary_keys" if ActiveRecord::gem_version < Gem::Version.create('7.1.0')
19
20
 
20
21
  # rubocop:disable Style/GlobalVars
21
-
22
+ #
23
+ if ActiveRecord.gem_version >= Gem::Version.create("7.2.0")
24
+ ActiveRecord::ConnectionAdapters.register("spanner", "ActiveRecord::ConnectionAdapters::SpannerAdapter")
25
+ end
22
26
  $spanner_test_database = "ar-test-#{SecureRandom.hex 4}"
23
27
 
24
28
  def connector_config
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
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.0.0", "< 7.2"]
30
+ spec.add_runtime_dependency "activerecord", [">= 6.1", "< 7.3"]
31
31
 
32
32
  spec.add_development_dependency "autotest-suffix", "~> 1.1"
33
33
  spec.add_development_dependency "bundler", "~> 2.0"
@@ -21,15 +21,15 @@ module ActiveRecord
21
21
  internal_execute sql, name, binds
22
22
  end
23
23
 
24
- def internal_exec_query sql, name = "SQL", binds = [], prepare: false, async: false
25
- result = internal_execute sql, name, binds, prepare: prepare, async: async
24
+ def internal_exec_query sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false
25
+ result = internal_execute sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry
26
26
  ActiveRecord::Result.new(
27
27
  result.fields.keys.map(&:to_s), result.rows.map(&:values)
28
28
  )
29
29
  end
30
30
 
31
31
  def internal_execute sql, name = "SQL", binds = [],
32
- prepare: false, async: false # rubocop:disable Lint/UnusedMethodArgument
32
+ prepare: false, async: false, allow_retry: false # rubocop:disable Lint/UnusedMethodArgument, Metrics/LineLength
33
33
  statement_type = sql_statement_type sql
34
34
  # Call `transform` to invoke any query transformers that might have been registered.
35
35
  sql = transform sql
@@ -320,6 +320,8 @@ module ActiveRecord
320
320
  elsif bind.class == Symbol
321
321
  # This ensures that for example :environment is sent as the string 'environment' to Cloud Spanner.
322
322
  type = :STRING
323
+ elsif bind.class == TrueClass || bind.class == FalseClass
324
+ type = :BOOL
323
325
  end
324
326
  [
325
327
  # Generates binds for named parameters in the format `@p1, @p2, ...`
@@ -30,17 +30,29 @@
30
30
 
31
31
  module ActiveRecord
32
32
  module ConnectionAdapters
33
+ QUOTED_COLUMN_NAMES = Concurrent::Map.new # :nodoc:
34
+ QUOTED_TABLE_NAMES = Concurrent::Map.new # :nodoc:
35
+
36
+ module Quoting
37
+ module ClassMethods
38
+ # This is used for ActiveRecord v8 and higher.
39
+ def quote_column_name name
40
+ QUOTED_COLUMN_NAMES[name] ||= "`#{name.to_s.gsub '`', '``'}`".freeze
41
+ end
42
+
43
+ def quote_table_name name
44
+ QUOTED_TABLE_NAMES[name] ||= "`#{name.to_s.gsub '.', '`.`'}`".freeze
45
+ end
46
+ end
47
+ end
33
48
  module Spanner
34
49
  module Quoting
35
- QUOTED_COLUMN_NAMES = Concurrent::Map.new # :nodoc:
36
- QUOTED_TABLE_NAMES = Concurrent::Map.new # :nodoc:
37
-
38
50
  def quote_column_name name
39
- QUOTED_COLUMN_NAMES[name] ||= "`#{super.gsub '`', '``'}`".freeze
51
+ QUOTED_COLUMN_NAMES[name] ||= "`#{name.to_s.gsub '`', '``'}`".freeze
40
52
  end
41
53
 
42
54
  def quote_table_name name
43
- QUOTED_TABLE_NAMES[name] ||= super.gsub(".", "`.`").freeze
55
+ QUOTED_TABLE_NAMES[name] ||= "`#{name.to_s.gsub '.', '`.`'}`".freeze
44
56
  end
45
57
 
46
58
  STR_ESCAPE_REGX = /[\n\r'\\]/.freeze
@@ -23,6 +23,7 @@ module ActiveRecord
23
23
  module SchemaStatements
24
24
  VERSION_6_1_0 = Gem::Version.create "6.1.0"
25
25
  VERSION_6_0_3 = Gem::Version.create "6.0.3"
26
+ VERSION_7_2 = Gem::Version.create "7.2.0"
26
27
 
27
28
  def current_database
28
29
  @connection.database_id
@@ -401,6 +402,16 @@ module ActiveRecord
401
402
  information_schema { |i| i.check_constraints table_name }
402
403
  end
403
404
 
405
+ if ActiveRecord.gem_version >= VERSION_7_2
406
+ def migration_context
407
+ pool.migration_context
408
+ end
409
+
410
+ def schema_migration
411
+ pool.schema_migration
412
+ end
413
+ end
414
+
404
415
  def assume_migrated_upto_version version
405
416
  version = version.to_i
406
417
  sm_table = quote_table_name schema_migration.table_name
@@ -28,16 +28,18 @@ require "activerecord_spanner_adapter/primary_key"
28
28
  require "activerecord_spanner_adapter/transaction"
29
29
 
30
30
  module ActiveRecord
31
- module ConnectionHandling # :nodoc:
32
- def spanner_connection config
33
- connection = ActiveRecordSpannerAdapter::Connection.new config
34
- connection.connect!
35
- ConnectionAdapters::SpannerAdapter.new connection, logger, nil, config
36
- rescue Google::Cloud::Error => error
37
- if error.instance_of? Google::Cloud::NotFoundError
38
- raise ActiveRecord::NoDatabaseError
39
- end
40
- raise error
31
+ if ActiveRecord.version < Gem::Version.new("7.2")
32
+ module ConnectionHandling # :nodoc:
33
+ def spanner_connection config
34
+ connection = ActiveRecordSpannerAdapter::Connection.new config
35
+ connection.connect!
36
+ ConnectionAdapters::SpannerAdapter.new connection, logger, nil, config
37
+ rescue Google::Cloud::Error => error
38
+ if error.instance_of? Google::Cloud::NotFoundError
39
+ raise ActiveRecord::NoDatabaseError
40
+ end
41
+ raise error
42
+ end
41
43
  end
42
44
  end
43
45
 
@@ -69,11 +71,21 @@ module ActiveRecord
69
71
  # Determines whether or not to log query binds when executing statements
70
72
  class_attribute :log_statement_binds, instance_writer: false, default: false
71
73
 
72
- def initialize connection, logger, connection_options, config
73
- @connection = connection
74
- @connection_options = connection_options
75
- super connection, logger, config
76
- @raw_connection ||= connection
74
+ def initialize config_or_deprecated_connection, deprecated_logger = nil,
75
+ deprecated_connection_options = nil, deprecated_config = nil
76
+ if config_or_deprecated_connection.is_a? Hash
77
+ @connection = ActiveRecordSpannerAdapter::Connection.new config_or_deprecated_connection
78
+ @connection.connect!
79
+ super config_or_deprecated_connection
80
+ @raw_connection ||= @connection
81
+ else
82
+ @connection = config_or_deprecated_connection
83
+ @connection_options = deprecated_connection_options
84
+ super config_or_deprecated_connection, deprecated_logger, deprecated_config
85
+ @raw_connection ||= config_or_deprecated_connection
86
+ end
87
+ # Spanner does not support unprepared statements
88
+ @prepared_statements = true
77
89
  end
78
90
 
79
91
  def max_identifier_length
@@ -197,12 +209,14 @@ module ActiveRecord
197
209
  raise "ActiveRecordSpannerAdapter does not support insert_sql with buffered_mutations transaction."
198
210
  end
199
211
 
200
- if insert.skip_duplicates? || insert.update_duplicates?
201
- raise NotImplementedError, "CloudSpanner does not support skip_duplicates and update_duplicates."
202
- end
203
-
204
212
  values_list, = insert.values_list
205
- "INSERT #{insert.into} #{values_list}"
213
+ prefix = "INSERT"
214
+ if insert.update_duplicates?
215
+ prefix += " OR UPDATE"
216
+ elsif insert.skip_duplicates?
217
+ prefix += " OR IGNORE"
218
+ end
219
+ "#{prefix} #{insert.into} #{values_list}"
206
220
  end
207
221
 
208
222
  module TypeMapBuilder
@@ -15,7 +15,11 @@ if defined?(Rails)
15
15
  end
16
16
 
17
17
  ActiveSupport.on_load :active_record do
18
- require "active_record/connection_adapters/spanner_adapter"
18
+ if Rails.version >= "7.2.0"
19
+ ActiveRecord::ConnectionAdapters.register("spanner", "ActiveRecord::ConnectionAdapters::SpannerAdapter")
20
+ else
21
+ require "active_record/connection_adapters/spanner_adapter"
22
+ end
19
23
  end
20
24
  end
21
25
  end
@@ -16,6 +16,7 @@ module ActiveRecord
16
16
 
17
17
  class Base
18
18
  VERSION_7_1 = Gem::Version.create "7.1.0"
19
+ VERSION_7_2 = Gem::Version.create "7.2.0"
19
20
 
20
21
  # Creates an object (or multiple objects) and saves it to the database. This method will use mutations instead
21
22
  # of DML if there is no active transaction, or if the active transaction has been created with the option
@@ -48,8 +49,26 @@ module ActiveRecord
48
49
  spanner_adapter? && connection&.current_spanner_transaction&.isolation == :buffered_mutations
49
50
  end
50
51
 
51
- def self._insert_record values, returning = []
52
- if !(buffered_mutations? || (primary_key && values.is_a?(Hash))) || !spanner_adapter?
52
+ def self._should_use_standard_insert_record? values
53
+ !(buffered_mutations? || (primary_key && values.is_a?(Hash))) || !spanner_adapter?
54
+ end
55
+
56
+ def self._internal_insert_record values
57
+ if ActiveRecord.gem_version < VERSION_7_2
58
+ _insert_record values
59
+ else
60
+ _insert_record nil, values
61
+ end
62
+ end
63
+
64
+ def self._insert_record *args
65
+ if ActiveRecord.gem_version < VERSION_7_2
66
+ values, returning = args
67
+ else
68
+ _connection, values, returning = args
69
+ end
70
+
71
+ if _should_use_standard_insert_record? values
53
72
  return super values if ActiveRecord.gem_version < VERSION_7_1
54
73
  return super
55
74
  end
@@ -111,8 +130,20 @@ module ActiveRecord
111
130
  _buffer_record values, :insert_or_update, returning
112
131
  end
113
132
 
114
- def self.insert_all _attributes, **_kwargs
115
- raise NotImplementedError, "Cloud Spanner does not support skip_duplicates. Use insert! or upsert instead."
133
+ def self.insert_all attributes, returning: nil, **_kwargs
134
+ if active_transaction? && buffered_mutations?
135
+ raise NotImplementedError,
136
+ "Spanner does not support skip_duplicates for mutations. " \
137
+ "Use a transaction that uses DML, or use insert! or upsert instead."
138
+ end
139
+ super
140
+ end
141
+
142
+ def self.insert! attributes, returning: nil, **kwargs
143
+ return super unless spanner_adapter?
144
+ return super if active_transaction? && !buffered_mutations?
145
+
146
+ insert_all! [attributes], returning: returning, **kwargs
116
147
  end
117
148
 
118
149
  def self.insert_all! attributes, returning: nil, **_kwargs
@@ -123,24 +154,27 @@ module ActiveRecord
123
154
  # The mutations will be sent as one batch when the transaction is committed.
124
155
  if active_transaction?
125
156
  attributes.each do |record|
126
- _insert_record record
157
+ _internal_insert_record record
127
158
  end
128
159
  else
129
160
  transaction isolation: :buffered_mutations do
130
161
  attributes.each do |record|
131
- _insert_record record
162
+ _internal_insert_record record
132
163
  end
133
164
  end
134
165
  end
135
166
  end
136
167
 
137
- def self.upsert_all attributes, returning: nil, unique_by: nil, **_kwargs
168
+ def self.upsert attributes, returning: nil, **kwargs
138
169
  return super unless spanner_adapter?
139
- if active_transaction? && !buffered_mutations?
140
- raise NotImplementedError, "Cloud Spanner does not support upsert using DML. " \
141
- "Use upsert outside a transaction block or in a transaction " \
142
- "block with isolation: :buffered_mutations"
143
- end
170
+ return super if active_transaction? && !buffered_mutations?
171
+
172
+ upsert_all [attributes], returning: returning, **kwargs
173
+ end
174
+
175
+ def self.upsert_all attributes, returning: nil, unique_by: nil, **kwargs
176
+ return super unless spanner_adapter?
177
+ return super if active_transaction? && !buffered_mutations?
144
178
 
145
179
  # This might seem inefficient, but is actually not, as it is only buffering a mutation locally.
146
180
  # The mutations will be sent as one batch when the transaction is committed.
@@ -5,5 +5,5 @@
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
7
  module ActiveRecordSpannerAdapter
8
- VERSION = "1.6.3".freeze
8
+ VERSION = "1.8.0".freeze
9
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-spanner-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.3
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Google LLC
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-31 00:00:00.000000000 Z
11
+ date: 2024-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: google-cloud-spanner
@@ -44,20 +44,20 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 6.0.0
47
+ version: '6.1'
48
48
  - - "<"
49
49
  - !ruby/object:Gem::Version
50
- version: '7.2'
50
+ version: '7.3'
51
51
  type: :runtime
52
52
  prerelease: false
53
53
  version_requirements: !ruby/object:Gem::Requirement
54
54
  requirements:
55
55
  - - ">="
56
56
  - !ruby/object:Gem::Version
57
- version: 6.0.0
57
+ version: '6.1'
58
58
  - - "<"
59
59
  - !ruby/object:Gem::Version
60
- version: '7.2'
60
+ version: '7.3'
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: autotest-suffix
63
63
  requirement: !ruby/object:Gem::Requirement
@@ -589,7 +589,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
589
589
  - !ruby/object:Gem::Version
590
590
  version: '0'
591
591
  requirements: []
592
- rubygems_version: 3.5.6
592
+ rubygems_version: 3.5.23
593
593
  signing_key:
594
594
  specification_version: 4
595
595
  summary: Rails ActiveRecord connector for Google Spanner Database