From 8debb2701d5505374f5d40028a4fb578acceed7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 5 Feb 2026 08:46:20 +0100 Subject: [PATCH] feat: support ActiveRecord 8.1.x Adds support for ActiveRecord 8.1.x. --- .../acceptance-tests-on-emulator.yaml | 4 +- .github/workflows/ci.yaml | 4 +- .../nightly-acceptance-tests-on-emulator.yaml | 4 +- .github/workflows/nightly-unit-tests.yaml | 4 +- Gemfile | 2 +- acceptance/cases/migration/index_test.rb | 4 +- activerecord-spanner-adapter.gemspec | 4 +- .../connection_adapters/spanner/column.rb | 27 +++++++++--- .../spanner/schema_creation.rb | 6 ++- .../spanner/schema_statements.rb | 44 ++++++++++++++----- .../spanner/type_mapping.rb | 25 +++++++++++ .../connection_adapters/spanner_adapter.rb | 22 +++------- .../connection.rb | 5 +++ 13 files changed, 109 insertions(+), 46 deletions(-) create mode 100644 lib/active_record/connection_adapters/spanner/type_mapping.rb diff --git a/.github/workflows/acceptance-tests-on-emulator.yaml b/.github/workflows/acceptance-tests-on-emulator.yaml index fc036b98..b3a0799b 100644 --- a/.github/workflows/acceptance-tests-on-emulator.yaml +++ b/.github/workflows/acceptance-tests-on-emulator.yaml @@ -19,11 +19,13 @@ jobs: max-parallel: 4 matrix: ruby: ["3.1", "3.2", "3.3", "3.4"] - ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"] + ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0", "~> 8.1.0"] # Exclude combinations that are not supported. exclude: - ruby: "3.1" ar: "~> 8.0.0" + - ruby: "3.1" + ar: "~> 8.1.0" - ruby: "3.4" ar: "~> 7.0.0" - ruby: "3.4" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3ff6cd90..8ceac1ea 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,11 +11,13 @@ jobs: max-parallel: 4 matrix: ruby: ["3.1", "3.2", "3.3", "3.4"] - ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"] + ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0", "~> 8.1.0"] # Exclude combinations that are not supported. exclude: - ruby: "3.1" ar: "~> 8.0.0" + - ruby: "3.1" + ar: "~> 8.1.0" - ruby: "3.4" ar: "~> 7.0.0" - ruby: "3.4" diff --git a/.github/workflows/nightly-acceptance-tests-on-emulator.yaml b/.github/workflows/nightly-acceptance-tests-on-emulator.yaml index 08def666..0212d76e 100644 --- a/.github/workflows/nightly-acceptance-tests-on-emulator.yaml +++ b/.github/workflows/nightly-acceptance-tests-on-emulator.yaml @@ -19,11 +19,13 @@ jobs: max-parallel: 4 matrix: ruby: ["3.1", "3.2", "3.3", "3.4"] - ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"] + ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0", "~> 8.1.0"] # Exclude combinations that are not supported. exclude: - ruby: "3.1" ar: "~> 8.0.0" + - ruby: "3.1" + ar: "~> 8.1.0" - ruby: "3.4" ar: "~> 7.0.0" - ruby: "3.4" diff --git a/.github/workflows/nightly-unit-tests.yaml b/.github/workflows/nightly-unit-tests.yaml index 9d3d2e74..f1e79a34 100644 --- a/.github/workflows/nightly-unit-tests.yaml +++ b/.github/workflows/nightly-unit-tests.yaml @@ -12,11 +12,13 @@ jobs: matrix: # Run acceptance tests all supported combinations of Ruby and ActiveRecord. ruby: ["3.1", "3.2", "3.3", "3.4"] - ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0"] + ar: ["~> 7.0.0", "~> 7.1.0", "~> 7.2.0", "~> 8.0.0", "~> 8.1.0"] # Exclude combinations that are not supported. exclude: - ruby: "3.1" ar: "~> 8.0.0" + - ruby: "3.1" + ar: "~> 8.1.0" - ruby: "3.4" ar: "~> 7.0.0" - ruby: "3.4" diff --git a/Gemfile b/Gemfile index 1ef94fd9..940d9aac 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ ar_version = ENV.fetch("AR_VERSION", "~> 7.1.0") gem "activerecord", ar_version gem "ostruct" gem "minitest", "~> 5.27.0" -gem "minitest-rg", "~> 5.3.0" +gem "minitest-rg", "~> 5.4.0" gem "pry", "~> 0.14.2" gem "pry-byebug", "~> 3.11.0" gem "mutex_m" diff --git a/acceptance/cases/migration/index_test.rb b/acceptance/cases/migration/index_test.rb index 3d50c007..fe70559a 100644 --- a/acceptance/cases/migration/index_test.rb +++ b/acceptance/cases/migration/index_test.rb @@ -57,7 +57,7 @@ def test_rename_index_too_long e = assert_raises(ArgumentError) { connection.rename_index(table_name, "old_idx", too_long_index_name) } - assert_match(/too long; the limit is #{connection.index_name_length} characters/, e.message) + assert_match(/too long/, e.message) assert connection.index_name_exists?(table_name, "old_idx") end @@ -79,7 +79,7 @@ def test_add_index_does_not_accept_too_long_index_names e = assert_raises(ArgumentError) { connection.add_index(table_name, "foo", name: too_long_index_name) } - assert_match(/too long; the limit is #{connection.index_name_length} characters/, e.message) + assert_match(/too long/, e.message) assert_not connection.index_name_exists?(table_name, too_long_index_name) connection.add_index(table_name, "foo", name: good_index_name) diff --git a/activerecord-spanner-adapter.gemspec b/activerecord-spanner-adapter.gemspec index fc93f4dd..e9aea825 100644 --- a/activerecord-spanner-adapter.gemspec +++ b/activerecord-spanner-adapter.gemspec @@ -29,12 +29,12 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency "activerecord", [">= 7.0", "< 9"] spec.add_development_dependency "autotest-suffix", "~> 1.1" - spec.add_development_dependency "bundler", "~> 2.0" + spec.add_development_dependency "bundler", [">= 2.0", "< 5.0"] spec.add_development_dependency "google-style", "~> 1.31.0" spec.add_development_dependency "minitest", "~> 5.10" spec.add_development_dependency "minitest-autotest", "~> 1.0" spec.add_development_dependency "minitest-focus", "~> 1.1" - spec.add_development_dependency "minitest-rg", "~> 5.2" + spec.add_development_dependency "minitest-rg", "~> 5.4" spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "redcarpet", "~> 3.0" spec.add_development_dependency "simplecov", "~> 0.9" diff --git a/lib/active_record/connection_adapters/spanner/column.rb b/lib/active_record/connection_adapters/spanner/column.rb index 1042e2b5..4d7fd647 100644 --- a/lib/active_record/connection_adapters/spanner/column.rb +++ b/lib/active_record/connection_adapters/spanner/column.rb @@ -10,13 +10,26 @@ module ActiveRecord module ConnectionAdapters module Spanner class Column < ConnectionAdapters::Column - # rubocop:disable Style/OptionalBooleanParameter - def initialize(name, default, sql_type_metadata = nil, null = true, - default_function = nil, collation: nil, comment: nil, - primary_key: false, **) - # rubocop:enable Style/OptionalBooleanParameter - super - @primary_key = primary_key + VERSION_8_1 = Gem::Version.create "8.1.0" + + if ActiveRecord.gem_version < VERSION_8_1 + # rubocop:disable Style/OptionalBooleanParameter + def initialize(name, default, sql_type_metadata = nil, null = true, + default_function = nil, collation: nil, comment: nil, + primary_key: false, **) + # rubocop:enable Style/OptionalBooleanParameter + super + @primary_key = primary_key + end + else + # rubocop:disable Style/OptionalBooleanParameter + def initialize(name, cast_type, default, sql_type_metadata = nil, null = true, + default_function = nil, collation: nil, comment: nil, + primary_key: false, **) + # rubocop:enable Style/OptionalBooleanParameter + super + @primary_key = primary_key + end end def auto_incremented_by_db? diff --git a/lib/active_record/connection_adapters/spanner/schema_creation.rb b/lib/active_record/connection_adapters/spanner/schema_creation.rb index cd506526..3186f09c 100644 --- a/lib/active_record/connection_adapters/spanner/schema_creation.rb +++ b/lib/active_record/connection_adapters/spanner/schema_creation.rb @@ -138,7 +138,11 @@ def add_column_options! column, sql, options sql << " NOT NULL" end if options.key? :default - sql << " DEFAULT (#{quote_default_expression options[:default], column})" + sql << if respond_to? :quote_default_expression_for_column_definition, :include_private + " DEFAULT (#{quote_default_expression_for_column_definition options[:default], column})" + else + " DEFAULT (#{quote_default_expression options[:default], column})" + end elsif column.type == :primary_key if @connection.use_auto_increment? sql << " AUTO_INCREMENT" diff --git a/lib/active_record/connection_adapters/spanner/schema_statements.rb b/lib/active_record/connection_adapters/spanner/schema_statements.rb index 22d6e0f7..5ac5e548 100644 --- a/lib/active_record/connection_adapters/spanner/schema_statements.rb +++ b/lib/active_record/connection_adapters/spanner/schema_statements.rb @@ -22,6 +22,7 @@ module Spanner # module SchemaStatements VERSION_7_2 = Gem::Version.create "7.2.0" + VERSION_8_1 = Gem::Version.create "8.1.0" def current_database @connection.database_id @@ -116,18 +117,37 @@ def column_definitions table_name information_schema { |i| i.table_columns table_name } end - def new_column_from_field _table_name, field, _definitions = nil - Spanner::Column.new \ - field.name, - field.default, - fetch_type_metadata(field.spanner_type, - field.ordinal_position, - field.allow_commit_timestamp, - field.generated, - is_identity: field.is_identity), - field.nullable, - field.default_function, - primary_key: field.primary_key + if ActiveRecord.gem_version < VERSION_8_1 + def new_column_from_field _table_name, field, _definitions = nil + Spanner::Column.new \ + field.name, + field.default, + fetch_type_metadata(field.spanner_type, + field.ordinal_position, + field.allow_commit_timestamp, + field.generated, + is_identity: field.is_identity), + field.nullable, + field.default_function, + primary_key: field.primary_key + end + else + def new_column_from_field _table_name, field, _definitions = nil + cast_type = type_map.lookup field.type + raise ArgumentError, "unknown type: `#{field.type}`" if cast_type.nil? + Spanner::Column.new \ + field.name, + cast_type, + field.default, + fetch_type_metadata(field.spanner_type, + field.ordinal_position, + field.allow_commit_timestamp, + field.generated, + is_identity: field.is_identity), + field.nullable, + field.default_function, + primary_key: field.primary_key + end end def fetch_type_metadata sql_type, ordinal_position = nil, allow_commit_timestamp = nil, generated = nil, diff --git a/lib/active_record/connection_adapters/spanner/type_mapping.rb b/lib/active_record/connection_adapters/spanner/type_mapping.rb new file mode 100644 index 00000000..e10f5035 --- /dev/null +++ b/lib/active_record/connection_adapters/spanner/type_mapping.rb @@ -0,0 +1,25 @@ +# Copyright 2026 Google LLC +# +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +# +# frozen_string_literal: true + +NATIVE_DATABASE_TYPES = { + primary_key: "INT64", + parent_key: "INT64", + string: { name: "STRING", limit: "MAX" }, + text: { name: "STRING", limit: "MAX" }, + integer: { name: "INT64" }, + bigint: { name: "INT64" }, + float: { name: "FLOAT64" }, + decimal: { name: "NUMERIC" }, + numeric: { name: "NUMERIC" }, + datetime: { name: "TIMESTAMP" }, + time: { name: "TIMESTAMP" }, + date: { name: "DATE" }, + binary: { name: "BYTES", limit: "MAX" }, + boolean: { name: "BOOL" }, + json: { name: "JSON" } +}.freeze diff --git a/lib/active_record/connection_adapters/spanner_adapter.rb b/lib/active_record/connection_adapters/spanner_adapter.rb index 463e12fb..2d91c5b5 100644 --- a/lib/active_record/connection_adapters/spanner_adapter.rb +++ b/lib/active_record/connection_adapters/spanner_adapter.rb @@ -13,6 +13,7 @@ require "active_record/connection_adapters/spanner/schema_statements" require "active_record/connection_adapters/spanner/schema_cache" require "active_record/connection_adapters/spanner/schema_definitions" +require "active_record/connection_adapters/spanner/type_mapping" require "active_record/connection_adapters/spanner/type_metadata" require "active_record/connection_adapters/spanner/quoting" require "active_record/type/spanner/array" @@ -46,23 +47,6 @@ def spanner_connection config module ConnectionAdapters class SpannerAdapter < AbstractAdapter ADAPTER_NAME = "spanner".freeze - NATIVE_DATABASE_TYPES = { - primary_key: "INT64", - parent_key: "INT64", - string: { name: "STRING", limit: "MAX" }, - text: { name: "STRING", limit: "MAX" }, - integer: { name: "INT64" }, - bigint: { name: "INT64" }, - float: { name: "FLOAT64" }, - decimal: { name: "NUMERIC" }, - numeric: { name: "NUMERIC" }, - datetime: { name: "TIMESTAMP" }, - time: { name: "TIMESTAMP" }, - date: { name: "DATE" }, - binary: { name: "BYTES", limit: "MAX" }, - boolean: { name: "BOOL" }, - json: { name: "JSON" } - }.freeze include Spanner::Quoting include Spanner::DatabaseStatements @@ -118,6 +102,10 @@ def native_database_types NATIVE_DATABASE_TYPES end + def self.native_database_types + NATIVE_DATABASE_TYPES + end + # Database def self.database_exists? config diff --git a/lib/activerecord_spanner_adapter/connection.rb b/lib/activerecord_spanner_adapter/connection.rb index d219ffa5..e9dd6f30 100644 --- a/lib/activerecord_spanner_adapter/connection.rb +++ b/lib/activerecord_spanner_adapter/connection.rb @@ -6,6 +6,7 @@ require "google/cloud/spanner" require "spanner_client_ext" +require "active_record/connection_adapters/spanner/type_mapping" require "activerecord_spanner_adapter/information_schema" require_relative "../active_record/connection_adapters/spanner/errors/transaction_mutation_limit_exceeded_error" @@ -43,6 +44,10 @@ def self.spanners config end end + def native_database_types + NATIVE_DATABASE_TYPES + end + # Clears the cached information about the underlying information schemas. # Call this method if you drop and recreate a database with the same name # to prevent the cached information to be used for the new database.