ndb 0.1.0 → 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 +34 -13
- data/lib/ndb/client.rb +76 -10
- data/lib/ndb/errors.rb +22 -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,46 @@ 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.
|
|
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
|
+
|
|
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)
|
|
92
|
+
|
|
93
|
+
ndb.delete("calls", { call_id: "c2" }) # => {"deleted" => 1}
|
|
94
|
+
ndb.delete("calls", { status: "failed" }, many: true)
|
|
88
95
|
```
|
|
89
96
|
|
|
90
|
-
-
|
|
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`.
|
|
91
101
|
- A duplicate id raises `Ndb::UniqueViolation` (code `duplicate_id`). A broken unique field raises it too (code `unique_violation`).
|
|
92
|
-
|
|
102
|
+
|
|
103
|
+
## Expiry (TTL like Redis)
|
|
104
|
+
|
|
105
|
+
The collection needs `expires_field`.
|
|
93
106
|
|
|
94
107
|
```ruby
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
ndb.ensure_collection("sessions", { id: "sid", expires_field: "exp" })
|
|
109
|
+
|
|
110
|
+
ndb.insert("sessions", [{ sid: "s1" }], expire_in: 3600) # seconds, or "1h" / "30m" / "1d"
|
|
111
|
+
ndb.update("sessions", { sid: "s1" }, { "$set" => { user: "u" } }, expire_in: "30m")
|
|
112
|
+
|
|
113
|
+
ndb.expire("sessions", "s1", in: 1800) # => {"id", "v", "expires_at"}
|
|
114
|
+
ndb.expire("sessions", "s1", at: Time.now + 7200)
|
|
115
|
+
ndb.ttl("sessions", "s1") # => 7199, -1 (never expires), or nil (missing or expired)
|
|
116
|
+
ndb.persist("sessions", "s1") # => {"id", "v"}
|
|
101
117
|
```
|
|
102
118
|
|
|
119
|
+
- `expire_in` wins over a value of `expires_field` in the same call. `ttl` rounds up to whole seconds.
|
|
120
|
+
- `expire` and `persist` raise `Ndb::NotFound` for a missing or expired document. `Ndb::TtlNotEnabled` means no `expires_field`; `Ndb::TtlDerived` means the expiry comes from `ttl` + `time`.
|
|
121
|
+
- Disk space of expired documents returns after compaction, and fully only with `wal_retention`; see [Freeing disk](https://github.com/thadeu/ndb/blob/main/docs/dev/configuration.md#freeing-disk).
|
|
122
|
+
|
|
103
123
|
## Find
|
|
104
124
|
|
|
105
125
|
`find` returns a `Ndb::Page` with `rows` (an array of Hash), `next_cursor`, `plan`, `examined` and `total`.
|
|
@@ -255,7 +275,7 @@ All errors inherit from `Ndb::Error`, which has `code` (the daemon code) and `st
|
|
|
255
275
|
| `NotReady` | `not_ready` |
|
|
256
276
|
| `NotFound` | `not_found` |
|
|
257
277
|
| `UnknownCollection` | `unknown_collection` |
|
|
258
|
-
| `
|
|
278
|
+
| `InvalidUpdate` and its subclasses | `unknown_operator`, `no_update`, `conflicting_update`, `invalid_field`, `unindexed_many`, `empty_filter` |
|
|
259
279
|
| `UniqueViolation` | `unique_violation`, `duplicate_id` |
|
|
260
280
|
| `CatalogConflict` | `catalog_conflict` |
|
|
261
281
|
| `CatalogInvalid` | `catalog_invalid` |
|
|
@@ -264,6 +284,7 @@ All errors inherit from `Ndb::Error`, which has `code` (the daemon code) and `st
|
|
|
264
284
|
| `InvalidCursor` | `cursor_invalid`, `cursor_mismatch`, `cursor_expired` |
|
|
265
285
|
| `WatchLagged` | `watch_lagged` |
|
|
266
286
|
| `HistoryTruncated` | `history_truncated` |
|
|
287
|
+
| `TtlNotEnabled`, `TtlDerived` | `ttl_not_enabled`, `ttl_derived` |
|
|
267
288
|
| `ConnectionError` | cannot reach the daemon. Subclasses: `ConnectError` (nothing was sent), `TimeoutError` |
|
|
268
289
|
|
|
269
290
|
Any other code is a plain `Ndb::Error`. A response that is not a daemon error has the code `http_status`.
|
data/lib/ndb/client.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "uri"
|
|
5
|
+
require "time"
|
|
5
6
|
|
|
6
7
|
module Ndb
|
|
7
8
|
Page = Struct.new(:rows, :next_cursor, :plan, :examined, :total)
|
|
@@ -40,22 +41,65 @@ module Ndb
|
|
|
40
41
|
page(post(coll, "aggregate", Conditions.regexps(body)))
|
|
41
42
|
end
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
# expire_in (seconds, or "30m" / "1h" / "1d") sets the expires_field of
|
|
45
|
+
# every document to now + expire_in, in the same transaction.
|
|
46
|
+
def insert(coll, docs, expire_in: nil)
|
|
47
|
+
body = { insert: docs }
|
|
48
|
+
body[:expire_in] = expire_in if expire_in
|
|
49
|
+
|
|
50
|
+
json(post(coll, "insert", body)).fetch("results")
|
|
51
|
+
end
|
|
52
|
+
|
|
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)))
|
|
45
71
|
end
|
|
46
72
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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))
|
|
77
|
+
end
|
|
50
78
|
|
|
51
|
-
|
|
79
|
+
# Makes a document expire, like EXPIRE in Redis: in: seconds or "30m", or
|
|
80
|
+
# at: a Time or an RFC 3339 String. Returns {"id", "v", "expires_at"}.
|
|
81
|
+
# Raises NotFound for a missing or expired document, TtlNotEnabled when
|
|
82
|
+
# the collection has no expires_field, TtlDerived when the expiry comes
|
|
83
|
+
# from ttl + time.
|
|
84
|
+
def expire(coll, id, **time)
|
|
85
|
+
body = { id: id }
|
|
86
|
+
body[:in] = time[:in] if time[:in]
|
|
87
|
+
body[:at] = time[:at].respond_to?(:utc) ? time[:at].utc.iso8601(9) : time[:at] if time[:at]
|
|
88
|
+
|
|
89
|
+
json(post(coll, "expire", body))
|
|
52
90
|
end
|
|
53
91
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
92
|
+
# Removes the expiry of a document, like PERSIST in Redis.
|
|
93
|
+
def persist(coll, id)
|
|
94
|
+
json(post(coll, "persist", id: id))
|
|
95
|
+
end
|
|
57
96
|
|
|
58
|
-
|
|
97
|
+
# The seconds left of a document (an Integer, rounded up), -1 when it
|
|
98
|
+
# never expires, nil when it is missing or expired.
|
|
99
|
+
def ttl(coll, id)
|
|
100
|
+
json(request("GET", "#{path(coll)}/ttl/#{URI.encode_www_form_component(id).gsub('+', '%20')}", nil)).fetch("ttl")
|
|
101
|
+
rescue NotFound
|
|
102
|
+
nil
|
|
59
103
|
end
|
|
60
104
|
|
|
61
105
|
# Streams the changes of a collection for a named consumer: first the ones
|
|
@@ -109,6 +153,28 @@ module Ndb
|
|
|
109
153
|
value.to_s.strip.empty? ? nil : value
|
|
110
154
|
end
|
|
111
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
|
+
|
|
112
178
|
def post(coll, verb, body)
|
|
113
179
|
request("POST", "#{path(coll)}/#{verb}", body)
|
|
114
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,19 @@ 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
|
+
|
|
44
|
+
# The collection has no expires_field, or its expiry comes from ttl + time.
|
|
45
|
+
class TtlNotEnabled < Error; end
|
|
46
|
+
class TtlDerived < Error; end
|
|
47
|
+
|
|
36
48
|
# The consumer was too slow: call watch again, it resumes from the last ack.
|
|
37
49
|
class WatchLagged < Error; end
|
|
38
50
|
|
|
@@ -46,7 +58,6 @@ module Ndb
|
|
|
46
58
|
"not_ready" => NotReady,
|
|
47
59
|
"not_found" => NotFound,
|
|
48
60
|
"unknown_collection" => UnknownCollection,
|
|
49
|
-
"version_conflict" => VersionConflict,
|
|
50
61
|
"unique_violation" => UniqueViolation,
|
|
51
62
|
"duplicate_id" => UniqueViolation,
|
|
52
63
|
"catalog_conflict" => CatalogConflict,
|
|
@@ -58,7 +69,15 @@ module Ndb
|
|
|
58
69
|
"cursor_mismatch" => InvalidCursor,
|
|
59
70
|
"cursor_expired" => InvalidCursor,
|
|
60
71
|
"watch_lagged" => WatchLagged,
|
|
61
|
-
"history_truncated" => HistoryTruncated
|
|
72
|
+
"history_truncated" => HistoryTruncated,
|
|
73
|
+
"ttl_not_enabled" => TtlNotEnabled,
|
|
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
|
|
62
81
|
}.freeze
|
|
63
82
|
|
|
64
83
|
def self.error_for(code, message, status)
|
data/lib/ndb/version.rb
CHANGED