ndb 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 +18 -18
- data/lib/ndb/client.rb +45 -13
- data/lib/ndb/errors.rb +16 -3
- data/lib/ndb/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: b60b364a32410e0d6c1356e427e4844bba9b31a65dcd0d5afb4fcb9acbafa3aa
|
|
4
|
+
data.tar.gz: e2a3603636b2ab7bcf922c9c0d759a98d871dc292bc9a2299ef3d284c6faaf26
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e26d00115fc7530e4f766437f1019202a644ada9a4a0127f96dbd23efcf9e83d6afefda557f2ad63f892be159d8a7c38bd18112bc75744fb740f6ceeddd9edf5
|
|
7
|
+
data.tar.gz: 45dde6bb1ceb82b3a381f2047f2bf09e33d1d8182214422537ee2a6dc97a0bdb01463e7f2eadeb4371007863f71a1566653081e53fb9db49620c1c77e9a4565c
|
data/README.md
CHANGED
|
@@ -80,26 +80,26 @@ results = ndb.insert("calls", [
|
|
|
80
80
|
])
|
|
81
81
|
results # => [{"id" => "c1", "v" => 1}, {"id" => "c2", "v" => 1}]
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
ndb.update("calls", "c1", set
|
|
83
|
+
# Atomic update by filter: the filter, then the write operators.
|
|
84
|
+
ndb.update("calls", { call_id: "c1" }, { "$set" => { status: "done" }, "$unset" => { caller: true } })
|
|
85
|
+
# => {"matched" => 1, "modified" => 1, "doc" => {"call_id" => "c1", ..., "_v" => 2}}
|
|
85
86
|
|
|
86
|
-
ndb.
|
|
87
|
-
ndb.
|
|
88
|
-
```
|
|
87
|
+
ndb.update("calls", { call_id: "c1", _v: 2 }, { "$set" => { duration: 50 } }) # only if the version is still 2
|
|
88
|
+
ndb.update("stock", { sku: "a", qty: { "$gte" => 1 } }, { "$inc" => { qty: -1 } }) # guarded decrement
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
- A missing document raises `Ndb::NotFound`.
|
|
90
|
+
ndb.update("calls", { status: "failed" }, { "$set" => { status: "retry" } }, many: true) # => {"matched" => 3, "modified" => 3}
|
|
91
|
+
ndb.update("counters", { name: "hits" }, { "$inc" => { n: 1 } }, upsert: true)
|
|
93
92
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
ndb.update("calls", "c1", set: { status: "done" }, if_version: version)
|
|
97
|
-
rescue Ndb::VersionConflict
|
|
98
|
-
version = ndb.find("calls", where: { call_id: "c1" }, versions: true).rows.first["_v"]
|
|
99
|
-
retry
|
|
100
|
-
end
|
|
93
|
+
ndb.delete("calls", { call_id: "c2" }) # => {"deleted" => 1}
|
|
94
|
+
ndb.delete("calls", { status: "failed" }, many: true)
|
|
101
95
|
```
|
|
102
96
|
|
|
97
|
+
- Operators: `$set`, `$unset`, `$inc`, `$push` (with `{ "$each" => [...] }`), `$pull`, `$min`, `$max`. Field names can be dot paths. A key of the operators without `$` raises `ArgumentError` before anything is sent.
|
|
98
|
+
- Nothing matched is not an error: `matched` is 0. A stale `_v` is a filter that matches nothing, so read again and retry. `modified` is 0 when the operators leave the document as it is.
|
|
99
|
+
- `many: true` needs the id or an indexed field in the filter (`Ndb::UnindexedMany`). `upsert: true` creates the document from the equality fields of the filter. The answer has `doc` only for one document.
|
|
100
|
+
- The contract errors are `Ndb::UnknownOperator`, `NoUpdate`, `ConflictingUpdate`, `InvalidField`, `UnindexedMany` and `EmptyFilter`, all under `Ndb::InvalidUpdate`.
|
|
101
|
+
- A duplicate id raises `Ndb::UniqueViolation` (code `duplicate_id`). A broken unique field raises it too (code `unique_violation`).
|
|
102
|
+
|
|
103
103
|
## Expiry (TTL like Redis)
|
|
104
104
|
|
|
105
105
|
The collection needs `expires_field`.
|
|
@@ -108,10 +108,10 @@ The collection needs `expires_field`.
|
|
|
108
108
|
ndb.ensure_collection("sessions", { id: "sid", expires_field: "exp" })
|
|
109
109
|
|
|
110
110
|
ndb.insert("sessions", [{ sid: "s1" }], expire_in: 3600) # seconds, or "1h" / "30m" / "1d"
|
|
111
|
-
ndb.update("sessions", "s1", set
|
|
111
|
+
ndb.update("sessions", { sid: "s1" }, { "$set" => { user: "u" } }, expire_in: "30m")
|
|
112
112
|
|
|
113
113
|
ndb.expire("sessions", "s1", in: 1800) # => {"id", "v", "expires_at"}
|
|
114
|
-
ndb.expire("sessions", "s1", at: Time.now + 7200
|
|
114
|
+
ndb.expire("sessions", "s1", at: Time.now + 7200)
|
|
115
115
|
ndb.ttl("sessions", "s1") # => 7199, -1 (never expires), or nil (missing or expired)
|
|
116
116
|
ndb.persist("sessions", "s1") # => {"id", "v"}
|
|
117
117
|
```
|
|
@@ -275,7 +275,7 @@ All errors inherit from `Ndb::Error`, which has `code` (the daemon code) and `st
|
|
|
275
275
|
| `NotReady` | `not_ready` |
|
|
276
276
|
| `NotFound` | `not_found` |
|
|
277
277
|
| `UnknownCollection` | `unknown_collection` |
|
|
278
|
-
| `
|
|
278
|
+
| `InvalidUpdate` and its subclasses | `unknown_operator`, `no_update`, `conflicting_update`, `invalid_field`, `unindexed_many`, `empty_filter` |
|
|
279
279
|
| `UniqueViolation` | `unique_violation`, `duplicate_id` |
|
|
280
280
|
| `CatalogConflict` | `catalog_conflict` |
|
|
281
281
|
| `CatalogInvalid` | `catalog_invalid` |
|
data/lib/ndb/client.rb
CHANGED
|
@@ -50,19 +50,30 @@ module Ndb
|
|
|
50
50
|
json(post(coll, "insert", body)).fetch("results")
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
# Atomic update by filter. The filter and the write operators go in one
|
|
54
|
+
# body; the daemon matches and writes in one step on the newest state, so
|
|
55
|
+
# concurrent updates never interleave.
|
|
56
|
+
#
|
|
57
|
+
# client.update("stock", { sku: "a", qty: { "$gte" => 1 } }, { "$inc" => { qty: -1 } })
|
|
58
|
+
# # => {"matched" => 1, "modified" => 1, "doc" => {...with "_v"}}
|
|
59
|
+
#
|
|
60
|
+
# Operators: $set, $unset, $inc, $push, $pull, $min, $max (dot paths work).
|
|
61
|
+
# Nothing matched is not an error: {"matched" => 0, "modified" => 0}. A
|
|
62
|
+
# version guard is a filter: { id: "x", _v: 7 }. many: true updates every
|
|
63
|
+
# match (no "doc" in the answer) and needs the id or an indexed field in
|
|
64
|
+
# the filter. upsert: true creates the document from the equality fields
|
|
65
|
+
# of the filter when nothing matches. expire_in (seconds, or "30m") sets
|
|
66
|
+
# the expires_field.
|
|
67
|
+
def update(coll, filter, ops, many: false, upsert: false, expire_in: nil)
|
|
68
|
+
query = write_query(many: many, upsert: upsert, expire_in: expire_in)
|
|
69
|
+
|
|
70
|
+
json(post(coll, "update#{query}", merge_update(filter, ops)))
|
|
59
71
|
end
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
json(post(coll, "delete", delete: change))
|
|
73
|
+
# Deletes the documents that the filter matches (one, or every match with
|
|
74
|
+
# many: true, which needs the id or an indexed field). {"deleted" => n}
|
|
75
|
+
def delete(coll, filter, many: false)
|
|
76
|
+
json(post(coll, "delete#{write_query(many: many)}", filter))
|
|
66
77
|
end
|
|
67
78
|
|
|
68
79
|
# Makes a document expire, like EXPIRE in Redis: in: seconds or "30m", or
|
|
@@ -70,11 +81,10 @@ module Ndb
|
|
|
70
81
|
# Raises NotFound for a missing or expired document, TtlNotEnabled when
|
|
71
82
|
# the collection has no expires_field, TtlDerived when the expiry comes
|
|
72
83
|
# from ttl + time.
|
|
73
|
-
def expire(coll, id,
|
|
84
|
+
def expire(coll, id, **time)
|
|
74
85
|
body = { id: id }
|
|
75
86
|
body[:in] = time[:in] if time[:in]
|
|
76
87
|
body[:at] = time[:at].respond_to?(:utc) ? time[:at].utc.iso8601(9) : time[:at] if time[:at]
|
|
77
|
-
body[:if_version] = if_version if if_version
|
|
78
88
|
|
|
79
89
|
json(post(coll, "expire", body))
|
|
80
90
|
end
|
|
@@ -143,6 +153,28 @@ module Ndb
|
|
|
143
153
|
value.to_s.strip.empty? ? nil : value
|
|
144
154
|
end
|
|
145
155
|
|
|
156
|
+
def write_query(many: false, upsert: false, expire_in: nil)
|
|
157
|
+
params = {}
|
|
158
|
+
|
|
159
|
+
params["many"] = "true" if many
|
|
160
|
+
params["upsert"] = "true" if upsert
|
|
161
|
+
params["expire_in"] = expire_in.to_s if expire_in
|
|
162
|
+
|
|
163
|
+
params.empty? ? "" : "?#{URI.encode_www_form(params)}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# The filter and the operators in one body. An operator key without "$"
|
|
167
|
+
# would be read as a filter key by the daemon, so it is refused here.
|
|
168
|
+
def merge_update(filter, ops)
|
|
169
|
+
ops.each_key do |key|
|
|
170
|
+
next if key.to_s.start_with?("$")
|
|
171
|
+
|
|
172
|
+
raise ArgumentError, "update operator #{key.to_s.inspect} must start with $ (for example \"$set\")"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
filter.to_h.transform_keys(&:to_s).merge(ops.to_h.transform_keys(&:to_s))
|
|
176
|
+
end
|
|
177
|
+
|
|
146
178
|
def post(coll, verb, body)
|
|
147
179
|
request("POST", "#{path(coll)}/#{verb}", body)
|
|
148
180
|
end
|
data/lib/ndb/errors.rb
CHANGED
|
@@ -25,7 +25,6 @@ module Ndb
|
|
|
25
25
|
class Unauthorized < Error; end
|
|
26
26
|
class NotFound < Error; end
|
|
27
27
|
class UnknownCollection < Error; end
|
|
28
|
-
class VersionConflict < Error; end
|
|
29
28
|
class UniqueViolation < Error; end
|
|
30
29
|
class CatalogConflict < Error; end
|
|
31
30
|
class CatalogInvalid < Error; end
|
|
@@ -33,6 +32,15 @@ module Ndb
|
|
|
33
32
|
class InvalidQuery < Error; end
|
|
34
33
|
class InvalidCursor < Error; end
|
|
35
34
|
|
|
35
|
+
# The update or delete body breaks the contract (see Client#update).
|
|
36
|
+
class InvalidUpdate < Error; end
|
|
37
|
+
class UnknownOperator < InvalidUpdate; end
|
|
38
|
+
class NoUpdate < InvalidUpdate; end
|
|
39
|
+
class ConflictingUpdate < InvalidUpdate; end
|
|
40
|
+
class InvalidField < InvalidUpdate; end
|
|
41
|
+
class UnindexedMany < InvalidUpdate; end
|
|
42
|
+
class EmptyFilter < InvalidUpdate; end
|
|
43
|
+
|
|
36
44
|
# The collection has no expires_field, or its expiry comes from ttl + time.
|
|
37
45
|
class TtlNotEnabled < Error; end
|
|
38
46
|
class TtlDerived < Error; end
|
|
@@ -50,7 +58,6 @@ module Ndb
|
|
|
50
58
|
"not_ready" => NotReady,
|
|
51
59
|
"not_found" => NotFound,
|
|
52
60
|
"unknown_collection" => UnknownCollection,
|
|
53
|
-
"version_conflict" => VersionConflict,
|
|
54
61
|
"unique_violation" => UniqueViolation,
|
|
55
62
|
"duplicate_id" => UniqueViolation,
|
|
56
63
|
"catalog_conflict" => CatalogConflict,
|
|
@@ -64,7 +71,13 @@ module Ndb
|
|
|
64
71
|
"watch_lagged" => WatchLagged,
|
|
65
72
|
"history_truncated" => HistoryTruncated,
|
|
66
73
|
"ttl_not_enabled" => TtlNotEnabled,
|
|
67
|
-
"ttl_derived" => TtlDerived
|
|
74
|
+
"ttl_derived" => TtlDerived,
|
|
75
|
+
"unknown_operator" => UnknownOperator,
|
|
76
|
+
"no_update" => NoUpdate,
|
|
77
|
+
"conflicting_update" => ConflictingUpdate,
|
|
78
|
+
"invalid_field" => InvalidField,
|
|
79
|
+
"unindexed_many" => UnindexedMany,
|
|
80
|
+
"empty_filter" => EmptyFilter
|
|
68
81
|
}.freeze
|
|
69
82
|
|
|
70
83
|
def self.error_for(code, message, status)
|
data/lib/ndb/version.rb
CHANGED