activerecord-spanner-adapter 1.6.2 → 1.8.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 +5 -7
- data/.github/workflows/acceptance-tests-on-production.yaml +1 -1
- data/.github/workflows/ci.yaml +5 -7
- data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +6 -33
- data/.github/workflows/nightly-acceptance-tests-on-production.yaml +1 -1
- data/.github/workflows/nightly-unit-tests.yaml +7 -33
- data/.kokoro/populate-secrets.sh +5 -1
- data/.kokoro/release.cfg +22 -12
- data/.kokoro/trampoline_v2.sh +19 -11
- data/.release-please-manifest.json +1 -1
- data/.trampolinerc +6 -1
- data/CHANGELOG.md +28 -0
- data/Gemfile +5 -4
- data/README.md +1 -0
- data/Rakefile +2 -2
- data/acceptance/cases/migration/schema_dumper_test.rb +33 -7
- 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 +5 -0
- data/acceptance/schema/schema.rb +1 -0
- data/acceptance/test_helper.rb +5 -1
- data/activerecord-spanner-adapter.gemspec +3 -1
- data/examples/snippets/bit-reversed-sequence/README.md +6 -0
- data/examples/snippets/bit-reversed-sequence/db/migrate/01_create_tables.rb +3 -6
- data/examples/snippets/bit-reversed-sequence/db/schema.rb +1 -1
- data/lib/active_record/connection_adapters/spanner/database_statements.rb +5 -3
- data/lib/active_record/connection_adapters/spanner/quoting.rb +17 -5
- data/lib/active_record/connection_adapters/spanner/schema_dumper.rb +3 -1
- data/lib/active_record/connection_adapters/spanner/schema_statements.rb +11 -0
- data/lib/active_record/connection_adapters/spanner_adapter.rb +34 -20
- data/lib/activerecord-spanner-adapter.rb +5 -1
- data/lib/activerecord_spanner_adapter/base.rb +46 -12
- data/lib/activerecord_spanner_adapter/version.rb +1 -1
- metadata +21 -9
- data/.github/workflows/release-please-label.yml +0 -25
- data/.github/workflows/release-please.yml +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e521009bd7ee056c90e4147edd3e6dfe012e7808abf8dcf2b9a0145f380d669d
|
4
|
+
data.tar.gz: 9be31f9f418a9ca5f80531924bc299a75c64826d09989c7318aff88eaccf8a41
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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: "~>
|
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:
|
data/.github/workflows/ci.yaml
CHANGED
@@ -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
|
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: "~>
|
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
|
-
|
22
|
-
|
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:
|
27
|
-
ar:
|
28
|
-
- ruby: 3.0
|
29
|
-
ar:
|
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,41 +10,15 @@ jobs:
|
|
10
10
|
strategy:
|
11
11
|
max-parallel: 4
|
12
12
|
matrix:
|
13
|
-
# Run
|
14
|
-
ruby: [2.7, 3.0, 3.1, 3.2]
|
15
|
-
ar: [
|
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:
|
19
|
-
ar:
|
20
|
-
- ruby: 3.0
|
21
|
-
ar:
|
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:
|
data/.kokoro/populate-secrets.sh
CHANGED
@@ -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
|
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: "
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
}
|
48
|
+
define_artifacts {
|
49
|
+
regex: "github/ruby-spanner-activerecord/pkg/*.gem"
|
50
|
+
strip_prefix: "github"
|
51
|
+
}
|
52
|
+
}
|
data/.kokoro/trampoline_v2.sh
CHANGED
@@ -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
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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.
|
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"
|
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,33 @@
|
|
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
|
+
|
22
|
+
### 1.6.3 (2024-08-31)
|
23
|
+
|
24
|
+
#### Bug Fixes
|
25
|
+
|
26
|
+
* a few Ruby DSL schema dump bug fixes ([#308](https://github.com/googleapis/ruby-spanner-activerecord/issues/308))
|
27
|
+
#### Documentation
|
28
|
+
|
29
|
+
* update bit-reversed sequence sample ([#303](https://github.com/googleapis/ruby-spanner-activerecord/issues/303))
|
30
|
+
|
3
31
|
### 1.6.2 (2024-02-19)
|
4
32
|
|
5
33
|
#### Bug Fixes
|
data/Gemfile
CHANGED
@@ -3,16 +3,17 @@ 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", "~>
|
7
|
-
gem "
|
6
|
+
gem "activerecord", ENV.fetch("AR_VERSION", "~> 7.1.0")
|
7
|
+
gem "ostruct"
|
8
|
+
gem "minitest", "~> 5.25.0"
|
8
9
|
gem "minitest-rg", "~> 5.3.0"
|
9
10
|
gem "pry", "~> 0.13.0"
|
10
11
|
gem "pry-byebug", "~> 3.9.0"
|
11
12
|
# Add sqlite3 for testing for compatibility with other adapters.
|
12
|
-
gem
|
13
|
+
gem 'sqlite3', '~> 1.4'
|
13
14
|
|
14
15
|
# Required for samples and testing.
|
15
|
-
install_if -> { ENV.fetch("AR_VERSION", "~>
|
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/README.md
CHANGED
@@ -77,6 +77,7 @@ __NOTE__: You do need to have [Docker](https://docs.docker.com/get-docker/) inst
|
|
77
77
|
Some noteworthy examples in the snippets directory:
|
78
78
|
- [quickstart](examples/snippets/quickstart): A simple application that shows how to create and query a simple database containing two tables.
|
79
79
|
- [migrations](examples/snippets/migrations): Shows a best-practice for executing migrations on Cloud Spanner.
|
80
|
+
- [bit-reversed-sequences](examples/snippets/bit-reversed-sequence): Shows how to use bit-reversed sequences for primary keys.
|
80
81
|
- [read-write-transactions](examples/snippets/read-write-transactions): Shows how to execute transactions on Cloud Spanner.
|
81
82
|
- [read-only-transactions](examples/snippets/read-only-transactions): Shows how to execute read-only transactions on Cloud Spanner.
|
82
83
|
- [bulk-insert](examples/snippets/bulk-insert): Shows the best way to insert a large number of new records.
|
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["
|
60
|
-
ENV["
|
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 =
|
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,32 +59,46 @@ 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
|
88
|
+
|
89
|
+
def test_dump_schema_contains_string_array
|
90
|
+
connection = pool_or_connection
|
91
|
+
schema = StringIO.new
|
92
|
+
ActiveRecord::SchemaDumper.dump connection, schema
|
93
|
+
assert schema.string.include?("t.string \"col_array_string\", array: true"), schema.string
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_dump_schema_index_storing
|
97
|
+
connection = pool_or_connection
|
98
|
+
schema = StringIO.new
|
99
|
+
ActiveRecord::SchemaDumper.dump connection, schema
|
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
|
101
|
+
end
|
76
102
|
end
|
77
103
|
end
|
78
104
|
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
|
@@ -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
|
@@ -226,6 +227,7 @@ CREATE TABLE singers (
|
|
226
227
|
lock_version INT64,
|
227
228
|
full_name STRING(MAX) AS (COALESCE(first_name || ' ', '') || last_name) STORED,
|
228
229
|
) PRIMARY KEY(singerid);
|
230
|
+
CREATE INDEX index_singers_on_last_name ON singers(last_name) STORING (tracks_count, first_name);
|
229
231
|
CREATE TABLE albums (
|
230
232
|
albumid INT64 NOT NULL,
|
231
233
|
singerid INT64 NOT NULL,
|
@@ -383,6 +385,7 @@ CREATE TABLE singers (
|
|
383
385
|
lock_version INT64,
|
384
386
|
full_name STRING(MAX) AS (COALESCE(first_name || ' ', '') || last_name) STORED,
|
385
387
|
) PRIMARY KEY(singerid);
|
388
|
+
CREATE INDEX index_singers_on_last_name ON singers(last_name) STORING (tracks_count, first_name);
|
386
389
|
CREATE TABLE albums (
|
387
390
|
singerid INT64 NOT NULL,
|
388
391
|
albumid INT64 NOT NULL,
|
@@ -547,6 +550,7 @@ CREATE TABLE singers (
|
|
547
550
|
lock_version INT64,
|
548
551
|
full_name STRING(MAX) AS (COALESCE(first_name || ' ', '') || last_name) STORED,
|
549
552
|
) PRIMARY KEY(singerid);
|
553
|
+
CREATE INDEX index_singers_on_last_name ON singers(last_name) STORING (tracks_count, first_name);
|
550
554
|
CREATE TABLE albums (
|
551
555
|
albumid INT64 NOT NULL,
|
552
556
|
singerid INT64 NOT NULL,
|
@@ -707,6 +711,7 @@ CREATE TABLE singers (
|
|
707
711
|
lock_version INT64,
|
708
712
|
full_name STRING(MAX) AS (COALESCE(first_name || ' ', '') || last_name) STORED,
|
709
713
|
) PRIMARY KEY(singerid);
|
714
|
+
CREATE INDEX index_singers_on_last_name ON singers(last_name) STORING (tracks_count, first_name);
|
710
715
|
CREATE TABLE albums (
|
711
716
|
singerid INT64 NOT NULL,
|
712
717
|
albumid INT64 NOT NULL,
|
data/acceptance/schema/schema.rb
CHANGED
@@ -126,6 +126,7 @@ def create_tables_in_test_schema
|
|
126
126
|
t.integer :lock_version
|
127
127
|
t.virtual :full_name, type: :string, as: "COALESCE(first_name || ' ', '') || last_name", stored: true
|
128
128
|
end
|
129
|
+
add_index :singers, :last_name, storing: %i[tracks_count first_name]
|
129
130
|
|
130
131
|
if is_7_1_or_higher?
|
131
132
|
create_table :albums, primary_key: [:singerid, :albumid] do |t|
|
data/acceptance/test_helper.rb
CHANGED
@@ -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
|
@@ -25,7 +25,9 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.required_ruby_version = ">= 2.7"
|
26
26
|
|
27
27
|
spec.add_dependency "google-cloud-spanner", "~> 2.18"
|
28
|
-
|
28
|
+
# Pin gRPC to 1.64.3, as 1.65 and 1.66 cause test runs to hang randomly.
|
29
|
+
spec.add_dependency "grpc", "1.64.3"
|
30
|
+
spec.add_runtime_dependency "activerecord", [">= 6.1", "< 7.3"]
|
29
31
|
|
30
32
|
spec.add_development_dependency "autotest-suffix", "~> 1.1"
|
31
33
|
spec.add_development_dependency "bundler", "~> 2.0"
|
@@ -5,6 +5,12 @@ This example shows how to use a bit-reversed sequence to generate the primary ke
|
|
5
5
|
See https://cloud.google.com/spanner/docs/primary-key-default-value#bit-reversed-sequence for more information
|
6
6
|
about bit-reversed sequences in Cloud Spanner.
|
7
7
|
|
8
|
+
## Requirements
|
9
|
+
Using bit-reversed sequences for generating primary key values in ActiveRecord has the following requirements:
|
10
|
+
1. You must use __ActiveRecord version 7.1 or higher__.
|
11
|
+
2. Your models must include a sequence name like this: `self.sequence_name = :singer_sequence`
|
12
|
+
3. You must create the bit-reversed sequence using a SQL statement in your migrations.
|
13
|
+
|
8
14
|
## Creating Tables with Bit-Reversed Sequences in ActiveRecord
|
9
15
|
You can create bit-reversed sequences using migrations in ActiveRecord by executing a SQL statement using the underlying
|
10
16
|
connection.
|
@@ -8,15 +8,12 @@ class CreateTables < ActiveRecord::Migration[7.1]
|
|
8
8
|
def change
|
9
9
|
# Execute the entire migration as one DDL batch.
|
10
10
|
connection.ddl_batch do
|
11
|
-
|
12
|
-
# connection.execute "create sequence singer_sequence OPTIONS (sequence_kind = 'bit_reversed_positive')"
|
11
|
+
connection.execute "create sequence singer_sequence OPTIONS (sequence_kind = 'bit_reversed_positive')"
|
13
12
|
|
14
13
|
# Explicitly define the primary key.
|
15
14
|
create_table :singers, id: false, primary_key: :singerid do |t|
|
16
|
-
|
17
|
-
|
18
|
-
# default: -> { "GET_NEXT_SEQUENCE_VALUE(SEQUENCE singer_sequence)" }
|
19
|
-
t.integer :singerid, primary_key: true, null: false, default: -> { "FARM_FINGERPRINT(GENERATE_UUID())" }
|
15
|
+
t.integer :singerid, primary_key: true, null: false,
|
16
|
+
default: -> { "GET_NEXT_SEQUENCE_VALUE(SEQUENCE singer_sequence)" }
|
20
17
|
t.string :first_name
|
21
18
|
t.string :last_name
|
22
19
|
end
|
@@ -19,7 +19,7 @@ ActiveRecord::Schema[7.1].define(version: 1) do
|
|
19
19
|
t.string "title"
|
20
20
|
end
|
21
21
|
|
22
|
-
create_table "singers", primary_key: "singerid", default: -> { "
|
22
|
+
create_table "singers", primary_key: "singerid", default: -> { "GET_NEXT_SEQUENCE_VALUE(SEQUENCE singer_sequence)" }, force: :cascade do |t|
|
23
23
|
t.string "first_name"
|
24
24
|
t.string "last_name"
|
25
25
|
end
|
@@ -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] ||= "`#{
|
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] ||=
|
55
|
+
QUOTED_TABLE_NAMES[name] ||= "`#{name.to_s.gsub '.', '`.`'}`".freeze
|
44
56
|
end
|
45
57
|
|
46
58
|
STR_ESCAPE_REGX = /[\n\r'\\]/.freeze
|
@@ -27,6 +27,8 @@ module ActiveRecord
|
|
27
27
|
spec = { type: schema_type(column).inspect }.merge! spec
|
28
28
|
end
|
29
29
|
|
30
|
+
spec[:array] = true if column.sql_type.start_with? "ARRAY<"
|
31
|
+
|
30
32
|
spec
|
31
33
|
end
|
32
34
|
|
@@ -54,7 +56,7 @@ module ActiveRecord
|
|
54
56
|
index_parts = super
|
55
57
|
index_parts << "null_filtered: #{index.null_filtered.inspect}" if index.null_filtered
|
56
58
|
index_parts << "interleave_in: #{index.interleave_in.inspect}" if index.interleave_in
|
57
|
-
index_parts << "storing: #{format_index_parts index.storing}" if index.storing.present?
|
59
|
+
index_parts << "storing: #{format_index_parts index.storing.sort}" if index.storing.present?
|
58
60
|
index_parts
|
59
61
|
end
|
60
62
|
|
@@ -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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
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
|
-
|
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.
|
52
|
-
|
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
|
115
|
-
|
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
|
-
|
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
|
-
|
162
|
+
_internal_insert_record record
|
132
163
|
end
|
133
164
|
end
|
134
165
|
end
|
135
166
|
end
|
136
167
|
|
137
|
-
def self.
|
168
|
+
def self.upsert attributes, returning: nil, **kwargs
|
138
169
|
return super unless spanner_adapter?
|
139
|
-
if active_transaction? && !buffered_mutations?
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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.
|
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.
|
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-
|
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
|
@@ -24,26 +24,40 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.18'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: grpc
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.64.3
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.64.3
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: activerecord
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
30
44
|
requirements:
|
31
45
|
- - ">="
|
32
46
|
- !ruby/object:Gem::Version
|
33
|
-
version: 6.
|
47
|
+
version: '6.1'
|
34
48
|
- - "<"
|
35
49
|
- !ruby/object:Gem::Version
|
36
|
-
version: '7.
|
50
|
+
version: '7.3'
|
37
51
|
type: :runtime
|
38
52
|
prerelease: false
|
39
53
|
version_requirements: !ruby/object:Gem::Requirement
|
40
54
|
requirements:
|
41
55
|
- - ">="
|
42
56
|
- !ruby/object:Gem::Version
|
43
|
-
version: 6.
|
57
|
+
version: '6.1'
|
44
58
|
- - "<"
|
45
59
|
- !ruby/object:Gem::Version
|
46
|
-
version: '7.
|
60
|
+
version: '7.3'
|
47
61
|
- !ruby/object:Gem::Dependency
|
48
62
|
name: autotest-suffix
|
49
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -230,8 +244,6 @@ files:
|
|
230
244
|
- ".github/workflows/nightly-acceptance-tests-on-emulator.yaml"
|
231
245
|
- ".github/workflows/nightly-acceptance-tests-on-production.yaml"
|
232
246
|
- ".github/workflows/nightly-unit-tests.yaml"
|
233
|
-
- ".github/workflows/release-please-label.yml"
|
234
|
-
- ".github/workflows/release-please.yml"
|
235
247
|
- ".github/workflows/rubocop.yaml"
|
236
248
|
- ".gitignore"
|
237
249
|
- ".kokoro/populate-secrets.sh"
|
@@ -577,7 +589,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
577
589
|
- !ruby/object:Gem::Version
|
578
590
|
version: '0'
|
579
591
|
requirements: []
|
580
|
-
rubygems_version: 3.5.
|
592
|
+
rubygems_version: 3.5.23
|
581
593
|
signing_key:
|
582
594
|
specification_version: 4
|
583
595
|
summary: Rails ActiveRecord connector for Google Spanner Database
|
@@ -1,25 +0,0 @@
|
|
1
|
-
name: release-please-label
|
2
|
-
on:
|
3
|
-
pull_request_target:
|
4
|
-
branches:
|
5
|
-
- main
|
6
|
-
types:
|
7
|
-
- opened
|
8
|
-
jobs:
|
9
|
-
release-please-label:
|
10
|
-
if: "${{ github.event.sender.login == 'yoshi-code-bot' && startsWith(github.event.pull_request.title, 'chore(main): release ') }}"
|
11
|
-
runs-on: ubuntu-latest
|
12
|
-
steps:
|
13
|
-
- name: ReleaseLabel
|
14
|
-
uses: actions/github-script@v7
|
15
|
-
with:
|
16
|
-
github-token: ${{secrets.YOSHI_APPROVER_TOKEN}}
|
17
|
-
script: |
|
18
|
-
core.info('Labeling release');
|
19
|
-
await github.rest.issues.addLabels({
|
20
|
-
owner: context.repo.owner,
|
21
|
-
repo: context.repo.repo,
|
22
|
-
issue_number: context.payload.pull_request.number,
|
23
|
-
labels: ['autorelease: pending']
|
24
|
-
});
|
25
|
-
core.info('Labeled');
|
@@ -1,38 +0,0 @@
|
|
1
|
-
name: Release-Please
|
2
|
-
on:
|
3
|
-
workflow_dispatch:
|
4
|
-
inputs:
|
5
|
-
gem:
|
6
|
-
description: "Name of single gem to release. Leave blank to check all gems."
|
7
|
-
required: false
|
8
|
-
args:
|
9
|
-
description: "Extra command line arguments."
|
10
|
-
required: false
|
11
|
-
|
12
|
-
jobs:
|
13
|
-
release-please:
|
14
|
-
if: ${{ github.repository == 'googleapis/ruby-spanner-activerecord' }}
|
15
|
-
runs-on: ubuntu-latest
|
16
|
-
env:
|
17
|
-
GITHUB_TOKEN: ${{ secrets.YOSHI_CODE_BOT_TOKEN }}
|
18
|
-
RELEASE_PLEASE_DISABLE: ${{ secrets.RELEASE_PLEASE_DISABLE }}
|
19
|
-
steps:
|
20
|
-
- name: Checkout repo
|
21
|
-
uses: actions/checkout@v4
|
22
|
-
- name: Install Ruby 3.0
|
23
|
-
uses: ruby/setup-ruby@v1
|
24
|
-
with:
|
25
|
-
ruby-version: "3.0"
|
26
|
-
- name: Install NodeJS 16.x
|
27
|
-
uses: actions/setup-node@v4
|
28
|
-
with:
|
29
|
-
node-version: "16.x"
|
30
|
-
- name: Install tools
|
31
|
-
run: "gem install --no-document toys"
|
32
|
-
- name: execute
|
33
|
-
run: |
|
34
|
-
toys release manifest -v \
|
35
|
-
--fork --skip-labeling \
|
36
|
-
--github-event-name=${{ github.event_name }} \
|
37
|
-
${{ github.event.inputs.args }} \
|
38
|
-
-- ${{ github.event.inputs.gem }}
|