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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87ca9e5d5883671444a8c9d6a9fec5191da2d44818b8f95e0511cb379715d4d3
4
- data.tar.gz: ea7e8e4fe8586958f478a0ac004719b1c4d2c873fa1168974900cd33859e1479
3
+ metadata.gz: 253e0ee05a3522f641294d615724214c6b03ebfd189f008544e9a41f0e671916
4
+ data.tar.gz: 7628e1f89db8f7649dd6795145b238a06af87cb1901e30337670c3f15b87378c
5
5
  SHA512:
6
- metadata.gz: 11e6a97c5f7b08549ce024a0a406d131fab9934efffa9bd410a7328f23bd39d55620b5e6ddc81c257bb78295804c3abaabd80cee41395e35b87d3b2cf902af34
7
- data.tar.gz: be34bdf6a52ade5558ec6fb50c07570f694731bd422c34d135a0560ae2f14f86c10a5e4bd7226964636d9a6f04ccd7a763d68e231042f6750df390079f3b5b31
6
+ metadata.gz: af199f51fb0fd808ac4920ca8eb16d50b8d6afa89a23cd311d5e39e92df87f93914368267afd373e7d8d282e50e517b892039008f3a2d234ef5bf1ea2b63bb83
7
+ data.tar.gz: b0703729ad90b504026467377aecdeaed17434010f044ede50d0645ef1e51d9dadef595c31ad1f1990d7366ba7f4101c934c716f4f8cb2152a70d0f846ad7e88
@@ -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: sudo apt-get update && sudo apt-get install libevent-dev libev-dev python3-httplib2
38
- - run: wget http://security.ubuntu.com/ubuntu/pool/universe/n/ncurses/libtinfo5_6.3-2ubuntu0.1_amd64.deb
39
- - run: sudo apt install ./libtinfo5_6.3-2ubuntu0.1_amd64.deb
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 }}
@@ -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.1.0'
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.
@@ -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
- Thread.current['__couchbaseorm_n1ql_config__'] || {
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(scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency])
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
- where = build_where(keys, values)
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
- result = cluster.query(n1ql_query, Couchbase::Options::Query.new(**options))
127
- CouchbaseOrm.logger.debug "N1QL query: #{n1ql_query} return #{result.rows.to_a.length} rows with scan_consistency : #{options[:scan_consistency]}"
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 = build_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 execute(n1ql_query)
31
- result = @model.cluster.query(n1ql_query, Couchbase::Options::Query.new(scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency]))
32
- CouchbaseOrm.logger.debug { "Relation query: #{n1ql_query} return #{result.rows.to_a.length} rows with scan_consistency : #{CouchbaseOrm::N1ql.config[:scan_consistency]}" }
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 = to_n1ql
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
- where = build_where
54
+ params = []
55
+ where = build_where_with_params(params)
45
56
  limit = build_limit
46
- update = build_update(**cond)
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
- result = @model.cluster.query(self.limit(1).to_n1ql, Couchbase::Options::Query.new(scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency]))
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
- result = @model.cluster.query(to_n1ql, Couchbase::Options::Query.new(scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency]))
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 build_where
188
- build_conds([[:type, @model.design_document]] + @where)
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 build_conds(conds)
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 build_update(**cond)
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 ? build_conds(_when.to_a) : ""
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.quote(v) || 'NULL'}"
237
+ "#{key}.#{k} = #{v.nil? ? 'NULL' : @model.bind(v, params)}"
221
238
  end.join(", ") + for_clause
222
239
  else
223
- "#{key} = #{@model.quote(value)}#{for_clause}"
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
- cluster.query("SELECT raw #{through_key} FROM `#{bucket.name}` where type = \"#{design_document}\" and #{foreign_key} = #{quote(values[0])}", options)
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 build_match(key, value)
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 #{quote(value)}"
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} = #{quote(value)}"
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} > #{quote(v)}"
62
+ matches << "#{key} > #{resolve_value(v, params)}"
36
63
  when :_gte
37
- matches << "#{key} >= #{quote(v)}"
64
+ matches << "#{key} >= #{resolve_value(v, params)}"
38
65
  when :_lt
39
- matches << "#{key} < #{quote(v)}"
66
+ matches << "#{key} < #{resolve_value(v, params)}"
40
67
  when :_lte
41
- matches << "#{key} <= #{quote(v)}"
68
+ matches << "#{key} <= #{resolve_value(v, params)}"
42
69
  when :_ne
43
- matches << "#{key} != #{quote(v)}"
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} >= #{quote(value.begin)}"
119
+ matches << "#{key} >= #{resolve_value(value.begin, params)}"
93
120
  if value.exclude_end?
94
- matches << "#{key} < #{quote(value.end)}"
121
+ matches << "#{key} < #{resolve_value(value.end, params)}"
95
122
  else
96
- matches << "#{key} <= #{quote(value.end)}"
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 #{quote(value)}"
140
+ "#{key} NOT IN #{resolve_value(value, params)}"
114
141
  else
115
- "#{key} != #{quote(value)}"
142
+ "#{key} != #{resolve_value(value, params)}"
116
143
  end
117
144
  end
118
145
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true, encoding: ASCII-8BIT
2
2
 
3
3
  module CouchbaseOrm
4
- VERSION = '3.0.3'
4
+ VERSION = '3.2.0'
5
5
  end
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).with("N1QL query: select raw meta().id from `#{CouchbaseOrm::Connection.bucket.name}` where type=\"n1_ql_test\" order by name DESC return 0 rows with scan_consistency : #{described_class::DEFAULT_SCAN_CONSISTENCY}")
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).with("N1QL query: select raw meta().id from `#{CouchbaseOrm::Connection.bucket.name}` where type=\"n1_ql_test\" order by name DESC return 0 rows with scan_consistency : not_bounded")
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).with("N1QL query: select raw meta().id from `#{CouchbaseOrm::Connection.bucket.name}` where type=\"n1_ql_test\" order by name DESC return 0 rows with scan_consistency : #{described_class::DEFAULT_SCAN_CONSISTENCY}")
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
@@ -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.3
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: 2026-06-26 00:00:00.000000000 Z
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.1.0
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: 3.0.3.1
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: