couchbase-orm 3.0.3 → 3.1.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: 9ece6e35ae742239bedb003f5865293f86164ba2899801d8f9b18efe93266e09
4
+ data.tar.gz: 85f84fa26a635d3fa319e2c5726eb429b52e576689690e61c50ace5b79333551
5
5
  SHA512:
6
- metadata.gz: 11e6a97c5f7b08549ce024a0a406d131fab9934efffa9bd410a7328f23bd39d55620b5e6ddc81c257bb78295804c3abaabd80cee41395e35b87d3b2cf902af34
7
- data.tar.gz: be34bdf6a52ade5558ec6fb50c07570f694731bd422c34d135a0560ae2f14f86c10a5e4bd7226964636d9a6f04ccd7a763d68e231042f6750df390079f3b5b31
6
+ metadata.gz: beea3033581402385e821e1d4512d0a3ae9f76df5ebd571c8907c67a216bf0680a065c1276387160208ca2a49e99310f88b7fd3b2e2147b1b5b1c92e1b4b6fba
7
+ data.tar.gz: 9dffa58a606270a0a0c7775b91f6af34cceb0c1bca77ac37f5eec94784e217c9e8322beed82b42e33366cae50e32b65175bb4dc1c5ea9290a305bdaa777578fc
@@ -95,10 +95,10 @@ module CouchbaseOrm
95
95
  end
96
96
  end
97
97
 
98
- def build_where(keys, values)
98
+ def build_where(keys, values, params: nil)
99
99
  where = values == NO_VALUE ? '' : keys.zip(Array.wrap(values))
100
100
  .reject { |key, value| key.nil? && value.nil? }
101
- .map { |key, value| build_match(key, value) }
101
+ .map { |key, value| build_match(key, value, params: params) }
102
102
  .join(" AND ")
103
103
  "type=\"#{design_document}\" #{"AND " + where unless where.blank?}"
104
104
  end
@@ -119,12 +119,17 @@ module CouchbaseOrm
119
119
  N1qlProxy.new(query_fn.call(bucket, values, Couchbase::Options::Query.new(**options)))
120
120
  else
121
121
  bucket_name = bucket.name
122
- where = build_where(keys, values)
122
+ params = []
123
+ where = build_where(keys, values, params: params)
123
124
  order = custom_order || build_order(keys, descending)
124
125
  limit = build_limit(limit)
125
126
  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]}"
127
+
128
+ query_options = options.merge(positional_parameters: params)
129
+ result = cluster.query(n1ql_query, Couchbase::Options::Query.new(**query_options))
130
+ CouchbaseOrm.logger.debug {
131
+ "N1QL query: #{n1ql_query} params: #{params.inspect} return #{result.rows.to_a.length} rows with scan_consistency: #{options[:scan_consistency]}"
132
+ }
128
133
  N1qlProxy.new(result)
129
134
  end
130
135
  end
@@ -21,31 +21,41 @@ module CouchbaseOrm
21
21
 
22
22
  def to_n1ql
23
23
  bucket_name = @model.bucket.name
24
- where = build_where
24
+ where = build_where_with_params(nil)
25
25
  order = build_order
26
26
  limit = build_limit
27
27
  "select raw meta().id from `#{bucket_name}` where #{where} order by #{order} #{limit}"
28
28
  end
29
29
 
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]}" }
30
+ def to_n1ql_with_params
31
+ bucket_name = @model.bucket.name
32
+ params = []
33
+ where = build_where_with_params(params)
34
+ order = build_order
35
+ limit = build_limit
36
+ ["select raw meta().id from `#{bucket_name}` where #{where} order by #{order} #{limit}", params]
37
+ end
38
+
39
+ def execute(n1ql_query, params = [])
40
+ result = @model.cluster.query(n1ql_query, build_query_options(positional_parameters: params))
41
+ CouchbaseOrm.logger.debug { "Relation query: #{n1ql_query} params: #{params.inspect} return #{result.rows.to_a.length} rows" }
33
42
  N1qlProxy.new(result)
34
43
  end
35
44
 
36
45
  def query
37
46
  CouchbaseOrm::logger.debug("Query: #{self}")
38
- n1ql_query = to_n1ql
39
- execute(n1ql_query)
47
+ n1ql_query, params = to_n1ql_with_params
48
+ execute(n1ql_query, params)
40
49
  end
41
-
50
+
42
51
  def update_all(**cond)
43
52
  bucket_name = @model.bucket.name
44
- where = build_where
53
+ params = []
54
+ where = build_where_with_params(params)
45
55
  limit = build_limit
46
- update = build_update(**cond)
56
+ update = build_update_with_params(params, **cond)
47
57
  n1ql_query = "update `#{bucket_name}` set #{update} where #{where} #{limit}"
48
- execute(n1ql_query)
58
+ execute(n1ql_query, params)
49
59
  end
50
60
 
51
61
  def ids
@@ -61,14 +71,16 @@ module CouchbaseOrm
61
71
  end
62
72
 
63
73
  def first
64
- result = @model.cluster.query(self.limit(1).to_n1ql, Couchbase::Options::Query.new(scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency]))
74
+ n1ql_query, params = self.limit(1).to_n1ql_with_params
75
+ result = @model.cluster.query(n1ql_query, build_query_options(positional_parameters: params))
65
76
  return unless (first_id = result.rows.to_a.first)
66
77
 
67
78
  @model.find(first_id, with_strict_loading: @strict_loading)
68
79
  end
69
80
 
70
81
  def last
71
- result = @model.cluster.query(to_n1ql, Couchbase::Options::Query.new(scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency]))
82
+ n1ql_query, params = to_n1ql_with_params
83
+ result = @model.cluster.query(n1ql_query, build_query_options(positional_parameters: params))
72
84
  last_id = result.rows.to_a.last
73
85
  @model.find(last_id, with_strict_loading: @strict_loading) if last_id
74
86
  end
@@ -166,7 +178,7 @@ module CouchbaseOrm
166
178
  .merge(Array.wrap(lorder).map{ |o| [o, :asc] }.to_h)
167
179
  .merge(horder)
168
180
  end
169
-
181
+
170
182
  def merge_where(conds, _not = false)
171
183
  @where + (_not ? conds.to_a.map{|k,v|[k,v,:not]} : conds.to_a)
172
184
  end
@@ -183,48 +195,54 @@ module CouchbaseOrm
183
195
  end.join(", ")
184
196
  order.empty? ? "meta().id" : order
185
197
  end
186
-
187
- def build_where
188
- build_conds([[:type, @model.design_document]] + @where)
198
+
199
+ def build_where_with_params(params)
200
+ build_conds_with_params([[nil, "type = #{@model.quote(@model.design_document)}"]] + @where, params)
189
201
  end
190
202
 
191
- def build_conds(conds)
203
+ def build_conds_with_params(conds, params)
192
204
  conds.map do |key, value, opt|
193
205
  if key
194
- opt == :not ?
195
- @model.build_not_match(key, value) :
196
- @model.build_match(key, value)
206
+ opt == :not ?
207
+ @model.build_not_match(key, value, params: params) :
208
+ @model.build_match(key, value, params: params)
197
209
  else
198
210
  value
199
211
  end
200
212
  end.join(" AND ")
201
213
  end
202
214
 
203
- def build_update(**cond)
215
+ def build_update_with_params(params, **cond)
204
216
  cond.map do |key, value|
205
- for_clause=""
217
+ for_clause = ""
206
218
  if value.is_a?(Hash) && value[:_for]
207
219
  path_clause = value.delete(:_for)
208
220
  var_clause = path_clause.to_s.split(".").last.singularize
209
-
221
+
210
222
  _when = value.delete(:_when)
211
- when_clause = _when ? build_conds(_when.to_a) : ""
212
-
213
- _set = value.delete(:_set)
223
+ when_clause = _when ? build_conds_with_params(_when.to_a, params) : ""
224
+
225
+ _set = value.delete(:_set)
214
226
  value = _set if _set
215
227
 
216
228
  for_clause = " for #{var_clause} in #{path_clause} when #{when_clause} end"
217
229
  end
218
230
  if value.is_a?(Hash)
219
231
  value.map do |k, v|
220
- "#{key}.#{k} = #{@model.quote(v) || 'NULL'}"
232
+ "#{key}.#{k} = #{v.nil? ? 'NULL' : @model.bind(v, params)}"
221
233
  end.join(", ") + for_clause
222
234
  else
223
- "#{key} = #{@model.quote(value)}#{for_clause}"
235
+ "#{key} = #{value.nil? ? 'NULL' : @model.bind(value, params)}#{for_clause}"
224
236
  end
225
237
  end.join(", ")
226
238
  end
227
239
 
240
+ def build_query_options(positional_parameters: [])
241
+ opts = { scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency] }
242
+ opts[:positional_parameters] = positional_parameters unless positional_parameters.empty?
243
+ Couchbase::Options::Query.new(**opts)
244
+ end
245
+
228
246
  def method_missing(method, *args, &block)
229
247
  if @model.respond_to?(method)
230
248
  scoping {
@@ -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.1.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,17 @@ 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
188
197
  end
189
198
 
190
199
  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)
metadata CHANGED
@@ -1,7 +1,7 @@
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.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen von Takach
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2026-06-26 00:00:00.000000000 Z
14
+ date: 2026-06-30 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activemodel