couchbase-orm 3.0.3 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +4 -12
- data/couchbase-orm.gemspec +1 -1
- data/docusaurus/docs/tutorial-ruby-couchbase-orm/07-sqlpp-queries.md +41 -0
- data/lib/couchbase-orm/n1ql.rb +19 -9
- data/lib/couchbase-orm/relation.rb +54 -31
- data/lib/couchbase-orm/utilities/has_many.rb +3 -1
- data/lib/couchbase-orm/utilities/query_helper.rb +52 -25
- data/lib/couchbase-orm/version.rb +1 -1
- data/spec/n1ql_spec.rb +31 -3
- data/spec/relation_spec.rb +82 -0
- metadata +4 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 253e0ee05a3522f641294d615724214c6b03ebfd189f008544e9a41f0e671916
|
|
4
|
+
data.tar.gz: 7628e1f89db8f7649dd6795145b238a06af87cb1901e30337670c3f15b87378c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: af199f51fb0fd808ac4920ca8eb16d50b8d6afa89a23cd311d5e39e92df87f93914368267afd373e7d8d282e50e517b892039008f3a2d234ef5bf1ea2b63bb83
|
|
7
|
+
data.tar.gz: b0703729ad90b504026467377aecdeaed17434010f044ede50d0645ef1e51d9dadef595c31ad1f1990d7366ba7f4101c934c716f4f8cb2152a70d0f846ad7e88
|
data/.github/workflows/test.yml
CHANGED
|
@@ -20,23 +20,15 @@ jobs:
|
|
|
20
20
|
- ruby: '3.3'
|
|
21
21
|
active-model: '7.2'
|
|
22
22
|
couchbase: '7.2.3'
|
|
23
|
-
- ruby: '3.2'
|
|
24
|
-
active-model: '7.2.0'
|
|
25
|
-
couchbase: '7.1.1'
|
|
26
|
-
- ruby: '3.2'
|
|
27
|
-
active-model: '7.1.0'
|
|
28
|
-
couchbase: '6.6.5'
|
|
29
|
-
- ruby: '3.1'
|
|
30
|
-
active-model: '7.1.0'
|
|
31
|
-
couchbase: '7.1.0'
|
|
32
23
|
fail-fast: false
|
|
33
24
|
runs-on: ubuntu-24.04
|
|
34
25
|
name: ${{ matrix.ruby }} rails-${{ matrix.active-model }} couchbase-${{ matrix.couchbase }}
|
|
35
26
|
steps:
|
|
36
27
|
- uses: actions/checkout@v3
|
|
37
|
-
- run:
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
- run: |
|
|
29
|
+
sudo apt-get update
|
|
30
|
+
sudo apt-get install -y libevent-dev libev-dev python3-httplib2
|
|
31
|
+
sudo apt-get install -y libtinfo5 || sudo apt-get install -y libtinfo6
|
|
40
32
|
- uses: ruby/setup-ruby@v1
|
|
41
33
|
with:
|
|
42
34
|
ruby-version: ${{ matrix.ruby }}
|
data/couchbase-orm.gemspec
CHANGED
|
@@ -15,7 +15,7 @@ Gem::Specification.new do |gem|
|
|
|
15
15
|
gem.summary = 'Couchbase ORM for Rails'
|
|
16
16
|
gem.description = 'A Couchbase ORM for Rails'
|
|
17
17
|
|
|
18
|
-
gem.required_ruby_version = '>= 3.
|
|
18
|
+
gem.required_ruby_version = '>= 3.3.0'
|
|
19
19
|
gem.require_paths = ['lib']
|
|
20
20
|
|
|
21
21
|
gem.add_runtime_dependency 'activemodel', ENV['ACTIVE_MODEL_VERSION'] || '>= 7.1'
|
|
@@ -147,6 +147,47 @@ docs = N1QLTest.by_custom_rating_values(key: [[1, 2]]).collect { |ob| ob.name }
|
|
|
147
147
|
|
|
148
148
|
In the above examples, the `collect` method is used to extract the `name` attribute from each document in the result set.
|
|
149
149
|
|
|
150
|
+
## 7.8 Prepared Statement Plan Caching
|
|
151
|
+
|
|
152
|
+
Couchbase Server can cache the query execution plan for a SQL++ query so that subsequent executions skip the planning step. This is controlled by the `adhoc` query option: `adhoc: false` tells the server to prepare and cache the plan on first execution and reuse it on subsequent ones.
|
|
153
|
+
|
|
154
|
+
### Default behaviour
|
|
155
|
+
|
|
156
|
+
By default CouchbaseOrm runs queries with `adhoc: true` (the Couchbase SDK default), meaning no plan caching. This preserves the existing behaviour — you opt into plan caching explicitly.
|
|
157
|
+
|
|
158
|
+
### Enabling caching for a specific call
|
|
159
|
+
|
|
160
|
+
Pass `adhoc: false` directly to the query method to prepare and cache the plan (useful for frequently repeated queries):
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
# Cache the plan for this query
|
|
164
|
+
N1QLTest.by_rating(key: 1, adhoc: false)
|
|
165
|
+
|
|
166
|
+
# Relation query with plan caching
|
|
167
|
+
User.where(country: 'FR').with(adhoc: false).to_a
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Enabling caching for a specific `n1ql` definition
|
|
171
|
+
|
|
172
|
+
Set `adhoc: false` in the macro options to always cache the plan for that particular query:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
n1ql :by_stable_filter, emit_key: [:name], adhoc: false
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Changing the global default
|
|
179
|
+
|
|
180
|
+
Override the thread-local config to change the default for all queries in the current thread:
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
# Enable plan caching for all queries in this thread
|
|
184
|
+
CouchbaseOrm::N1ql.config(adhoc: false)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Override priority
|
|
188
|
+
|
|
189
|
+
From highest to lowest: **per-call kwarg** > **per-`n1ql`-definition option** > **`N1ql.config`** > **default (`true`)**.
|
|
190
|
+
|
|
150
191
|
## 7.7 Indexing for SQL++
|
|
151
192
|
|
|
152
193
|
To optimize the performance of SQL++ queries, it's important to create appropriate indexes on the fields used in the query conditions. Couchbase Server provides a way to create indexes using the Index service.
|
data/lib/couchbase-orm/n1ql.rb
CHANGED
|
@@ -9,6 +9,7 @@ module CouchbaseOrm
|
|
|
9
9
|
extend ActiveSupport::Concern
|
|
10
10
|
NO_VALUE = :no_value_specified
|
|
11
11
|
DEFAULT_SCAN_CONSISTENCY = :request_plus
|
|
12
|
+
DEFAULT_ADHOC = true
|
|
12
13
|
# sanitize for injection query
|
|
13
14
|
def self.sanitize(value)
|
|
14
15
|
if value.is_a?(String)
|
|
@@ -22,9 +23,10 @@ module CouchbaseOrm
|
|
|
22
23
|
|
|
23
24
|
def self.config(new_config = nil)
|
|
24
25
|
Thread.current['__couchbaseorm_n1ql_config__'] = new_config if new_config
|
|
25
|
-
|
|
26
|
-
scan_consistency: DEFAULT_SCAN_CONSISTENCY
|
|
27
|
-
|
|
26
|
+
{
|
|
27
|
+
scan_consistency: DEFAULT_SCAN_CONSISTENCY,
|
|
28
|
+
adhoc: DEFAULT_ADHOC
|
|
29
|
+
}.merge(Thread.current['__couchbaseorm_n1ql_config__'] || {})
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
module ClassMethods
|
|
@@ -57,7 +59,10 @@ module CouchbaseOrm
|
|
|
57
59
|
@indexes[name] = method_opts
|
|
58
60
|
|
|
59
61
|
singleton_class.__send__(:define_method, name) do |key: NO_VALUE, **opts, &result_modifier|
|
|
60
|
-
opts = options.merge(opts).reverse_merge(
|
|
62
|
+
opts = options.merge(opts).reverse_merge(
|
|
63
|
+
scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency],
|
|
64
|
+
adhoc: CouchbaseOrm::N1ql.config[:adhoc]
|
|
65
|
+
)
|
|
61
66
|
values = key == NO_VALUE ? NO_VALUE : convert_values(method_opts[:emit_key], key)
|
|
62
67
|
current_query = run_query(method_opts[:emit_key], values, query_fn, custom_order: custom_order, **opts.except(:include_docs, :key))
|
|
63
68
|
if result_modifier
|
|
@@ -95,10 +100,10 @@ module CouchbaseOrm
|
|
|
95
100
|
end
|
|
96
101
|
end
|
|
97
102
|
|
|
98
|
-
def build_where(keys, values)
|
|
103
|
+
def build_where(keys, values, params: nil)
|
|
99
104
|
where = values == NO_VALUE ? '' : keys.zip(Array.wrap(values))
|
|
100
105
|
.reject { |key, value| key.nil? && value.nil? }
|
|
101
|
-
.map { |key, value| build_match(key, value) }
|
|
106
|
+
.map { |key, value| build_match(key, value, params: params) }
|
|
102
107
|
.join(" AND ")
|
|
103
108
|
"type=\"#{design_document}\" #{"AND " + where unless where.blank?}"
|
|
104
109
|
end
|
|
@@ -119,12 +124,17 @@ module CouchbaseOrm
|
|
|
119
124
|
N1qlProxy.new(query_fn.call(bucket, values, Couchbase::Options::Query.new(**options)))
|
|
120
125
|
else
|
|
121
126
|
bucket_name = bucket.name
|
|
122
|
-
|
|
127
|
+
params = []
|
|
128
|
+
where = build_where(keys, values, params: params)
|
|
123
129
|
order = custom_order || build_order(keys, descending)
|
|
124
130
|
limit = build_limit(limit)
|
|
125
131
|
n1ql_query = "select raw meta().id from `#{bucket_name}` where #{where} order by #{order} #{limit}"
|
|
126
|
-
|
|
127
|
-
|
|
132
|
+
|
|
133
|
+
query_options = options.merge(positional_parameters: params)
|
|
134
|
+
result = cluster.query(n1ql_query, Couchbase::Options::Query.new(**query_options))
|
|
135
|
+
CouchbaseOrm.logger.debug {
|
|
136
|
+
"N1QL query: #{n1ql_query} params: #{params.inspect} return #{result.rows.to_a.length} rows with scan_consistency: #{options[:scan_consistency]}"
|
|
137
|
+
}
|
|
128
138
|
N1qlProxy.new(result)
|
|
129
139
|
end
|
|
130
140
|
end
|
|
@@ -3,7 +3,7 @@ module CouchbaseOrm
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
class CouchbaseOrm_Relation
|
|
6
|
-
def initialize(model:, where: where = nil, order: order = nil, limit: limit = nil, _not: _not = false, strict_loading: strict_loading = false)
|
|
6
|
+
def initialize(model:, where: where = nil, order: order = nil, limit: limit = nil, _not: _not = false, strict_loading: strict_loading = false, query_options: query_options = {})
|
|
7
7
|
CouchbaseOrm::logger.debug "CouchbaseOrm_Relation init: #{model} where:#{where.inspect} not:#{_not.inspect} order:#{order.inspect} limit: #{limit} strict_loading: #{strict_loading}"
|
|
8
8
|
@model = model
|
|
9
9
|
@limit = limit
|
|
@@ -12,6 +12,7 @@ module CouchbaseOrm
|
|
|
12
12
|
@order = merge_order(**order) if order
|
|
13
13
|
@where = merge_where(where, _not) if where
|
|
14
14
|
@strict_loading = strict_loading
|
|
15
|
+
@query_options = query_options || {}
|
|
15
16
|
CouchbaseOrm::logger.debug "- #{to_s}"
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -21,31 +22,41 @@ module CouchbaseOrm
|
|
|
21
22
|
|
|
22
23
|
def to_n1ql
|
|
23
24
|
bucket_name = @model.bucket.name
|
|
24
|
-
where =
|
|
25
|
+
where = build_where_with_params(nil)
|
|
25
26
|
order = build_order
|
|
26
27
|
limit = build_limit
|
|
27
28
|
"select raw meta().id from `#{bucket_name}` where #{where} order by #{order} #{limit}"
|
|
28
29
|
end
|
|
29
30
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
def to_n1ql_with_params
|
|
32
|
+
bucket_name = @model.bucket.name
|
|
33
|
+
params = []
|
|
34
|
+
where = build_where_with_params(params)
|
|
35
|
+
order = build_order
|
|
36
|
+
limit = build_limit
|
|
37
|
+
["select raw meta().id from `#{bucket_name}` where #{where} order by #{order} #{limit}", params]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def execute(n1ql_query, params = [])
|
|
41
|
+
result = @model.cluster.query(n1ql_query, build_query_options(positional_parameters: params))
|
|
42
|
+
CouchbaseOrm.logger.debug { "Relation query: #{n1ql_query} params: #{params.inspect} return #{result.rows.to_a.length} rows" }
|
|
33
43
|
N1qlProxy.new(result)
|
|
34
44
|
end
|
|
35
45
|
|
|
36
46
|
def query
|
|
37
47
|
CouchbaseOrm::logger.debug("Query: #{self}")
|
|
38
|
-
n1ql_query =
|
|
39
|
-
execute(n1ql_query)
|
|
48
|
+
n1ql_query, params = to_n1ql_with_params
|
|
49
|
+
execute(n1ql_query, params)
|
|
40
50
|
end
|
|
41
|
-
|
|
51
|
+
|
|
42
52
|
def update_all(**cond)
|
|
43
53
|
bucket_name = @model.bucket.name
|
|
44
|
-
|
|
54
|
+
params = []
|
|
55
|
+
where = build_where_with_params(params)
|
|
45
56
|
limit = build_limit
|
|
46
|
-
update =
|
|
57
|
+
update = build_update_with_params(params, **cond)
|
|
47
58
|
n1ql_query = "update `#{bucket_name}` set #{update} where #{where} #{limit}"
|
|
48
|
-
execute(n1ql_query)
|
|
59
|
+
execute(n1ql_query, params)
|
|
49
60
|
end
|
|
50
61
|
|
|
51
62
|
def ids
|
|
@@ -60,15 +71,21 @@ module CouchbaseOrm
|
|
|
60
71
|
!!@strict_loading
|
|
61
72
|
end
|
|
62
73
|
|
|
74
|
+
def with(opts = {})
|
|
75
|
+
CouchbaseOrm_Relation.new(**initializer_arguments.merge(query_options: @query_options.merge(opts)))
|
|
76
|
+
end
|
|
77
|
+
|
|
63
78
|
def first
|
|
64
|
-
|
|
79
|
+
n1ql_query, params = self.limit(1).to_n1ql_with_params
|
|
80
|
+
result = @model.cluster.query(n1ql_query, build_query_options(positional_parameters: params))
|
|
65
81
|
return unless (first_id = result.rows.to_a.first)
|
|
66
82
|
|
|
67
83
|
@model.find(first_id, with_strict_loading: @strict_loading)
|
|
68
84
|
end
|
|
69
85
|
|
|
70
86
|
def last
|
|
71
|
-
|
|
87
|
+
n1ql_query, params = to_n1ql_with_params
|
|
88
|
+
result = @model.cluster.query(n1ql_query, build_query_options(positional_parameters: params))
|
|
72
89
|
last_id = result.rows.to_a.last
|
|
73
90
|
@model.find(last_id, with_strict_loading: @strict_loading) if last_id
|
|
74
91
|
end
|
|
@@ -156,7 +173,7 @@ module CouchbaseOrm
|
|
|
156
173
|
end
|
|
157
174
|
|
|
158
175
|
def initializer_arguments
|
|
159
|
-
{ model: @model, order: @order, where: @where, limit: @limit, strict_loading: @strict_loading }
|
|
176
|
+
{ model: @model, order: @order, where: @where, limit: @limit, strict_loading: @strict_loading, query_options: @query_options }
|
|
160
177
|
end
|
|
161
178
|
|
|
162
179
|
def merge_order(*lorder, **horder)
|
|
@@ -166,7 +183,7 @@ module CouchbaseOrm
|
|
|
166
183
|
.merge(Array.wrap(lorder).map{ |o| [o, :asc] }.to_h)
|
|
167
184
|
.merge(horder)
|
|
168
185
|
end
|
|
169
|
-
|
|
186
|
+
|
|
170
187
|
def merge_where(conds, _not = false)
|
|
171
188
|
@where + (_not ? conds.to_a.map{|k,v|[k,v,:not]} : conds.to_a)
|
|
172
189
|
end
|
|
@@ -183,48 +200,54 @@ module CouchbaseOrm
|
|
|
183
200
|
end.join(", ")
|
|
184
201
|
order.empty? ? "meta().id" : order
|
|
185
202
|
end
|
|
186
|
-
|
|
187
|
-
def
|
|
188
|
-
|
|
203
|
+
|
|
204
|
+
def build_where_with_params(params)
|
|
205
|
+
build_conds_with_params([[nil, "type = #{@model.quote(@model.design_document)}"]] + @where, params)
|
|
189
206
|
end
|
|
190
207
|
|
|
191
|
-
def
|
|
208
|
+
def build_conds_with_params(conds, params)
|
|
192
209
|
conds.map do |key, value, opt|
|
|
193
210
|
if key
|
|
194
|
-
opt == :not ?
|
|
195
|
-
@model.build_not_match(key, value) :
|
|
196
|
-
@model.build_match(key, value)
|
|
211
|
+
opt == :not ?
|
|
212
|
+
@model.build_not_match(key, value, params: params) :
|
|
213
|
+
@model.build_match(key, value, params: params)
|
|
197
214
|
else
|
|
198
215
|
value
|
|
199
216
|
end
|
|
200
217
|
end.join(" AND ")
|
|
201
218
|
end
|
|
202
219
|
|
|
203
|
-
def
|
|
220
|
+
def build_update_with_params(params, **cond)
|
|
204
221
|
cond.map do |key, value|
|
|
205
|
-
for_clause=""
|
|
222
|
+
for_clause = ""
|
|
206
223
|
if value.is_a?(Hash) && value[:_for]
|
|
207
224
|
path_clause = value.delete(:_for)
|
|
208
225
|
var_clause = path_clause.to_s.split(".").last.singularize
|
|
209
|
-
|
|
226
|
+
|
|
210
227
|
_when = value.delete(:_when)
|
|
211
|
-
when_clause = _when ?
|
|
212
|
-
|
|
213
|
-
_set = value.delete(:_set)
|
|
228
|
+
when_clause = _when ? build_conds_with_params(_when.to_a, params) : ""
|
|
229
|
+
|
|
230
|
+
_set = value.delete(:_set)
|
|
214
231
|
value = _set if _set
|
|
215
232
|
|
|
216
233
|
for_clause = " for #{var_clause} in #{path_clause} when #{when_clause} end"
|
|
217
234
|
end
|
|
218
235
|
if value.is_a?(Hash)
|
|
219
236
|
value.map do |k, v|
|
|
220
|
-
"#{key}.#{k} = #{@model.
|
|
237
|
+
"#{key}.#{k} = #{v.nil? ? 'NULL' : @model.bind(v, params)}"
|
|
221
238
|
end.join(", ") + for_clause
|
|
222
239
|
else
|
|
223
|
-
"#{key} = #{@model.
|
|
240
|
+
"#{key} = #{value.nil? ? 'NULL' : @model.bind(value, params)}#{for_clause}"
|
|
224
241
|
end
|
|
225
242
|
end.join(", ")
|
|
226
243
|
end
|
|
227
244
|
|
|
245
|
+
def build_query_options(positional_parameters: [])
|
|
246
|
+
opts = CouchbaseOrm::N1ql.config.merge(@query_options)
|
|
247
|
+
opts[:positional_parameters] = positional_parameters unless positional_parameters.empty?
|
|
248
|
+
Couchbase::Options::Query.new(**opts)
|
|
249
|
+
end
|
|
250
|
+
|
|
228
251
|
def method_missing(method, *args, &block)
|
|
229
252
|
if @model.respond_to?(method)
|
|
230
253
|
scoping {
|
|
@@ -243,7 +266,7 @@ module CouchbaseOrm
|
|
|
243
266
|
|
|
244
267
|
delegate :ids, :update_all, :delete_all, :count, :empty?, :filter, :reduce, :find_by, to: :all
|
|
245
268
|
|
|
246
|
-
delegate :where, :not, :order, :limit, :all, :strict_loading, :strict_loading?, to: :relation
|
|
269
|
+
delegate :where, :not, :order, :limit, :all, :strict_loading, :strict_loading?, :with, to: :relation
|
|
247
270
|
end
|
|
248
271
|
end
|
|
249
272
|
end
|
|
@@ -96,7 +96,9 @@ module CouchbaseOrm
|
|
|
96
96
|
klass.class_eval do
|
|
97
97
|
n1ql remote_method, emit_key: 'id', query_fn: proc { |bucket, values, options|
|
|
98
98
|
raise ArgumentError, "values[0] must not be blank" if values[0].blank?
|
|
99
|
-
|
|
99
|
+
n1ql_query = "SELECT raw #{through_key} FROM `#{bucket.name}` where type = \"#{design_document}\" and #{foreign_key} = $1"
|
|
100
|
+
options.positional_parameters([values[0]])
|
|
101
|
+
cluster.query(n1ql_query, options)
|
|
100
102
|
}
|
|
101
103
|
end
|
|
102
104
|
else
|
|
@@ -4,7 +4,34 @@ module CouchbaseOrm
|
|
|
4
4
|
|
|
5
5
|
module ClassMethods
|
|
6
6
|
|
|
7
|
-
def
|
|
7
|
+
def serialize_for_binding(value)
|
|
8
|
+
if value.is_a?(Array)
|
|
9
|
+
value.map { |v| serialize_for_binding(v) }
|
|
10
|
+
elsif [DateTime, Time].any? { |clazz| value.is_a?(clazz) } || (value.respond_to?(:acts_like?) && value.acts_like?(:time))
|
|
11
|
+
value.iso8601(@precision || 0)
|
|
12
|
+
elsif value.is_a?(Date)
|
|
13
|
+
value.to_s
|
|
14
|
+
else
|
|
15
|
+
value
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def bind(value, params)
|
|
20
|
+
if value.nil?
|
|
21
|
+
nil
|
|
22
|
+
else
|
|
23
|
+
params << serialize_for_binding(value)
|
|
24
|
+
"$#{params.length}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Renders a value either as a positional parameter (when +params+ is
|
|
29
|
+
# provided) or as an inline quoted literal (when it is nil).
|
|
30
|
+
def resolve_value(value, params)
|
|
31
|
+
params ? bind(value, params) : quote(value)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build_match(key, value, params: nil)
|
|
8
35
|
use_is_null = self.properties_always_exists_in_document
|
|
9
36
|
key = "meta().id" if key.to_s == "id"
|
|
10
37
|
case
|
|
@@ -13,35 +40,35 @@ module CouchbaseOrm
|
|
|
13
40
|
when value.nil? && !use_is_null
|
|
14
41
|
"#{key} IS NOT VALUED"
|
|
15
42
|
when value.is_a?(Hash) && attribute_types[key.to_s].is_a?(CouchbaseOrm::Types::Array)
|
|
16
|
-
"any #{key.to_s.singularize} in #{key} satisfies (#{build_match_hash("#{key.to_s.singularize}", value)}) end"
|
|
43
|
+
"any #{key.to_s.singularize} in #{key} satisfies (#{build_match_hash("#{key.to_s.singularize}", value, params: params)}) end"
|
|
17
44
|
when value.is_a?(Hash) && !attribute_types[key.to_s].is_a?(CouchbaseOrm::Types::Array)
|
|
18
|
-
build_match_hash(key, value)
|
|
45
|
+
build_match_hash(key, value, params: params)
|
|
19
46
|
when value.is_a?(Array) && value.include?(nil)
|
|
20
|
-
"(#{build_match(key, nil)} OR #{build_match(key, value.compact)})"
|
|
47
|
+
"(#{build_match(key, nil, params: params)} OR #{build_match(key, value.compact, params: params)})"
|
|
21
48
|
when value.is_a?(Array)
|
|
22
|
-
"#{key} IN #{
|
|
49
|
+
"#{key} IN #{resolve_value(value, params)}"
|
|
23
50
|
when value.is_a?(Range)
|
|
24
|
-
build_match_range(key, value)
|
|
51
|
+
build_match_range(key, value, params: params)
|
|
25
52
|
else
|
|
26
|
-
"#{key} = #{
|
|
53
|
+
"#{key} = #{resolve_value(value, params)}"
|
|
27
54
|
end
|
|
28
55
|
end
|
|
29
56
|
|
|
30
|
-
def build_match_hash(key, value)
|
|
57
|
+
def build_match_hash(key, value, params: nil)
|
|
31
58
|
matches = []
|
|
32
59
|
value.each do |k, v|
|
|
33
60
|
case k
|
|
34
61
|
when :_gt
|
|
35
|
-
matches << "#{key} > #{
|
|
62
|
+
matches << "#{key} > #{resolve_value(v, params)}"
|
|
36
63
|
when :_gte
|
|
37
|
-
matches << "#{key} >= #{
|
|
64
|
+
matches << "#{key} >= #{resolve_value(v, params)}"
|
|
38
65
|
when :_lt
|
|
39
|
-
matches << "#{key} < #{
|
|
66
|
+
matches << "#{key} < #{resolve_value(v, params)}"
|
|
40
67
|
when :_lte
|
|
41
|
-
matches << "#{key} <= #{
|
|
68
|
+
matches << "#{key} <= #{resolve_value(v, params)}"
|
|
42
69
|
when :_ne
|
|
43
|
-
matches << "#{key} != #{
|
|
44
|
-
|
|
70
|
+
matches << "#{key} != #{resolve_value(v, params)}"
|
|
71
|
+
|
|
45
72
|
# TODO v2
|
|
46
73
|
# when :_in
|
|
47
74
|
# matches << "#{key} IN #{quote(v)}"
|
|
@@ -65,7 +92,7 @@ module CouchbaseOrm
|
|
|
65
92
|
# matches << "#{key} MATCH #{quote(v)}"
|
|
66
93
|
# when :_nmatch
|
|
67
94
|
# matches << "#{key} NOT MATCH #{quote(v)}"
|
|
68
|
-
|
|
95
|
+
|
|
69
96
|
# TODO v3
|
|
70
97
|
# when :_any
|
|
71
98
|
# matches << "#{key} ANY #{quote(v)}"
|
|
@@ -80,26 +107,26 @@ module CouchbaseOrm
|
|
|
80
107
|
#when :_nwithin
|
|
81
108
|
# matches << "#{key} NOT WITHIN #{quote(v)}"
|
|
82
109
|
else
|
|
83
|
-
matches << build_match("#{key}.#{k}", v)
|
|
110
|
+
matches << build_match("#{key}.#{k}", v, params: params)
|
|
84
111
|
end
|
|
85
112
|
end
|
|
86
|
-
|
|
113
|
+
|
|
87
114
|
matches.join(" AND ")
|
|
88
115
|
end
|
|
89
116
|
|
|
90
|
-
def build_match_range(key, value)
|
|
117
|
+
def build_match_range(key, value, params: nil)
|
|
91
118
|
matches = []
|
|
92
|
-
matches << "#{key} >= #{
|
|
119
|
+
matches << "#{key} >= #{resolve_value(value.begin, params)}"
|
|
93
120
|
if value.exclude_end?
|
|
94
|
-
matches << "#{key} < #{
|
|
121
|
+
matches << "#{key} < #{resolve_value(value.end, params)}"
|
|
95
122
|
else
|
|
96
|
-
matches << "#{key} <= #{
|
|
123
|
+
matches << "#{key} <= #{resolve_value(value.end, params)}"
|
|
97
124
|
end
|
|
98
125
|
matches.join(" AND ")
|
|
99
126
|
end
|
|
100
127
|
|
|
101
128
|
|
|
102
|
-
def build_not_match(key, value)
|
|
129
|
+
def build_not_match(key, value, params: nil)
|
|
103
130
|
use_is_null = self.properties_always_exists_in_document
|
|
104
131
|
key = "meta().id" if key.to_s == "id"
|
|
105
132
|
case
|
|
@@ -108,11 +135,11 @@ module CouchbaseOrm
|
|
|
108
135
|
when value.nil? && !use_is_null
|
|
109
136
|
"#{key} IS VALUED"
|
|
110
137
|
when value.is_a?(Array) && value.include?(nil)
|
|
111
|
-
"(#{build_not_match(key, nil)} AND #{build_not_match(key, value.compact)})"
|
|
138
|
+
"(#{build_not_match(key, nil, params: params)} AND #{build_not_match(key, value.compact, params: params)})"
|
|
112
139
|
when value.is_a?(Array)
|
|
113
|
-
"#{key} NOT IN #{
|
|
140
|
+
"#{key} NOT IN #{resolve_value(value, params)}"
|
|
114
141
|
else
|
|
115
|
-
"#{key} != #{
|
|
142
|
+
"#{key} != #{resolve_value(value, params)}"
|
|
116
143
|
end
|
|
117
144
|
end
|
|
118
145
|
|
data/spec/n1ql_spec.rb
CHANGED
|
@@ -172,7 +172,10 @@ describe CouchbaseOrm::N1ql do
|
|
|
172
172
|
it "should log the default scan_consistency when n1ql query is executed" do
|
|
173
173
|
allow(CouchbaseOrm.logger).to receive(:debug)
|
|
174
174
|
N1QLTest.by_rating_reverse()
|
|
175
|
-
expect(CouchbaseOrm.logger).to have_received(:debug).at_least(:once)
|
|
175
|
+
expect(CouchbaseOrm.logger).to have_received(:debug).at_least(:once) do |&block|
|
|
176
|
+
msg = block ? block.call : nil
|
|
177
|
+
msg == "N1QL query: select raw meta().id from `#{CouchbaseOrm::Connection.bucket.name}` where type=\"n1_ql_test\" order by name DESC params: [] return 0 rows with scan_consistency: #{described_class::DEFAULT_SCAN_CONSISTENCY}"
|
|
178
|
+
end
|
|
176
179
|
end
|
|
177
180
|
|
|
178
181
|
it "should log the set scan_consistency when n1ql query is executed with a specific scan_consistency" do
|
|
@@ -180,11 +183,36 @@ describe CouchbaseOrm::N1ql do
|
|
|
180
183
|
default_n1ql_config = CouchbaseOrm::N1ql.config
|
|
181
184
|
CouchbaseOrm::N1ql.config({ scan_consistency: :not_bounded })
|
|
182
185
|
N1QLTest.by_rating_reverse()
|
|
183
|
-
expect(CouchbaseOrm.logger).to have_received(:debug).at_least(:once)
|
|
186
|
+
expect(CouchbaseOrm.logger).to have_received(:debug).at_least(:once) do |&block|
|
|
187
|
+
msg = block ? block.call : nil
|
|
188
|
+
msg == "N1QL query: select raw meta().id from `#{CouchbaseOrm::Connection.bucket.name}` where type=\"n1_ql_test\" order by name DESC params: [] return 0 rows with scan_consistency: not_bounded"
|
|
189
|
+
end
|
|
184
190
|
|
|
185
191
|
CouchbaseOrm::N1ql.config(default_n1ql_config)
|
|
186
192
|
N1QLTest.by_rating_reverse()
|
|
187
|
-
expect(CouchbaseOrm.logger).to have_received(:debug).at_least(:once)
|
|
193
|
+
expect(CouchbaseOrm.logger).to have_received(:debug).at_least(:once) do |&block|
|
|
194
|
+
msg = block ? block.call : nil
|
|
195
|
+
msg == "N1QL query: select raw meta().id from `#{CouchbaseOrm::Connection.bucket.name}` where type=\"n1_ql_test\" order by name DESC params: [] return 0 rows with scan_consistency: #{described_class::DEFAULT_SCAN_CONSISTENCY}"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it "should use adhoc: true by default (no prepared statement plan caching)" do
|
|
200
|
+
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: true)).and_call_original
|
|
201
|
+
N1QLTest.by_rating_reverse()
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it "should allow overriding adhoc per call to enable plan caching" do
|
|
205
|
+
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
|
|
206
|
+
N1QLTest.by_rating_reverse(adhoc: false)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it "should respect N1ql.config adhoc setting" do
|
|
210
|
+
default_config = CouchbaseOrm::N1ql.config
|
|
211
|
+
CouchbaseOrm::N1ql.config({ adhoc: false })
|
|
212
|
+
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
|
|
213
|
+
N1QLTest.by_rating_reverse()
|
|
214
|
+
ensure
|
|
215
|
+
CouchbaseOrm::N1ql.config(default_config)
|
|
188
216
|
end
|
|
189
217
|
|
|
190
218
|
after(:all) do
|
data/spec/relation_spec.rb
CHANGED
|
@@ -310,6 +310,55 @@ describe CouchbaseOrm::Relation do
|
|
|
310
310
|
expect(RelationModel.empty?).to eq(false)
|
|
311
311
|
end
|
|
312
312
|
|
|
313
|
+
describe "parameterized queries" do
|
|
314
|
+
it "should return parameterized query with to_n1ql_with_params" do
|
|
315
|
+
relation = RelationModel.where(active: true, name: "Jane")
|
|
316
|
+
n1ql, params = relation.send(:to_n1ql_with_params)
|
|
317
|
+
expect(n1ql).to include("type = 'relation_model'")
|
|
318
|
+
expect(n1ql).to include("active = $1")
|
|
319
|
+
expect(n1ql).to include("name = $2")
|
|
320
|
+
expect(n1ql).not_to include("'Jane'")
|
|
321
|
+
expect(params).to eq([true, "Jane"])
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it "should parameterize NOT conditions" do
|
|
325
|
+
relation = RelationModel.not(active: true)
|
|
326
|
+
n1ql, params = relation.send(:to_n1ql_with_params)
|
|
327
|
+
expect(n1ql).to include("active != $1")
|
|
328
|
+
expect(params).to eq([true])
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
it "should parameterize range conditions" do
|
|
332
|
+
relation = RelationModel.where(age: 10..30)
|
|
333
|
+
n1ql, params = relation.send(:to_n1ql_with_params)
|
|
334
|
+
expect(n1ql).to include("age >= $1")
|
|
335
|
+
expect(n1ql).to include("age <= $2")
|
|
336
|
+
expect(params).to eq([10, 30])
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
it "should parameterize hash operator conditions" do
|
|
340
|
+
relation = RelationModel.where(age: { _gte: 18, _lt: 65 })
|
|
341
|
+
n1ql, params = relation.send(:to_n1ql_with_params)
|
|
342
|
+
expect(n1ql).to include("age >= $1")
|
|
343
|
+
expect(n1ql).to include("age < $2")
|
|
344
|
+
expect(params).to eq([18, 65])
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
it "should pass through string conditions without parameterization" do
|
|
348
|
+
relation = RelationModel.where("active = true")
|
|
349
|
+
n1ql, params = relation.send(:to_n1ql_with_params)
|
|
350
|
+
expect(n1ql).to include("(active = true)")
|
|
351
|
+
expect(params).to eq([])
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
it "should parameterize array IN conditions" do
|
|
355
|
+
relation = RelationModel.where(name: ["Alice", "Bob"])
|
|
356
|
+
n1ql, params = relation.send(:to_n1ql_with_params)
|
|
357
|
+
expect(n1ql).to include("name IN $1")
|
|
358
|
+
expect(params).to eq([["Alice", "Bob"]])
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
313
362
|
describe "operators" do
|
|
314
363
|
it "should query by gte and lte" do
|
|
315
364
|
_m1 = RelationModel.create!(age: 10)
|
|
@@ -452,5 +501,38 @@ describe CouchbaseOrm::Relation do
|
|
|
452
501
|
end
|
|
453
502
|
end
|
|
454
503
|
end
|
|
504
|
+
|
|
505
|
+
it "should use adhoc: true by default (no prepared statement plan caching)" do
|
|
506
|
+
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: true)).and_call_original
|
|
507
|
+
RelationModel.where(active: true).ids
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
describe "adhoc option via with" do
|
|
511
|
+
it "should return a relation when calling with(adhoc:)" do
|
|
512
|
+
expect(RelationModel.all.with(adhoc: false)).to be_a(CouchbaseOrm::Relation::CouchbaseOrm_Relation)
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
it "should pass adhoc: false to query options when set on the relation" do
|
|
516
|
+
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
|
|
517
|
+
RelationModel.where(active: true).with(adhoc: false).ids
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
it "should override N1ql.config adhoc when set on the relation" do
|
|
521
|
+
default_config = CouchbaseOrm::N1ql.config
|
|
522
|
+
CouchbaseOrm::N1ql.config(adhoc: false)
|
|
523
|
+
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: true)).and_call_original
|
|
524
|
+
RelationModel.where(active: true).with(adhoc: true).ids
|
|
525
|
+
ensure
|
|
526
|
+
CouchbaseOrm::N1ql.config(default_config)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
it "should be chainable with other relation methods" do
|
|
530
|
+
m1 = RelationModel.create!(active: true, age: 10)
|
|
531
|
+
_m2 = RelationModel.create!(active: false, age: 20)
|
|
532
|
+
expect(Couchbase::Options::Query).to receive(:new).with(hash_including(adhoc: false)).and_call_original
|
|
533
|
+
result = RelationModel.where(active: true).order(:age).with(adhoc: false).to_a
|
|
534
|
+
expect(result).to match_array([m1])
|
|
535
|
+
end
|
|
536
|
+
end
|
|
455
537
|
end
|
|
456
538
|
|
metadata
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: couchbase-orm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0
|
|
4
|
+
version: 3.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stephen von Takach
|
|
8
8
|
- Gauthier Monserand
|
|
9
9
|
- Pierre Merlin
|
|
10
10
|
- Julien Burnet-Fauche
|
|
11
|
-
autorequire:
|
|
12
11
|
bindir: bin
|
|
13
12
|
cert_chain: []
|
|
14
|
-
date:
|
|
13
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
15
14
|
dependencies:
|
|
16
15
|
- !ruby/object:Gem::Dependency
|
|
17
16
|
name: activemodel
|
|
@@ -210,7 +209,6 @@ dependencies:
|
|
|
210
209
|
- !ruby/object:Gem::Version
|
|
211
210
|
version: '0'
|
|
212
211
|
description: A Couchbase ORM for Rails
|
|
213
|
-
email:
|
|
214
212
|
executables: []
|
|
215
213
|
extensions: []
|
|
216
214
|
extra_rdoc_files: []
|
|
@@ -354,7 +352,6 @@ metadata:
|
|
|
354
352
|
bug_tracker_uri: https://github.com/Couchbase-Ecosystem/couchbase-ruby-orm/issues
|
|
355
353
|
documentation_uri: https://www.couchbase-ruby-orm.com/
|
|
356
354
|
homepage_uri: https://github.com/Couchbase-Ecosystem/couchbase-ruby-orm
|
|
357
|
-
post_install_message:
|
|
358
355
|
rdoc_options: []
|
|
359
356
|
require_paths:
|
|
360
357
|
- lib
|
|
@@ -362,15 +359,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
362
359
|
requirements:
|
|
363
360
|
- - ">="
|
|
364
361
|
- !ruby/object:Gem::Version
|
|
365
|
-
version: 3.
|
|
362
|
+
version: 3.3.0
|
|
366
363
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
367
364
|
requirements:
|
|
368
365
|
- - ">="
|
|
369
366
|
- !ruby/object:Gem::Version
|
|
370
367
|
version: '0'
|
|
371
368
|
requirements: []
|
|
372
|
-
rubygems_version:
|
|
373
|
-
signing_key:
|
|
369
|
+
rubygems_version: 4.0.10
|
|
374
370
|
specification_version: 4
|
|
375
371
|
summary: Couchbase ORM for Rails
|
|
376
372
|
test_files:
|