diff --git a/.github/workflows/base_benchmark.yml b/.github/workflows/base_benchmark.yml index f6bb3a29..1a7d9b79 100644 --- a/.github/workflows/base_benchmark.yml +++ b/.github/workflows/base_benchmark.yml @@ -10,35 +10,13 @@ jobs: checks: write strategy: matrix: - include: - - ruby: '2.7' - gemfile: '5.2.7' - couchbase: '7.1.0' - - ruby: '2.7' - gemfile: '6.0.0' - couchbase: '7.1.0' - - ruby: '2.7' - gemfile: '6.0.0' - couchbase: '7.6.3' - - ruby: '2.7' - gemfile: '7.0.0' - couchbase: '7.1.0' - - ruby: '2.7' - gemfile: '7.0.0' - couchbase: '7.6.3' - # ruby 3.0 minimimun required rails 6.0.3 - # - ruby: '3.0' - # gemfile: '5.2.7' - # couchbase: '7.1.0' - - ruby: '3.0' + ruby: ['3.0', '3.1', '3.2', '3.3'] + gemfile: ['6.1.7.7', '7.0.0', '7.1.0'] + couchbase: ['7.2.0', '7.6.3', '8.0.0'] + exclude: + # Ruby 3.3 doesn't support Rails 6.1 + - ruby: '3.3' gemfile: '6.1.7.7' - couchbase: '7.1.0' - - ruby: '3.0' - gemfile: '7.0.0' - couchbase: '7.1.0' - - ruby: '3.0' - gemfile: '7.0.0' - couchbase: '7.6.3' fail-fast: false runs-on: ubuntu-22.04 name: Base Benchmark ${{ matrix.ruby }} rails-${{ matrix.gemfile }} couchbase-${{ matrix.couchbase }} diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index bb62dfd6..95e6011b 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -22,6 +22,6 @@ jobs: uses: ruby/setup-ruby@v1 with: bundler-cache: true - ruby-version: 2.7 + ruby-version: 3.0 - name: Run rubocop run: bundle exec rubocop \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 36456eb7..6cb23c70 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,35 +10,13 @@ jobs: test: strategy: matrix: - include: - - ruby: '2.7' - gemfile: '5.2.7' - couchbase: '7.1.0' - - ruby: '2.7' - gemfile: '6.0.0' - couchbase: '7.1.0' - - ruby: '2.7' - gemfile: '6.0.0' - couchbase: '7.6.3' - - ruby: '2.7' - gemfile: '7.0.0' - couchbase: '7.1.0' - - ruby: '2.7' - gemfile: '7.0.0' - couchbase: '7.6.3' - # ruby 3.0 minimimun required rails 6.0.3 - # - ruby: '3.0' - # gemfile: '5.2.7' - # couchbase: '7.1.0' - - ruby: '3.0' + ruby: ['3.0', '3.1', '3.2', '3.3'] + gemfile: ['6.1.7.7', '7.0.0', '7.1.0'] + couchbase: ['7.2.0', '7.6.3', '8.0.0'] + exclude: + # Ruby 3.3 doesn't support Rails 6.1 + - ruby: '3.3' gemfile: '6.1.7.7' - couchbase: '7.1.0' - - ruby: '3.0' - gemfile: '7.0.0' - couchbase: '7.1.0' - - ruby: '3.0' - gemfile: '7.0.0' - couchbase: '7.6.3' fail-fast: false runs-on: ubuntu-22.04 name: ${{ matrix.ruby }} rails-${{ matrix.gemfile }} couchbase-${{ matrix.couchbase }} diff --git a/.rubocop.yml b/.rubocop.yml index cdc2aa5c..959dfacf 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -9,7 +9,7 @@ require: - rubocop-rake AllCops: - TargetRubyVersion: 2.7 + TargetRubyVersion: 3.0 CacheRootDirectory: rubocop_cache Exclude: - 'tmp/**/*' diff --git a/ci/run_couchbase.sh b/ci/run_couchbase.sh index 8a4b3e6f..a6aabd05 100755 --- a/ci/run_couchbase.sh +++ b/ci/run_couchbase.sh @@ -14,9 +14,10 @@ sudo service couchbase-server status /opt/couchbase/bin/couchbase-cli cluster-init -c 127.0.0.1:8091 --cluster-username=admin --cluster-password=password --cluster-ramsize=320 --cluster-index-ramsize=256 --cluster-fts-ramsize=256 --services=data,index,query,fts sleep 5 /opt/couchbase/bin/couchbase-cli server-info -c 127.0.0.1:8091 -u admin -p password -/opt/couchbase/bin/couchbase-cli bucket-create -c 127.0.0.1:8091 -u admin -p password --bucket=$BUCKET --bucket-type=couchbase --bucket-ramsize=160 --bucket-replica=0 --enable-flush=1 --wait -sleep 1 +/opt/couchbase/bin/couchbase-cli bucket-create -c 127.0.0.1:8091 -u admin -p password --bucket=$BUCKET --bucket-type=couchbase --bucket-ramsize=160 --bucket-replica=0 --enable-flush=1 --storage-backend couchstore --wait +sleep 3 /opt/couchbase/bin/couchbase-cli user-manage -c 127.0.0.1:8091 -u admin -p password --set --rbac-username $USER --rbac-password $PASSWORD --rbac-name "Auto Tester" --roles admin --auth-domain local +sleep 2 curl http://admin:password@localhost:8093/query/service -d "statement=CREATE INDEX \`default_type\` ON \`$BUCKET\`(\`type\`)" curl http://admin:password@localhost:8093/query/service -d "statement=CREATE INDEX \`default_rating\` ON \`$BUCKET\`(\`rating\`)" curl http://admin:password@localhost:8093/query/service -d "statement=CREATE INDEX \`default_name\` ON \`$BUCKET\`(\`name\`)" diff --git a/couchbase-orm.gemspec b/couchbase-orm.gemspec index d1bc71d4..a72169e1 100644 --- a/couchbase-orm.gemspec +++ b/couchbase-orm.gemspec @@ -12,16 +12,16 @@ Gem::Specification.new do |gem| gem.summary = 'Couchbase ORM for Rails' gem.description = 'A Couchbase ORM for Rails' - gem.required_ruby_version = '>= 2.7.0' + gem.required_ruby_version = '>= 3.0' gem.require_paths = ['lib'] - gem.add_runtime_dependency 'activemodel', ENV['ACTIVE_MODEL_VERSION'] || '>= 5.2', '< 7.1' - gem.add_runtime_dependency 'activerecord', ENV['ACTIVE_MODEL_VERSION'] || '>= 5.2', '< 7.1' + gem.add_runtime_dependency 'activemodel', ENV['ACTIVE_MODEL_VERSION'] || '>= 6.1.7.7', '<= 7.1' + gem.add_runtime_dependency 'activerecord', ENV['ACTIVE_MODEL_VERSION'] || '>= 6.1.7.7', '<= 7.1' - gem.add_runtime_dependency 'couchbase', '~> 3.3.0' + gem.add_runtime_dependency 'couchbase', '~> 3.4.5' gem.add_runtime_dependency 'radix', '~> 2.2' # converting numbers to and from any base - gem.add_development_dependency 'actionpack', ENV['ACTIVE_MODEL_VERSION'] || '>= 5.2', '< 7.1' + gem.add_development_dependency 'actionpack', ENV['ACTIVE_MODEL_VERSION'] || '>= 6.1.7.7', '<= 7.1' gem.add_development_dependency 'base64' gem.add_development_dependency 'mapotempo_rubocop', '<1.0' gem.add_development_dependency 'pry' diff --git a/docker-compose.yml b/docker-compose.yml index 55cbb5a1..36e42263 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.7' x-app-args: &app-args BUNDLE_VERSION: ${BUNDLE_VERSION:-2.4.22} - RUBY_VERSION: ${RUBY_VERSION:-2.7-slim-bullseye} + RUBY_VERSION: ${RUBY_VERSION:-3.0-slim-bullseye} BUNDLE_WITHOUT: production services: @@ -12,7 +12,7 @@ services: <<: *app-args context: . dockerfile: Dockerfile - image: dev.example.com/mapotempo/couchbase-orm:ruby-${RUBY_VERSION:-2.7-slim-bullseye}_bundle-${BUNDLE_VERSION:-2.4.22} + image: dev.example.com/mapotempo/couchbase-orm:ruby-${RUBY_VERSION:-3.0-slim-bullseye}_bundle-${BUNDLE_VERSION:-2.4.22} volumes: - ./:/srv/app/ - app_cache_vendor:/srv/app/vendor @@ -21,7 +21,7 @@ services: - COUCHBASE_USER=tester - COUCHBASE_PASSWORD=password123 - COUCHBASE_BUCKET=default - - ACTIVE_MODEL_VERSION=5.2.7 + - ACTIVE_MODEL_VERSION=6.1.7.7 - LOG_LEVEL=debug tty: true depends_on: diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 46803d43..cc3d33fc 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -64,6 +64,16 @@ def table_exists? true end + # Stub connection for ActiveRecord::Timestamp compatibility + def connection + @connection ||= Struct.new(:default_timezone).new(:utc) + end + + # ActiveRecord 7.1 compatibility + def composite_primary_key? + false + end + def _reflect_on_association(_attribute) false end @@ -85,12 +95,30 @@ def attribute_names attribute_types.keys end end + + # Rails 7.1+ renamed generate_alias_attributes to generate_alias_attribute_methods + # ActiveRecord still calls the old method name, so we need to provide compatibility + # The old method had no parameters, so we just provide an empty implementation + if ActiveModel::VERSION::MAJOR >= 7 && ActiveModel::VERSION::MINOR >= 1 + def generate_alias_attributes(*args) + # In Rails 7.1+, this method was renamed and signature changed + # ActiveRecord 7.1 still calls it with no args, so we handle that case + return if args.empty? + + generate_alias_attribute_methods(*args) + end + end end def _has_attribute?(attr_name) attribute_names.include?(attr_name.to_s) end + # ActiveRecord 7.1 compatibility + def primary_key_values_present? + !id.nil? + end + def attribute_for_inspect(attr_name) value = send(attr_name) value.inspect @@ -136,6 +164,17 @@ def read_attribute(attr_name, &block) end class Document + class << self + def descendants + @__descendants ||= [] # rubocop:disable Naming/MemoizedInstanceVariableName + end + + def inherited(subclass) + super + descendants << subclass + end + end + include ::ActiveModel::Model include ::ActiveModel::Dirty include ::ActiveModel::Attributes @@ -154,6 +193,9 @@ class Document define_model_callbacks :initialize, :find, only: :after define_model_callbacks :create, :destroy, :save, :update + # Prevent duplicate validation errors (similar to ActiveRecord::AutosaveAssociation) + after_validation :_ensure_no_duplicate_errors + Metadata = Struct.new(:cas) class MismatchTypeError < RuntimeError; end @@ -218,6 +260,10 @@ def write_attribute(attr_name, value) @attributes.write_from_user(name, value) end + + def _ensure_no_duplicate_errors + errors.uniq! + end end class NestedDocument < Document diff --git a/lib/couchbase-orm/views.rb b/lib/couchbase-orm/views.rb index e1b1c07d..e6a00d2b 100644 --- a/lib/couchbase-orm/views.rb +++ b/lib/couchbase-orm/views.rb @@ -222,7 +222,24 @@ def ensure_design_document! document = Couchbase::Management::DesignDocument.new document.views = views_actual document.name = @design_document - bucket.view_indexes.upsert_design_document(document, :production) + + # Retry logic for view document creation (handles race conditions and storage backend issues) + max_retries = 3 + retry_count = 0 + begin + bucket.view_indexes.upsert_design_document(document, :production) + rescue Couchbase::Error::DesignDocumentNotFound, Couchbase::Error::InternalServerFailure => e + retry_count += 1 + if retry_count <= max_retries + sleep_time = retry_count * 0.5 # exponential backoff: 0.5s, 1s, 1.5s + CouchbaseOrm.logger.warn("View document upsert failed (attempt #{retry_count}/#{max_retries}), retrying in #{sleep_time}s: #{e.message}") + sleep(sleep_time) + retry + else + CouchbaseOrm.logger.error("View document upsert failed after #{max_retries} retries: #{e.message}") + raise + end + end true else