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 +4 -4
- data/.github/workflows/release.yml +28 -0
- data/.github/workflows/test.yml +4 -12
- data/README.md +27 -1
- data/RELEASE.md +6 -0
- data/couchbase-orm.gemspec +1 -1
- data/docusaurus/docs/tutorial-ruby-couchbase-orm/03-defining-models.md +39 -0
- data/docusaurus/docs/tutorial-ruby-couchbase-orm/07-sqlpp-queries.md +41 -0
- data/docusaurus/docs/tutorial-ruby-couchbase-orm/09-nested-documents.md +19 -0
- data/docusaurus/docs/tutorial-ruby-couchbase-orm/11-encryption.md +11 -0
- data/docusaurus/docs/tutorial-ruby-couchbase-orm/12-logging.md +14 -0
- data/docusaurus/docs/tutorial-ruby-couchbase-orm/13-troubleshooting.md +2 -0
- data/lib/couchbase-orm/active_record_compat.rb +0 -26
- data/lib/couchbase-orm/base.rb +3 -0
- data/lib/couchbase-orm/n1ql.rb +9 -4
- data/lib/couchbase-orm/persistence.rb +4 -0
- data/lib/couchbase-orm/proxies/bucket_proxy.rb +2 -8
- data/lib/couchbase-orm/relation.rb +9 -4
- data/lib/couchbase-orm/types.rb +0 -7
- data/lib/couchbase-orm/unknown_attributes.rb +82 -0
- data/lib/couchbase-orm/version.rb +1 -1
- data/spec/base_spec.rb +137 -0
- data/spec/couchbase-orm/active_record_compat_spec.rb +73 -0
- data/spec/couchbase-orm/changeable_spec.rb +37 -0
- data/spec/n1ql_spec.rb +19 -0
- data/spec/proxies/bucket_proxy_spec.rb +53 -0
- data/spec/relation_spec.rb +33 -0
- data/spec/type_encrypted_spec.rb +20 -0
- data/spec/type_nested_spec.rb +92 -0
- data/spec/unknown_attributes_spec.rb +227 -0
- data/spec/utilities/ignored_properties_spec.rb +17 -0
- metadata +9 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 90eb0b8fb472134579071ce58442b518bd42009d3647363116f041eb65b13a85
|
|
4
|
+
data.tar.gz: 066cad6c6ee614382262d035d8d86e55e8d829f42733d21daddd9a91deb03786
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
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/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::
|
|
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
|
|
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'
|
|
@@ -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
|
data/lib/couchbase-orm/base.rb
CHANGED
|
@@ -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
|
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
|
|
@@ -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
|
-
|
|
27
|
-
|
|
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 =
|
|
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
|
data/lib/couchbase-orm/types.rb
CHANGED
|
@@ -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
|