-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathn1ql.rb
More file actions
143 lines (126 loc) · 6.22 KB
/
Copy pathn1ql.rb
File metadata and controls
143 lines (126 loc) · 6.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# 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
NO_VALUE = :no_value_specified
DEFAULT_SCAN_CONSISTENCY = :request_plus
# sanitize for injection query
def self.sanitize(value)
if value.is_a?(String)
value.gsub("'", "''").gsub("\\"){"\\\\"}.gsub('"', '\"')
elsif value.is_a?(Array)
value.map{ |v| sanitize(v) }
else
value
end
end
def self.config(new_config = nil)
Thread.current['__couchbaseorm_n1ql_config__'] = new_config if new_config
Thread.current['__couchbaseorm_n1ql_config__'] || {
scan_consistency: DEFAULT_SCAN_CONSISTENCY
}
end
module ClassMethods
# 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 N1QL queries for a model
# class Post < CouchbaseOrm::Base
# n1ql :by_rating, emit_key: :rating
# end
#
# Post.by_rating do |response|
# # ...
# 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)
end
options = N1QL_DEFAULTS.merge(options)
method_opts = {}
method_opts[:emit_key] = emit_key
@indexes ||= {}
@indexes[name] = method_opts
singleton_class.__send__(:define_method, name) do |key: NO_VALUE, **opts, &result_modifier|
opts = options.merge(opts).reverse_merge(scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency])
values = key == NO_VALUE ? NO_VALUE : convert_values(method_opts[:emit_key], key)
current_query = run_query(method_opts[:emit_key], values, query_fn, custom_order: custom_order, **opts.except(:include_docs, :key))
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
N1QL_DEFAULTS = { include_docs: true }
# add a n1ql query 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
define_singleton_method find_method do |value|
send n1ql_method, key: [value]
end
end
private
def convert_values(keys, values)
return values if keys.empty? && Array.wrap(values).any?
keys.zip(Array.wrap(values)).map do |key, value_before_type_cast|
serialize_value(key, value_before_type_cast)
end
end
def build_where(keys, values, params: nil)
where = values == NO_VALUE ? '' : keys.zip(Array.wrap(values))
.reject { |key, value| key.nil? && value.nil? }
.map { |key, value| build_match(key, value, params: params) }
.join(" AND ")
if params
type_placeholder = bind(design_document, params)
"type=#{type_placeholder} #{"AND " + where unless where.blank?}"
else
"type=\"#{design_document}\" #{"AND " + where unless where.blank?}"
end
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_limit(limit)
limit ? "limit #{limit}" : ""
end
def run_query(keys, values, query_fn, custom_order: nil, descending: false, limit: nil, **options)
if query_fn
N1qlProxy.new(query_fn.call(bucket, values, Couchbase::Options::Query.new(**options)))
else
bucket_name = bucket.name
params = []
where = build_where(keys, values, params: params)
order = custom_order || build_order(keys, descending)
limit = build_limit(limit)
n1ql_query = "select raw meta().id from `#{bucket_name}` where #{where} order by #{order} #{limit}"
query_options = options.merge(positional_parameters: params)
result = cluster.query(n1ql_query, Couchbase::Options::Query.new(**query_options))
CouchbaseOrm.logger.debug {
"N1QL query: #{n1ql_query} params: #{params.inspect} return #{result.rows.to_a.length} rows with scan_consistency: #{options[:scan_consistency]}"
}
N1qlProxy.new(result)
end
end
end
end
end