From ae74bda2cc981f748677c8301bbc1e88a846af4e Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Fri, 8 Jul 2022 15:38:24 +0200 Subject: [PATCH 01/25] Add ActiveRecord::Core --- .rbenv-vars | 4 + couchbase-orm.gemspec | 2 + lib/couchbase-orm.rb | 7 +- lib/couchbase-orm/associations.rb | 11 +- lib/couchbase-orm/base.rb | 202 +++++++++++++----------- lib/couchbase-orm/locale/en.yml | 5 + lib/couchbase-orm/n1ql.rb | 2 +- lib/couchbase-orm/persistence.rb | 79 ++++----- lib/couchbase-orm/utilities/enum.rb | 14 +- lib/couchbase-orm/utilities/has_many.rb | 2 +- lib/couchbase-orm/utilities/index.rb | 2 +- lib/couchbase-orm/views.rb | 6 +- spec/base_spec.rb | 26 ++- spec/enum_spec.rb | 34 ++++ spec/index_spec.rb | 10 +- spec/persistence_spec.rb | 16 +- spec/views_spec.rb | 2 +- 17 files changed, 261 insertions(+), 163 deletions(-) create mode 100644 .rbenv-vars create mode 100644 lib/couchbase-orm/locale/en.yml create mode 100644 spec/enum_spec.rb diff --git a/.rbenv-vars b/.rbenv-vars new file mode 100644 index 00000000..3aa07c1b --- /dev/null +++ b/.rbenv-vars @@ -0,0 +1,4 @@ +COUCHBASE_USER=cb_admin +COUCHBASE_PASSWORD=cb_admin_pwd +COUCHBASE_BUCKET=default +ACTIVE_MODEL_VERSION=7.0.2.4 diff --git a/couchbase-orm.gemspec b/couchbase-orm.gemspec index 79eba3a5..74ca76fa 100644 --- a/couchbase-orm.gemspec +++ b/couchbase-orm.gemspec @@ -14,6 +14,8 @@ Gem::Specification.new do |gem| gem.require_paths = ["lib"] gem.add_runtime_dependency 'activemodel', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.0' + gem.add_runtime_dependency 'activerecord', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.0' + gem.add_runtime_dependency 'couchbase' gem.add_runtime_dependency 'radix', '~> 2.2' # converting numbers to and from any base diff --git a/lib/couchbase-orm.rb b/lib/couchbase-orm.rb index 4c6257a6..970efd04 100644 --- a/lib/couchbase-orm.rb +++ b/lib/couchbase-orm.rb @@ -1,5 +1,10 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +require "active_support/lazy_load_hooks" +ActiveSupport.on_load(:i18n) do + I18n.load_path << File.expand_path("couchbase-orm/locale/en.yml", __dir__) +end + module CouchbaseOrm autoload :Error, 'couchbase-orm/error' autoload :Connection, 'couchbase-orm/connection' @@ -8,7 +13,7 @@ module CouchbaseOrm autoload :HasMany, 'couchbase-orm/utilities/has_many' def self.logger - @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT) + @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT).tap { |l| l.level = Logger::INFO } end def self.logger=(logger) diff --git a/lib/couchbase-orm/associations.rb b/lib/couchbase-orm/associations.rb index 0d21f94c..71514d5b 100644 --- a/lib/couchbase-orm/associations.rb +++ b/lib/couchbase-orm/associations.rb @@ -114,9 +114,8 @@ def has_and_belongs_to_many(name, **options) old, new = previous_changes[ref] adds = (new || []) - (old || []) subs = (old || []) - (new || []) - - update_has_and_belongs_to_many_reverse_association(assoc, adds, true, **options) - update_has_and_belongs_to_many_reverse_association(assoc, subs, false, **options) + update_has_and_belongs_to_many_reverse_association(assoc, adds, true, **options) if adds.any? + update_has_and_belongs_to_many_reverse_association(assoc, subs, false, **options) if subs.any? end after_create save_method @@ -167,9 +166,11 @@ def update_has_and_belongs_to_many_reverse_association(assoc, keys, is_add, **op elsif !is_add && index tab = tab.dup tab.delete_at(index) + else + next end - v.__send__(:"#{remote_method}=", tab) - v.__send__(:save!) + v[remote_method] = tab + v.save! end end diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 185d7ad6..7e43bcb9 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -2,6 +2,9 @@ require 'active_model' +require 'active_record' +require 'active_record/database_configurations' + require 'active_support/hash_with_indifferent_access' require 'couchbase' require 'couchbase-orm/error' @@ -19,13 +22,74 @@ module CouchbaseOrm + + module ActiveRecordCompat + # try to avoid dependencies on too many active record classes + # by exemple we don't want to go down to the concept of tables + + extend ActiveSupport::Concern + + module ClassMethods + def reset_primary_key + primary_key = "id" + end + + def primary_key + "id" + end + + def base_class? + true + end + + def base_class + self + end + + def abstract_class? + false + end + + def table_exists? + true + end + + def connected? + true + end + + def symbol_column_to_string(name) + name.to_s + end + end + + def respond_to?(name) + # skip ActiveRecord::AttributeMethods respond_to? method + method(:respond_to?).super_method.super_method.call(name) + end + + def _has_attribute?(attr_name) + attribute_names.include?(attr_name.to_s) + end + + def attribute_for_inspect(attr_name) + value = send(attr_name) + value.inspect + end + end + class Base include ::ActiveModel::Model include ::ActiveModel::Dirty + include ::ActiveModel::Attributes include ::ActiveModel::Serializers::JSON include ::ActiveModel::Validations include ::ActiveModel::Validations::Callbacks + + include ::ActiveRecord::Core + include ActiveRecordCompat + define_model_callbacks :initialize, :only => :after define_model_callbacks :create, :destroy, :save, :update @@ -45,6 +109,7 @@ class Base class << self + def connect(**options) @bucket = BucketProxy.new(::MTLibcouchbase::Bucket.new(**options)) end @@ -73,33 +138,6 @@ def uuid_generator=(generator) @uuid_generator = generator end - def attribute(*names, **options) - @attributes ||= {} - names.each do |name| - name = name.to_sym - - @attributes[name] = options - - unless self.instance_methods.include?(name) - define_method(name) do - read_attribute(name) - end - end - - eq_meth = :"#{name}=" - unless self.instance_methods.include?(eq_meth) - define_method(eq_meth) do |value| - value = yield(value) if block_given? - write_attribute(name, value) - end - end - end - end - - def attributes - @attributes ||= {} - end - def find(*ids, quiet: false) CouchbaseOrm.logger.debug { "Base.find(l##{ids.length}) #{ids}" } @@ -128,59 +166,44 @@ def exists?(id) collection.exists(id).exists end alias_method :has_key?, :exists? + end class MismatchTypeError < RuntimeError; end # Add support for libcouchbase response objects def initialize(model = nil, ignore_doc_type: false, **attributes) + CouchbaseOrm.logger.debug "Initialize model #{model} with #{attributes}" @__metadata__ = Metadata.new - # Assign default values - @__attributes__ = ::ActiveSupport::HashWithIndifferentAccess.new({type: self.class.design_document}) - self.class.attributes.each do |key, options| - default = options[:default] - if default.respond_to?(:call) - write_attribute key, default.call - else - write_attribute key, default - end - end + super() if model case model when Couchbase::Collection::GetResult - CouchbaseOrm.logger.debug "Initialize with Couchbase::Collection::GetResult" - doc = model.content || raise('empty response provided') - type = doc.delete('type') + doc = HashWithIndifferentAccess.new(model.content) || raise('empty response provided') + type = doc.delete(:type) doc.delete(:id) if type && !ignore_doc_type && type.to_s != self.class.design_document raise CouchbaseOrm::Error::TypeMismatchError.new("document type mismatch, #{type} != #{self.class.design_document}", self) end - @__metadata__.key = attributes[:id] + self.id = attributes[:id] if attributes[:id] @__metadata__.cas = model.cas - # This ensures that defaults are applied - @__attributes__.merge! doc + assign_attributes(doc) when CouchbaseOrm::Base - CouchbaseOrm.logger.debug "Initialize with CouchbaseOrm::Base" - clear_changes_information - attributes = model.attributes - attributes.delete(:id) - attributes.delete('type') - super(attributes) + super(model.attributes.except(:id, 'type')) else clear_changes_information - super(attributes.merge(Hash(model))) + assign_attributes(**attributes.merge(Hash(model))) end else clear_changes_information super(attributes) end - yield self if block_given? run_callbacks :initialize @@ -189,58 +212,62 @@ def initialize(model = nil, ignore_doc_type: false, **attributes) # Document ID is a special case as it is not stored in the document def id - @__metadata__.key || @id + @id.presence end def id=(value) - raise 'ID cannot be changed' if @__metadata__.cas + raise 'ID cannot be changed' if @__metadata__.cas && value attribute_will_change!(:id) @id = value.to_s end - def read_attribute(attr_name) - @__attributes__[attr_name] + def [](key) + send(key) end - alias_method :[], :read_attribute - def write_attribute(attr_name, value) - unless value.nil? - coerce = self.class.attributes[attr_name][:type] - value = Kernel.send(coerce.to_s, value) if coerce - end - attribute_will_change!(attr_name) unless @__attributes__[attr_name] == value - @__attributes__[attr_name] = value + def []=(key, value) + CouchbaseOrm.logger.debug "Set attribute #{key} to #{value}" + send(:"#{key}=", value) end - alias_method :[]=, :write_attribute + + # def write_attribute(attr_name, value) + # unless value.nil? + # coerce = self.class.attributes[attr_name][:type] + # value = Kernel.send(coerce.to_s, value) if coerce + # end + # attribute_will_change!(attr_name) unless @__attributes__[attr_name] == value + # @__attributes__[attr_name] = value + # end + # alias_method :[]=, :write_attribute # # Add support for Serialization: # http://guides.rubyonrails.org/active_model_basics.html#serialization # - def attributes - copy = @__attributes__.merge({id: id}) - copy.delete(:type) - copy - end + # def attributes + # copy = @__attributes__.merge({id: id}) + # copy.delete(:type) + # copy + # end - def attributes=(attributes) - attributes.each do |key, value| - setter = :"#{key}=" - send(setter, value) if respond_to?(setter) - end - end + # def attributes=(attributes) + # attributes.each do |key, value| + # setter = :"#{key}=" + # send(setter, value) if respond_to?(setter) + # end + # end - ID_LOOKUP = ['id', :id].freeze - def attribute(name) - return self.id if ID_LOOKUP.include?(name) - @__attributes__[name] - end - alias_method :read_attribute_for_serialization, :attribute + # ID_LOOKUP = ['id', :id].freeze + # def attribute(name) + # return self.id if ID_LOOKUP.include?(name) + # @__attributes__[name] + # end + # alias_method :read_attribute_for_serialization, :attribute - def attribute=(name, value) - __send__(:"#{name}=", value) - end + # def attribute=(name, value) + # __send__(:"#{name}=", value) + # end # @@ -279,12 +306,7 @@ def eql?(other) # # Returns a boolean. def ==(other) - case other - when self.class - hash == other.hash - else - false - end + super || other.instance_of?(self.class) && !id.nil? && other.id == id end end end diff --git a/lib/couchbase-orm/locale/en.yml b/lib/couchbase-orm/locale/en.yml new file mode 100644 index 00000000..8e241faf --- /dev/null +++ b/lib/couchbase-orm/locale/en.yml @@ -0,0 +1,5 @@ +en: + couchbase: + errors: + messages: + record_invalid: "Validation failed: %{errors}" diff --git a/lib/couchbase-orm/n1ql.rb b/lib/couchbase-orm/n1ql.rb index 2f4562f7..ddf3937a 100644 --- a/lib/couchbase-orm/n1ql.rb +++ b/lib/couchbase-orm/n1ql.rb @@ -38,7 +38,7 @@ module ClassMethods def n1ql(name, query_fn: nil, emit_key: [], **options) emit_key = Array.wrap(emit_key) emit_key.each do |key| - raise "unknown emit_key attribute for n1ql :#{name}, emit_key: :#{key}" if key && @attributes[key].nil? + raise "unknown emit_key attribute for n1ql :#{name}, emit_key: :#{key}" if key && !attribute_names.include?(key.to_s) end options = N1QL_DEFAULTS.merge(options) method_opts = {} diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index 6e30e820..c174af16 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -7,6 +7,9 @@ module CouchbaseOrm module Persistence extend ActiveSupport::Concern + included do + attribute :id, :string + end module ClassMethods def create(attributes = nil, &block) @@ -54,20 +57,19 @@ def inherited(child) # Returns true if this object hasn't been saved yet -- that is, a record # for the object doesn't exist in the database yet; otherwise, returns false. def new_record? - @__metadata__.cas.nil? && @__metadata__.key.nil? + @__metadata__.cas.nil? && id.blank? end alias_method :new?, :new_record? # Returns true if this object has been destroyed, otherwise returns false. def destroyed? - !!(@__metadata__.cas && @__metadata__.key.nil?) + !!(@__metadata__.cas && id.blank?) end # Returns true if the record is persisted, i.e. it's not a new record and it was # not destroyed, otherwise returns false. def persisted? - # Changed? is provided by ActiveModel::Dirty - !!@__metadata__.key + id.present? end alias_method :exists?, :persisted? @@ -88,6 +90,7 @@ def save(**options) # By default, #save! always runs validations. If any of them fail # CouchbaseOrm::Error::RecordInvalid gets raised, and the record won't be saved. def save!(**options) + CouchbaseOrm.logger.debug { "Will save! : #{id} -> #{attributes}" } self.class.fail_validate!(self) unless self.save(**options) self end @@ -99,12 +102,10 @@ def save!(**options) # The record is simply removed, no callbacks are executed. def delete(with_cas: false, **options) options[:cas] = @__metadata__.cas if with_cas - CouchbaseOrm.logger.debug "Data - Delete #{@__metadata__.key}" - self.class.collection.remove(@__metadata__.key, **options) - - @__metadata__.key = nil - @id = nil + CouchbaseOrm.logger.debug "Data - Delete #{self.id}" + self.class.collection.remove(self.id, **options) + self.id = nil clear_changes_information self.freeze self @@ -124,11 +125,10 @@ def destroy(with_cas: false, **options) destroy_associations! options[:cas] = @__metadata__.cas if with_cas - CouchbaseOrm.logger.debug "Data - Delete #{@__metadata__.key}" - self.class.collection.remove(@__metadata__.key, **options) - - @__metadata__.key = nil - @id = nil + CouchbaseOrm.logger.debug "Data - Delete #{id}" + self.class.collection.remove(id, **options) + + self.id = nil clear_changes_information freeze @@ -146,6 +146,10 @@ def update_attribute(name, value) changed? ? save(validate: false) : true end + def assign_attributes(hash) + super(hash.with_indifferent_access.except("type")) + end + # Updates the attributes of the model from the passed-in hash and saves the # record. If the object is invalid, the saving will fail and false will be returned. def update(hash) @@ -167,8 +171,7 @@ def update!(hash) # except if there is more than 16 attributes, in which case # the whole record is saved. def update_columns(with_cas: false, **hash) - _id = @__metadata__.key - raise "unable to update columns, model not persisted" unless _id + raise "unable to update columns, model not persisted" unless id assign_attributes(hash) @@ -178,15 +181,13 @@ def update_columns(with_cas: false, **hash) # There is a limit of 16 subdoc operations per request resp = if hash.length <= 16 self.class.collection.mutate_in( - _id, + id, hash.map { |k, v| Couchbase::MutateInSpec.replace(k.to_s, v) } ) else # Fallback to writing the whole document - @__attributes__[:type] = self.class.design_document - @__attributes__.delete(:id) - CouchbaseOrm.logger.debug { "Data - Replace #{_id} #{@__attributes__.to_s.truncate(200)}" } - self.class.collection.replace(_id, @__attributes__, **options) + CouchbaseOrm.logger.debug { "Data - Replace #{id} #{attributes.to_s.truncate(200)}" } + self.class.collection.replace(id, attributes.except(:id).merge(type: self.class.design_document), **options) end # Ensure the model is up to date @@ -200,13 +201,11 @@ def update_columns(with_cas: false, **hash) # # This method finds record by its key and modifies the receiver in-place: def reload - key = @__metadata__.key - raise "unable to reload, model not persisted" unless key + raise "unable to reload, model not persisted" unless id - CouchbaseOrm.logger.debug "Data - Get #{key}" - resp = self.class.collection.get!(key) - @__attributes__ = ::ActiveSupport::HashWithIndifferentAccess.new(resp.content) - @__metadata__.key = key + CouchbaseOrm.logger.debug "Data - Get #{id}" + resp = self.class.collection.get!(id) + assign_attributes(resp.content.except("id")) # API return a nil id @__metadata__.cas = resp.cas reset_associations @@ -216,8 +215,8 @@ def reload # Updates the TTL of the document def touch(**options) - CouchbaseOrm.logger.debug "Data - Touch #{@__metadata__.key}" - res = self.class.collection.touch(@__metadata__.key, async: false, **options) + CouchbaseOrm.logger.debug "Data - Touch #{id}" + _res = self.class.collection.touch(id, async: false, **options) @__metadata__.cas = resp.cas self end @@ -232,17 +231,11 @@ def _update_record(with_cas: false, **options) run_callbacks :update do run_callbacks :save do - # Ensure the type is set - @__attributes__[:type] = self.class.design_document - @__attributes__.delete(:id) - - _id = @__metadata__.key options[:cas] = @__metadata__.cas if with_cas - CouchbaseOrm.logger.debug { "_update_record - replace #{_id} #{@__attributes__.to_s.truncate(200)}" } - resp = self.class.collection.replace(_id, @__attributes__, Couchbase::Options::Replace.new(**options)) + CouchbaseOrm.logger.debug { "_update_record - replace #{id} #{attributes.to_s.truncate(200)}" } + resp = self.class.collection.replace(id, attributes.except(:id).merge(type: self.class.design_document), Couchbase::Options::Replace.new(**options)) # Ensure the model is up to date - @__metadata__.key = _id @__metadata__.cas = resp.cas changes_applied @@ -255,18 +248,12 @@ def _create_record(**options) run_callbacks :create do run_callbacks :save do - # Ensure the type is set - @__attributes__[:type] = self.class.design_document - @__attributes__.delete(:id) - - _id = @id || self.class.uuid_generator.next(self) - CouchbaseOrm.logger.debug { "_create_record - Upsert #{_id} #{@__attributes__.to_s.truncate(200)}" } - #resp = self.class.collection.add(_id, @__attributes__, **options) + assign_attributes(id: self.class.uuid_generator.next(self)) unless self.id + CouchbaseOrm.logger.debug { "_create_record - Upsert #{id} #{attributes.to_s.truncate(200)}" } - resp = self.class.collection.upsert(_id, @__attributes__, Couchbase::Options::Upsert.new(**options)) + resp = self.class.collection.upsert(self.id, attributes.except(:id).merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options)) # Ensure the model is up to date - @__metadata__.key = _id @__metadata__.cas = resp.cas changes_applied diff --git a/lib/couchbase-orm/utilities/enum.rb b/lib/couchbase-orm/utilities/enum.rb index 87d1fa1d..6032f90c 100644 --- a/lib/couchbase-orm/utilities/enum.rb +++ b/lib/couchbase-orm/utilities/enum.rb @@ -27,7 +27,19 @@ def enum(options) else default_value = 1 end - attribute name, default: default_value + attribute name, :integer, default: default_value + + define_method "#{name}=" do |value| + unless value.nil? + value = case value + when Symbol, String + self.class.const_get(name.to_s.upcase)[value.to_sym] + else + Integer(value) + end + end + super(value) + end # keep the attribute's value within bounds before_save do |record| diff --git a/lib/couchbase-orm/utilities/has_many.rb b/lib/couchbase-orm/utilities/has_many.rb index d03458b0..7885ffba 100644 --- a/lib/couchbase-orm/utilities/has_many.rb +++ b/lib/couchbase-orm/utilities/has_many.rb @@ -103,5 +103,5 @@ def build_index_n1ql(klass, remote_class, remote_method, through_key, foreign_ke end end end - end + end end diff --git a/lib/couchbase-orm/utilities/index.rb b/lib/couchbase-orm/utilities/index.rb index f12d0ecd..6119550c 100644 --- a/lib/couchbase-orm/utilities/index.rb +++ b/lib/couchbase-orm/utilities/index.rb @@ -70,7 +70,7 @@ def index(attrs, name = nil, presence: true, &processor) if presence attrs.each do |attr| validates attr, presence: true - define_attribute_methods attr + attribute attr end end diff --git a/lib/couchbase-orm/views.rb b/lib/couchbase-orm/views.rb index 530554cf..9ead45ad 100644 --- a/lib/couchbase-orm/views.rb +++ b/lib/couchbase-orm/views.rb @@ -25,10 +25,10 @@ module ClassMethods def view(name, map: nil, emit_key: nil, reduce: nil, **options) if emit_key.class == Array emit_key.each do |key| - raise "unknown emit_key attribute for view :#{name}, emit_key: :#{key}" if key && @attributes[key].nil? + raise "unknown emit_key attribute for view :#{name}, emit_key: :#{key}" if key && !attribute_names.include?(key.to_s) end else - raise "unknown emit_key attribute for view :#{name}, emit_key: :#{emit_key}" if emit_key && @attributes[emit_key].nil? + raise "unknown emit_key attribute for view :#{name}, emit_key: :#{emit_key}" if emit_key && !attribute_names.include?(emit_key.to_s) end options = ViewDefaults.merge(options) @@ -49,7 +49,7 @@ def view(name, map: nil, emit_key: nil, reduce: nil, **options) else emit_key = emit_key || :created_at - if emit_key != :created_at && self.attributes[emit_key][:type].to_s == 'Array' + if emit_key != :created_at && false # FIXME: not longer supported, need tests # self.attributes[emit_key][:type].to_s == 'Array' method_opts[:map] = <<-EMAP function(doc) { var i; diff --git a/spec/base_spec.rb b/spec/base_spec.rb index 0a898c39..bafcbd65 100644 --- a/spec/base_spec.rb +++ b/spec/base_spec.rb @@ -4,11 +4,12 @@ class BaseTest < CouchbaseOrm::Base - attribute :name, :job + attribute :name, :string + attribute :job, :string end class CompareTest < CouchbaseOrm::Base - attribute :age + attribute :age, :integer end @@ -33,6 +34,11 @@ class CompareTest < CouchbaseOrm::Base base3.delete end + it "should be inspectable" do + base = BaseTest.create!(name: 'joe') + expect(base.inspect).to eq("#") + end + it "should load database responses" do base = BaseTest.create!(name: 'joe') resp = BaseTest.bucket.default_collection.get(base.id) @@ -58,7 +64,7 @@ class CompareTest < CouchbaseOrm::Base base = BaseTest.create!(name: 'joe') base_id = base.id - expect(base.to_json).to eq({name: 'joe', job: nil, id: base_id}.to_json) + expect(base.to_json).to eq({id: base_id, name: 'joe', job: nil}.to_json) expect(base.to_json(only: :name)).to eq({name: 'joe'}.to_json) base.destroy @@ -121,6 +127,20 @@ class CompareTest < CouchbaseOrm::Base end end + it "should set the attribute on creation" do + base = BaseTest.create!(name: 'joe') + expect(base.name).to eq('joe') + ensure + base.destroy + end + + it "should support getting the attribute by key" do + base = BaseTest.create!(name: 'joe') + expect(base[:name]).to eq('joe') + ensure + base.destroy + end + describe BaseTest do it_behaves_like "ActiveModel" end diff --git a/spec/enum_spec.rb b/spec/enum_spec.rb new file mode 100644 index 00000000..76036183 --- /dev/null +++ b/spec/enum_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true, encoding: ASCII-8BIT + +require File.expand_path("../support", __FILE__) + +class EnumTest < CouchbaseOrm::Base + enum rating: [:awesome, :good, :okay, :bad], default: :okay + enum color: [:red, :green, :blue] +end + +describe CouchbaseOrm::Base do + it "should create an attribute" do + base = EnumTest.create!(rating: :good, color: :red) + expect(base.attribute_names).to eq(["id", "rating", "color"]) + end + + it "should set the attribute" do + base = EnumTest.create!(rating: :good, color: :red) + expect(base.rating).to_not be_nil + expect(base.color).to_not be_nil + end + + it "should convert it to an int" do + base = EnumTest.create!(rating: :good, color: :red) + expect(base.rating).to eq 2 + expect(base.color).to eq 1 + end + + it "should use default value" do + base = EnumTest.create! + expect(base.rating).to eq 3 + expect(base.color).to eq 1 + end +end + diff --git a/spec/index_spec.rb b/spec/index_spec.rb index cf4436e3..8a724500 100644 --- a/spec/index_spec.rb +++ b/spec/index_spec.rb @@ -17,7 +17,7 @@ class NoUniqueIndexTest < CouchbaseOrm::Base index :email, presence: false end -class EnumTest < CouchbaseOrm::Base +class IndexEnumTest < CouchbaseOrm::Base enum visibility: [:group, :authority, :public], default: :authority enum color: [:red, :green, :blue] end @@ -86,22 +86,22 @@ class EnumTest < CouchbaseOrm::Base it "should work with enumerators" do # Test symbol - enum = EnumTest.create!(visibility: :public) + enum = IndexEnumTest.create!(visibility: :public) expect(enum.visibility).to eq(3) enum.destroy # Test number - enum = EnumTest.create!(visibility: 2) + enum = IndexEnumTest.create!(visibility: 2) expect(enum.visibility).to eq(2) enum.destroy # Test default - enum = EnumTest.create! + enum = IndexEnumTest.create! expect(enum.visibility).to eq(2) enum.destroy # Test default default - enum = EnumTest.create! + enum = IndexEnumTest.create! expect(enum.color).to eq(1) end diff --git a/spec/persistence_spec.rb b/spec/persistence_spec.rb index 71052bc3..6c83212a 100644 --- a/spec/persistence_spec.rb +++ b/spec/persistence_spec.rb @@ -4,7 +4,9 @@ class BasicModel < CouchbaseOrm::Base - attribute :name, :address, :age + attribute :name + attribute :address + attribute :age end class ModelWithDefaults < CouchbaseOrm::Base @@ -14,7 +16,9 @@ class ModelWithDefaults < CouchbaseOrm::Base end class ModelWithCallbacks < CouchbaseOrm::Base - attribute :name, :address, :age + attribute :name + attribute :address + attribute :age before_create :update_name before_save :set_address @@ -32,7 +36,8 @@ def set_age; self.age = 30; end end class ModelWithValidations < CouchbaseOrm::Base - attribute :name, :address, type: String + attribute :name, type: String + attribute :address, type: String attribute :age, type: :Integer validates :name, presence: true @@ -205,7 +210,8 @@ class ModelWithValidations < CouchbaseOrm::Base expect(model.save!).to be(model) # coercion will fail here - expect{ model.age = "a23" }.to raise_error(ArgumentError) + model.age = "a23" + expect{ model.save! }.to raise_error(CouchbaseOrm::Error::RecordInvalid) model.destroy end @@ -224,7 +230,7 @@ class ModelWithValidations < CouchbaseOrm::Base model.reload expect(model.changed?).to be(false) - expect(model.id).to be(id) + expect(model.id).to eq(id) model.destroy expect(model.destroyed?).to be(true) diff --git a/spec/views_spec.rb b/spec/views_spec.rb index 57af27a2..08ddb768 100644 --- a/spec/views_spec.rb +++ b/spec/views_spec.rb @@ -53,7 +53,7 @@ class ViewTest < CouchbaseOrm::Base it "should perform a map-reduce and return the view" do ViewTest.ensure_design_document! - mod = ViewTest.create! name: :bob, rating: :good + ViewTest.create! name: :bob, rating: :good docs = ViewTest.all.collect { |ob| ob.destroy From e9abcb6e55226448ecab2ad620e15a2ae494628d Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 7 Sep 2022 18:53:01 +0200 Subject: [PATCH 02/25] Add automatic timestamping --- lib/couchbase-orm/base.rb | 5 +++++ spec/base_spec.rb | 13 ++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 7e43bcb9..bf2c94ae 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -61,6 +61,10 @@ def connected? def symbol_column_to_string(name) name.to_s end + + def column_names # fixme should be an alias + attribute_names + end end def respond_to?(name) @@ -94,6 +98,7 @@ class Base define_model_callbacks :create, :destroy, :save, :update include Persistence + include ::ActiveRecord::Timestamp # must be included after Persistence include Associations include Views include N1ql diff --git a/spec/base_spec.rb b/spec/base_spec.rb index bafcbd65..6cf00a3b 100644 --- a/spec/base_spec.rb +++ b/spec/base_spec.rb @@ -12,7 +12,9 @@ class CompareTest < CouchbaseOrm::Base attribute :age, :integer end - +class TimestampTest < CouchbaseOrm::Base + attribute :created_at, :datetime +end describe CouchbaseOrm::Base do it "should be comparable to other objects" do @@ -140,6 +142,15 @@ class CompareTest < CouchbaseOrm::Base ensure base.destroy end + + it "should have timestamp attributes for create in model" do + expect(TimestampTest.timestamp_attributes_for_create_in_model).to eq(["created_at"]) + end + + it "should generate a timestamp on creation" do + base = TimestampTest.create!() + expect(base.created_at).to be_a(Time) + end describe BaseTest do it_behaves_like "ActiveModel" From b458710e721d380f6242d055aa67fc67b716cfff Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 8 Sep 2022 10:13:16 +0200 Subject: [PATCH 03/25] Add tests on types --- spec/type_spec.rb | 90 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 spec/type_spec.rb diff --git a/spec/type_spec.rb b/spec/type_spec.rb new file mode 100644 index 00000000..7148e8c3 --- /dev/null +++ b/spec/type_spec.rb @@ -0,0 +1,90 @@ +require File.expand_path("../support", __FILE__) + + +class TypeTest < CouchbaseOrm::Base + attribute :name, :string + attribute :age, :integer + attribute :size, :float + attribute :renewale_date, :date + attribute :subscribed_at, :datetime + attribute :active, :boolean + view :all +end + +TypeTest.ensure_design_document! + +describe CouchbaseOrm::Base do + before(:each) do + TypeTest.all.each(&:destroy) + end + + it "should be createable" do + t = TypeTest.create! + expect(t).to be_a(TypeTest) + end + + it "should be able to set attributes" do + t = TypeTest.new + t.name = "joe" + t.age = 20 + t.size = 1.5 + t.renewale_date = Date.today + t.subscribed_at = Time.now + t.active = true + t.save! + + expect(t.name).to eq("joe") + expect(t.age).to eq(20) + expect(t.size).to eq(1.5) + expect(t.renewale_date).to eq(Date.today) + expect(t.subscribed_at).to be_a(Time) + expect(t.active).to eq(true) + end + + it "should be able to set attributes with a hash" do + t = TypeTest.new(name: "joe", age: 20, size: 1.5, renewale_date: Date.today, subscribed_at: Time.now, active: true) + t.save! + + expect(t.name).to eq("joe") + expect(t.age).to eq(20) + expect(t.size).to eq(1.5) + expect(t.renewale_date).to eq(Date.today) + expect(t.subscribed_at).to be_a(Time) + expect(t.active).to eq(true) + end + + it "should be able to set attributes with a hash with indifferent access" do + t = TypeTest.new(ActiveSupport::HashWithIndifferentAccess.new(name: "joe", age: 20, size: 1.5, renewale_date: Date.today, subscribed_at: Time.now, active: true)) + t.save! + + expect(t.name).to eq("joe") + expect(t.age).to eq(20) + expect(t.size).to eq(1.5) + expect(t.renewale_date).to eq(Date.today) + expect(t.subscribed_at).to be_a(Time) + expect(t.active).to eq(true) + end + + it "should be able to type cast attributes" do + t = TypeTest.new(name: "joe", age: "20", size: "1.5", renewale_date: Date.today.to_s, subscribed_at: Time.now.to_s, active: "true") + t.save! + + expect(t.name).to eq("joe") + expect(t.age).to eq(20) + expect(t.size).to eq(1.5) + expect(t.renewale_date).to eq(Date.today) + expect(t.subscribed_at).to be_a(Time) + expect(t.active).to eq(true) + end + + it "should be consistent with active record on failed cast" do + t = TypeTest.new(name: "joe", age: "joe", size: "joe", renewale_date: "joe", subscribed_at: "joe", active: "true") + t.save! + + expect(t.age).to eq 0 + expect(t.size).to eq 0.0 + expect(t.renewale_date).to eq nil + expect(t.subscribed_at).to eq nil + expect(t.active).to eq true + end +end From c172d0727c0af01b3b7f64bc4ef137fa7a4c7be0 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 8 Sep 2022 10:16:45 +0200 Subject: [PATCH 04/25] remove rbenv-vars --- .gitignore | 1 + .rbenv-vars | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 .rbenv-vars diff --git a/.gitignore b/.gitignore index a0219977..63dba921 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.sw? .DS_Store +.rbenv-vars coverage rdoc html diff --git a/.rbenv-vars b/.rbenv-vars deleted file mode 100644 index 3aa07c1b..00000000 --- a/.rbenv-vars +++ /dev/null @@ -1,4 +0,0 @@ -COUCHBASE_USER=cb_admin -COUCHBASE_PASSWORD=cb_admin_pwd -COUCHBASE_BUCKET=default -ACTIVE_MODEL_VERSION=7.0.2.4 From 1234056829d99509915cef159d6a0679d11975db Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 8 Sep 2022 18:06:03 +0200 Subject: [PATCH 05/25] WIP --- lib/couchbase-orm.rb | 2 +- lib/couchbase-orm/base.rb | 37 +++++++ lib/couchbase-orm/n1ql.rb | 40 +++++--- lib/couchbase-orm/utilities/index.rb | 3 + spec/type_spec.rb | 144 ++++++++++++++++++++++++--- 5 files changed, 201 insertions(+), 25 deletions(-) diff --git a/lib/couchbase-orm.rb b/lib/couchbase-orm.rb index 970efd04..86503834 100644 --- a/lib/couchbase-orm.rb +++ b/lib/couchbase-orm.rb @@ -13,7 +13,7 @@ module CouchbaseOrm autoload :HasMany, 'couchbase-orm/utilities/has_many' def self.logger - @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT).tap { |l| l.level = Logger::INFO } + @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT).tap { |l| l.level = Logger::DEBUG } end def self.logger=(logger) diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index bf2c94ae..167857b3 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -65,6 +65,41 @@ def symbol_column_to_string(name) def column_names # fixme should be an alias attribute_names end + + # for Arel + + # def table_alias + # bucket.name + # end + + # def quote_table_name(table_name) + # "quoted_table_name(#{table_name})" + # end + + # def quote_column_name(column_name) + # "quoted_column_name(#{column_name})" + # end + + # def able_to_type_cast? + # return true + # end + + # def type_cast_for_database(key, value) + # attribute_types[key.to_s].cast(value) + # end + + # def quote(value) + # case value + # when String + # "'#{value}'" + # when Numeric + # value.to_s + # when NilClass + # "NULL" + # else + # value + # end + # end end def respond_to?(name) @@ -115,6 +150,8 @@ class Base class << self + + def connect(**options) @bucket = BucketProxy.new(::MTLibcouchbase::Bucket.new(**options)) end diff --git a/lib/couchbase-orm/n1ql.rb b/lib/couchbase-orm/n1ql.rb index ddf3937a..8517ca03 100644 --- a/lib/couchbase-orm/n1ql.rb +++ b/lib/couchbase-orm/n1ql.rb @@ -49,8 +49,13 @@ def n1ql(name, query_fn: nil, emit_key: [], **options) singleton_class.__send__(:define_method, name) do |**opts, &result_modifier| opts = options.merge(opts).reverse_merge(scan_consistency: :request_plus) +<<<<<<< HEAD values = convert_values(opts.delete(:key)) current_query = run_query(method_opts[:emit_key], values, query_fn, **opts.except(:include_docs)) +======= + values = convert_values(method_opts[:emit_key], opts.delete(:key)) + current_query = run_query(method_opts[:emit_key], values, select: select, **opts.except(:include_docs)) +>>>>>>> 1d54e67 (WIP) if result_modifier opts[:include_docs] = true @@ -73,23 +78,34 @@ def index_n1ql(attr, validate: true, find_method: nil, n1ql_method: nil) validates(attr, presence: true) if validate n1ql n1ql_method, emit_key: attr - instance_eval " - def self.#{find_method}(#{attr}) - #{n1ql_method}(key: #{attr}) - end - " + define_singleton_method find_method do |value| + send n1ql_method, key: value + end end private - def convert_values(values) - Array.wrap(values).compact.map do |v| - if v.class == String - "'#{N1ql.sanitize(v)}'" - elsif v.class == Date || v.class == Time - "'#{v.iso8601(3)}'" + def convert_values(keys, values) + keys.zip(Array.wrap(values)).map do |key, value_before_type_cast| + CouchbaseOrm.logger.debug "convert_values: #{key} => #{value_before_type_cast.inspect}" + + # cast value to type + value = attribute_types[key.to_s].cast(value_before_type_cast) + + ## prototype using arel builder + # t = ::Arel::Attribute.new(self, key).eq(value_before_type_cast) + # compiled = Arel::Visitors::PostgreSQL.new(self).compile(t) + # puts compiled + + # then quote and sanitize + if value.class == String + "'#{N1ql.sanitize(value)}'" + elsif value.class == Date + "'#{value.iso8601}'" + elsif value.class == Time + "'#{value}'" else - N1ql.sanitize(v).to_s + N1ql.sanitize(value).to_s end end end diff --git a/lib/couchbase-orm/utilities/index.rb b/lib/couchbase-orm/utilities/index.rb index 6119550c..61bc479d 100644 --- a/lib/couchbase-orm/utilities/index.rb +++ b/lib/couchbase-orm/utilities/index.rb @@ -50,6 +50,7 @@ def index(attrs, name = nil, presence: true, &processor) # use the bucket key as an index - lookup records by attr values define_singleton_method(find_by_method) do |*values| key = self.send(class_bucket_key_method, *values) + CouchbaseOrm.logger.debug("#{find_by_method}: #{class_bucket_key_method} with values #{values.inspect} give key: #{key}") id = self.collection.get(key)&.content if id mod = self.find_by_id(id) @@ -57,6 +58,8 @@ def index(attrs, name = nil, presence: true, &processor) # Clean up record if the id doesn't exist self.collection.remove(key) + else + CouchbaseOrm.logger.debug("#{find_by_method}: #{key} not found") end nil diff --git a/spec/type_spec.rb b/spec/type_spec.rb index 7148e8c3..88b74e5f 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -5,17 +5,43 @@ class TypeTest < CouchbaseOrm::Base attribute :name, :string attribute :age, :integer attribute :size, :float - attribute :renewale_date, :date + attribute :renewal_date, :date attribute :subscribed_at, :datetime attribute :active, :boolean - view :all + + n1ql :all + + index :age, presence: false + index :renewal_date, presence: false +end + +class N1qlTypeTest < CouchbaseOrm::Base + attribute :name, :string + attribute :age, :integer + attribute :size, :float + attribute :renewal_date, :date + attribute :subscribed_at, :datetime + attribute :active, :boolean + + n1ql :all + + index_n1ql :name, validate: false + index_n1ql :age, validate: false + index_n1ql :size, validate: false + index_n1ql :active, validate: false + index_n1ql :renewal_date, validate: false + index_n1ql :subscribed_at, validate: false + + n1ql :by_both_dates, emit_key: [:renewal_date, :subscribed_at], presence: false end TypeTest.ensure_design_document! +N1qlTypeTest.ensure_design_document! describe CouchbaseOrm::Base do before(:each) do TypeTest.all.each(&:destroy) + N1qlTypeTest.all.each(&:destroy) end it "should be createable" do @@ -28,7 +54,7 @@ class TypeTest < CouchbaseOrm::Base t.name = "joe" t.age = 20 t.size = 1.5 - t.renewale_date = Date.today + t.renewal_date = Date.today t.subscribed_at = Time.now t.active = true t.save! @@ -36,55 +62,149 @@ class TypeTest < CouchbaseOrm::Base expect(t.name).to eq("joe") expect(t.age).to eq(20) expect(t.size).to eq(1.5) - expect(t.renewale_date).to eq(Date.today) + expect(t.renewal_date).to eq(Date.today) expect(t.subscribed_at).to be_a(Time) expect(t.active).to eq(true) end it "should be able to set attributes with a hash" do - t = TypeTest.new(name: "joe", age: 20, size: 1.5, renewale_date: Date.today, subscribed_at: Time.now, active: true) + t = TypeTest.new(name: "joe", age: 20, size: 1.5, renewal_date: Date.today, subscribed_at: Time.now, active: true) t.save! expect(t.name).to eq("joe") expect(t.age).to eq(20) expect(t.size).to eq(1.5) - expect(t.renewale_date).to eq(Date.today) + expect(t.renewal_date).to eq(Date.today) expect(t.subscribed_at).to be_a(Time) expect(t.active).to eq(true) end + it "should be able to be stored and retrieved" do + now = Time.now + t = TypeTest.create!(name: "joe", age: 20, size: 1.5, renewal_date: Date.today, subscribed_at: now, active: true) + t2 = TypeTest.find(t.id) + + expect(t2.name).to eq("joe") + expect(t2.age).to eq(20) + expect(t2.size).to eq(1.5) + expect(t2.renewal_date).to eq(Date.today) + expect(t2.subscribed_at).to eq(now.utc.change(usec: 0)) + expect(t2.active).to eq(true) + end + + it "should be able to query by age" do + t = TypeTest.create!(age: 20) + _t2 = TypeTest.create!(age: 40) + expect(TypeTest.find_by_age(20)).to eq t + end + + it "should be able to query by age and type cast" do + t = TypeTest.create!(age: "20") + expect(TypeTest.find_by_age(20)).to eq t + expect(TypeTest.find_by_age("20")).to eq t + end + + it "should be able to query by date" do + t = TypeTest.create!(renewal_date: Date.today) + _t2 = TypeTest.create!(renewal_date: Date.today + 1) + expect(TypeTest.find_by_renewal_date(Date.today)).to eq t + end + + it "should be able to query by date and type cast" do + t = TypeTest.create!(renewal_date: Date.today.to_s) + expect(TypeTest.find_by_renewal_date(Date.today)).to eq t + expect(TypeTest.find_by_renewal_date(Date.today.to_s)).to eq t + end + it "should be able to set attributes with a hash with indifferent access" do - t = TypeTest.new(ActiveSupport::HashWithIndifferentAccess.new(name: "joe", age: 20, size: 1.5, renewale_date: Date.today, subscribed_at: Time.now, active: true)) + t = TypeTest.new(ActiveSupport::HashWithIndifferentAccess.new(name: "joe", age: 20, size: 1.5, renewal_date: Date.today, subscribed_at: Time.now, active: true)) t.save! expect(t.name).to eq("joe") expect(t.age).to eq(20) expect(t.size).to eq(1.5) - expect(t.renewale_date).to eq(Date.today) + expect(t.renewal_date).to eq(Date.today) expect(t.subscribed_at).to be_a(Time) expect(t.active).to eq(true) end it "should be able to type cast attributes" do - t = TypeTest.new(name: "joe", age: "20", size: "1.5", renewale_date: Date.today.to_s, subscribed_at: Time.now.to_s, active: "true") + t = TypeTest.new(name: "joe", age: "20", size: "1.5", renewal_date: Date.today.to_s, subscribed_at: Time.now.to_s, active: "true") t.save! expect(t.name).to eq("joe") expect(t.age).to eq(20) expect(t.size).to eq(1.5) - expect(t.renewale_date).to eq(Date.today) + expect(t.renewal_date).to eq(Date.today) expect(t.subscribed_at).to be_a(Time) expect(t.active).to eq(true) end it "should be consistent with active record on failed cast" do - t = TypeTest.new(name: "joe", age: "joe", size: "joe", renewale_date: "joe", subscribed_at: "joe", active: "true") + t = TypeTest.new(name: "joe", age: "joe", size: "joe", renewal_date: "joe", subscribed_at: "joe", active: "true") t.save! expect(t.age).to eq 0 expect(t.size).to eq 0.0 - expect(t.renewale_date).to eq nil + expect(t.renewal_date).to eq nil expect(t.subscribed_at).to eq nil expect(t.active).to eq true end + + it "should be able to query by name" do + t = N1qlTypeTest.create!(name: "joe") + _t2 = N1qlTypeTest.create!(name: "john") + expect(N1qlTypeTest.find_by_name("joe").to_a).to eq [t] + end + + pending "should be able to query by nil value" do + t = N1qlTypeTest.create!() + _t2 = N1qlTypeTest.create!(name: "john") + expect(N1qlTypeTest.find_by_name(nil).to_a).to eq [t] + end + + pending "should be able to query by array value" do + t = N1qlTypeTest.create!(name: "laura") + t2 = N1qlTypeTest.create!(name: "joe") + _t3 = N1qlTypeTest.create!(name: "john") + expect(N1qlTypeTest.find_by_name(["laura", "joe"]).to_a).to match_array [t, t2] + end + + it "should be able to query by integer" do + t = N1qlTypeTest.create!(age: 20) + t2 = N1qlTypeTest.create!(age: 20) + _t3 = N1qlTypeTest.create!(age: 40) + expect(N1qlTypeTest.find_by_age(20).to_a).to match_array [t, t2] + end + + it "should be able to query by integer and type cast" do + t = N1qlTypeTest.create!(age: "20") + expect(N1qlTypeTest.find_by_age(20).to_a).to eq [t] + expect(N1qlTypeTest.find_by_age("20").to_a).to eq [t] + end + + it "should be able to query by date" do + t = N1qlTypeTest.create!(renewal_date: Date.today) + _t2 = N1qlTypeTest.create!(renewal_date: Date.today + 1) + expect(N1qlTypeTest.find_by_renewal_date(Date.today).to_a).to eq [t] + end + + it "should be able to query by time" do + now = Time.now + t = N1qlTypeTest.create!(subscribed_at: now) + _t2 = N1qlTypeTest.create!(subscribed_at: now + 1) + expect(N1qlTypeTest.find_by_subscribed_at(now).to_a).to eq [t] + end + + it "should be able to query by boolean" do + t = N1qlTypeTest.create!(active: true) + _t2 = N1qlTypeTest.create!(active: false) + expect(N1qlTypeTest.find_by_active(true).to_a).to eq [t] + end + + it "should be able to query by float" do + t = N1qlTypeTest.create!(size: 1.5) + _t2 = N1qlTypeTest.create!(size: 2.5) + expect(N1qlTypeTest.find_by_size(1.5).to_a).to eq [t] + end end From 65971eefdaa2c397d325341cbeb6cb0a885b1299 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Fri, 9 Sep 2022 10:40:59 +0200 Subject: [PATCH 06/25] fix tests --- lib/couchbase-orm/n1ql.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/couchbase-orm/n1ql.rb b/lib/couchbase-orm/n1ql.rb index 8517ca03..ef913535 100644 --- a/lib/couchbase-orm/n1ql.rb +++ b/lib/couchbase-orm/n1ql.rb @@ -49,14 +49,8 @@ def n1ql(name, query_fn: nil, emit_key: [], **options) singleton_class.__send__(:define_method, name) do |**opts, &result_modifier| opts = options.merge(opts).reverse_merge(scan_consistency: :request_plus) -<<<<<<< HEAD - values = convert_values(opts.delete(:key)) + values = convert_values(method_opts[:emit_key], opts.delete(:key)) if opts[:key] current_query = run_query(method_opts[:emit_key], values, query_fn, **opts.except(:include_docs)) -======= - values = convert_values(method_opts[:emit_key], opts.delete(:key)) - current_query = run_query(method_opts[:emit_key], values, select: select, **opts.except(:include_docs)) ->>>>>>> 1d54e67 (WIP) - if result_modifier opts[:include_docs] = true current_query.results &result_modifier From a06c1b01bd189f1c1ccf4d949fc2e2392c6c2b2e Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 14 Sep 2022 09:29:58 +0200 Subject: [PATCH 07/25] Some cleanup --- lib/couchbase-orm.rb | 2 +- lib/couchbase-orm/base.rb | 87 ++------------------------------------- 2 files changed, 5 insertions(+), 84 deletions(-) diff --git a/lib/couchbase-orm.rb b/lib/couchbase-orm.rb index 86503834..970efd04 100644 --- a/lib/couchbase-orm.rb +++ b/lib/couchbase-orm.rb @@ -13,7 +13,7 @@ module CouchbaseOrm autoload :HasMany, 'couchbase-orm/utilities/has_many' def self.logger - @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT).tap { |l| l.level = Logger::DEBUG } + @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT).tap { |l| l.level = Logger::INFO } end def self.logger=(logger) diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 167857b3..8157c3e8 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -62,44 +62,9 @@ def symbol_column_to_string(name) name.to_s end - def column_names # fixme should be an alias + def column_names # can't be an alias for now attribute_names end - - # for Arel - - # def table_alias - # bucket.name - # end - - # def quote_table_name(table_name) - # "quoted_table_name(#{table_name})" - # end - - # def quote_column_name(column_name) - # "quoted_column_name(#{column_name})" - # end - - # def able_to_type_cast? - # return true - # end - - # def type_cast_for_database(key, value) - # attribute_types[key.to_s].cast(value) - # end - - # def quote(value) - # case value - # when String - # "'#{value}'" - # when Numeric - # value.to_s - # when NilClass - # "NULL" - # else - # value - # end - # end end def respond_to?(name) @@ -231,7 +196,7 @@ def initialize(model = nil, ignore_doc_type: false, **attributes) raise CouchbaseOrm::Error::TypeMismatchError.new("document type mismatch, #{type} != #{self.class.design_document}", self) end - self.id = attributes[:id] if attributes[:id] + self.id = attributes[:id].presence if attributes[:id].present? @__metadata__.cas = model.cas assign_attributes(doc) @@ -254,13 +219,13 @@ def initialize(model = nil, ignore_doc_type: false, **attributes) # Document ID is a special case as it is not stored in the document def id - @id.presence + @id end def id=(value) raise 'ID cannot be changed' if @__metadata__.cas && value attribute_will_change!(:id) - @id = value.to_s + @id = value.to_s.presence end def [](key) @@ -272,50 +237,6 @@ def []=(key, value) send(:"#{key}=", value) end - # def write_attribute(attr_name, value) - # unless value.nil? - # coerce = self.class.attributes[attr_name][:type] - # value = Kernel.send(coerce.to_s, value) if coerce - # end - # attribute_will_change!(attr_name) unless @__attributes__[attr_name] == value - # @__attributes__[attr_name] = value - # end - # alias_method :[]=, :write_attribute - - # - # Add support for Serialization: - # http://guides.rubyonrails.org/active_model_basics.html#serialization - # - - # def attributes - # copy = @__attributes__.merge({id: id}) - # copy.delete(:type) - # copy - # end - - # def attributes=(attributes) - # attributes.each do |key, value| - # setter = :"#{key}=" - # send(setter, value) if respond_to?(setter) - # end - # end - - # ID_LOOKUP = ['id', :id].freeze - # def attribute(name) - # return self.id if ID_LOOKUP.include?(name) - # @__attributes__[name] - # end - # alias_method :read_attribute_for_serialization, :attribute - - # def attribute=(name, value) - # __send__(:"#{name}=", value) - # end - - - # - # Add support for comparisons - # - # Public: Allows for access to ActiveModel functionality. # # Returns self. From beb4504190fd06a0a8dee178641ed2df9193d227 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 14 Sep 2022 09:42:55 +0200 Subject: [PATCH 08/25] More cleanup --- lib/couchbase-orm/base.rb | 4 ---- lib/couchbase-orm/n1ql.rb | 5 ----- lib/couchbase-orm/views.rb | 16 +--------------- spec/base_spec.rb | 7 +++++++ 4 files changed, 8 insertions(+), 24 deletions(-) diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 8157c3e8..ad6e3ec9 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -114,9 +114,6 @@ class Base class << self - - - def connect(**options) @bucket = BucketProxy.new(::MTLibcouchbase::Bucket.new(**options)) end @@ -173,7 +170,6 @@ def exists?(id) collection.exists(id).exists end alias_method :has_key?, :exists? - end class MismatchTypeError < RuntimeError; end diff --git a/lib/couchbase-orm/n1ql.rb b/lib/couchbase-orm/n1ql.rb index ef913535..fa724e38 100644 --- a/lib/couchbase-orm/n1ql.rb +++ b/lib/couchbase-orm/n1ql.rb @@ -85,11 +85,6 @@ def convert_values(keys, values) # cast value to type value = attribute_types[key.to_s].cast(value_before_type_cast) - - ## prototype using arel builder - # t = ::Arel::Attribute.new(self, key).eq(value_before_type_cast) - # compiled = Arel::Visitors::PostgreSQL.new(self).compile(t) - # puts compiled # then quote and sanitize if value.class == String diff --git a/lib/couchbase-orm/views.rb b/lib/couchbase-orm/views.rb index 9ead45ad..98399954 100644 --- a/lib/couchbase-orm/views.rb +++ b/lib/couchbase-orm/views.rb @@ -48,27 +48,13 @@ def view(name, map: nil, emit_key: nil, reduce: nil, **options) EMAP else emit_key = emit_key || :created_at - - if emit_key != :created_at && false # FIXME: not longer supported, need tests # self.attributes[emit_key][:type].to_s == 'Array' - method_opts[:map] = <<-EMAP -function(doc) { - var i; - if (doc.type === "{{design_document}}") { - for (i = 0; i < doc.#{emit_key}.length; i += 1) { - emit(doc.#{emit_key}[i], null); - } - } -} -EMAP - else - method_opts[:map] = <<-EMAP + method_opts[:map] = <<-EMAP function(doc) { if (doc.type === "{{design_document}}") { emit(doc.#{emit_key}, null); } } EMAP - end end end diff --git a/spec/base_spec.rb b/spec/base_spec.rb index 6cf00a3b..432b8245 100644 --- a/spec/base_spec.rb +++ b/spec/base_spec.rb @@ -81,6 +81,12 @@ class TimestampTest < CouchbaseOrm::Base base.name = 'change' expect(base.changes.empty?).to be(false) + # Attributes are set by key + base = BaseTest.new + base[:name] = 'bob' + expect(base.changes.empty?).to be(false) + + # Attributes are set by initializer from hash base = BaseTest.new({name: 'bob'}) expect(base.changes.empty?).to be(false) expect(base.previous_changes.empty?).to be(true) @@ -94,6 +100,7 @@ class TimestampTest < CouchbaseOrm::Base base = BaseTest.new(base) expect(base.changes.empty?).to be(false) expect(base.previous_changes.empty?).to be(true) + ensure base.destroy if base.id end From 3663957400393538c7af776c113c77602f0c2065 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 14 Sep 2022 10:57:52 +0200 Subject: [PATCH 09/25] Add pry and pry stack explorer in tests --- couchbase-orm.gemspec | 1 + spec/support.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/couchbase-orm.gemspec b/couchbase-orm.gemspec index 74ca76fa..36cdb64a 100644 --- a/couchbase-orm.gemspec +++ b/couchbase-orm.gemspec @@ -23,6 +23,7 @@ Gem::Specification.new do |gem| gem.add_development_dependency 'rspec', '~> 3.7' gem.add_development_dependency 'yard', '~> 0.9' gem.add_development_dependency 'pry' + gem.add_development_dependency 'pry-stack_explorer' gem.add_development_dependency 'simplecov' gem.files = `git ls-files`.split("\n") diff --git a/spec/support.rb b/spec/support.rb index 68cec06a..fe647064 100644 --- a/spec/support.rb +++ b/spec/support.rb @@ -3,6 +3,8 @@ require 'couchbase-orm' require 'minitest/assertions' require 'active_model/lint' +require 'pry' +require 'pry-stack_explorer' SimpleCov.start do add_group 'Core', [/lib\/couchbase-orm\/(?!(proxies|utilities))/, 'lib/couchbase-orm.rb'] From 8c8c8bed7076f48cada8741cde3feb5b353b0934 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 14 Sep 2022 10:58:24 +0200 Subject: [PATCH 10/25] Add env var to set logger in debug --- lib/couchbase-orm.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/couchbase-orm.rb b/lib/couchbase-orm.rb index 970efd04..fd573143 100644 --- a/lib/couchbase-orm.rb +++ b/lib/couchbase-orm.rb @@ -13,7 +13,7 @@ module CouchbaseOrm autoload :HasMany, 'couchbase-orm/utilities/has_many' def self.logger - @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT).tap { |l| l.level = Logger::INFO } + @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT).tap { |l| l.level = Logger::INFO unless ENV["COUCHBASE_ORM_DEBUG"] } end def self.logger=(logger) From 2d501be815832e48a81c0b36b11bfde173b92f37 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 14 Sep 2022 10:58:55 +0200 Subject: [PATCH 11/25] Cleanup --- lib/couchbase-orm/base.rb | 2 +- lib/couchbase-orm/persistence.rb | 2 +- spec/has_many_spec.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index ad6e3ec9..9dbffa14 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -192,7 +192,7 @@ def initialize(model = nil, ignore_doc_type: false, **attributes) raise CouchbaseOrm::Error::TypeMismatchError.new("document type mismatch, #{type} != #{self.class.design_document}", self) end - self.id = attributes[:id].presence if attributes[:id].present? + self.id = attributes[:id] if attributes[:id].present? @__metadata__.cas = model.cas assign_attributes(doc) diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index c174af16..23b9461c 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -125,7 +125,7 @@ def destroy(with_cas: false, **options) destroy_associations! options[:cas] = @__metadata__.cas if with_cas - CouchbaseOrm.logger.debug "Data - Delete #{id}" + CouchbaseOrm.logger.debug "Data - Destroy #{id}" self.class.collection.remove(id, **options) self.id = nil diff --git a/spec/has_many_spec.rb b/spec/has_many_spec.rb index b4069507..c03e4f32 100644 --- a/spec/has_many_spec.rb +++ b/spec/has_many_spec.rb @@ -50,8 +50,8 @@ second = @object_test_class.create! name: :jane rate1 = @rating_test_class.create! rating: :awesome, "object_#{@context}_test": first - rate2 = @rating_test_class.create! rating: :bad, "object_#{@context}_test": second - rate3 = @rating_test_class.create! rating: :good, "object_#{@context}_test": first + _rate2 = @rating_test_class.create! rating: :bad, "object_#{@context}_test": second + _rate3 = @rating_test_class.create! rating: :good, "object_#{@context}_test": first ort = @object_rating_test_class.create! "object_#{@context}_test": first, "rating_#{@context}_test": rate1 @object_rating_test_class.create! "object_#{@context}_test": second, "rating_#{@context}_test": rate1 From 9dc2fc42e4693317fb00fb65f9b71e3236813190 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 14 Sep 2022 10:59:28 +0200 Subject: [PATCH 12/25] handle typecast of array of values for custom query --- lib/couchbase-orm/n1ql.rb | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/couchbase-orm/n1ql.rb b/lib/couchbase-orm/n1ql.rb index fa724e38..2cd76c51 100644 --- a/lib/couchbase-orm/n1ql.rb +++ b/lib/couchbase-orm/n1ql.rb @@ -80,11 +80,18 @@ def index_n1ql(attr, validate: true, find_method: nil, n1ql_method: nil) private def convert_values(keys, values) - keys.zip(Array.wrap(values)).map do |key, value_before_type_cast| - CouchbaseOrm.logger.debug "convert_values: #{key} => #{value_before_type_cast.inspect}" - + raise ArgumentError, "Empty keys but values are present, can't type cast" if keys.empty? && Array.wrap(values).any? + keys.zip(Array.wrap(values)).map do |key, value_before_type_cast| # cast value to type - value = attribute_types[key.to_s].cast(value_before_type_cast) + value = if value_before_type_cast.is_a?(Array) + value_before_type_cast.map do |v| + attribute_types[key.to_s].cast(v) + end + else + attribute_types[key.to_s].cast(value_before_type_cast) + end + + CouchbaseOrm.logger.debug "convert_values: #{key} => #{value_before_type_cast.inspect} => #{value}" # then quote and sanitize if value.class == String From 7a628bedd31750e7d2b1c1ec0792bec3ebd417b8 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 14 Sep 2022 11:00:16 +0200 Subject: [PATCH 13/25] raise instead of producing wrong queries --- lib/couchbase-orm/utilities/has_many.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/couchbase-orm/utilities/has_many.rb b/lib/couchbase-orm/utilities/has_many.rb index 7885ffba..9f43613d 100644 --- a/lib/couchbase-orm/utilities/has_many.rb +++ b/lib/couchbase-orm/utilities/has_many.rb @@ -37,6 +37,7 @@ class #{class_name} < CouchbaseOrm::Base return self.instance_variable_get(instance_var) if instance_variable_defined?(instance_var) remote_klass = remote_class.constantize + raise ArgumentError, "Can't find #{remote_method} without an id" unless self.id.present? enum = klass.__send__(remote_method, key: self.id) { |row| case type when :n1ql @@ -93,7 +94,8 @@ def build_index_view(klass, remote_class, remote_method, through_key, foreign_ke def build_index_n1ql(klass, remote_class, remote_method, through_key, foreign_key) if remote_class klass.class_eval do - n1ql remote_method, query_fn: proc { |bucket, values, options| + n1ql remote_method, emit_key: 'id', query_fn: proc { |bucket, values, options| + raise ArgumentError, "values[0] must not be blank" if values[0].blank? cluster.query("SELECT raw #{through_key} FROM `#{bucket.name}` where type = \"#{design_document}\" and #{foreign_key} = #{values[0]}", options) } end From ba42fe18745624d770a678091ddc8403cea7c44c Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 14 Sep 2022 11:04:02 +0200 Subject: [PATCH 14/25] cleanup old exploration method --- lib/couchbase-orm/base.rb | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 9dbffa14..18690295 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -42,25 +42,25 @@ def base_class? true end - def base_class - self - end + # def base_class + # self + # end - def abstract_class? - false - end + # def abstract_class? + # false + # end - def table_exists? - true - end + # def table_exists? + # true + # end - def connected? - true - end + # def connected? + # true + # end - def symbol_column_to_string(name) - name.to_s - end + # def symbol_column_to_string(name) + # name.to_s + # end def column_names # can't be an alias for now attribute_names From 96267e60b718fd14aeeeb67bef48a454af5385d8 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 14 Sep 2022 11:04:21 +0200 Subject: [PATCH 15/25] cleanup old exploration method --- lib/couchbase-orm/base.rb | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 18690295..30ba9baf 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -42,26 +42,6 @@ def base_class? true end - # def base_class - # self - # end - - # def abstract_class? - # false - # end - - # def table_exists? - # true - # end - - # def connected? - # true - # end - - # def symbol_column_to_string(name) - # name.to_s - # end - def column_names # can't be an alias for now attribute_names end From 8cdb0e37ef7bf44032ff14a87046785aa1b2d329 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 14 Sep 2022 11:05:58 +0200 Subject: [PATCH 16/25] cleanup old exploration method --- lib/couchbase-orm/base.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 30ba9baf..0d0fbda7 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -47,11 +47,6 @@ def column_names # can't be an alias for now end end - def respond_to?(name) - # skip ActiveRecord::AttributeMethods respond_to? method - method(:respond_to?).super_method.super_method.call(name) - end - def _has_attribute?(attr_name) attribute_names.include?(attr_name.to_s) end From f032d3286bc299917b8d67b25d9f14457d8909d1 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 14 Sep 2022 13:44:56 +0200 Subject: [PATCH 17/25] Improve support of custom types, serialization and datetime/timestamp support --- lib/couchbase-orm/base.rb | 5 +- lib/couchbase-orm/n1ql.rb | 4 +- lib/couchbase-orm/persistence.rb | 15 ++++-- lib/couchbase-orm/types.rb | 5 ++ lib/couchbase-orm/types/date_time.rb | 13 +++++ lib/couchbase-orm/types/timestamp.rb | 18 +++++++ lib/couchbase-orm/utilities/index.rb | 3 +- spec/type_spec.rb | 75 +++++++++++++++++++++++++++- 8 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 lib/couchbase-orm/types.rb create mode 100644 lib/couchbase-orm/types/date_time.rb create mode 100644 lib/couchbase-orm/types/timestamp.rb diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 0d0fbda7..9d64c6f1 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -12,6 +12,7 @@ require 'couchbase-orm/n1ql' require 'couchbase-orm/persistence' require 'couchbase-orm/associations' +require 'couchbase-orm/types' require 'couchbase-orm/proxies/bucket_proxy' require 'couchbase-orm/proxies/collection_proxy' require 'couchbase-orm/utilities/join' @@ -30,10 +31,6 @@ module ActiveRecordCompat extend ActiveSupport::Concern module ClassMethods - def reset_primary_key - primary_key = "id" - end - def primary_key "id" end diff --git a/lib/couchbase-orm/n1ql.rb b/lib/couchbase-orm/n1ql.rb index 2cd76c51..b4c1f3be 100644 --- a/lib/couchbase-orm/n1ql.rb +++ b/lib/couchbase-orm/n1ql.rb @@ -85,10 +85,10 @@ def convert_values(keys, values) # cast value to type value = if value_before_type_cast.is_a?(Array) value_before_type_cast.map do |v| - attribute_types[key.to_s].cast(v) + attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(v)) end else - attribute_types[key.to_s].cast(value_before_type_cast) + attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(value_before_type_cast)) end CouchbaseOrm.logger.debug "convert_values: #{key} => #{value_before_type_cast.inspect} => #{value}" diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index 23b9461c..20408744 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -224,7 +224,12 @@ def touch(**options) protected - + def serialized_attributes + attributes.map { |k, v| + [k, self.class.attribute_types[k].serialize(v)] + }.to_h + end + def _update_record(with_cas: false, **options) return false unless perform_validations(:update, options) return true unless changed? @@ -232,8 +237,8 @@ def _update_record(with_cas: false, **options) run_callbacks :update do run_callbacks :save do options[:cas] = @__metadata__.cas if with_cas - CouchbaseOrm.logger.debug { "_update_record - replace #{id} #{attributes.to_s.truncate(200)}" } - resp = self.class.collection.replace(id, attributes.except(:id).merge(type: self.class.design_document), Couchbase::Options::Replace.new(**options)) + CouchbaseOrm.logger.debug { "_update_record - replace #{id} #{serialized_attributes.to_s.truncate(200)}" } + resp = self.class.collection.replace(id, serialized_attributes.except(:id).merge(type: self.class.design_document), Couchbase::Options::Replace.new(**options)) # Ensure the model is up to date @__metadata__.cas = resp.cas @@ -249,9 +254,9 @@ def _create_record(**options) run_callbacks :create do run_callbacks :save do assign_attributes(id: self.class.uuid_generator.next(self)) unless self.id - CouchbaseOrm.logger.debug { "_create_record - Upsert #{id} #{attributes.to_s.truncate(200)}" } + CouchbaseOrm.logger.debug { "_create_record - Upsert #{id} #{serialized_attributes.to_s.truncate(200)}" } - resp = self.class.collection.upsert(self.id, attributes.except(:id).merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options)) + resp = self.class.collection.upsert(self.id, serialized_attributes.except(:id).merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options)) # Ensure the model is up to date @__metadata__.cas = resp.cas diff --git a/lib/couchbase-orm/types.rb b/lib/couchbase-orm/types.rb new file mode 100644 index 00000000..37d39058 --- /dev/null +++ b/lib/couchbase-orm/types.rb @@ -0,0 +1,5 @@ +require "couchbase-orm/types/date_time" +require "couchbase-orm/types/timestamp" + +ActiveModel::Type.register(:datetime, CouchbaseOrm::Types::DateTime) +ActiveModel::Type.register(:timestamp, CouchbaseOrm::Types::Timestamp) diff --git a/lib/couchbase-orm/types/date_time.rb b/lib/couchbase-orm/types/date_time.rb new file mode 100644 index 00000000..a9a26984 --- /dev/null +++ b/lib/couchbase-orm/types/date_time.rb @@ -0,0 +1,13 @@ +module CouchbaseOrm + module Types + class DateTime < ActiveModel::Type::DateTime + def cast(value) + super(value)&.utc + end + + def serialize(value) + value&.iso8601 + end + end + end +end diff --git a/lib/couchbase-orm/types/timestamp.rb b/lib/couchbase-orm/types/timestamp.rb new file mode 100644 index 00000000..465f3bce --- /dev/null +++ b/lib/couchbase-orm/types/timestamp.rb @@ -0,0 +1,18 @@ +module CouchbaseOrm + module Types + class Timestamp < ActiveModel::Type::DateTime + def cast(value) + return nil if value.nil? + return Time.at(value) if value.is_a?(Integer) + return Time.at(value.to_i) if value.is_a?(String) && value =~ /^[0-9]+$/ + return value.utc if value.is_a?(Time) + super(value) + end + + def serialize(value) + value&.to_i&.to_s + end + end + end +end + diff --git a/lib/couchbase-orm/utilities/index.rb b/lib/couchbase-orm/utilities/index.rb index 61bc479d..1820ffb8 100644 --- a/lib/couchbase-orm/utilities/index.rb +++ b/lib/couchbase-orm/utilities/index.rb @@ -31,7 +31,7 @@ def index(attrs, name = nil, presence: true, &processor) # collect a list of values for each key component attribute define_method(bucket_key_vals_method) do - attrs.collect {|attr| self[attr]} + attrs.collect {|attr| self.class.attribute_types[attr.to_s].cast(self[attr])} end @@ -40,6 +40,7 @@ def index(attrs, name = nil, presence: true, &processor) #---------------- # simple wrapper around the processor proc if supplied define_singleton_method(processor_method) do |*values| + values = attrs.zip(values).map { |attr,value| attribute_types[attr.to_s].serialize(attribute_types[attr.to_s].cast(value)) } if processor processor.call(values.length == 1 ? values.first : values) else diff --git a/spec/type_spec.rb b/spec/type_spec.rb index 88b74e5f..19fa7a97 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -1,5 +1,14 @@ require File.expand_path("../support", __FILE__) +require "active_model" + +class DateTimeWith3Decimal < CouchbaseOrm::Types::DateTime + def serialize(value) + value&.iso8601(3) + end +end + +ActiveModel::Type.register(:datetime3decimal, DateTimeWith3Decimal) class TypeTest < CouchbaseOrm::Base attribute :name, :string @@ -7,12 +16,16 @@ class TypeTest < CouchbaseOrm::Base attribute :size, :float attribute :renewal_date, :date attribute :subscribed_at, :datetime + attribute :some_time, :timestamp + attribute :precision_time, :datetime3decimal attribute :active, :boolean n1ql :all index :age, presence: false index :renewal_date, presence: false + index :some_time, presence: false + index :precision_time, presence: false end class N1qlTypeTest < CouchbaseOrm::Base @@ -21,6 +34,8 @@ class N1qlTypeTest < CouchbaseOrm::Base attribute :size, :float attribute :renewal_date, :date attribute :subscribed_at, :datetime + attribute :some_time, :timestamp + attribute :precision_time, :datetime3decimal attribute :active, :boolean n1ql :all @@ -30,14 +45,26 @@ class N1qlTypeTest < CouchbaseOrm::Base index_n1ql :size, validate: false index_n1ql :active, validate: false index_n1ql :renewal_date, validate: false + index_n1ql :some_time, validate: false index_n1ql :subscribed_at, validate: false - + index_n1ql :precision_time, validate: false n1ql :by_both_dates, emit_key: [:renewal_date, :subscribed_at], presence: false end TypeTest.ensure_design_document! N1qlTypeTest.ensure_design_document! +describe CouchbaseOrm::Types::Timestamp do + it "should cast an integer to time" do + t = Time.at(Time.now.to_i) + expect(CouchbaseOrm::Types::Timestamp.new.cast(t.to_i)).to eq(t) + end + it "should cast an integer string to time" do + t = Time.at(Time.now.to_i) + expect(CouchbaseOrm::Types::Timestamp.new.cast(t.to_s)).to eq(t) + end +end + describe CouchbaseOrm::Base do before(:each) do TypeTest.all.each(&:destroy) @@ -116,6 +143,36 @@ class N1qlTypeTest < CouchbaseOrm::Base expect(TypeTest.find_by_renewal_date(Date.today.to_s)).to eq t end + it "should be able to query by time" do + now = Time.now + t = TypeTest.create!(name: "t", some_time: now) + _t2 = TypeTest.create!(name: "t2", some_time: now + 1) + expect(TypeTest.find_by_some_time(now)).to eq t + end + + it "should be able to query by time and type cast" do + now = Time.now + now_s = now.to_i.to_s + t = TypeTest.create!(some_time: now_s) + expect(TypeTest.find_by_some_time(now)).to eq t + expect(TypeTest.find_by_some_time(now_s)).to eq t + end + + it "should be able to query by custom type" do + now = Time.now + t = TypeTest.create!(precision_time: now) + _t2 = TypeTest.create!(precision_time: now + 1) + expect(TypeTest.find_by_precision_time(now)).to eq t + end + + it "should be able to query by custom type and type cast" do + now = Time.now + now_s = now.utc.iso8601(3) + t = TypeTest.create!(precision_time: now_s) + expect(TypeTest.find_by_precision_time(now)).to eq t + expect(TypeTest.find_by_precision_time(now_s)).to eq t + end + it "should be able to set attributes with a hash with indifferent access" do t = TypeTest.new(ActiveSupport::HashWithIndifferentAccess.new(name: "joe", age: 20, size: 1.5, renewal_date: Date.today, subscribed_at: Time.now, active: true)) t.save! @@ -189,13 +246,27 @@ class N1qlTypeTest < CouchbaseOrm::Base expect(N1qlTypeTest.find_by_renewal_date(Date.today).to_a).to eq [t] end - it "should be able to query by time" do + it "should be able to query by datetime" do now = Time.now t = N1qlTypeTest.create!(subscribed_at: now) _t2 = N1qlTypeTest.create!(subscribed_at: now + 1) expect(N1qlTypeTest.find_by_subscribed_at(now).to_a).to eq [t] end + it "should be able to query by timestamp" do + now = Time.now + t = N1qlTypeTest.create!(some_time: now) + _t2 = N1qlTypeTest.create!(some_time: now + 1) + expect(N1qlTypeTest.find_by_some_time(now).to_a).to eq [t] + end + + it "should be able to query by custom type" do + now = Time.now + t = N1qlTypeTest.create!(precision_time: now) + _t2 = N1qlTypeTest.create!(precision_time: now + 1) + expect(N1qlTypeTest.find_by_precision_time(now).to_a).to eq [t] + end + it "should be able to query by boolean" do t = N1qlTypeTest.create!(active: true) _t2 = N1qlTypeTest.create!(active: false) From 253c29c576c49488b9fcfb4446687e61bf9aef30 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 14 Sep 2022 13:51:30 +0200 Subject: [PATCH 18/25] Encode timestamp as integer --- lib/couchbase-orm/types/timestamp.rb | 2 +- spec/type_spec.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/couchbase-orm/types/timestamp.rb b/lib/couchbase-orm/types/timestamp.rb index 465f3bce..c95a438c 100644 --- a/lib/couchbase-orm/types/timestamp.rb +++ b/lib/couchbase-orm/types/timestamp.rb @@ -10,7 +10,7 @@ def cast(value) end def serialize(value) - value&.to_i&.to_s + value&.to_i end end end diff --git a/spec/type_spec.rb b/spec/type_spec.rb index 19fa7a97..7afbf834 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -148,6 +148,7 @@ class N1qlTypeTest < CouchbaseOrm::Base t = TypeTest.create!(name: "t", some_time: now) _t2 = TypeTest.create!(name: "t2", some_time: now + 1) expect(TypeTest.find_by_some_time(now)).to eq t + binding.pry end it "should be able to query by time and type cast" do From 0663864b682c08397169cf5740d7c7881b7f24a1 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 14 Sep 2022 13:55:37 +0200 Subject: [PATCH 19/25] remove debugger --- spec/type_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/type_spec.rb b/spec/type_spec.rb index 7afbf834..19fa7a97 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -148,7 +148,6 @@ class N1qlTypeTest < CouchbaseOrm::Base t = TypeTest.create!(name: "t", some_time: now) _t2 = TypeTest.create!(name: "t2", some_time: now + 1) expect(TypeTest.find_by_some_time(now)).to eq t - binding.pry end it "should be able to query by time and type cast" do From 8f5cfd0d8e7a5ffedaaa9d5dfcbf1de870aae813 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Wed, 14 Sep 2022 14:48:31 +0200 Subject: [PATCH 20/25] handle date at the serialization stage, not the quoting one --- lib/couchbase-orm/n1ql.rb | 6 +----- lib/couchbase-orm/types.rb | 2 ++ lib/couchbase-orm/types/date.rb | 13 +++++++++++++ spec/type_spec.rb | 1 + 4 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 lib/couchbase-orm/types/date.rb diff --git a/lib/couchbase-orm/n1ql.rb b/lib/couchbase-orm/n1ql.rb index b4c1f3be..016d74ab 100644 --- a/lib/couchbase-orm/n1ql.rb +++ b/lib/couchbase-orm/n1ql.rb @@ -91,15 +91,11 @@ def convert_values(keys, values) attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(value_before_type_cast)) end - CouchbaseOrm.logger.debug "convert_values: #{key} => #{value_before_type_cast.inspect} => #{value}" + CouchbaseOrm.logger.debug "convert_values: #{key} => #{value_before_type_cast.inspect} => #{value.inspect} #{value.class} #{attribute_types[key.to_s]}" # then quote and sanitize if value.class == String "'#{N1ql.sanitize(value)}'" - elsif value.class == Date - "'#{value.iso8601}'" - elsif value.class == Time - "'#{value}'" else N1ql.sanitize(value).to_s end diff --git a/lib/couchbase-orm/types.rb b/lib/couchbase-orm/types.rb index 37d39058..e6a375d2 100644 --- a/lib/couchbase-orm/types.rb +++ b/lib/couchbase-orm/types.rb @@ -1,5 +1,7 @@ +require "couchbase-orm/types/date" require "couchbase-orm/types/date_time" require "couchbase-orm/types/timestamp" +ActiveModel::Type.register(:date, CouchbaseOrm::Types::Date) ActiveModel::Type.register(:datetime, CouchbaseOrm::Types::DateTime) ActiveModel::Type.register(:timestamp, CouchbaseOrm::Types::Timestamp) diff --git a/lib/couchbase-orm/types/date.rb b/lib/couchbase-orm/types/date.rb new file mode 100644 index 00000000..5d0d223c --- /dev/null +++ b/lib/couchbase-orm/types/date.rb @@ -0,0 +1,13 @@ +module CouchbaseOrm + module Types + class Date < ActiveModel::Type::Date + def cast(value) + super(value) + end + + def serialize(value) + value&.iso8601 + end + end + end +end diff --git a/spec/type_spec.rb b/spec/type_spec.rb index 19fa7a97..c0d06c64 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -1,6 +1,7 @@ require File.expand_path("../support", __FILE__) require "active_model" +require "couchbase-orm/types" class DateTimeWith3Decimal < CouchbaseOrm::Types::DateTime def serialize(value) From 4b8b17a2c7a8c849e4fc3885ee88052b81abbe61 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 15 Sep 2022 08:49:37 +0200 Subject: [PATCH 21/25] small consistency fix --- lib/couchbase-orm/types/date.rb | 4 ---- lib/couchbase-orm/types/timestamp.rb | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/couchbase-orm/types/date.rb b/lib/couchbase-orm/types/date.rb index 5d0d223c..2e836cf8 100644 --- a/lib/couchbase-orm/types/date.rb +++ b/lib/couchbase-orm/types/date.rb @@ -1,10 +1,6 @@ module CouchbaseOrm module Types class Date < ActiveModel::Type::Date - def cast(value) - super(value) - end - def serialize(value) value&.iso8601 end diff --git a/lib/couchbase-orm/types/timestamp.rb b/lib/couchbase-orm/types/timestamp.rb index c95a438c..51f8acbc 100644 --- a/lib/couchbase-orm/types/timestamp.rb +++ b/lib/couchbase-orm/types/timestamp.rb @@ -6,7 +6,7 @@ def cast(value) return Time.at(value) if value.is_a?(Integer) return Time.at(value.to_i) if value.is_a?(String) && value =~ /^[0-9]+$/ return value.utc if value.is_a?(Time) - super(value) + super(value).utc end def serialize(value) From 938a7033bcf1ac3e0e03145978c1696ae164e8b5 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 15 Sep 2022 11:07:05 +0200 Subject: [PATCH 22/25] Support rails 5.2 with types --- .github/workflows/test.yml | 2 +- couchbase-orm.gemspec | 4 +-- lib/couchbase-orm/base.rb | 52 ++++++++++++++++++++++++++++++-- lib/couchbase-orm/persistence.rb | 4 +-- lib/couchbase-orm/types.rb | 7 +++++ spec/base_spec.rb | 9 +++--- spec/type_spec.rb | 16 ++++++++++ 7 files changed, 82 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bbaff0ef..9454ce29 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: gemfile: '7.0.0' couchbase: '7.1.0' - ruby: '2.6' - gemfile: '5.1.7' + gemfile: '5.2.8.1' couchbase: '7.1.0' fail-fast: false runs-on: ubuntu-20.04 diff --git a/couchbase-orm.gemspec b/couchbase-orm.gemspec index 36cdb64a..7e85f2f5 100644 --- a/couchbase-orm.gemspec +++ b/couchbase-orm.gemspec @@ -13,8 +13,8 @@ Gem::Specification.new do |gem| gem.required_ruby_version = '>= 2.1.0' gem.require_paths = ["lib"] - gem.add_runtime_dependency 'activemodel', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.0' - gem.add_runtime_dependency 'activerecord', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.0' + gem.add_runtime_dependency 'activemodel', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.2' + gem.add_runtime_dependency 'activerecord', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.2' gem.add_runtime_dependency 'couchbase' gem.add_runtime_dependency 'radix', '~> 2.2' # converting numbers to and from any base diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 9d64c6f1..3542a7e4 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -3,8 +3,11 @@ require 'active_model' require 'active_record' -require 'active_record/database_configurations' - +if ActiveModel::VERSION::MAJOR >= 6 + require 'active_record/database_configurations' +else + require 'active_model/type' +end require 'active_support/hash_with_indifferent_access' require 'couchbase' require 'couchbase-orm/error' @@ -42,6 +45,28 @@ def base_class? def column_names # can't be an alias for now attribute_names end + + if ActiveModel::VERSION::MAJOR < 6 + def attribute_names + attribute_types.keys + end + + def abstract_class? + false + end + + def connected? + true + end + + def table_exists? + true + end + + # def partial_writes? + # partial_updates? && partial_inserts? + # end + end end def _has_attribute?(attr_name) @@ -52,6 +77,26 @@ def attribute_for_inspect(attr_name) value = send(attr_name) value.inspect end + + if ActiveModel::VERSION::MAJOR < 6 + def attribute_names + self.class.attribute_names + end + + def has_attribute?(attr_name) + @attributes.key?(attr_name.to_s) + end + + def attribute_present?(attribute) + value = send(attribute) + !value.nil? && !(value.respond_to?(:empty?) && value.empty?) + end + + def _write_attribute(attr_name, value) + @attributes.write_from_user(attr_name.to_s, value) + value + end + end end class Base @@ -70,6 +115,7 @@ class Base define_model_callbacks :create, :destroy, :save, :update include Persistence + include ::ActiveRecord::AttributeMethods::Dirty include ::ActiveRecord::Timestamp # must be included after Persistence include Associations include Views @@ -173,7 +219,7 @@ def initialize(model = nil, ignore_doc_type: false, **attributes) super(model.attributes.except(:id, 'type')) else clear_changes_information - assign_attributes(**attributes.merge(Hash(model))) + assign_attributes(**attributes.merge(Hash(model)).symbolize_keys) end else clear_changes_information diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index 20408744..537b3a90 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -230,7 +230,7 @@ def serialized_attributes }.to_h end - def _update_record(with_cas: false, **options) + def _update_record(*_args, with_cas: false, **options) return false unless perform_validations(:update, options) return true unless changed? @@ -248,7 +248,7 @@ def _update_record(with_cas: false, **options) end end end - def _create_record(**options) + def _create_record(*_args, **options) return false unless perform_validations(:create, options) run_callbacks :create do diff --git a/lib/couchbase-orm/types.rb b/lib/couchbase-orm/types.rb index e6a375d2..e6674c0d 100644 --- a/lib/couchbase-orm/types.rb +++ b/lib/couchbase-orm/types.rb @@ -2,6 +2,13 @@ require "couchbase-orm/types/date_time" require "couchbase-orm/types/timestamp" +if ActiveModel::VERSION::MAJOR < 6 + # In Rails 5, the type system cannot allow overriding the default types + ActiveModel::Type.registry.instance_variable_get(:@registrations).delete_if do |k| + k.matches?(:date) || k.matches?(:datetime) || k.matches?(:timestamp) + end +end + ActiveModel::Type.register(:date, CouchbaseOrm::Types::Date) ActiveModel::Type.register(:datetime, CouchbaseOrm::Types::DateTime) ActiveModel::Type.register(:timestamp, CouchbaseOrm::Types::Timestamp) diff --git a/spec/base_spec.rb b/spec/base_spec.rb index 432b8245..ae05ee75 100644 --- a/spec/base_spec.rb +++ b/spec/base_spec.rb @@ -94,7 +94,6 @@ class TimestampTest < CouchbaseOrm::Base # A saved model should have no changes base = BaseTest.create!(name: 'joe') expect(base.changes.empty?).to be(true) - expect(base.previous_changes.empty?).to be(false) # Attributes are copied from the existing model base = BaseTest.new(base) @@ -149,9 +148,11 @@ class TimestampTest < CouchbaseOrm::Base ensure base.destroy end - - it "should have timestamp attributes for create in model" do - expect(TimestampTest.timestamp_attributes_for_create_in_model).to eq(["created_at"]) + + if ActiveModel::VERSION::MAJOR >= 6 + it "should have timestamp attributes for create in model" do + expect(TimestampTest.timestamp_attributes_for_create_in_model).to eq(["created_at"]) + end end it "should generate a timestamp on creation" do diff --git a/spec/type_spec.rb b/spec/type_spec.rb index c0d06c64..e66a2efe 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -66,6 +66,22 @@ class N1qlTypeTest < CouchbaseOrm::Base end end +describe CouchbaseOrm::Types::Date do + it "should cast an string to date" do + d = Date.today + expect(CouchbaseOrm::Types::Date.new.cast(d.to_s)).to eq(d) + end + + it "should serialize date to string" do + d = Date.today + expect(CouchbaseOrm::Types::Date.new.serialize(d)).to eq(d.to_s) + end + + it "should get the type from the registry" do + expect(ActiveModel::Type.lookup(:date)).to eq(CouchbaseOrm::Types::Date.new) + end +end + describe CouchbaseOrm::Base do before(:each) do TypeTest.all.each(&:destroy) From 77dc5f167ee337344a33ea1057a2c20ff121006f Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 15 Sep 2022 11:17:26 +0200 Subject: [PATCH 23/25] Review cleanup --- lib/couchbase-orm/base.rb | 6 +++--- lib/couchbase-orm/n1ql.rb | 4 ++-- lib/couchbase-orm/persistence.rb | 4 ++-- lib/couchbase-orm/proxies/n1ql_proxy.rb | 2 +- lib/couchbase-orm/utilities/index.rb | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 3542a7e4..ea298952 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -184,7 +184,7 @@ def find_by_id(*ids, **options) alias_method :[], :find_by_id def exists?(id) - CouchbaseOrm.logger.debug "Data - Exists? #{id}" + CouchbaseOrm.logger.debug { "Data - Exists? #{id}" } collection.exists(id).exists end alias_method :has_key?, :exists? @@ -194,7 +194,7 @@ class MismatchTypeError < RuntimeError; end # Add support for libcouchbase response objects def initialize(model = nil, ignore_doc_type: false, **attributes) - CouchbaseOrm.logger.debug "Initialize model #{model} with #{attributes}" + CouchbaseOrm.logger.debug { "Initialize model #{model} with #{attributes.to_s.truncate(200)}" } @__metadata__ = Metadata.new super() @@ -247,7 +247,7 @@ def [](key) end def []=(key, value) - CouchbaseOrm.logger.debug "Set attribute #{key} to #{value}" + CouchbaseOrm.logger.debug { "Set attribute #{key} to #{value}" } send(:"#{key}=", value) end diff --git a/lib/couchbase-orm/n1ql.rb b/lib/couchbase-orm/n1ql.rb index 016d74ab..857a42db 100644 --- a/lib/couchbase-orm/n1ql.rb +++ b/lib/couchbase-orm/n1ql.rb @@ -91,7 +91,7 @@ def convert_values(keys, values) attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(value_before_type_cast)) end - CouchbaseOrm.logger.debug "convert_values: #{key} => #{value_before_type_cast.inspect} => #{value.inspect} #{value.class} #{attribute_types[key.to_s]}" + CouchbaseOrm.logger.debug { "convert_values: #{key} => #{value_before_type_cast.inspect} => #{value.inspect} #{value.class} #{attribute_types[key.to_s]}" } # then quote and sanitize if value.class == String @@ -131,7 +131,7 @@ def run_query(keys, values, query_fn, descending: false, limit: nil, **options) limit = build_limit(limit) n1ql_query = "select raw meta().id from `#{bucket_name}` where #{where} order by #{order} #{limit}" result = cluster.query(n1ql_query, Couchbase::Options::Query.new(**options)) - CouchbaseOrm.logger.debug "N1QL query: #{n1ql_query} return #{result.rows.to_a.length} rows" + CouchbaseOrm.logger.debug { "N1QL query: #{n1ql_query} return #{result.rows.to_a.length} rows" } N1qlProxy.new(result) end end diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index 537b3a90..f3bc03cc 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -57,7 +57,7 @@ def inherited(child) # Returns true if this object hasn't been saved yet -- that is, a record # for the object doesn't exist in the database yet; otherwise, returns false. def new_record? - @__metadata__.cas.nil? && id.blank? + @__metadata__.cas.nil? && id.nil? end alias_method :new?, :new_record? @@ -90,7 +90,7 @@ def save(**options) # By default, #save! always runs validations. If any of them fail # CouchbaseOrm::Error::RecordInvalid gets raised, and the record won't be saved. def save!(**options) - CouchbaseOrm.logger.debug { "Will save! : #{id} -> #{attributes}" } + CouchbaseOrm.logger.debug { "Will save! : #{id} -> #{attributes.to_s.truncate(200)}" } self.class.fail_validate!(self) unless self.save(**options) self end diff --git a/lib/couchbase-orm/proxies/n1ql_proxy.rb b/lib/couchbase-orm/proxies/n1ql_proxy.rb index 62a8ab3b..db4e9363 100644 --- a/lib/couchbase-orm/proxies/n1ql_proxy.rb +++ b/lib/couchbase-orm/proxies/n1ql_proxy.rb @@ -12,7 +12,7 @@ def initialize(proxyfied) @current_query = self.to_s return @results if @results - CouchbaseOrm.logger.debug 'Query - ' + self.to_s + CouchbaseOrm.logger.debug { 'Query - ' + self.to_s } results = @proxyfied.rows results = results.map { |r| block.call(r) } if block diff --git a/lib/couchbase-orm/utilities/index.rb b/lib/couchbase-orm/utilities/index.rb index 1820ffb8..0df9ef5c 100644 --- a/lib/couchbase-orm/utilities/index.rb +++ b/lib/couchbase-orm/utilities/index.rb @@ -51,7 +51,7 @@ def index(attrs, name = nil, presence: true, &processor) # use the bucket key as an index - lookup records by attr values define_singleton_method(find_by_method) do |*values| key = self.send(class_bucket_key_method, *values) - CouchbaseOrm.logger.debug("#{find_by_method}: #{class_bucket_key_method} with values #{values.inspect} give key: #{key}") + CouchbaseOrm.logger.debug { "#{find_by_method}: #{class_bucket_key_method} with values #{values.inspect} give key: #{key}" } id = self.collection.get(key)&.content if id mod = self.find_by_id(id) @@ -107,7 +107,7 @@ def index(attrs, name = nil, presence: true, &processor) begin check_ref_id = record.class.collection.get(original_key) if check_ref_id && check_ref_id.content == record.id - CouchbaseOrm.logger.debug "Removing old key #{original_key}" + CouchbaseOrm.logger.debug { "Removing old key #{original_key}" } record.class.collection.remove(original_key, cas: check_ref_id.cas) end end From bca347a22a4d242a2f20a6b72a9476358c94b08f Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 15 Sep 2022 11:18:40 +0200 Subject: [PATCH 24/25] whitespace cleanup --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9454ce29..02685a0a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: include: - ruby: '3.0' gemfile: '7.0.0' - couchbase: '6.6.5' + couchbase: '6.6.5' - ruby: '3.0' gemfile: '7.0.0' couchbase: '7.1.0' @@ -25,7 +25,7 @@ jobs: couchbase: '7.1.0' fail-fast: false runs-on: ubuntu-20.04 - name: ${{ matrix.ruby }} rails-${{ matrix.gemfile }} couchbase-${{ matrix.couchbase }} + name: ${{ matrix.ruby }} rails-${{ matrix.gemfile }} couchbase-${{ matrix.couchbase }} steps: - uses: actions/checkout@v2 - run: sudo apt-get update && sudo apt-get install libevent-dev libev-dev python-httplib2 From 9cb5b05019d6876823b4360a51e3f5006c39d1a4 Mon Sep 17 00:00:00 2001 From: Gauthier Monserand Date: Thu, 15 Sep 2022 11:24:17 +0200 Subject: [PATCH 25/25] trigger CI