couchbase-orm 3.1.0 → 3.3.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: 9ece6e35ae742239bedb003f5865293f86164ba2899801d8f9b18efe93266e09
4
- data.tar.gz: 85f84fa26a635d3fa319e2c5726eb429b52e576689690e61c50ace5b79333551
3
+ metadata.gz: 90eb0b8fb472134579071ce58442b518bd42009d3647363116f041eb65b13a85
4
+ data.tar.gz: 066cad6c6ee614382262d035d8d86e55e8d829f42733d21daddd9a91deb03786
5
5
  SHA512:
6
- metadata.gz: beea3033581402385e821e1d4512d0a3ae9f76df5ebd571c8907c67a216bf0680a065c1276387160208ca2a49e99310f88b7fd3b2e2147b1b5b1c92e1b4b6fba
7
- data.tar.gz: 9dffa58a606270a0a0c7775b91f6af34cceb0c1bca77ac37f5eec94784e217c9e8322beed82b42e33366cae50e32b65175bb4dc1c5ea9290a305bdaa777578fc
6
+ metadata.gz: 2cfd5683e131c28d10499da7c42ab2000688bed5dc5382c9b7a2beab99cb8b142da16b9327966636777ca32c34bee101185c1fb8a8b1862203339b5a27adc24c
7
+ data.tar.gz: 393a5523c5a5ffb2154b8ac426e199179cebc48356fd32a7170ad6c1eed7a6984a56c4990e3e0601d88710071c22b50b8ad87c41a067b1dc5b4bdd0fec29a281
@@ -0,0 +1,28 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - '[0-9]+.[0-9]+.[0-9]+'
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ github-release:
13
+ runs-on: ubuntu-24.04
14
+ steps:
15
+ - uses: actions/checkout@v3
16
+ with:
17
+ fetch-depth: 0
18
+ - name: Verify tag is on master
19
+ run: |
20
+ git fetch origin master
21
+ if ! git merge-base --is-ancestor "${{ github.sha }}" origin/master; then
22
+ echo "::error::Tag ${{ github.ref_name }} (${{ github.sha }}) is not an ancestor of origin/master. Refusing to create a GitHub Release for a commit that hasn't been merged to master."
23
+ exit 1
24
+ fi
25
+ - name: Create GitHub Release
26
+ env:
27
+ GH_TOKEN: ${{ github.token }}
28
+ run: gh release create "${{ github.ref_name }}" --title "${{ github.ref_name }}" --generate-notes
@@ -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 }}
data/README.md CHANGED
@@ -195,13 +195,39 @@ Check this couchbase help page to learn more on what's possible with compound ke
195
195
  Ex : Compound keys allows to decide the order of the results, and you can reverse it by passing `descending: true`
196
196
 
197
197
  ```ruby
198
- class Comment < CouchbaseOrm::Base19
198
+ class Comment < CouchbaseOrm::Base
199
199
  self.ignored_properties = [:old_name] # ignore old_name property in the model
200
200
  self.properties_always_exists_in_document = true # use is null for nil value instead of not valued for performance purpose, only possible if all properties always exists in document
201
201
  end
202
202
  ```
203
203
  You can specify `properties_always_exists_in_document` to true if all properties always exists in document, this will allow to use `is null` instead of `not valued` for nil value, this will improve performance.
204
204
 
205
+ ## Schema evolution
206
+
207
+ `ignored_properties` (above) drops a named, fixed list of legacy keys wherever a document is
208
+ decoded. For the more general case of a rolling/canary deploy - where a document written by a pod
209
+ running newer code can carry an attribute a pod still running older code hasn't declared yet -
210
+ there is `raise_on_unknown_attributes`:
211
+
212
+ ```ruby
213
+ class Comment < CouchbaseOrm::Base
214
+ self.raise_on_unknown_attributes = false # tolerate any undeclared document key instead of raising
215
+ end
216
+ ```
217
+
218
+ With the default (`true`), an undeclared key anywhere in `new`, `assign_attributes`, `find` or
219
+ `reload` raises `ActiveModel::UnknownAttributeError`, same as plain ActiveModel. Set it to `false`
220
+ and undeclared keys are dropped instead, with a warning logged the first time each one is seen on
221
+ a given class. Unlike `ignored_properties`, it is a `class_attribute`, so it is inherited by
222
+ subclasses and can be set once for a whole hierarchy (e.g. on your app's own base class) or
223
+ globally via `CouchbaseOrm::Document.raise_on_unknown_attributes = false`.
224
+
225
+ Because a dropped key was never assigned to an attribute, it is not written back out either: if
226
+ the model is saved afterwards, the key disappears from the stored document. If you need to
227
+ preserve an unknown key across a load-then-save round trip rather than only tolerate it on load,
228
+ `ignored_properties` won't help with that either - both mechanisms are read-side tolerance, not a
229
+ schemaless passthrough.
230
+
205
231
  WARNING: If a document exists without a property, the query will failed! So you must be sure that all documents have all properties.
206
232
 
207
233
 
data/RELEASE.md CHANGED
@@ -34,6 +34,10 @@
34
34
  git tag X.Y.Z
35
35
  git push origin X.Y.Z
36
36
 
37
+ Pushing the tag automatically triggers the `Release` GitHub Actions workflow, which creates the
38
+ corresponding GitHub Release (with auto-generated notes) once it confirms the tag's commit is on
39
+ `master`.
40
+
37
41
  3. Verify Clean State
38
42
 
39
43
  Ensure your local master branch is clean and up-to-date:
@@ -66,6 +70,8 @@
66
70
 
67
71
  Post-Release
68
72
 
73
+ - Verify the GitHub Release was created automatically at
74
+ https://github.com/Couchbase-Ecosystem/couchbase-ruby-orm/releases/tag/X.Y.Z
69
75
  - Announce the release if applicable (changelog, team communication, etc.)
70
76
  - Update any documentation that references the version number
71
77
 
@@ -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'
@@ -236,4 +236,43 @@ Validations are automatically run when saving a document. If any validations fai
236
236
 
237
237
  By leveraging validations, you can ensure the quality and consistency of your data before it is persisted to Couchbase Server.
238
238
 
239
+ ## 3.7. Handling Unknown Document Properties
240
+
241
+ By default, CouchbaseOrm requires every property found on a document to have a matching declared
242
+ attribute: loading a document with an undeclared key raises `ActiveModel::UnknownAttributeError`.
243
+ This is normally what you want, but it becomes a problem during a rolling or canary deploy: a pod
244
+ running newer code can write a document with an attribute that a pod still running older code
245
+ hasn't declared yet, and that older pod will raise the moment it tries to read the document back.
246
+
247
+ Set `raise_on_unknown_attributes` to `false` on a model to tolerate this instead:
248
+
249
+ ```ruby
250
+ class User < CouchbaseOrm::Base
251
+ self.raise_on_unknown_attributes = false
252
+
253
+ attribute :name, :string
254
+ end
255
+
256
+ User.find(id) # a document with an extra, undeclared key no longer raises;
257
+ # the key is simply dropped and a warning is logged.
258
+ ```
259
+
260
+ A few things worth knowing:
261
+
262
+ - It applies everywhere a document (or a hash) is turned into attributes: `new`, `assign_attributes`,
263
+ `find`, and `reload`.
264
+ - It is a `class_attribute`, so unlike `ignored_properties` it is **inherited** by subclasses, and it
265
+ can be set once for a whole hierarchy - for example on your application's own base class - rather
266
+ than repeated per model. You can also set a global default with
267
+ `CouchbaseOrm::Document.raise_on_unknown_attributes = false` from a Rails initializer.
268
+ - A key is considered "known" if the model responds to the corresponding writer, not just if it is a
269
+ declared `attribute` - so a `belongs_to`/`has_and_belongs_to_many` association key (e.g. `parent:`)
270
+ is never treated as unknown, even though its underlying attribute is actually named `parent_id`.
271
+ - A dropped key is not written back out: since it was never assigned to an attribute, saving the
272
+ model afterwards omits it from the stored document, the same trade-off `ignored_properties` makes.
273
+ - Unlike `ignored_properties`, which removes a fixed, named list of keys wherever a document is
274
+ decoded, `raise_on_unknown_attributes` tolerates *any* undeclared key. Use `ignored_properties`
275
+ when you know exactly which legacy keys you're phasing out; use `raise_on_unknown_attributes` when
276
+ you want a model to be structurally resilient to future attributes it doesn't know about yet.
277
+
239
278
  With the model definition covered, including attributes, callbacks, and validations, you're ready to start querying and persisting data using CouchbaseOrm. In the next section, we'll explore the querying capabilities of CouchbaseOrm and how to retrieve data from Couchbase Server efficiently.
@@ -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.
@@ -135,4 +135,23 @@ Nested documents provide a powerful way to model complex data structures and rel
135
135
 
136
136
  However, it's important to consider the trade-offs when using nested documents. Embedding too much data within a single document can lead to large document sizes and potential performance issues. It's recommended to use nested documents judiciously and to consider the access patterns and data relationships of your application.
137
137
 
138
+ ## 9.8. Unknown Properties on Nested Documents
139
+
140
+ `raise_on_unknown_attributes` (see [3.7](./03-defining-models.md#37-handling-unknown-document-properties))
141
+ applies to `CouchbaseOrm::NestedDocument` the same way it does to `CouchbaseOrm::Base`, but the
142
+ setting is per nested class - it is not inherited through composition from the parent document.
143
+ A tolerant parent embedding a strict nested class still raises if that nested document carries an
144
+ undeclared property, and vice versa:
145
+
146
+ ```ruby
147
+ class Part < CouchbaseOrm::NestedDocument
148
+ self.raise_on_unknown_attributes = false # only Part tolerates unknown properties
149
+ attribute :name, :string
150
+ end
151
+
152
+ class Car < CouchbaseOrm::Base
153
+ attribute :parts, :array, type: Part # Car's own setting (default: true) is unaffected
154
+ end
155
+ ```
156
+
138
157
  In the next section, we'll explore enums in CouchbaseOrm and how they can be used to define a fixed set of values for an attribute.
@@ -141,6 +141,17 @@ CouchbaseOrm handles the storage format for encrypted attributes but does not pe
141
141
  - All actual encryption/decryption is your application's responsibility
142
142
  - Values must be valid Base64-encoded strings
143
143
 
144
+ The `encrypted$` unwrapping in step 2 above happens for **any** key with that prefix, whether or
145
+ not it maps to a declared `:encrypted` attribute. This means that if you are retiring an encrypted
146
+ attribute with [`raise_on_unknown_attributes = false`](./03-defining-models.md#37-handling-unknown-document-properties)
147
+ instead of `ignored_properties`, a single rule covers both `legacy_field` and
148
+ `encrypted$legacy_field` in the underlying document - you don't need to list both spellings.
149
+ One case only `ignored_properties` can handle: if an `encrypted$`-prefixed key's value is not a
150
+ `{alg:, ciphertext:}`-shaped hash (e.g. it was left over from a different serialization format),
151
+ unwrapping raises before `raise_on_unknown_attributes` ever gets a chance to filter it - only
152
+ naming the exact key with `ignored_properties`, which strips it before unwrapping is attempted,
153
+ handles that case.
154
+
144
155
  ## 11.4. Considerations and Best Practices
145
156
 
146
157
  When using encrypted attributes in CouchbaseOrm, consider the following best practices:
@@ -46,3 +46,17 @@ D, [2024-05-24T11:48:00.113166 #234447] DEBUG -- : _update_record - replace user
46
46
  I, [2024-05-24T11:48:00.115239 #234447] INFO -- : User user-1-vncZNSYZj updated email to john.doe@example.com
47
47
  ```
48
48
 
49
+ ## 12.2. Unknown Attribute Warnings
50
+
51
+ When a model has [`raise_on_unknown_attributes`](./03-defining-models.md#37-handling-unknown-document-properties)
52
+ set to `false`, every `assign_attributes` call (from `new`, `find`, `reload`, ...) that drops an
53
+ undeclared key logs at `DEBUG` level. To avoid flooding your logs on high-traffic models, a `WARN`
54
+ is only emitted the first time a given (model class, property name) pair is seen in the process:
55
+
56
+ ```
57
+ D, [...] DEBUG -- : User: ignoring unknown properties ["legacy_field"]
58
+ W, [...] WARN -- : User: ignoring unknown document properties legacy_field (raise_on_unknown_attributes is false for this class - they will not be persisted if the document is saved)
59
+ ```
60
+
61
+ That warning tracking is capped so it cannot grow unbounded.
62
+
@@ -16,6 +16,8 @@ Here are some common issues you may encounter while using CouchbaseOrm:
16
16
 
17
17
  5. **Unexpected Behavior**: If you experience unexpected behavior or results, double-check your code logic, query conditions, and attribute assignments. Ensure that you are using the correct methods, parameters, and data types.
18
18
 
19
+ 6. **`ActiveModel::UnknownAttributeError` right after a deploy**: This typically happens during a rolling or canary deploy, when a document written by a pod running newer code carries an attribute a pod still running older code hasn't declared yet. See [Handling Unknown Document Properties](./03-defining-models.md#37-handling-unknown-document-properties) for `raise_on_unknown_attributes`, or `ignored_properties` if you're phasing out a specific, known set of legacy keys instead.
20
+
19
21
  ## 15.2. Debugging Tips
20
22
 
21
23
  When troubleshooting issues with CouchbaseOrm, consider the following debugging tips:
@@ -44,12 +44,6 @@ module CouchbaseOrm
44
44
  def type_for_attribute(attribute)
45
45
  attribute_types[attribute]
46
46
  end
47
-
48
- if ActiveModel::VERSION::MAJOR < 6
49
- def attribute_names
50
- attribute_types.keys
51
- end
52
- end
53
47
  end
54
48
 
55
49
  def slice(*methods)
@@ -68,25 +62,5 @@ module CouchbaseOrm
68
62
  value = send(attr_name)
69
63
  value.inspect
70
64
  end
71
-
72
- if ActiveModel::VERSION::MAJOR < 6
73
- def attribute_names
74
- self.class.attribute_names
75
- end
76
-
77
- def has_attribute?(attr_name)
78
- @attributes.key?(attr_name.to_s)
79
- end
80
-
81
- def attribute_present?(attribute)
82
- value = send(attribute)
83
- !value.nil? && !(value.respond_to?(:empty?) && value.empty?)
84
- end
85
-
86
- def _write_attribute(attr_name, value)
87
- @attributes.write_from_user(attr_name.to_s, value)
88
- value
89
- end
90
- end
91
65
  end
92
66
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true, encoding: ASCII-8BIT
2
2
 
3
3
 
4
+ require 'set'
4
5
  require 'active_model'
5
6
  require 'active_support/hash_with_indifferent_access'
6
7
  require 'couchbase'
@@ -26,6 +27,7 @@ require 'couchbase-orm/json_transcoder'
26
27
  require 'couchbase-orm/timestamps'
27
28
  require 'couchbase-orm/active_record_compat'
28
29
  require 'couchbase-orm/strict_loading'
30
+ require 'couchbase-orm/unknown_attributes'
29
31
  require 'couchbase-orm/json_schema/validation'
30
32
  require 'couchbase-orm/utilities/properties_always_exists_in_document'
31
33
 
@@ -43,6 +45,7 @@ module CouchbaseOrm
43
45
  include ActiveRecordCompat
44
46
  include StrictLoading
45
47
  include Encrypt
48
+ include UnknownAttributes
46
49
 
47
50
  extend Enum
48
51
  extend IgnoredProperties
@@ -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
@@ -157,6 +157,10 @@ module CouchbaseOrm
157
157
  hash = hash.with_indifferent_access if hash.is_a?(Hash)
158
158
  super(hash.except("type"))
159
159
  end
160
+ # ActiveModel::AttributeAssignment aliases attributes= to its own
161
+ # assign_attributes at include-time, so without this, `model.attributes = hash`
162
+ # would bypass the "type" stripping above.
163
+ alias_method :attributes=, :assign_attributes
160
164
 
161
165
  # Updates the attributes of the model from the passed-in hash and saves the
162
166
  # record. If the object is invalid, the saving will fail and false will be returned.
@@ -23,14 +23,8 @@ module CouchbaseOrm
23
23
  end
24
24
  end
25
25
 
26
- if RUBY_VERSION.to_i >= 3
27
- def method_missing(name, *args, **options, &block)
28
- @proxyfied.public_send(name, *args, **options, &block)
29
- end
30
- else
31
- def method_missing(name, *args, &block)
32
- @proxyfied.public_send(name, *args, &block)
33
- end
26
+ def method_missing(name, *args, **options, &block)
27
+ @proxyfied.public_send(name, *args, **options, &block)
34
28
  end
35
29
  end
36
30
  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
 
@@ -70,6 +71,10 @@ module CouchbaseOrm
70
71
  !!@strict_loading
71
72
  end
72
73
 
74
+ def with(opts = {})
75
+ CouchbaseOrm_Relation.new(**initializer_arguments.merge(query_options: @query_options.merge(opts)))
76
+ end
77
+
73
78
  def first
74
79
  n1ql_query, params = self.limit(1).to_n1ql_with_params
75
80
  result = @model.cluster.query(n1ql_query, build_query_options(positional_parameters: params))
@@ -168,7 +173,7 @@ module CouchbaseOrm
168
173
  end
169
174
 
170
175
  def initializer_arguments
171
- { 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 }
172
177
  end
173
178
 
174
179
  def merge_order(*lorder, **horder)
@@ -238,7 +243,7 @@ module CouchbaseOrm
238
243
  end
239
244
 
240
245
  def build_query_options(positional_parameters: [])
241
- opts = { scan_consistency: CouchbaseOrm::N1ql.config[:scan_consistency] }
246
+ opts = CouchbaseOrm::N1ql.config.merge(@query_options)
242
247
  opts[:positional_parameters] = positional_parameters unless positional_parameters.empty?
243
248
  Couchbase::Options::Query.new(**opts)
244
249
  end
@@ -261,7 +266,7 @@ module CouchbaseOrm
261
266
 
262
267
  delegate :ids, :update_all, :delete_all, :count, :empty?, :filter, :reduce, :find_by, to: :all
263
268
 
264
- 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
265
270
  end
266
271
  end
267
272
  end
@@ -5,13 +5,6 @@ require "couchbase-orm/types/array"
5
5
  require "couchbase-orm/types/nested"
6
6
  require "couchbase-orm/types/encrypted"
7
7
 
8
- if ActiveModel::VERSION::MAJOR < 6
9
- # In Rails 5, the type system cannot allow overriding the default types
10
- ActiveModel::Type.registry.instance_variable_get(:@registrations).delete_if do |k|
11
- k.matches?(:date) || k.matches?(:datetime) || k.matches?(:timestamp)
12
- end
13
- end
14
-
15
8
  ActiveModel::Type.register(:date, CouchbaseOrm::Types::Date)
16
9
  ActiveModel::Type.register(:datetime, CouchbaseOrm::Types::DateTime)
17
10
  ActiveModel::Type.register(:timestamp, CouchbaseOrm::Types::Timestamp)
@@ -0,0 +1,82 @@
1
+ require 'set'
2
+ require 'active_support/concern'
3
+
4
+ module CouchbaseOrm
5
+ # Controls what happens when a document (or a Hash/Parameters passed to
6
+ # `new`/`assign_attributes`) carries a key that has no corresponding
7
+ # attribute or writer on the model.
8
+ #
9
+ # By default (`raise_on_unknown_attributes = true`) this is unchanged
10
+ # ActiveModel behaviour: an `ActiveModel::UnknownAttributeError` is
11
+ # raised. Setting it to `false` on a model (it is inherited, like any
12
+ # `class_attribute`) makes unknown keys tolerated instead: they are
13
+ # dropped before assignment and a warning is logged.
14
+ #
15
+ # This exists to survive rolling/canary deploys: a document written by a
16
+ # pod running newer code can carry attributes a pod still running older
17
+ # code does not know about yet. Without this, reading that document on
18
+ # the old code raises and the request fails.
19
+ #
20
+ # A key is considered "known" using the same test ActiveModel itself uses
21
+ # to decide whether to raise (`respond_to?(:"#{key}=")`), not attribute
22
+ # membership - this matters because association writers (`belongs_to`,
23
+ # `has_and_belongs_to_many`) define a setter (e.g. `parent=`) for a name
24
+ # that is not itself a declared attribute (the attribute is `parent_id`).
25
+ # Filtering by attribute name would silently drop those.
26
+ #
27
+ # Unknown keys are only ever dropped from the in-memory assignment. If
28
+ # the model is saved afterwards, `serialized_attributes` only emits
29
+ # declared attributes, so the unknown key disappears from the stored
30
+ # document too - the same trade-off `ignored_properties` already makes.
31
+ module UnknownAttributes
32
+ extend ActiveSupport::Concern
33
+
34
+ # Caps the process-wide memory used to track which (class, key)
35
+ # warnings have already been emitted. Unknown keys come from
36
+ # document content, i.e. are not under the application's control,
37
+ # so this must be bounded. Once the cap is hit, unknown attributes
38
+ # are still filtered (and still logged at debug level) - only the
39
+ # warn-once escalation stops.
40
+ MAX_WARNED = 1_000
41
+
42
+ @warned = Set.new
43
+
44
+ included do
45
+ class_attribute :raise_on_unknown_attributes,
46
+ instance_accessor: false, instance_predicate: false, default: true
47
+ end
48
+
49
+ # Filters out unknown keys before assignment when
50
+ # `raise_on_unknown_attributes` is false; otherwise unchanged.
51
+ def assign_attributes(attributes)
52
+ return super if self.class.raise_on_unknown_attributes
53
+ # Garbage input (nil, an Integer, a bare Object...) must still get
54
+ # ActiveModel's own "you must pass a hash" ArgumentError from
55
+ # `super`, not a NoMethodError from calling each_pair on it below.
56
+ return super unless attributes.respond_to?(:each_pair)
57
+
58
+ unknown = attributes.each_pair.filter_map { |key, _| key unless respond_to?(:"#{key}=") }
59
+ return super if unknown.empty?
60
+
61
+ UnknownAttributes.report(self.class, unknown)
62
+ super(attributes.except(*unknown))
63
+ end
64
+ alias_method :attributes=, :assign_attributes
65
+
66
+ class << self
67
+ # @api private
68
+ def report(klass, keys)
69
+ CouchbaseOrm.logger.debug { "#{klass.name}: ignoring unknown properties #{keys.inspect}" }
70
+
71
+ newly_warned = @warned.size >= MAX_WARNED ? [] : keys.select { |key| @warned.add?("#{klass.name}##{key}") }
72
+ return if newly_warned.empty?
73
+
74
+ CouchbaseOrm.logger.warn(
75
+ "#{klass.name}: ignoring unknown document properties #{newly_warned.join(', ')} " \
76
+ "(raise_on_unknown_attributes is false for this class - " \
77
+ "they will not be persisted if the document is saved)"
78
+ )
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true, encoding: ASCII-8BIT
2
2
 
3
3
  module CouchbaseOrm
4
- VERSION = '3.1.0'
4
+ VERSION = '3.3.0'
5
5
  end