Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev has many n1ql #8

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
language: ruby
rvm:
- ruby-2.4.2
- ruby-2.3.5
- ruby-2.5.7
- ruby-head
- jruby-9.1.13.0
- jruby-9.2.9.0
- jruby-head
- rubinius
- rubinius-3.86
Expand All @@ -24,6 +24,7 @@ before_install:
- /opt/couchbase/bin/couchbase-cli bucket-create -c 127.0.0.1:8091 -u admin -p password --bucket=default --bucket-type=couchbase --bucket-ramsize=160 --bucket-replica=0 --wait
- sleep 1
- /opt/couchbase/bin/couchbase-cli user-manage -c 127.0.0.1:8091 -u admin -p password --set --rbac-username tester --rbac-password password123 --rbac-name "Auto Tester" --roles admin --auth-domain local
- curl http://admin:password@localhost:8093/query/service -d 'statement=CREATE INDEX `default_type` ON `default`(`type`)'
- export TRAVIS_TEST=true
matrix:
allow_failures:
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ source "https://rubygems.org"
gemspec

gem "rubysl", :platform => :rbx
gem 'libcouchbase', github: 'Mapotempo/libcouchbase', tag: '1.3.2-mapo'
2 changes: 1 addition & 1 deletion couchbase-orm.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Gem::Specification.new do |gem|
gem.required_ruby_version = '>= 2.1.0'
gem.require_paths = ["lib"]

gem.add_runtime_dependency 'libcouchbase', '~> 1.2'
# gem.add_runtime_dependency 'libcouchbase', '~> 1.2'
gem.add_runtime_dependency 'activemodel', '>= 4.0', '< 6.0'
gem.add_runtime_dependency 'radix', '~> 2.2' # converting numbers to and from any base

Expand Down
4 changes: 3 additions & 1 deletion lib/couchbase-orm/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require 'active_support/hash_with_indifferent_access'
require 'couchbase-orm/error'
require 'couchbase-orm/views'
require 'couchbase-orm/n1ql'
require 'couchbase-orm/persistence'
require 'couchbase-orm/associations'
require 'couchbase-orm/utilities/join'
Expand All @@ -28,6 +29,7 @@ class Base
include Persistence
include Associations
include Views
include N1ql

extend Join
extend Enum
Expand Down Expand Up @@ -225,7 +227,7 @@ def attribute(name)
@__attributes__[name]
end
alias_method :read_attribute_for_serialization, :attribute

def attribute=(name, value)
__send__(:"#{name}=", value)
end
Expand Down
128 changes: 128 additions & 0 deletions lib/couchbase-orm/n1ql.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# frozen_string_literal: true

require 'active_model'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/object/try'

module CouchbaseOrm
module N1ql
extend ActiveSupport::Concern
# Defines a query N1QL for the model
# #
# # @param [Symbol, String, Array] names names of the views
# # @param [Hash] options options passed to the {Couchbase::N1QL}
# #
# # @example Define some views for a model
# # class Post < CouchbaseOrm::Base
# # n1ql :all
# # n1ql :by_rating, emit_key: :rating
# # end
# #
# # Post.by_rating.stream do |response|
# # # ...
# # end
# TODO: add range keys [:startkey, :endkey]
module ClassMethods
def n1ql(name, query: 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?
end
options = VIEW_DEFAULTS.merge(options)
method_opts = {}
method_opts[:emit_key] = emit_key

@indexes ||= {}
@indexes[name] = method_opts

singleton_class.__send__(:define_method, name) do |**opts, &result_modifier|
opts = options.merge(opts)

values = convert_values(opts[:key])
current_query = build_query(method_opts[:emit_key], values, query, opts)

if result_modifier
opts[:include_docs] = true
current_query.results &result_modifier
elsif opts[:include_docs]
current_query.results { |res|
find(res)
}
else
current_query.results
end
end
end

# add a view and lookup method to the model for finding all records
# using a value in the supplied attr.
def index_n1ql(attr, validate: true, find_method: nil, n1ql_method: nil)
n1ql_method ||= "by_#{attr}"
find_method ||= "find_#{n1ql_method}"

validates(attr, presence: true) if validate
n1ql n1ql_method, emit_key: attr

instance_eval "
def self.#{find_method}(#{attr})
#{n1ql_method}(key: #{attr})
end
"
end

VIEW_DEFAULTS = { include_docs: true }

private

# TODO: secure it for injection query
def convert_values(values)
Array.wrap(values).compact.map do |v|
if v.class == String
"\"#{v}\""
elsif v.class == Date || v.class == Time
"\"#{v.iso8601(3)}\""
else
v.to_s
end
end
end

def build_where(keys, values)
where = keys.each_with_index
.reject { |key, i| values.try(:[], i).nil? }
.map { |key, i| "#{key} = #{values[i] }" }
.join(" and ")
"type=\"#{design_document}\" #{"and " + where unless where.blank?}"
end

#
# order-by-clause ::= ORDER BY ordering-term [ ',' ordering-term ]*
# ordering-term ::= expr [ ASC | DESC ] [ NULLS ( FIRST | LAST ) ]
# see https://docs.couchbase.com/server/5.0/n1ql/n1ql-language-reference/orderby.html
def build_order(keys, descending)
"#{keys.dup.push("meta().id").map { |k| "#{k} #{descending ? "desc" : "asc" }" }.join(",")}"
end

def build_query(keys, values, query, descending: false, limit: nil, **options)
if query
query.call(bucket, values)
else
bucket_name = bucket.bucket
where = build_where(keys, values)
order = build_order(keys, descending)
query = bucket.n1ql
.select("raw meta().id")
.from("`#{bucket_name}`")
.where(where)
if order
query = query.order_by(order)
end
if limit
query = query.limit(limit)
end
query
end
end
end
end
end
131 changes: 82 additions & 49 deletions lib/couchbase-orm/utilities/has_many.rb
Original file line number Diff line number Diff line change
@@ -1,73 +1,106 @@
module CouchbaseOrm
module HasMany
private

# :foreign_key, :class_name, :through
def has_many(model, class_name: nil, foreign_key: nil, through: nil, through_class: nil, through_key: nil, **options)
class_name = (class_name || model.to_s.singularize.camelcase).to_s
def has_many(model, class_name: nil, foreign_key: nil, through: nil, through_class: nil, through_key: nil, type: :view, **options)
class_name = (class_name || model.to_s.singularize.camelcase).to_s
foreign_key = (foreign_key || ActiveSupport::Inflector.foreign_key(self.name)).to_sym
if through || through_class
remote_class = class_name
class_name = (through_class || through.to_s.camelcase).to_s
through_key = (through_key || "#{remote_class.underscore}_id").to_sym
remote_method = :"by_#{foreign_key}_with_#{through_key}"
else
remote_method = :"find_by_#{foreign_key}"
end

if through || through_class
remote_class = class_name
class_name = (through_class || through.to_s.camelcase).to_s
through_key = (through_key || "#{remote_class.underscore}_id").to_sym
remote_method = :"by_#{foreign_key}_with_#{through_key}"
else
remote_method = :"find_by_#{foreign_key}"
end
relset_varname = "@#{model}_rel_set"

relset_varname = "@#{model}_rel_set"
klass = begin
class_name.constantize
rescue NameError => e
puts "WARNING: #{class_name} referenced in #{self.name} before it was aded"

klass = begin
class_name.constantize
rescue NameError => e
puts "WARNING: #{class_name} referenced in #{self.name} before it was loaded"
# Open the class early - load order will have to be changed to prevent this.
# Warning notice required as a misspelling will not raise an error
Object.class_eval <<-EKLASS
class #{class_name} < CouchbaseOrm::Base
attribute :#{foreign_key}
end
EKLASS
class_name.constantize
end

build_index(type, klass, remote_class, remote_method, through_key, foreign_key)

# Open the class early - load order will have to be changed to prevent this.
# Warning notice required as a misspelling will not raise an error
Object.class_eval <<-EKLASS
class #{class_name} < CouchbaseOrm::Base
attribute :#{foreign_key}
if remote_class
define_method(model) do
return self.instance_variable_get(relset_varname) if instance_variable_defined?(relset_varname)

remote_klass = remote_class.constantize
enum = klass.__send__(remote_method, key: self.id) { |row|
case type
when :n1ql
remote_klass.find(row)
else
remote_klass.find(row.value[through_key])
end
EKLASS
class_name.constantize
}

self.instance_variable_set(relset_varname, enum)
end
else
define_method(model) do
return self.instance_variable_get(relset_varname) if instance_variable_defined?(relset_varname)
self.instance_variable_set(relset_varname, klass.__send__(remote_method, self.id))
end
end

@associations ||= []
@associations << [model, options[:dependent]]
end

def build_index(type, klass, remote_class, remote_method, through_key, foreign_key)
case type
when :n1ql
build_index_n1ql(klass, remote_class, remote_method, through_key, foreign_key)
else
build_index_view(klass, remote_class, remote_method, through_key, foreign_key)
end
end

def build_index_view(klass, remote_class, remote_method, through_key, foreign_key)
if remote_class
klass.class_eval do
view remote_method, map: <<-EMAP
function(doc) {
if (doc.type === "{{design_document}}" && doc.#{through_key}) {
emit(doc.#{foreign_key}, null);
}
}
EMAP
end

define_method(model) do
return self.instance_variable_get(relset_varname) if instance_variable_defined?(relset_varname)

remote_klass = remote_class.constantize
enum = klass.__send__(remote_method, key: self.id) { |row|
remote_klass.find(row.value[through_key])
view remote_method, map: <<-EMAP
function(doc) {
if (doc.type === "{{design_document}}" && doc.#{through_key}) {
emit(doc.#{foreign_key}, null);
}
}

self.instance_variable_set(relset_varname, enum)
EMAP
end
else
klass.class_eval do
index_view foreign_key, validate: false
index_view foreign_key, validate: false
end
end
end

define_method(model) do
return self.instance_variable_get(relset_varname) if instance_variable_defined?(relset_varname)
self.instance_variable_set(relset_varname, klass.__send__(remote_method, self.id))
def build_index_n1ql(klass, remote_class, remote_method, through_key, foreign_key)
if remote_class
klass.class_eval do
n1ql remote_method, query: proc { |bucket, values|
bucket_name = bucket.bucket
bucket.n1ql.select("raw #{through_key}")
.from("`#{bucket_name}`")
.where("type=\"#{design_document}\" and #{foreign_key} = #{values[0]}")
}
end
else
klass.class_eval do
index_n1ql foreign_key, validate: false
end
end

@associations ||= []
@associations << [model, options[:dependent]]
end
end
end
end
Loading