Skip to content

Commit

Permalink
Merge pull request Mapotempo#51 from doctolib/add_basic_relation
Browse files Browse the repository at this point in the history
Add basic relation
  • Loading branch information
giallon committed Sep 22, 2022
2 parents cd98db6 + a742428 commit 2a8aaee
Show file tree
Hide file tree
Showing 14 changed files with 478 additions and 93 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ Like views, it's possible to use N1QL to process some requests used for filterin
```ruby
class Comment < CouchbaseOrm::Base
attribute :author, :body, type: String
n1ql :all # => emits :id and will return all comments
n1ql :by_author, emit_key: :author

# Generates two functions:
Expand Down
33 changes: 16 additions & 17 deletions lib/couchbase-orm/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
require 'couchbase-orm/persistence'
require 'couchbase-orm/associations'
require 'couchbase-orm/types'
require 'couchbase-orm/relation'
require 'couchbase-orm/proxies/bucket_proxy'
require 'couchbase-orm/proxies/collection_proxy'
require 'couchbase-orm/utilities/join'
require 'couchbase-orm/utilities/enum'
require 'couchbase-orm/utilities/index'
require 'couchbase-orm/utilities/has_many'
require 'couchbase-orm/utilities/ensure_unique'
require 'couchbase-orm/utilities/query_helper'


module CouchbaseOrm
Expand All @@ -46,26 +48,22 @@ 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 abstract_class?
false
end
def connected?
true
end

def connected?
true
end
def table_exists?
true
end

def table_exists?
true
if ActiveModel::VERSION::MAJOR < 6
def attribute_names
attribute_types.keys
end

# def partial_writes?
# partial_updates? && partial_inserts?
# end
end
end

Expand Down Expand Up @@ -119,7 +117,9 @@ class Base
include ::ActiveRecord::Timestamp # must be included after Persistence
include Associations
include Views
include QueryHelper
include N1ql
include Relation

extend Join
extend Enum
Expand Down Expand Up @@ -247,7 +247,6 @@ def [](key)
end

def []=(key, value)
CouchbaseOrm.logger.debug { "Set attribute #{key} to #{value}" }
send(:"#{key}=", value)
end

Expand Down
39 changes: 3 additions & 36 deletions lib/couchbase-orm/n1ql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ module ClassMethods
#
# @example Define some N1QL queries for a model
# class Post < CouchbaseOrm::Base
# n1ql :all
# n1ql :by_rating, emit_key: :rating
# end
#
Expand All @@ -36,6 +35,8 @@ module ClassMethods
# end
# TODO: add range keys [:startkey, :endkey]
def n1ql(name, query_fn: nil, emit_key: [], custom_order: nil, **options)
raise ArgumentError, "#{self} already respond_to? #{name}" if self.respond_to?(name)

emit_key = Array.wrap(emit_key)
emit_key.each do |key|
raise "unknown emit_key attribute for n1ql :#{name}, emit_key: :#{key}" if key && !attribute_names.include?(key.to_s)
Expand Down Expand Up @@ -82,41 +83,7 @@ def index_n1ql(attr, validate: true, find_method: nil, n1ql_method: nil)
def convert_values(keys, values)
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 = if value_before_type_cast.is_a?(Array)
value_before_type_cast.map do |v|
attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(v))
end
else
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]}" }

value
end
end

def quote(value)
if value.is_a? String
"'#{N1ql.sanitize(value)}'"
elsif value.is_a? Array
"[#{value.map{|v|quote(v)}.join(', ')}]"
elsif value.nil?
nil
else
N1ql.sanitize(value).to_s
end
end

def build_match(key, value)
case
when value.nil?
"#{key} IS NOT VALUED"
when value.is_a?(Array)
"#{key} IN #{quote(value)}"
else
"#{key} = #{quote(value)}"
serialize_value(key, value_before_type_cast)
end
end

Expand Down
1 change: 0 additions & 1 deletion lib/couchbase-orm/persistence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ 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.to_s.truncate(200)}" }
self.class.fail_validate!(self) unless self.save(**options)
self
end
Expand Down
4 changes: 3 additions & 1 deletion lib/couchbase-orm/proxies/results_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module CouchbaseOrm
class ResultsProxy
def initialize(proxyfied)
@proxyfied = proxyfied

raise ArgumentError, "Proxyfied object must respond to :to_a" unless @proxyfied.respond_to?(:to_a)

proxyfied.public_methods.each do |method|
next if self.public_methods.include?(method)
Expand All @@ -13,7 +15,7 @@ def initialize(proxyfied)
end
end
end

def method_missing(m, *args, &block)
@proxyfied.to_a.send(m, *args, &block)
end
Expand Down
142 changes: 142 additions & 0 deletions lib/couchbase-orm/relation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
module CouchbaseOrm
module Relation
extend ActiveSupport::Concern

class CouchbaseOrm_Relation
def initialize(model:, where: where = nil, order: order = nil, limit: limit = nil, _not: _not = false)
CouchbaseOrm::logger.debug "CouchbaseOrm_Relation init: #{model} where:#{where.inspect} not:#{_not.inspect} order:#{order.inspect} limit: #{limit}"
@model = model
@limit = limit
@where = []
@order = {}
@order = merge_order(**order) if order
@where = merge_where(where, _not) if where
CouchbaseOrm::logger.debug "- #{to_s}"
end

def to_s
"CouchbaseOrm_Relation: #{@model} where:#{@where.inspect} order:#{@order.inspect} limit: #{@limit}"
end

def to_n1ql
bucket_name = @model.bucket.name
where = build_where
order = build_order
limit = build_limit
"select raw meta().id from `#{bucket_name}` where #{where} order by #{order} #{limit}"
end

def query
CouchbaseOrm::logger.debug("Query: #{self}")
n1ql_query = to_n1ql
result = @model.cluster.query(n1ql_query, Couchbase::Options::Query.new(scan_consistency: :request_plus))
CouchbaseOrm.logger.debug { "Relation query: #{n1ql_query} return #{result.rows.to_a.length} rows" }
N1qlProxy.new(result)
end

def ids
query.to_a
end

def count
query.count
end

def to_ary
query.results { |res| @model.find(res) }.to_ary
end

alias :to_a :to_ary

delegate :each, :map, :collect, :to => :to_ary

def delete_all
CouchbaseOrm::logger.debug{ "Delete all: #{self}" }
ids = query.to_a
CouchbaseOrm::Connection.bucket.default_collection.remove_multi(ids) unless ids.empty?
end

def where(**conds)
CouchbaseOrm_Relation.new(**initializer_arguments.merge(where: merge_where(conds)))
end

def not(**conds)
CouchbaseOrm_Relation.new(**initializer_arguments.merge(where: merge_where(conds, _not: true)))
end

def order(*lorder, **horder)
CouchbaseOrm_Relation.new(**initializer_arguments.merge(order: merge_order(*lorder, **horder)))
end

def limit(limit)
CouchbaseOrm_Relation.new(**initializer_arguments.merge(limit: limit))
end

def all
CouchbaseOrm_Relation.new(**initializer_arguments)
end

private

def build_limit
@limit ? "limit #{@limit}" : ""
end

def initializer_arguments
{ model: @model, order: @order, where: @where, limit: @limit }
end

def merge_order(*lorder, **horder)
raise ArgumentError, "invalid order passed by list: #{lorder.inspect}, must be symbols" unless lorder.all? { |o| o.is_a? Symbol }
raise ArgumentError, "Invalid order passed by hash: #{horder.inspect}, must be symbol -> :asc|:desc" unless horder.all? { |k, v| k.is_a?(Symbol) && [:asc, :desc].include?(v) }
@order
.merge(Array.wrap(lorder).map{ |o| [o, :asc] }.to_h)
.merge(horder)
end

def merge_where(conds, _not = false)
@where + (_not ? conds.to_a.map{|k,v|[k,v,:not]} : conds.to_a)
end

def build_order
order = @order.map do |key, value|
"#{key} #{value}"
end.join(", ")
order.empty? ? "meta().id" : order
end

def build_where
([[:type, @model.design_document]] + @where).map do |key, value, opt|
opt == :not ?
@model.build_not_match(key, value) :
@model.build_match(key, value)
end.join(" AND ")
end
end

module ClassMethods
def where(**conds)
CouchbaseOrm_Relation.new(model: self, where: conds)
end

def not(**conds)
CouchbaseOrm_Relation.new(model: self, where: conds, _not: true)
end

def order(*ordersl, **ordersh)
order = ordersh.reverse_merge(ordersl.map{ |o| [o, :asc] }.to_h)
CouchbaseOrm_Relation.new(model: self, order: order)
end

def limit(limit)
CouchbaseOrm_Relation.new(model: self, limit: limit)
end

def all
CouchbaseOrm_Relation.new(model: self)
end

delegate :ids, :delete_all, :count, to: :all
end
end
end
59 changes: 59 additions & 0 deletions lib/couchbase-orm/utilities/query_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module CouchbaseOrm
module QueryHelper
extend ActiveSupport::Concern

module ClassMethods

def build_match(key, value)
case
when value.nil?
"#{key} IS NOT VALUED"
when value.is_a?(Array) && value.include?(nil)
"(#{build_match(key, nil)} OR #{build_match(key, value.compact)})"
when value.is_a?(Array)
"#{key} IN #{quote(value)}"
else
"#{key} = #{quote(value)}"
end
end

def build_not_match(key, value)
case
when value.nil?
"#{key} IS VALUED"
when value.is_a?(Array) && value.include?(nil)
"(#{build_not_match(key, nil)} AND #{build_not_match(key, value.compact)})"
when value.is_a?(Array)
"#{key} NOT IN #{quote(value)}"
else
"#{key} != #{quote(value)}"
end
end

def serialize_value(key, 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].serialize(attribute_types[key.to_s].cast(v))
end
else
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]}" }
value
end

def quote(value)
if value.is_a? String
"'#{N1ql.sanitize(value)}'"
elsif value.is_a? Array
"[#{value.map{|v|quote(v)}.join(', ')}]"
elsif value.nil?
nil
else
N1ql.sanitize(value).to_s
end
end
end
end
end
2 changes: 2 additions & 0 deletions lib/couchbase-orm/views.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ module ClassMethods
# # ...
# end
def view(name, map: nil, emit_key: nil, reduce: nil, **options)
raise ArgumentError, "#{self} already respond_to? #{name}" if self.respond_to?(name)

if emit_key.class == Array
emit_key.each do |key|
raise "unknown emit_key attribute for view :#{name}, emit_key: :#{key}" if key && !attribute_names.include?(key.to_s)
Expand Down
Loading

0 comments on commit 2a8aaee

Please sign in to comment.