activerecord-spanner-adapter 2.4.0 → 2.5.1

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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -1
  3. data/.github/workflows/acceptance-tests-on-emulator.yaml +3 -1
  4. data/.github/workflows/ci.yaml +3 -1
  5. data/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +3 -1
  6. data/.github/workflows/nightly-unit-tests.yaml +3 -1
  7. data/.release-please-manifest.json +1 -1
  8. data/CHANGELOG.md +16 -0
  9. data/Gemfile +6 -1
  10. data/README.md +1 -0
  11. data/acceptance/cases/migration/change_schema_test.rb +1 -1
  12. data/acceptance/cases/migration/index_test.rb +2 -2
  13. data/acceptance/cases/migration/schema_dumper_test.rb +1 -1
  14. data/acceptance/cases/tasks/database_tasks_test.rb +8 -0
  15. data/acceptance/cases/type/all_types_test.rb +15 -4
  16. data/acceptance/cases/type/time_test.rb +123 -3
  17. data/acceptance/schema/schema.rb +2 -0
  18. data/activerecord-spanner-adapter.gemspec +2 -2
  19. data/examples/snippets/Rakefile +21 -0
  20. data/lib/active_record/connection_adapters/spanner/column.rb +20 -7
  21. data/lib/active_record/connection_adapters/spanner/schema_creation.rb +5 -1
  22. data/lib/active_record/connection_adapters/spanner/schema_statements.rb +32 -12
  23. data/lib/active_record/connection_adapters/spanner/type_mapping.rb +26 -0
  24. data/lib/active_record/connection_adapters/spanner_adapter.rb +8 -17
  25. data/lib/active_record/type/spanner/spanner_active_record_converter.rb +1 -0
  26. data/lib/active_record/type/spanner/time.rb +22 -9
  27. data/lib/active_record/type/spanner/uuid.rb +19 -0
  28. data/lib/activerecord_spanner_adapter/connection.rb +5 -0
  29. data/lib/activerecord_spanner_adapter/version.rb +1 -1
  30. metadata +13 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb3f5f2106d42a64cf32616e119f188cc13a1b81acfa040765ae4b5cb2838cf1
4
- data.tar.gz: 5ff65bbec67470ddc479f6ba985e9d31c0562848b9f0739e697ceb1d6715e3dc
3
+ metadata.gz: 69f3ecf02a5d1f545a2a9a0182a768e9bad73c8223b54acdc38e38b9f5a639fb
4
+ data.tar.gz: 641d598c91a69eeb4af93fced37f9842334fb42d95378013711d9fa08ef10f88
5
5
  SHA512:
6
- metadata.gz: 939b508009ff79969637045e526dab1a73ed2f9b979fc6812447b2b98e673dce42e2d55bbf924fc6b372c35e481a374f8fc556c853b7861b8344735549ca3521
7
- data.tar.gz: f9c2dc8d41a7748a12daa8e342ae020c5109f793f62f0d2c506842d7030cac9d9578e169e356728f80e7c590e0a48bfa75fd05c235b121c25c9858e84cd25463
6
+ metadata.gz: 951478249b5dc96f1dec3b9b1aa0b8764a361963e129283e5cb22ae391fc85d3572cf5a3056442c09a46732237524c89807aede0f0bb55b8c938c92b6a891475
7
+ data.tar.gz: 2d36a28118c79c5680227a8a7cba2ba3a07586168229151bbeae764ecf1ef36e70c6bb4327703d5835e18fa5b0093cfc9a49f6f2555ecb3ab670bbf2cda4cbda
data/.github/CODEOWNERS CHANGED
@@ -4,4 +4,4 @@
4
4
  # For syntax help see:
5
5
  # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
6
6
 
7
- * @googleapis/ruby-team @olavloite @rahul2393 @ansh0l
7
+ * @googleapis/cloud-sdk-ruby-team @olavloite @rahul2393 @sakthivelmanii
@@ -19,11 +19,13 @@ jobs:
19
19
  max-parallel: 4
20
20
  matrix:
21
21
  ruby: ["3.1", "3.2", "3.3", "3.4"]
22
- ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"]
22
+ ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0", "~> 8.1.0"]
23
23
  # Exclude combinations that are not supported.
24
24
  exclude:
25
25
  - ruby: "3.1"
26
26
  ar: "~> 8.0.0"
27
+ - ruby: "3.1"
28
+ ar: "~> 8.1.0"
27
29
  - ruby: "3.4"
28
30
  ar: "~> 7.0.0"
29
31
  - ruby: "3.4"
@@ -11,11 +11,13 @@ jobs:
11
11
  max-parallel: 4
12
12
  matrix:
13
13
  ruby: ["3.1", "3.2", "3.3", "3.4"]
14
- ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"]
14
+ ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0", "~> 8.1.0"]
15
15
  # Exclude combinations that are not supported.
16
16
  exclude:
17
17
  - ruby: "3.1"
18
18
  ar: "~> 8.0.0"
19
+ - ruby: "3.1"
20
+ ar: "~> 8.1.0"
19
21
  - ruby: "3.4"
20
22
  ar: "~> 7.0.0"
21
23
  - ruby: "3.4"
@@ -19,11 +19,13 @@ jobs:
19
19
  max-parallel: 4
20
20
  matrix:
21
21
  ruby: ["3.1", "3.2", "3.3", "3.4"]
22
- ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"]
22
+ ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0", "~> 8.1.0"]
23
23
  # Exclude combinations that are not supported.
24
24
  exclude:
25
25
  - ruby: "3.1"
26
26
  ar: "~> 8.0.0"
27
+ - ruby: "3.1"
28
+ ar: "~> 8.1.0"
27
29
  - ruby: "3.4"
28
30
  ar: "~> 7.0.0"
29
31
  - ruby: "3.4"
@@ -12,11 +12,13 @@ jobs:
12
12
  matrix:
13
13
  # Run acceptance tests all supported combinations of Ruby and ActiveRecord.
14
14
  ruby: ["3.1", "3.2", "3.3", "3.4"]
15
- ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"]
15
+ ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0", "~> 8.1.0"]
16
16
  # Exclude combinations that are not supported.
17
17
  exclude:
18
18
  - ruby: "3.1"
19
19
  ar: "~> 8.0.0"
20
+ - ruby: "3.1"
21
+ ar: "~> 8.1.0"
20
22
  - ruby: "3.4"
21
23
  ar: "~> 7.0.0"
22
24
  - ruby: "3.4"
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "2.4.0"
2
+ ".": "2.5.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ### 2.5.1 (2026-06-30)
4
+
5
+ #### Bug Fixes
6
+
7
+ * use DateTime instead of Time for time data type ([#395](https://github.com/googleapis/ruby-spanner-activerecord/issues/395))
8
+
9
+ ### 2.5.0 (2026-02-11)
10
+
11
+ #### Features
12
+
13
+ * add support for the UUID data type ([#387](https://github.com/googleapis/ruby-spanner-activerecord/issues/387))
14
+ * support ActiveRecord 8.1.x ([#385](https://github.com/googleapis/ruby-spanner-activerecord/issues/385))
15
+ #### Documentation
16
+
17
+ * update README to reflect support for Rails 8.1.x ([#388](https://github.com/googleapis/ruby-spanner-activerecord/issues/388))
18
+
3
19
  ### 2.4.0 (2026-02-04)
4
20
 
5
21
  #### Features
data/Gemfile CHANGED
@@ -7,7 +7,7 @@ ar_version = ENV.fetch("AR_VERSION", "~> 7.1.0")
7
7
  gem "activerecord", ar_version
8
8
  gem "ostruct"
9
9
  gem "minitest", "~> 5.27.0"
10
- gem "minitest-rg", "~> 5.3.0"
10
+ gem "minitest-rg", "~> 5.4.0"
11
11
  gem "pry", "~> 0.14.2"
12
12
  gem "pry-byebug", "~> 3.11.0"
13
13
  gem "mutex_m"
@@ -22,3 +22,8 @@ end
22
22
  # Required for samples
23
23
  gem "docker-api"
24
24
  gem "sinatra-activerecord"
25
+
26
+ # Force google-protobuf to compile from source to avoid ABI issues in CI
27
+ if ENV["CI"]
28
+ gem "google-protobuf", force_ruby_platform: true
29
+ end
data/README.md CHANGED
@@ -15,6 +15,7 @@ This project provides a Spanner adapter for ActiveRecord. It supports the follow
15
15
  - ActiveRecord 7.1.x with Ruby 3.1 and higher.
16
16
  - ActiveRecord 7.2.x with Ruby 3.1 and higher.
17
17
  - ActiveRecord 8.0.x with Ruby 3.2 and higher.
18
+ - ActiveRecord 8.1.x with Ruby 3.2 and higher.
18
19
 
19
20
  Known limitations are listed in the [Limitations](#limitations) section.
20
21
  Please report any problems that you might encounter by [creating a new issue](https://github.com/googleapis/ruby-spanner-activerecord/issues/new).
@@ -300,7 +300,7 @@ module ActiveRecord
300
300
 
301
301
  column = connection.columns(:testings).find { |c| c.name == "foo" }
302
302
 
303
- assert_equal :time, column.type
303
+ assert_equal :datetime, column.type
304
304
  assert_equal "TIMESTAMP", column.sql_type
305
305
  end
306
306
 
@@ -57,7 +57,7 @@ module ActiveRecord
57
57
  e = assert_raises(ArgumentError) {
58
58
  connection.rename_index(table_name, "old_idx", too_long_index_name)
59
59
  }
60
- assert_match(/too long; the limit is #{connection.index_name_length} characters/, e.message)
60
+ assert_match(/too long/, e.message)
61
61
 
62
62
  assert connection.index_name_exists?(table_name, "old_idx")
63
63
  end
@@ -79,7 +79,7 @@ module ActiveRecord
79
79
  e = assert_raises(ArgumentError) {
80
80
  connection.add_index(table_name, "foo", name: too_long_index_name)
81
81
  }
82
- assert_match(/too long; the limit is #{connection.index_name_length} characters/, e.message)
82
+ assert_match(/too long/, e.message)
83
83
 
84
84
  assert_not connection.index_name_exists?(table_name, too_long_index_name)
85
85
  connection.add_index(table_name, "foo", name: good_index_name)
@@ -76,7 +76,7 @@ module ActiveRecord
76
76
  connection = pool_or_connection
77
77
  schema = StringIO.new
78
78
  ActiveRecord::SchemaDumper.dump connection, schema
79
- assert schema.string.include?("t.time \"last_updated\", allow_commit_timestamp: true"), schema.string
79
+ assert_match(/t\.(datetime|timestamp) "last_updated",.*allow_commit_timestamp: true/, schema.string, schema.string)
80
80
  end
81
81
 
82
82
  def test_dump_schema_contains_virtual_column
@@ -129,6 +129,7 @@ module ActiveRecord
129
129
  col_date DATE,
130
130
  col_timestamp TIMESTAMP,
131
131
  col_json JSON,
132
+ col_uuid UUID,
132
133
  col_array_string ARRAY<STRING(MAX)>,
133
134
  col_array_int64 ARRAY<INT64>,
134
135
  col_array_float64 ARRAY<FLOAT64>,
@@ -138,6 +139,7 @@ module ActiveRecord
138
139
  col_array_date ARRAY<DATE>,
139
140
  col_array_timestamp ARRAY<TIMESTAMP>,
140
141
  col_array_json ARRAY<JSON>,
142
+ col_array_uuid ARRAY<UUID>,
141
143
  ) PRIMARY KEY(id);
142
144
  CREATE TABLE firms (
143
145
  id INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE),
@@ -287,6 +289,7 @@ INSERT INTO `schema_migrations` (version) VALUES
287
289
  col_date DATE,
288
290
  col_timestamp TIMESTAMP,
289
291
  col_json JSON,
292
+ col_uuid UUID,
290
293
  col_array_string ARRAY<STRING(MAX)>,
291
294
  col_array_int64 ARRAY<INT64>,
292
295
  col_array_float64 ARRAY<FLOAT64>,
@@ -296,6 +299,7 @@ INSERT INTO `schema_migrations` (version) VALUES
296
299
  col_array_date ARRAY<DATE>,
297
300
  col_array_timestamp ARRAY<TIMESTAMP>,
298
301
  col_array_json ARRAY<JSON>,
302
+ col_array_uuid ARRAY<UUID>,
299
303
  ) PRIMARY KEY(id);
300
304
  CREATE TABLE firms (
301
305
  id INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE),
@@ -463,6 +467,7 @@ CREATE TABLE all_types (
463
467
  col_date DATE,
464
468
  col_timestamp TIMESTAMP,
465
469
  col_json JSON,
470
+ col_uuid UUID,
466
471
  col_array_string ARRAY<STRING(MAX)>,
467
472
  col_array_int64 ARRAY<INT64>,
468
473
  col_array_float64 ARRAY<FLOAT64>,
@@ -472,6 +477,7 @@ CREATE TABLE all_types (
472
477
  col_array_date ARRAY<DATE>,
473
478
  col_array_timestamp ARRAY<TIMESTAMP>,
474
479
  col_array_json ARRAY<JSON>,
480
+ col_array_uuid ARRAY<UUID>,
475
481
  ) PRIMARY KEY(id);
476
482
  CREATE TABLE ar_internal_metadata (
477
483
  key STRING(MAX) NOT NULL,
@@ -624,6 +630,7 @@ CREATE TABLE all_types (
624
630
  col_date DATE,
625
631
  col_timestamp TIMESTAMP,
626
632
  col_json JSON,
633
+ col_uuid UUID,
627
634
  col_array_string ARRAY<STRING(MAX)>,
628
635
  col_array_int64 ARRAY<INT64>,
629
636
  col_array_float64 ARRAY<FLOAT64>,
@@ -633,6 +640,7 @@ CREATE TABLE all_types (
633
640
  col_array_date ARRAY<DATE>,
634
641
  col_array_timestamp ARRAY<TIMESTAMP>,
635
642
  col_array_json ARRAY<JSON>,
643
+ col_array_uuid ARRAY<UUID>,
636
644
  ) PRIMARY KEY(id);
637
645
  CREATE TABLE ar_internal_metadata (
638
646
  key STRING(MAX) NOT NULL,
@@ -30,7 +30,8 @@ module ActiveRecord
30
30
  AllTypes.create col_string: "string", col_int64: 100, col_float64: 3.14, col_numeric: 6.626, col_bool: true,
31
31
  col_bytes: StringIO.new("bytes"), col_date: ::Date.new(2021, 6, 23),
32
32
  col_timestamp: ::Time.new(2021, 6, 23, 17, 8, 21, "+02:00"),
33
- col_json: { kind: "user_renamed", change: %w[jack john]},
33
+ col_json: { kind: "user_renamed", change: %w[jack john] },
34
+ col_uuid: "3840ba25-55df-4cb7-a210-1fe37278954f",
34
35
  col_array_string: ["string1", nil, "string2"],
35
36
  col_array_int64: [100, nil, 200, "300"],
36
37
  col_array_float64: [3.14, nil, 2.0/3.0, "3.14"],
@@ -42,7 +43,8 @@ module ActiveRecord
42
43
  ::Time.new(2021, 6, 24, 17, 8, 21, "+02:00"), "2021-06-25 17:08:21 +02:00"],
43
44
  col_array_json: [{ kind: "user_renamed", change: %w[jack john]}, nil, \
44
45
  { kind: "user_renamed", change: %w[alice meredith]},
45
- "{\"kind\":\"user_renamed\",\"change\":[\"bob\",\"carol\"]}"]
46
+ "{\"kind\":\"user_renamed\",\"change\":[\"bob\",\"carol\"]}"],
47
+ col_array_uuid: ["e986c19c-9bf3-44f8-851d-710ad3d0067b", nil, "a02f50b6-2860-48dc-acd6-49b42abc3094"]
46
48
  end
47
49
 
48
50
  def test_create_record
@@ -67,6 +69,7 @@ module ActiveRecord
67
69
  assert_equal ::Time.new(2021, 6, 23, 17, 8, 21, "+02:00").utc, record.col_timestamp.utc
68
70
  assert_equal ({"kind" => "user_renamed", "change" => %w[jack john]}),
69
71
  record.col_json
72
+ assert_equal "3840ba25-55df-4cb7-a210-1fe37278954f", record.col_uuid
70
73
 
71
74
  assert_equal ["string1", nil, "string2"], record.col_array_string
72
75
  assert_equal [100, nil, 200, 300], record.col_array_int64
@@ -85,6 +88,7 @@ module ActiveRecord
85
88
  {"kind" => "user_renamed", "change" => %w[alice meredith]}, \
86
89
  "{\"kind\":\"user_renamed\",\"change\":[\"bob\",\"carol\"]}"],
87
90
  record.col_array_json
91
+ assert_equal ["e986c19c-9bf3-44f8-851d-710ad3d0067b", nil, "a02f50b6-2860-48dc-acd6-49b42abc3094"], record.col_array_uuid
88
92
  end
89
93
  end
90
94
 
@@ -100,6 +104,7 @@ module ActiveRecord
100
104
  col_date: ::Date.new(2021, 6, 28),
101
105
  col_timestamp: ::Time.new(2021, 6, 28, 11, 22, 21, "+02:00"),
102
106
  col_json: { kind: "user_created", change: %w[jack alice]},
107
+ col_uuid: "b493579f-f4f6-41f6-a520-8373ecf1cde4",
103
108
  col_array_string: ["new string 1", "new string 2"],
104
109
  col_array_int64: [300, 200, 100],
105
110
  col_array_float64: [1.1, 2.2, 3.3],
@@ -108,7 +113,8 @@ module ActiveRecord
108
113
  col_array_bytes: [StringIO.new("new bytes 1"), StringIO.new("new bytes 2")],
109
114
  col_array_date: [::Date.new(2021, 6, 28)],
110
115
  col_array_timestamp: [::Time.utc(2020, 12, 31, 0, 0, 0)],
111
- col_array_json: [{ kind: "user_created", change: %w[jack alice]}]
116
+ col_array_json: [{ kind: "user_created", change: %w[jack alice]}],
117
+ col_array_uuid: ["a664b1eb-4dc7-4795-9ed8-28fab6644cea", nil, "1c794d81-bc6d-4771-9c92-a920d28a7aaa"]
112
118
  end
113
119
 
114
120
  # Verify that the record was updated.
@@ -123,6 +129,7 @@ module ActiveRecord
123
129
  assert_equal ::Time.new(2021, 6, 28, 11, 22, 21, "+02:00").utc, record.col_timestamp.utc
124
130
  assert_equal ({"kind" => "user_created", "change" => %w[jack alice]}),
125
131
  record.col_json
132
+ assert_equal "b493579f-f4f6-41f6-a520-8373ecf1cde4", record.col_uuid
126
133
 
127
134
  assert_equal ["new string 1", "new string 2"], record.col_array_string
128
135
  assert_equal [300, 200, 100], record.col_array_int64
@@ -135,6 +142,8 @@ module ActiveRecord
135
142
  assert_equal [::Time.utc(2020, 12, 31, 0, 0, 0)], record.col_array_timestamp.map(&:utc)
136
143
  assert_equal [{"kind" => "user_created", "change" => %w[jack alice]}],
137
144
  record.col_array_json
145
+ assert_equal ["a664b1eb-4dc7-4795-9ed8-28fab6644cea", nil, "1c794d81-bc6d-4771-9c92-a920d28a7aaa"],
146
+ record.col_array_uuid
138
147
  end
139
148
  end
140
149
 
@@ -151,7 +160,8 @@ module ActiveRecord
151
160
  col_array_bytes: [],
152
161
  col_array_date: [],
153
162
  col_array_timestamp: [],
154
- col_array_json: []
163
+ col_array_json: [],
164
+ col_array_uuid: []
155
165
  end
156
166
 
157
167
  record = AllTypes.find record.id
@@ -164,6 +174,7 @@ module ActiveRecord
164
174
  assert_equal [], record.col_array_date
165
175
  assert_equal [], record.col_array_timestamp
166
176
  assert_equal [], record.col_array_json
177
+ assert_equal [], record.col_array_uuid
167
178
  end
168
179
  end
169
180
  end
@@ -45,7 +45,7 @@ module ActiveRecord
45
45
  end
46
46
 
47
47
  def test_date_time_string_value_with_subsecond_precision
48
- expected_time = ::Time.local 2017, 07, 4, 14, 19, 10, 897761
48
+ expected_time = ::Time.utc 2017, 7, 4, 14, 19, 10, 897761
49
49
 
50
50
  string_value = "2017-07-04 14:19:10.897761"
51
51
 
@@ -53,6 +53,7 @@ module ActiveRecord
53
53
  assert_equal expected_time, record.start_time.utc
54
54
 
55
55
  record.save!
56
+ record.reload
56
57
  assert_equal expected_time, record.start_time.utc
57
58
 
58
59
  assert_equal record, TestTypeModel.find_by(start_time: string_value)
@@ -60,12 +61,13 @@ module ActiveRecord
60
61
 
61
62
  def test_date_time_with_string_value_with_non_iso_format
62
63
  string_value = "04/07/2017 2:19pm"
63
- expected_time = ::Time.local 2017, 07, 4, 14, 19
64
+ expected_time = ::Time.utc 2017, 7, 4, 14, 19
64
65
 
65
66
  record = TestTypeModel.new start_time: string_value
66
67
  assert_equal expected_time, record.start_time
67
68
 
68
69
  record.save!
70
+ record.reload
69
71
  assert_equal expected_time, record.start_time.utc
70
72
 
71
73
  assert_equal record, TestTypeModel.find_by(start_time: string_value)
@@ -82,6 +84,124 @@ module ActiveRecord
82
84
 
83
85
  assert_equal expected_time, record.start_time
84
86
  end
87
+
88
+ def test_multiparameter_datetime
89
+ expected_time = ::Time.utc 2020, 12, 25, 10, 30, 0
90
+ record = TestTypeModel.new start_time: { 1 => 2020, 2 => 12, 3 => 25, 4 => 10, 5 => 30 }
91
+
92
+ assert_equal expected_time, record.start_time
93
+
94
+ record.save!
95
+ record.reload
96
+
97
+ assert_equal expected_time, record.start_time
98
+ end
99
+
100
+ def test_date_time_string_value_with_timezone_aware_attributes
101
+ old_zone = ::Time.zone
102
+ ::Time.zone = "UTC"
103
+ TestTypeModel.time_zone_aware_attributes = true
104
+ TestTypeModel.reset_column_information
105
+
106
+ string_value = "2017-07-04 14:19:10.897761"
107
+ expected_time = ::Time.zone.local 2017, 7, 4, 14, 19, 10, 897761
108
+
109
+ record = TestTypeModel.new start_time: string_value
110
+ assert_equal expected_time, record.start_time
111
+
112
+ record.save!
113
+ record.reload
114
+ assert_equal expected_time, record.start_time
115
+
116
+ assert_equal record, TestTypeModel.find_by(start_time: string_value)
117
+ ensure
118
+ TestTypeModel.time_zone_aware_attributes = false
119
+ TestTypeModel.reset_column_information
120
+ ::Time.zone = old_zone
121
+ end
122
+
123
+ def test_string_value_with_paris_timezone
124
+ old_zone = ::Time.zone
125
+ ::Time.zone = "Paris" # Paris is UTC+2 in July (DST)
126
+ TestTypeModel.time_zone_aware_attributes = true
127
+ TestTypeModel.reset_column_information
128
+
129
+ begin
130
+ string_value = "2017-07-04 14:19:10.897761123"
131
+ # 14:19:10 in Paris (DST) is 12:19:10 UTC
132
+ # Subseconds: 897761123 nanoseconds = 897761.123 microseconds
133
+ expected_time = ::Time.zone.local(2017, 7, 4, 14, 19, 10, Rational(897761123, 1000))
134
+
135
+ record = TestTypeModel.new start_time: string_value
136
+ assert_equal expected_time, record.start_time
137
+ assert_equal 897761123, record.start_time.nsec
138
+ assert_instance_of ActiveSupport::TimeWithZone, record.start_time
139
+ assert_equal "Paris", record.start_time.time_zone.name
140
+
141
+ record.save!
142
+
143
+ # Verify directly in the database using the raw Spanner client (bypasses ActiveRecord)
144
+ raw_client = Google::Cloud::Spanner.new(
145
+ project: connector_config["project"],
146
+ emulator_host: connector_config["emulator_host"]
147
+ )
148
+ db_client = raw_client.client(
149
+ connector_config["instance"],
150
+ connector_config["database"]
151
+ )
152
+
153
+ results = db_client.execute "SELECT start_time FROM test_types WHERE id = #{record.id}"
154
+ raw_value = results.rows.first[:start_time]
155
+
156
+ # Spanner client returns TIMESTAMP as a UTC Time object
157
+ expected_raw_time = ::Time.utc(2017, 7, 4, 12, 19, 10, Rational(897761123, 1000))
158
+ assert_equal expected_raw_time, raw_value
159
+ assert_equal 897761123, raw_value.nsec
160
+ assert_equal "UTC", raw_value.zone
161
+
162
+ record.reload
163
+
164
+ assert_equal expected_time, record.start_time
165
+ assert_equal 897761123, record.start_time.nsec
166
+ assert_instance_of ActiveSupport::TimeWithZone, record.start_time
167
+ assert_equal "Paris", record.start_time.time_zone.name
168
+ ensure
169
+ TestTypeModel.time_zone_aware_attributes = false
170
+ TestTypeModel.reset_column_information
171
+ ::Time.zone = old_zone
172
+ end
173
+ end
174
+
175
+ def test_string_value_with_utc_and_local_timezones
176
+ # 1. With 'Z' (UTC)
177
+ string_value_utc = "2017-07-04 14:19:10.897761123Z"
178
+ expected_time_utc = ::Time.utc 2017, 7, 4, 14, 19, 10, Rational(897761123, 1000)
179
+ record = TestTypeModel.new start_time: string_value_utc
180
+ assert_equal expected_time_utc, record.start_time
181
+ assert_equal 897761123, record.start_time.nsec
182
+
183
+ # 2. With Offset (+02:00)
184
+ string_value_offset = "2017-07-04 14:19:10.897761123+02:00"
185
+ # 14:19:10+02:00 is 12:19:10 UTC
186
+ expected_time_offset = ::Time.utc 2017, 7, 4, 12, 19, 10, Rational(897761123, 1000)
187
+ record = TestTypeModel.new start_time: string_value_offset
188
+ assert_equal expected_time_offset, record.start_time
189
+ assert_equal 897761123, record.start_time.nsec
190
+
191
+ # 3. Without offset, but with ActiveRecord.default_timezone = :local
192
+ old_default_timezone = ActiveRecord.default_timezone
193
+ ActiveRecord.default_timezone = :local
194
+ begin
195
+ string_value_no_offset = "2017-07-04 14:19:10.897761123"
196
+ # Should be interpreted as local time
197
+ expected_time_local = ::Time.local 2017, 7, 4, 14, 19, 10, Rational(897761123, 1000)
198
+ record = TestTypeModel.new start_time: string_value_no_offset
199
+ assert_equal expected_time_local, record.start_time
200
+ assert_equal 897761123, record.start_time.nsec
201
+ ensure
202
+ ActiveRecord.default_timezone = old_default_timezone
203
+ end
204
+ end
85
205
  end
86
206
  end
87
- end
207
+ end
@@ -24,6 +24,7 @@ def create_tables_in_test_schema
24
24
  t.column :col_date, :date
25
25
  t.column :col_timestamp, :datetime
26
26
  t.column :col_json, :json
27
+ t.column :col_uuid, :uuid
27
28
 
28
29
  t.column :col_array_string, :string, array: true
29
30
  t.column :col_array_int64, :bigint, array: true
@@ -34,6 +35,7 @@ def create_tables_in_test_schema
34
35
  t.column :col_array_date, :date, array: true
35
36
  t.column :col_array_timestamp, :datetime, array: true
36
37
  t.column :col_array_json, :json, array: true
38
+ t.column :col_array_uuid, :uuid, array: true
37
39
  end
38
40
 
39
41
  create_table :firms do |t|
@@ -29,12 +29,12 @@ Gem::Specification.new do |spec|
29
29
  spec.add_runtime_dependency "activerecord", [">= 7.0", "< 9"]
30
30
 
31
31
  spec.add_development_dependency "autotest-suffix", "~> 1.1"
32
- spec.add_development_dependency "bundler", "~> 2.0"
32
+ spec.add_development_dependency "bundler", [">= 2.0", "< 5.0"]
33
33
  spec.add_development_dependency "google-style", "~> 1.31.0"
34
34
  spec.add_development_dependency "minitest", "~> 5.10"
35
35
  spec.add_development_dependency "minitest-autotest", "~> 1.0"
36
36
  spec.add_development_dependency "minitest-focus", "~> 1.1"
37
- spec.add_development_dependency "minitest-rg", "~> 5.2"
37
+ spec.add_development_dependency "minitest-rg", "~> 5.4"
38
38
  spec.add_development_dependency "rake", "~> 13.0"
39
39
  spec.add_development_dependency "redcarpet", "~> 3.0"
40
40
  spec.add_development_dependency "simplecov", "~> 0.9"
@@ -6,6 +6,7 @@
6
6
 
7
7
  require_relative "config/environment"
8
8
  require "docker"
9
+ require "google/cloud/spanner"
9
10
 
10
11
  def fetch_samples
11
12
  Dir.entries(".").select do |entry|
@@ -50,6 +51,25 @@ task :run, [:sample] do |_t, args|
50
51
  run_sample sample
51
52
  end
52
53
 
54
+ def wait_for_emulator
55
+ puts "Waiting for Spanner emulator to be ready..."
56
+ spanner = Google::Cloud::Spanner.new project: "test-project", emulator_host: "127.0.0.1:9010"
57
+ retries = 20
58
+ begin
59
+ # Make a cheap API call to verify the emulator is actually responding
60
+ spanner.instances
61
+ puts "Spanner emulator is ready."
62
+ rescue StandardError => e
63
+ if retries > 0
64
+ retries -= 1
65
+ sleep 0.5
66
+ retry
67
+ else
68
+ puts "Timed out waiting for Spanner emulator. Last error: #{e.message}"
69
+ end
70
+ end
71
+ end
72
+
53
73
  def run_sample sample
54
74
  puts "Running #{sample}"
55
75
  puts "Downloading Spanner emulator image..."
@@ -68,6 +88,7 @@ def run_sample sample
68
88
  begin
69
89
  puts "Starting Spanner emulator..."
70
90
  container.start!
91
+ wait_for_emulator
71
92
  Dir.chdir sample do
72
93
  sh "ruby ../bin/create_emulator_instance.rb"
73
94
  sh "rake db:migrate"
@@ -10,13 +10,26 @@ module ActiveRecord
10
10
  module ConnectionAdapters
11
11
  module Spanner
12
12
  class Column < ConnectionAdapters::Column
13
- # rubocop:disable Style/OptionalBooleanParameter
14
- def initialize(name, default, sql_type_metadata = nil, null = true,
15
- default_function = nil, collation: nil, comment: nil,
16
- primary_key: false, **)
17
- # rubocop:enable Style/OptionalBooleanParameter
18
- super
19
- @primary_key = primary_key
13
+ VERSION_8_1 = Gem::Version.create "8.1.0"
14
+
15
+ if ActiveRecord.gem_version < VERSION_8_1
16
+ # rubocop:disable Style/OptionalBooleanParameter
17
+ def initialize(name, default, sql_type_metadata = nil, null = true,
18
+ default_function = nil, collation: nil, comment: nil,
19
+ primary_key: false, **)
20
+ # rubocop:enable Style/OptionalBooleanParameter
21
+ super
22
+ @primary_key = primary_key
23
+ end
24
+ else
25
+ # rubocop:disable Style/OptionalBooleanParameter
26
+ def initialize(name, cast_type, default, sql_type_metadata = nil, null = true,
27
+ default_function = nil, collation: nil, comment: nil,
28
+ primary_key: false, **)
29
+ # rubocop:enable Style/OptionalBooleanParameter
30
+ super
31
+ @primary_key = primary_key
32
+ end
20
33
  end
21
34
 
22
35
  def auto_incremented_by_db?
@@ -138,7 +138,11 @@ module ActiveRecord
138
138
  sql << " NOT NULL"
139
139
  end
140
140
  if options.key? :default
141
- sql << " DEFAULT (#{quote_default_expression options[:default], column})"
141
+ sql << if respond_to? :quote_default_expression_for_column_definition, :include_private
142
+ " DEFAULT (#{quote_default_expression_for_column_definition options[:default], column})"
143
+ else
144
+ " DEFAULT (#{quote_default_expression options[:default], column})"
145
+ end
142
146
  elsif column.type == :primary_key
143
147
  if @connection.use_auto_increment?
144
148
  sql << " AUTO_INCREMENT"
@@ -22,6 +22,7 @@ module ActiveRecord
22
22
  #
23
23
  module SchemaStatements
24
24
  VERSION_7_2 = Gem::Version.create "7.2.0"
25
+ VERSION_8_1 = Gem::Version.create "8.1.0"
25
26
 
26
27
  def current_database
27
28
  @connection.database_id
@@ -116,18 +117,37 @@ module ActiveRecord
116
117
  information_schema { |i| i.table_columns table_name }
117
118
  end
118
119
 
119
- def new_column_from_field _table_name, field, _definitions = nil
120
- Spanner::Column.new \
121
- field.name,
122
- field.default,
123
- fetch_type_metadata(field.spanner_type,
124
- field.ordinal_position,
125
- field.allow_commit_timestamp,
126
- field.generated,
127
- is_identity: field.is_identity),
128
- field.nullable,
129
- field.default_function,
130
- primary_key: field.primary_key
120
+ if ActiveRecord.gem_version < VERSION_8_1
121
+ def new_column_from_field _table_name, field, _definitions = nil
122
+ Spanner::Column.new \
123
+ field.name,
124
+ field.default,
125
+ fetch_type_metadata(field.spanner_type,
126
+ field.ordinal_position,
127
+ field.allow_commit_timestamp,
128
+ field.generated,
129
+ is_identity: field.is_identity),
130
+ field.nullable,
131
+ field.default_function,
132
+ primary_key: field.primary_key
133
+ end
134
+ else
135
+ def new_column_from_field _table_name, field, _definitions = nil
136
+ cast_type = type_map.lookup field.type
137
+ raise ArgumentError, "unknown type: `#{field.type}`" if cast_type.nil?
138
+ Spanner::Column.new \
139
+ field.name,
140
+ cast_type,
141
+ field.default,
142
+ fetch_type_metadata(field.spanner_type,
143
+ field.ordinal_position,
144
+ field.allow_commit_timestamp,
145
+ field.generated,
146
+ is_identity: field.is_identity),
147
+ field.nullable,
148
+ field.default_function,
149
+ primary_key: field.primary_key
150
+ end
131
151
  end
132
152
 
133
153
  def fetch_type_metadata sql_type, ordinal_position = nil, allow_commit_timestamp = nil, generated = nil,
@@ -0,0 +1,26 @@
1
+ # Copyright 2026 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
+ NATIVE_DATABASE_TYPES = {
10
+ primary_key: "INT64",
11
+ parent_key: "INT64",
12
+ string: { name: "STRING", limit: "MAX" },
13
+ text: { name: "STRING", limit: "MAX" },
14
+ integer: { name: "INT64" },
15
+ bigint: { name: "INT64" },
16
+ float: { name: "FLOAT64" },
17
+ decimal: { name: "NUMERIC" },
18
+ numeric: { name: "NUMERIC" },
19
+ datetime: { name: "TIMESTAMP" },
20
+ time: { name: "TIMESTAMP" },
21
+ date: { name: "DATE" },
22
+ binary: { name: "BYTES", limit: "MAX" },
23
+ boolean: { name: "BOOL" },
24
+ json: { name: "JSON" },
25
+ uuid: { name: "UUID" }
26
+ }.freeze
@@ -13,12 +13,14 @@ require "active_record/connection_adapters/spanner/database_statements"
13
13
  require "active_record/connection_adapters/spanner/schema_statements"
14
14
  require "active_record/connection_adapters/spanner/schema_cache"
15
15
  require "active_record/connection_adapters/spanner/schema_definitions"
16
+ require "active_record/connection_adapters/spanner/type_mapping"
16
17
  require "active_record/connection_adapters/spanner/type_metadata"
17
18
  require "active_record/connection_adapters/spanner/quoting"
18
19
  require "active_record/type/spanner/array"
19
20
  require "active_record/type/spanner/bytes"
20
21
  require "active_record/type/spanner/spanner_active_record_converter"
21
22
  require "active_record/type/spanner/time"
23
+ require "active_record/type/spanner/uuid"
22
24
  require "arel/visitors/spanner"
23
25
  require "activerecord_spanner_adapter/base"
24
26
  require "activerecord_spanner_adapter/connection"
@@ -46,23 +48,6 @@ module ActiveRecord
46
48
  module ConnectionAdapters
47
49
  class SpannerAdapter < AbstractAdapter
48
50
  ADAPTER_NAME = "spanner".freeze
49
- NATIVE_DATABASE_TYPES = {
50
- primary_key: "INT64",
51
- parent_key: "INT64",
52
- string: { name: "STRING", limit: "MAX" },
53
- text: { name: "STRING", limit: "MAX" },
54
- integer: { name: "INT64" },
55
- bigint: { name: "INT64" },
56
- float: { name: "FLOAT64" },
57
- decimal: { name: "NUMERIC" },
58
- numeric: { name: "NUMERIC" },
59
- datetime: { name: "TIMESTAMP" },
60
- time: { name: "TIMESTAMP" },
61
- date: { name: "DATE" },
62
- binary: { name: "BYTES", limit: "MAX" },
63
- boolean: { name: "BOOL" },
64
- json: { name: "JSON" }
65
- }.freeze
66
51
 
67
52
  include Spanner::Quoting
68
53
  include Spanner::DatabaseStatements
@@ -118,6 +103,10 @@ module ActiveRecord
118
103
  NATIVE_DATABASE_TYPES
119
104
  end
120
105
 
106
+ def self.native_database_types
107
+ NATIVE_DATABASE_TYPES
108
+ end
109
+
121
110
  # Database
122
111
 
123
112
  def self.database_exists? config
@@ -262,6 +251,7 @@ module ActiveRecord
262
251
  register_class_with_limit m, %r{^STRING}i, Type::String
263
252
  m.register_type "TIMESTAMP", ActiveRecord::Type::Spanner::Time.new
264
253
  m.register_type "JSON", ActiveRecord::Type::Json.new
254
+ m.register_type "UUID", ActiveRecord::Type::Spanner::Uuid.new
265
255
 
266
256
  register_array_types m
267
257
  end
@@ -277,6 +267,7 @@ module ActiveRecord
277
267
  m.register_type %r{^ARRAY<STRING\((MAX|d+)\)>}i, Type::Spanner::Array.new(Type::String.new)
278
268
  m.register_type %r{^ARRAY<TIMESTAMP>}i, Type::Spanner::Array.new(ActiveRecord::Type::Spanner::Time.new)
279
269
  m.register_type %r{^ARRAY<JSON>}i, Type::Spanner::Array.new(ActiveRecord::Type::Json.new)
270
+ m.register_type %r{^ARRAY<UUID>}i, Type::Spanner::Array.new(ActiveRecord::Type::Spanner::Uuid.new)
280
271
  end
281
272
 
282
273
  def extract_limit sql_type
@@ -37,6 +37,7 @@ module ActiveRecord
37
37
  when ActiveModel::Type::DateTime, ActiveModel::Type::Time, ActiveRecord::Type::Spanner::Time then :TIMESTAMP
38
38
  when ActiveModel::Type::Date then :DATE
39
39
  when ActiveRecord::Type::Json then :JSON
40
+ when ActiveRecord::Type::Spanner::Uuid then :UUID
40
41
  when ActiveRecord::Type::Spanner::Array then [convert_active_model_type_to_spanner(type.element_type)]
41
42
  end
42
43
  end
@@ -9,7 +9,24 @@
9
9
  module ActiveRecord
10
10
  module Type
11
11
  module Spanner
12
- class Time < ActiveRecord::Type::Time
12
+ class Time < ActiveRecord::Type::DateTime
13
+ def cast_value value
14
+ if value.is_a? ::String
15
+ return if value.empty?
16
+ begin
17
+ if ActiveRecord.default_timezone == :utc
18
+ ::DateTime.parse(value).to_time.getutc
19
+ else
20
+ ::Time.parse(value).getlocal
21
+ end
22
+ rescue StandardError
23
+ super
24
+ end
25
+ else
26
+ super
27
+ end
28
+ end
29
+
13
30
  def serialize_with_isolation_level value, isolation_level
14
31
  if value == :commit_timestamp
15
32
  return "PENDING_COMMIT_TIMESTAMP()" if isolation_level == :dml
@@ -24,18 +41,14 @@ module ActiveRecord
24
41
  val.acts_like?(:time) ? val.utc.rfc3339(9) : val
25
42
  end
26
43
 
27
- def user_input_in_time_zone value
28
- return value.in_time_zone if value.is_a? ::Time
29
- super value
44
+ def value_from_multiparameter_assignment values
45
+ defaults = { 1 => 2000, 2 => 1, 3 => 1 }
46
+ super defaults.merge(values)
30
47
  end
31
48
 
32
49
  private
33
50
 
34
- def cast_value value
35
- if value.is_a? ::String
36
- value = value.empty? ? nil : ::Time.parse(value)
37
- end
38
-
51
+ def apply_seconds_precision value
39
52
  value
40
53
  end
41
54
  end
@@ -0,0 +1,19 @@
1
+ # Copyright 2026 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
+ module ActiveRecord
10
+ module Type
11
+ module Spanner
12
+ class Uuid < ActiveModel::Type::Value
13
+ def type
14
+ :uuid
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -6,6 +6,7 @@
6
6
 
7
7
  require "google/cloud/spanner"
8
8
  require "spanner_client_ext"
9
+ require "active_record/connection_adapters/spanner/type_mapping"
9
10
  require "activerecord_spanner_adapter/information_schema"
10
11
  require_relative "../active_record/connection_adapters/spanner/errors/transaction_mutation_limit_exceeded_error"
11
12
 
@@ -43,6 +44,10 @@ module ActiveRecordSpannerAdapter
43
44
  end
44
45
  end
45
46
 
47
+ def native_database_types
48
+ NATIVE_DATABASE_TYPES
49
+ end
50
+
46
51
  # Clears the cached information about the underlying information schemas.
47
52
  # Call this method if you drop and recreate a database with the same name
48
53
  # to prevent the cached information to be used for the new database.
@@ -5,5 +5,5 @@
5
5
  # https://opensource.org/licenses/MIT.
6
6
 
7
7
  module ActiveRecordSpannerAdapter
8
- VERSION = "2.4.0".freeze
8
+ VERSION = "2.5.1".freeze
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-spanner-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Google LLC
@@ -75,16 +75,22 @@ dependencies:
75
75
  name: bundler
76
76
  requirement: !ruby/object:Gem::Requirement
77
77
  requirements:
78
- - - "~>"
78
+ - - ">="
79
79
  - !ruby/object:Gem::Version
80
80
  version: '2.0'
81
+ - - "<"
82
+ - !ruby/object:Gem::Version
83
+ version: '5.0'
81
84
  type: :development
82
85
  prerelease: false
83
86
  version_requirements: !ruby/object:Gem::Requirement
84
87
  requirements:
85
- - - "~>"
88
+ - - ">="
86
89
  - !ruby/object:Gem::Version
87
90
  version: '2.0'
91
+ - - "<"
92
+ - !ruby/object:Gem::Version
93
+ version: '5.0'
88
94
  - !ruby/object:Gem::Dependency
89
95
  name: google-style
90
96
  requirement: !ruby/object:Gem::Requirement
@@ -147,14 +153,14 @@ dependencies:
147
153
  requirements:
148
154
  - - "~>"
149
155
  - !ruby/object:Gem::Version
150
- version: '5.2'
156
+ version: '5.4'
151
157
  type: :development
152
158
  prerelease: false
153
159
  version_requirements: !ruby/object:Gem::Requirement
154
160
  requirements:
155
161
  - - "~>"
156
162
  - !ruby/object:Gem::Version
157
- version: '5.2'
163
+ version: '5.4'
158
164
  - !ruby/object:Gem::Dependency
159
165
  name: rake
160
166
  requirement: !ruby/object:Gem::Requirement
@@ -553,6 +559,7 @@ files:
553
559
  - lib/active_record/connection_adapters/spanner/schema_definitions.rb
554
560
  - lib/active_record/connection_adapters/spanner/schema_dumper.rb
555
561
  - lib/active_record/connection_adapters/spanner/schema_statements.rb
562
+ - lib/active_record/connection_adapters/spanner/type_mapping.rb
556
563
  - lib/active_record/connection_adapters/spanner/type_metadata.rb
557
564
  - lib/active_record/connection_adapters/spanner_adapter.rb
558
565
  - lib/active_record/tasks/spanner_database_tasks.rb
@@ -560,6 +567,7 @@ files:
560
567
  - lib/active_record/type/spanner/bytes.rb
561
568
  - lib/active_record/type/spanner/spanner_active_record_converter.rb
562
569
  - lib/active_record/type/spanner/time.rb
570
+ - lib/active_record/type/spanner/uuid.rb
563
571
  - lib/activerecord-spanner-adapter.rb
564
572
  - lib/activerecord_spanner_adapter/base.rb
565
573
  - lib/activerecord_spanner_adapter/connection.rb