ndb-rails 0.1.1 → 0.1.2
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/README.md +14 -5
- data/lib/ndb/rails/errors.rb +6 -3
- data/lib/ndb/rails/persistence.rb +59 -4
- data/lib/ndb/rails/record.rb +6 -5
- data/lib/ndb/rails/relation.rb +14 -0
- data/lib/ndb/rails/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cb272d6aadee253c818c30de3c3820c510903f06d42dac23dd56837bb585280a
|
|
4
|
+
data.tar.gz: 4ccfbb876e868ebea9b229a5ef44292ad3240ed55933a46e3a5a432158f25012
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c24be778d6fc65e219b829d7c8219e810ecc3505a39c0670077e07a17c60a37824d5814a6869dd6926fa52eed09a85b6615622779be126e8c87e582ece20a0d
|
|
7
|
+
data.tar.gz: 6360ba54f2208b4b438f269cfbbfd172fe0ebf9fd6907df523369d5241f88eec5a36ca617504dbde0553624fff00156b09c7cb892b05790d34e00cc4dde51f55
|
data/README.md
CHANGED
|
@@ -112,13 +112,21 @@ call = Call.find("c1") # raises Ndb::NotFound
|
|
|
112
112
|
Call.find_by(status: "failed") # nil when missing
|
|
113
113
|
Call.find_by!(status: "x") # raises NotFound
|
|
114
114
|
|
|
115
|
-
call.update!(status: "done") #
|
|
115
|
+
call.update!(status: "done") # $set of the changed fields only, filtered by id and version
|
|
116
116
|
call.version # the document version
|
|
117
117
|
|
|
118
|
+
call.increment!(:duration) # atomic $inc on the daemon, refreshes the attribute and the version
|
|
119
|
+
call.decrement!(:duration, 5)
|
|
120
|
+
|
|
118
121
|
call.destroy
|
|
122
|
+
|
|
123
|
+
# Atomic writes on every match of a where (it needs the id or an indexed field).
|
|
124
|
+
# update takes the operators of the client and returns the number of modified documents.
|
|
125
|
+
Call.where(status: "failed").update({ "$set" => { "status" => "retry" } })
|
|
126
|
+
Call.where(status: "retry").delete # returns how many it deleted
|
|
119
127
|
```
|
|
120
128
|
|
|
121
|
-
- Every load asks for the version (`_v`). `save`, `update` and `destroy`
|
|
129
|
+
- Every load asks for the version (`_v`). `save`, `update` and `destroy` put the id and `_v` in the filter of the write. If another writer changed the document first, the filter matches nothing and you get `Ndb::StaleObjectError` (code `stale_object`).
|
|
122
130
|
- Setting an attribute to `nil` and saving removes the field from the document.
|
|
123
131
|
- A duplicate id raises `RecordNotUnique` (code `duplicate_id`).
|
|
124
132
|
- `persisted?`, `new_record?`, `destroyed?`, `id` and `version` work as in ActiveRecord.
|
|
@@ -187,7 +195,7 @@ Call.where(status: "answered").first(5)
|
|
|
187
195
|
Call.where(status: "failed").exists?
|
|
188
196
|
```
|
|
189
197
|
|
|
190
|
-
- Chained `where` calls are ANDed.
|
|
198
|
+
- Chained `where` calls are ANDed. See the [query language](https://github.com/thadeu/ndb/blob/main/docs/dev/query.md) for the full operator list.
|
|
191
199
|
- The order of the `order` keys is the order of the sort. The id is always the last key.
|
|
192
200
|
- `page(n).per(size)` sets offset paging. The default page size is 25.
|
|
193
201
|
- `pluck` asks the daemon for only those fields.
|
|
@@ -321,7 +329,7 @@ A blob behaves like a read-only IO (`read`, `each_chunk`, `eof?`, `rewind`). A l
|
|
|
321
329
|
|
|
322
330
|
## Errors
|
|
323
331
|
|
|
324
|
-
The transport and daemon errors are defined by the gem `ndb`. They all live in the `Ndb` module (`Ndb::Error`, `Ndb::NotFound`, ...). For an ActiveRecord-like `rescue`, this gem adds
|
|
332
|
+
The transport and daemon errors are defined by the gem `ndb`. They all live in the `Ndb` module (`Ndb::Error`, `Ndb::NotFound`, ...). For an ActiveRecord-like `rescue`, this gem adds one alias: `Ndb::RecordNotUnique` is `Ndb::UniqueViolation` (the same class). `Ndb::StaleObjectError`, `Ndb::RecordInvalid` and `Ndb::RecordNotSaved` exist only here, and they inherit from `Ndb::Error`. Every error has `code` (the daemon code) and `status` (the HTTP status).
|
|
325
333
|
|
|
326
334
|
| Class | Code |
|
|
327
335
|
|---|---|
|
|
@@ -329,7 +337,8 @@ The transport and daemon errors are defined by the gem `ndb`. They all live in t
|
|
|
329
337
|
| `NotReady` | `not_ready` |
|
|
330
338
|
| `NotFound` | `not_found` |
|
|
331
339
|
| `UnknownCollection` | `unknown_collection` |
|
|
332
|
-
| `StaleObjectError` | `
|
|
340
|
+
| `StaleObjectError` | `stale_object` (raised by the gem, not sent by the daemon) |
|
|
341
|
+
| `InvalidUpdate` and its subclasses | `unknown_operator`, `no_update`, `conflicting_update`, `invalid_field`, `unindexed_many`, `empty_filter` |
|
|
333
342
|
| `RecordNotUnique` | `unique_violation`, `duplicate_id` |
|
|
334
343
|
| `CatalogConflict` | `catalog_conflict` |
|
|
335
344
|
| `CatalogInvalid` | `catalog_invalid` |
|
data/lib/ndb/rails/errors.rb
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Ndb
|
|
4
|
-
# ActiveRecord-like
|
|
5
|
-
#
|
|
6
|
-
StaleObjectError = VersionConflict
|
|
4
|
+
# ActiveRecord-like name for a daemon error. It is the same class, so a
|
|
5
|
+
# rescue of either name catches the same error.
|
|
7
6
|
RecordNotUnique = UniqueViolation
|
|
8
7
|
|
|
8
|
+
# A save or destroy matched nothing: the record changed or was deleted
|
|
9
|
+
# since it was loaded (its "_v" guard did not match).
|
|
10
|
+
class StaleObjectError < Error; end
|
|
11
|
+
|
|
9
12
|
# Raised by save!/create!/update! when validations fail.
|
|
10
13
|
class RecordInvalid < Error
|
|
11
14
|
attr_reader :record
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
module Ndb
|
|
4
4
|
class RecordNotSaved < Error; end
|
|
5
5
|
|
|
6
|
-
# Writes: dirty attributes -> set
|
|
6
|
+
# Writes: dirty attributes -> $set/$unset, filtered by the id and the loaded
|
|
7
|
+
# version ("_v"). A write that matches nothing is a stale record.
|
|
7
8
|
module Persistence
|
|
8
9
|
extend ActiveSupport::Concern
|
|
9
10
|
|
|
@@ -130,13 +131,24 @@ module Ndb
|
|
|
130
131
|
|
|
131
132
|
def destroy
|
|
132
133
|
result = run_callbacks(:destroy) do
|
|
133
|
-
|
|
134
|
+
delete_record if persisted?
|
|
134
135
|
@destroyed = true
|
|
135
136
|
end
|
|
136
137
|
|
|
137
138
|
result ? self : false
|
|
138
139
|
end
|
|
139
140
|
|
|
141
|
+
# Adds by (default 1) to a number attribute in one atomic write on the
|
|
142
|
+
# daemon, and refreshes the attribute and the version. Other unsaved
|
|
143
|
+
# changes stay unsaved.
|
|
144
|
+
def increment!(attribute, by = 1)
|
|
145
|
+
add_to!(attribute, by)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def decrement!(attribute, by = 1)
|
|
149
|
+
add_to!(attribute, -by)
|
|
150
|
+
end
|
|
151
|
+
|
|
140
152
|
def ==(other)
|
|
141
153
|
other.instance_of?(self.class) && !id.nil? && id == other.id
|
|
142
154
|
end
|
|
@@ -160,6 +172,38 @@ module Ndb
|
|
|
160
172
|
self
|
|
161
173
|
end
|
|
162
174
|
|
|
175
|
+
def record_filter
|
|
176
|
+
filter = { self.class.primary_key => id }
|
|
177
|
+
|
|
178
|
+
filter["_v"] = version if version
|
|
179
|
+
|
|
180
|
+
filter
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def delete_record
|
|
184
|
+
deleted = self.class.ndb_delete(record_filter)["deleted"]
|
|
185
|
+
|
|
186
|
+
raise StaleObjectError.new("#{self.class.name} #{id} changed or was deleted", code: "stale_object") if deleted.zero?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def add_to!(attribute, by)
|
|
190
|
+
name = attribute.to_s
|
|
191
|
+
|
|
192
|
+
raise RecordNotSaved, "Cannot update a record that is not saved" unless persisted?
|
|
193
|
+
raise ArgumentError, "#{name} is not an attribute of #{self.class.name}" unless self.class.attribute_names.include?(name)
|
|
194
|
+
|
|
195
|
+
result = self.class.ndb_update({ self.class.primary_key => id }, { "$inc" => { name => by } })
|
|
196
|
+
|
|
197
|
+
raise NotFound.new("#{self.class.name} #{id} was deleted", code: "not_found") if result["matched"].zero?
|
|
198
|
+
|
|
199
|
+
@version = result["doc"]["_v"]
|
|
200
|
+
|
|
201
|
+
public_send("#{name}=", result["doc"][name])
|
|
202
|
+
clear_attribute_changes([name])
|
|
203
|
+
|
|
204
|
+
self
|
|
205
|
+
end
|
|
206
|
+
|
|
163
207
|
def loaded!(version)
|
|
164
208
|
@persisted = true
|
|
165
209
|
@version = version
|
|
@@ -183,9 +227,11 @@ module Ndb
|
|
|
183
227
|
set, unset = changed_fields
|
|
184
228
|
|
|
185
229
|
unless set.empty? && unset.empty? && @expire_in.nil?
|
|
186
|
-
result = self.class.ndb_update(
|
|
230
|
+
result = self.class.ndb_update(record_filter, update_ops(set, unset), expire_in: @expire_in)
|
|
231
|
+
|
|
232
|
+
raise StaleObjectError.new("#{self.class.name} #{id} changed or was deleted", code: "stale_object") if result["matched"].zero?
|
|
187
233
|
|
|
188
|
-
@version = result["
|
|
234
|
+
@version = result["doc"]["_v"]
|
|
189
235
|
end
|
|
190
236
|
|
|
191
237
|
changes_applied
|
|
@@ -194,6 +240,15 @@ module Ndb
|
|
|
194
240
|
end
|
|
195
241
|
end
|
|
196
242
|
|
|
243
|
+
def update_ops(set, unset)
|
|
244
|
+
ops = {}
|
|
245
|
+
|
|
246
|
+
ops["$set"] = set unless set.empty?
|
|
247
|
+
ops["$unset"] = unset.to_h { |name| [name, true] } unless unset.empty?
|
|
248
|
+
|
|
249
|
+
ops.empty? ? { "$set" => {} } : ops
|
|
250
|
+
end
|
|
251
|
+
|
|
197
252
|
def insert_document
|
|
198
253
|
attribute_names.each_with_object({}) do |name, doc|
|
|
199
254
|
value = json_attribute(name)
|
data/lib/ndb/rails/record.rb
CHANGED
|
@@ -124,7 +124,7 @@ module Ndb
|
|
|
124
124
|
end
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
-
# Every load asks for "_v", so a loaded record can
|
|
127
|
+
# Every load asks for "_v", so a loaded record can guard its write with it.
|
|
128
128
|
def ndb_find(body)
|
|
129
129
|
ensure_collection!
|
|
130
130
|
|
|
@@ -143,16 +143,17 @@ module Ndb
|
|
|
143
143
|
Rails.client.insert(collection_name, [doc], expire_in: expire_in).first
|
|
144
144
|
end
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
# Atomic update by filter, see Ndb::Client#update.
|
|
147
|
+
def ndb_update(filter, ops, **options)
|
|
147
148
|
ensure_collection!
|
|
148
149
|
|
|
149
|
-
Rails.client.update(collection_name,
|
|
150
|
+
Rails.client.update(collection_name, filter, ops, **options)
|
|
150
151
|
end
|
|
151
152
|
|
|
152
|
-
def ndb_delete(
|
|
153
|
+
def ndb_delete(filter, **options)
|
|
153
154
|
ensure_collection!
|
|
154
155
|
|
|
155
|
-
Rails.client.delete(collection_name,
|
|
156
|
+
Rails.client.delete(collection_name, filter, **options)
|
|
156
157
|
end
|
|
157
158
|
|
|
158
159
|
private
|
data/lib/ndb/rails/relation.rb
CHANGED
|
@@ -78,6 +78,20 @@ module Ndb
|
|
|
78
78
|
query
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
+
# Atomic writes. The daemon matches the where and writes in one step on
|
|
82
|
+
# every match, so the where needs the id or an indexed field
|
|
83
|
+
# (Ndb::UnindexedMany otherwise). ops is the operator Hash of
|
|
84
|
+
# Ndb::Client#update ({ "$inc" => { "n" => 1 } }); returns the number of
|
|
85
|
+
# modified documents.
|
|
86
|
+
def update(ops, expire_in: nil)
|
|
87
|
+
klass.ndb_update(Conditions.combine(values[:where]), ops, many: true, expire_in: expire_in).fetch("modified")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Deletes every match (same rule as update) and returns how many.
|
|
91
|
+
def delete
|
|
92
|
+
klass.ndb_delete(Conditions.combine(values[:where]), many: true).fetch("deleted")
|
|
93
|
+
end
|
|
94
|
+
|
|
81
95
|
# Loaders
|
|
82
96
|
|
|
83
97
|
def load
|
data/lib/ndb/rails/version.rb
CHANGED