tarantool-crud 0.1.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 +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE +21 -0
- data/README.md +123 -0
- data/lib/tarantool/crud/call.rb +41 -0
- data/lib/tarantool/crud/client.rb +23 -0
- data/lib/tarantool/crud/conditions.rb +41 -0
- data/lib/tarantool/crud/defaults.rb +58 -0
- data/lib/tarantool/crud/errors.rb +82 -0
- data/lib/tarantool/crud/many_result.rb +23 -0
- data/lib/tarantool/crud/result.rb +36 -0
- data/lib/tarantool/crud/scan.rb +32 -0
- data/lib/tarantool/crud/space.rb +82 -0
- data/lib/tarantool/crud/version.rb +7 -0
- data/lib/tarantool/crud.rb +14 -0
- metadata +77 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8ecc01ae9122ffa4404d90dc8257541b26eed6fbb503a2eefaaa8be7d10b4ccf
|
|
4
|
+
data.tar.gz: e5ed1f866cee6873e51dcc70490ffaede45c79fdd690f49c2457649afee4fafc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9bc5da2356ccb4477cb6b37053cac70743bf952d2799675fef5d442c1202a270d947c1f5e21a8fd478d929dcdfbf0c441050f4a798d06594895d034fa316c154
|
|
7
|
+
data.tar.gz: d9bd55c7c4f033b0943b9782daa5bd8441544a574c4722f1d5e83eb071bf2b8cba5045ea669e883ce649bd2288bea16ca4fb56dd5bf07ca108bec136157262f4
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- `Tarantool::Crud::Client` over `Tarantool::Client` or `Pool`, per-space handles.
|
|
8
|
+
- insert, replace, upsert (tuple or object), get, update, delete, select, each, count, min, max, len, truncate.
|
|
9
|
+
- `*_many` and `*_many!` batch operations.
|
|
10
|
+
- Conditions from Arrays or Hashes with ranges.
|
|
11
|
+
- Read and write defaults filtered by the options each crud function accepts.
|
|
12
|
+
- `storage_info` and `stats`; `stats` accepts `timeout:` and falls back to the `read:` timeout plus `timeout_margin`.
|
|
13
|
+
- Depends on `tarantool-client ~> 0.1.0`.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Starlexxx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# tarantool-crud
|
|
2
|
+
|
|
3
|
+
Ruby wrapper over [tarantool/crud](https://github.com/tarantool/crud) for sharded (vshard) Tarantool 2.11+ and 3.x
|
|
4
|
+
clusters, built on [tarantool-client](../tarantool-client).
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
Requirements: Ruby >= 3.2, Tarantool 2.11+ or 3.x, tarantool/crud module installed on routers (tested with 1.8.0).
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem 'tarantool-crud'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
require 'tarantool/crud'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
crud must be installed and initialised on the routers the client connects to.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
pool = Tarantool::Client::Pool.new(['tcp://app:secret@router-1:3301', 'tcp://app:secret@router-2:3301'])
|
|
24
|
+
crud = Tarantool::Crud::Client.new(pool, read: { mode: 'read', balance: true }, write: {}, timeout_margin: 0.5)
|
|
25
|
+
users = crud.space(:users)
|
|
26
|
+
|
|
27
|
+
users.insert([1, nil, 'a@b.c', 20]) # crud.insert
|
|
28
|
+
users.insert({ id: 1, email: 'a@b.c', age: 20 }) # crud.insert_object
|
|
29
|
+
users.get(1) # => { id: 1, bucket_id: 7, email: "a@b.c", age: 20 } or nil
|
|
30
|
+
users.update(1, [['+', 'age', 1]])
|
|
31
|
+
users.upsert({ id: 1, email: 'a@b.c', age: 20 }, [['+', 'age', 1]]) # => nil
|
|
32
|
+
users.delete(1)
|
|
33
|
+
|
|
34
|
+
page = users.select({ age: 18.. }, first: 50) # Result
|
|
35
|
+
users.select({ age: 18.. }, first: 50, after: page.last) # after: accepts a Hash row or a tuple Array
|
|
36
|
+
users.each({ age: 18.. }, batch_size: 500) { |row| } # Enumerator without a block; batch_size is the page size of underlying selects
|
|
37
|
+
users.count({ age: 18...65 })
|
|
38
|
+
users.min(:age_index)
|
|
39
|
+
users.len
|
|
40
|
+
users.truncate
|
|
41
|
+
|
|
42
|
+
result = users.insert_many(rows) # ManyResult, never raises crud errors
|
|
43
|
+
result.errors # [Tarantool::Crud::CallError, ...] with #operation_data
|
|
44
|
+
users.insert_many!(rows) # raises Tarantool::Crud::ManyError
|
|
45
|
+
|
|
46
|
+
pairs = [
|
|
47
|
+
[{ id: 1, email: 'a@b.c', age: 20 }, [['+', 'age', 1]]],
|
|
48
|
+
[{ id: 2, email: 'b@c.d', age: 25 }, [['+', 'age', 1]]],
|
|
49
|
+
]
|
|
50
|
+
users.upsert_many(pairs) # each element is [tuple, operations]
|
|
51
|
+
|
|
52
|
+
crud.storage_info
|
|
53
|
+
crud.stats(:users)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Any object responding to `call(function, arguments, timeout:)` can serve as the transport: `Tarantool::Client` or
|
|
57
|
+
`Tarantool::Client::Pool`.
|
|
58
|
+
|
|
59
|
+
Write Hash tuples and Hash conditions with braces. Bare `key: value` arguments are crud options.
|
|
60
|
+
|
|
61
|
+
## Results
|
|
62
|
+
|
|
63
|
+
`select` returns a `Result`: Enumerable of Hash rows (Symbol keys, taken from crud's metadata), plus `tuples`
|
|
64
|
+
(raw Arrays), `metadata`, `first`, `last`, `size`, `empty?`. `each` yields the same Hash rows page by page, and
|
|
65
|
+
returns an Enumerator without a block. Single-row operations (`get`, `insert`, `update`, ...) return a Hash row or
|
|
66
|
+
`nil`, not a `Result`; `upsert` always returns `nil`.
|
|
67
|
+
|
|
68
|
+
`insert_many`/`replace_many`/`upsert_many` return a `ManyResult`, which adds `errors` and `success?`. If the whole
|
|
69
|
+
call fails (missing space, empty row list, ...), crud reports it as `InsertManyError`/`ReplaceManyError`/
|
|
70
|
+
`UpsertManyError` inside `errors` with no rows, rather than raising.
|
|
71
|
+
|
|
72
|
+
## Conditions
|
|
73
|
+
|
|
74
|
+
An Array of `[operator, field, value]` or a Hash:
|
|
75
|
+
|
|
76
|
+
| Hash value | Conditions |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `18` | `['==', 'age', 18]` |
|
|
79
|
+
| `18..65` | `['>=', 'age', 18], ['<=', 'age', 65]` |
|
|
80
|
+
| `18...65` | `['>=', 'age', 18], ['<', 'age', 65]` |
|
|
81
|
+
| `18..` / `..65` / `...65` | `['>=', 'age', 18]` / `['<=', 'age', 65]` / `['<', 'age', 65]` |
|
|
82
|
+
|
|
83
|
+
## Options
|
|
84
|
+
|
|
85
|
+
Keyword options go to crud unchanged. `timeout:` is also used as the request timeout plus `timeout_margin`.
|
|
86
|
+
|
|
87
|
+
`read:` defaults apply to `get select each count min max len storage_info stats`. `write:` defaults apply to the
|
|
88
|
+
other operations. crud rejects options a function does not know, so a default is sent only to functions that accept
|
|
89
|
+
it: `prefer_replica`/`balance` skip `min`/`max`, and `truncate` gets only `timeout`/`vshard_router`. Options given
|
|
90
|
+
to a call are never filtered. Use Symbol keys.
|
|
91
|
+
|
|
92
|
+
`stats` takes only `timeout:` and sends crud no options: the given or default timeout, plus `timeout_margin`, only
|
|
93
|
+
bounds the request.
|
|
94
|
+
|
|
95
|
+
The `mode:` option here is the crud/vshard router option (`'read'` or `'write'` strings), not the `Pool` routing mode (which uses symbols like `:prefer_ro`).
|
|
96
|
+
|
|
97
|
+
## Errors
|
|
98
|
+
|
|
99
|
+
`Tarantool::Crud::Error` has `message` (the crud error message), `str` (crud's `tostring` of the full error, e.g. `"InsertError: ..."`), `class_name`, `file`, `line`, and `operation_data`. Subclasses: `InsertError`,
|
|
100
|
+
`ReplaceError`, `UpsertError`, `GetError`, `UpdateError`, `DeleteError`, `SelectError`, `CountError`, `MinMaxError`,
|
|
101
|
+
`LenError`, `TruncateError`, `CallError`, `InsertManyError`, `ReplaceManyError`, `UpsertManyError`.
|
|
102
|
+
`Tarantool::Crud::ManyError` is a separate `Tarantool::Error` subclass raised by the `*_many!` bang variants; it
|
|
103
|
+
carries `result` (and `errors`), not the crud error fields. Transport and server errors from tarantool-client
|
|
104
|
+
propagate unchanged.
|
|
105
|
+
|
|
106
|
+
A crud `timeout:` that runs out on the router arrives as the function's `Crud::*Error` with "Timeout exceeded"
|
|
107
|
+
in the message. Only a request that gets no answer within the transport timeout (`timeout:` plus
|
|
108
|
+
`timeout_margin`) raises `Tarantool::TimeoutError`.
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
podman compose -f tarantool-crud/docker-compose.yml up -d --wait --build
|
|
114
|
+
cd tarantool-crud
|
|
115
|
+
INTEGRATION=1 TARANTOOL_CRUD_URI=tcp://app:secret@localhost:3311 bundle exec rspec
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Public API
|
|
119
|
+
|
|
120
|
+
`Tarantool::Crud::Client`, `Space`, `Result`, `ManyResult`, and the error classes above.
|
|
121
|
+
|
|
122
|
+
Everything else, including anything tagged `@api private` in its source comment, is an implementation detail
|
|
123
|
+
and may change in any release.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
module Crud
|
|
5
|
+
# One crud.<function> invocation: merges defaults, sets the transport timeout, unpacks errors.
|
|
6
|
+
# @api private
|
|
7
|
+
class Call
|
|
8
|
+
def initialize(caller, defaults:, timeout_margin:)
|
|
9
|
+
@caller = caller
|
|
10
|
+
@defaults = defaults
|
|
11
|
+
@timeout_margin = timeout_margin
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def value(function, arguments, options)
|
|
15
|
+
result, error = perform(function, arguments, options)
|
|
16
|
+
raise Error.from(error) if error
|
|
17
|
+
|
|
18
|
+
result
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def many(function, arguments, options) = ManyResult.from_crud(*perform(function, arguments, options))
|
|
22
|
+
|
|
23
|
+
# For functions that take no options: the timeout, given or default, bounds only the transport.
|
|
24
|
+
def without_options(function, arguments, timeout)
|
|
25
|
+
timeout ||= @defaults.options_for(function)[:timeout]
|
|
26
|
+
|
|
27
|
+
@caller.call("crud.#{function}", arguments, timeout: transport_timeout(timeout))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def perform(function, arguments, options)
|
|
33
|
+
options = @defaults.options_for(function).merge(options)
|
|
34
|
+
|
|
35
|
+
@caller.call("crud.#{function}", [*arguments, options], timeout: transport_timeout(options[:timeout]))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def transport_timeout(timeout) = timeout && (timeout + @timeout_margin)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
module Crud
|
|
5
|
+
# Entry point: crud functions over any caller responding to call(function, arguments, timeout:).
|
|
6
|
+
class Client
|
|
7
|
+
def initialize(caller, read: {}, write: {}, timeout_margin: 0.5)
|
|
8
|
+
@call = Call.new(caller, defaults: Defaults.new(read:, write:), timeout_margin:)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def space(name) = Space.new(name, @call)
|
|
12
|
+
|
|
13
|
+
def storage_info(**options) = @call.value('storage_info', [], options)
|
|
14
|
+
|
|
15
|
+
# crud.stats takes no options and encodes an empty table as an Array.
|
|
16
|
+
def stats(space = nil, timeout: nil)
|
|
17
|
+
stats = @call.without_options('stats', [space&.to_s].compact, timeout).first
|
|
18
|
+
|
|
19
|
+
stats.is_a?(Hash) ? stats : {}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
module Crud
|
|
5
|
+
# Converts Array or Hash conditions into crud's [[operator, field, value]] form.
|
|
6
|
+
# @api private
|
|
7
|
+
module Conditions
|
|
8
|
+
OPERATORS = %w[< <= == = >= >].freeze
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def build(conditions)
|
|
12
|
+
case conditions
|
|
13
|
+
when nil then nil
|
|
14
|
+
when Hash then conditions.flat_map { |field, value| from_pair(field.to_s, value) }
|
|
15
|
+
else conditions.map { |operator, field, value| [check(operator.to_s), stringify(field), value] }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def from_pair(field, value)
|
|
22
|
+
return [['==', field, value]] unless value.is_a?(Range)
|
|
23
|
+
|
|
24
|
+
bounds = []
|
|
25
|
+
bounds << ['>=', field, value.begin] unless value.begin.nil?
|
|
26
|
+
bounds << [value.exclude_end? ? '<' : '<=', field, value.end] unless value.end.nil?
|
|
27
|
+
|
|
28
|
+
bounds
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def check(operator)
|
|
32
|
+
return operator if OPERATORS.include?(operator)
|
|
33
|
+
|
|
34
|
+
raise ArgumentError, "unknown condition operator: #{operator}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def stringify(field) = field.is_a?(Symbol) ? field.to_s : field
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
module Crud
|
|
5
|
+
# Client-wide read and write options. crud rejects options a function does not know, so defaults are
|
|
6
|
+
# narrowed to what each function accepts (crud 1.8.0); options given to a call are sent as is.
|
|
7
|
+
# @api private
|
|
8
|
+
class Defaults
|
|
9
|
+
READ_FUNCTIONS = %w[get select count min max len storage_info stats].freeze
|
|
10
|
+
|
|
11
|
+
READ = %i[timeout request_timeout bucket_id prefer_replica balance mode vshard_router].freeze
|
|
12
|
+
BORDERS = %i[timeout request_timeout fields mode vshard_router fetch_latest_metadata].freeze
|
|
13
|
+
SERVICE = %i[timeout vshard_router].freeze
|
|
14
|
+
WRITE = %i[timeout bucket_id fields vshard_router noreturn fetch_latest_metadata].freeze
|
|
15
|
+
TUPLE = (WRITE + %i[add_space_schema_hash]).freeze
|
|
16
|
+
OBJECT = (TUPLE + %i[skip_nullability_check_on_flatten]).freeze
|
|
17
|
+
MANY = %i[timeout fields stop_on_error rollback_on_error vshard_router noreturn fetch_latest_metadata].freeze
|
|
18
|
+
TUPLE_MANY = (MANY + %i[add_space_schema_hash]).freeze
|
|
19
|
+
OBJECT_MANY = (MANY + %i[skip_nullability_check_on_flatten]).freeze
|
|
20
|
+
|
|
21
|
+
ACCEPTED = {
|
|
22
|
+
'get' => READ + %i[fields fetch_latest_metadata],
|
|
23
|
+
'select' => READ + %i[after first batch_size force_map_call fields fullscan fetch_latest_metadata
|
|
24
|
+
readview readview_info yield_every],
|
|
25
|
+
'count' => READ + %i[force_map_call fullscan yield_every],
|
|
26
|
+
'min' => BORDERS,
|
|
27
|
+
'max' => BORDERS,
|
|
28
|
+
'len' => READ - %i[bucket_id],
|
|
29
|
+
'storage_info' => SERVICE,
|
|
30
|
+
'stats' => %i[timeout],
|
|
31
|
+
'truncate' => SERVICE,
|
|
32
|
+
'insert' => TUPLE,
|
|
33
|
+
'insert_object' => OBJECT,
|
|
34
|
+
'replace' => TUPLE,
|
|
35
|
+
'replace_object' => OBJECT,
|
|
36
|
+
'upsert' => TUPLE,
|
|
37
|
+
'upsert_object' => TUPLE,
|
|
38
|
+
'update' => WRITE,
|
|
39
|
+
'delete' => WRITE,
|
|
40
|
+
'insert_many' => TUPLE_MANY,
|
|
41
|
+
'insert_object_many' => OBJECT_MANY,
|
|
42
|
+
'replace_many' => TUPLE_MANY,
|
|
43
|
+
'replace_object_many' => OBJECT_MANY,
|
|
44
|
+
'upsert_many' => TUPLE_MANY,
|
|
45
|
+
'upsert_object_many' => MANY
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
def initialize(read:, write:)
|
|
49
|
+
@read = read
|
|
50
|
+
@write = write
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def options_for(function)
|
|
54
|
+
(READ_FUNCTIONS.include?(function) ? @read : @write).slice(*ACCEPTED.fetch(function))
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
module Crud
|
|
5
|
+
# Error returned by a crud function; the message is crud's err field.
|
|
6
|
+
class Error < Tarantool::Error
|
|
7
|
+
attr_reader :class_name, :str, :file, :line, :operation_data
|
|
8
|
+
|
|
9
|
+
# @api private
|
|
10
|
+
def self.from(error)
|
|
11
|
+
CLASSES.fetch(error['class_name'], Error).new(
|
|
12
|
+
error['err'],
|
|
13
|
+
class_name: error['class_name'], str: error['str'], file: error['file'], line: error['line'],
|
|
14
|
+
operation_data: error['operation_data']
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(message = nil, class_name: nil, str: nil, file: nil, line: nil, operation_data: nil)
|
|
19
|
+
super(message)
|
|
20
|
+
@class_name = class_name
|
|
21
|
+
@str = str
|
|
22
|
+
@file = file
|
|
23
|
+
@line = line
|
|
24
|
+
@operation_data = operation_data
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class InsertError < Error; end
|
|
29
|
+
class ReplaceError < Error; end
|
|
30
|
+
class UpsertError < Error; end
|
|
31
|
+
class GetError < Error; end
|
|
32
|
+
class UpdateError < Error; end
|
|
33
|
+
class DeleteError < Error; end
|
|
34
|
+
class SelectError < Error; end
|
|
35
|
+
class CountError < Error; end
|
|
36
|
+
class MinMaxError < Error; end
|
|
37
|
+
class LenError < Error; end
|
|
38
|
+
class TruncateError < Error; end
|
|
39
|
+
|
|
40
|
+
# One row of a *_many call failed; operation_data holds that row.
|
|
41
|
+
class CallError < Error; end
|
|
42
|
+
|
|
43
|
+
# A whole *_many call failed before any row was processed.
|
|
44
|
+
class InsertManyError < Error; end
|
|
45
|
+
class ReplaceManyError < Error; end
|
|
46
|
+
class UpsertManyError < Error; end
|
|
47
|
+
|
|
48
|
+
# Raised by *_many! when any row failed; result holds the rows that were written.
|
|
49
|
+
# An aggregate, not one crud function error, hence not a Crud::Error.
|
|
50
|
+
class ManyError < Tarantool::Error
|
|
51
|
+
attr_reader :result
|
|
52
|
+
|
|
53
|
+
def initialize(result)
|
|
54
|
+
@result = result
|
|
55
|
+
super("#{result.errors.size} crud operation(s) failed, first: #{result.errors.first.message}")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def errors = result.errors
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class Error
|
|
62
|
+
CLASSES = {
|
|
63
|
+
'InsertError' => InsertError,
|
|
64
|
+
'ReplaceError' => ReplaceError,
|
|
65
|
+
'UpsertError' => UpsertError,
|
|
66
|
+
'GetError' => GetError,
|
|
67
|
+
'UpdateError' => UpdateError,
|
|
68
|
+
'DeleteError' => DeleteError,
|
|
69
|
+
'SelectError' => SelectError,
|
|
70
|
+
'CountError' => CountError,
|
|
71
|
+
'BorderError' => MinMaxError,
|
|
72
|
+
'LenError' => LenError,
|
|
73
|
+
'TruncateError' => TruncateError,
|
|
74
|
+
'CallError' => CallError,
|
|
75
|
+
'InsertManyError' => InsertManyError,
|
|
76
|
+
'ReplaceManyError' => ReplaceManyError,
|
|
77
|
+
'UpsertManyError' => UpsertManyError
|
|
78
|
+
}.freeze
|
|
79
|
+
private_constant :CLASSES
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
module Crud
|
|
5
|
+
# Result of a *_many call: the rows written plus the errors of the rows that failed.
|
|
6
|
+
class ManyResult < Result
|
|
7
|
+
attr_reader :errors
|
|
8
|
+
|
|
9
|
+
# @api private
|
|
10
|
+
def self.from_crud(response, errors = nil)
|
|
11
|
+
super(response, errors: Array(errors).map { |error| Error.from(error) })
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @api private
|
|
15
|
+
def initialize(errors:, **)
|
|
16
|
+
super(**)
|
|
17
|
+
@errors = errors
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def success? = errors.empty?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
module Crud
|
|
5
|
+
# Rows returned by a crud function, as Hashes keyed by field name.
|
|
6
|
+
class Result
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
attr_reader :metadata, :tuples
|
|
10
|
+
|
|
11
|
+
# @api private
|
|
12
|
+
def self.from_crud(response, **)
|
|
13
|
+
response ||= {}
|
|
14
|
+
|
|
15
|
+
new(metadata: response.fetch('metadata', []).map { |field| field.transform_keys(&:to_sym) },
|
|
16
|
+
tuples: response.fetch('rows', []), **)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @api private
|
|
20
|
+
def initialize(metadata:, tuples:)
|
|
21
|
+
@metadata = metadata
|
|
22
|
+
@tuples = tuples
|
|
23
|
+
names = metadata.map { |field| field[:name].to_sym }
|
|
24
|
+
@rows = tuples.map { |tuple| names.zip(tuple).to_h }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def each(&) = @rows.each(&)
|
|
28
|
+
|
|
29
|
+
def last = @rows.last
|
|
30
|
+
|
|
31
|
+
def size = @rows.size
|
|
32
|
+
|
|
33
|
+
def empty? = @rows.empty?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
module Crud
|
|
5
|
+
# Iterates a select batch by batch, continuing after the last tuple until a short batch arrives.
|
|
6
|
+
# @api private
|
|
7
|
+
class Scan
|
|
8
|
+
def initialize(space, conditions, batch_size:, options:)
|
|
9
|
+
raise ArgumentError, 'batch_size must be positive' unless batch_size.positive?
|
|
10
|
+
|
|
11
|
+
@space = space
|
|
12
|
+
@conditions = conditions
|
|
13
|
+
@batch_size = batch_size
|
|
14
|
+
@options = options
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def each(&block)
|
|
18
|
+
return to_enum(:each) unless block_given?
|
|
19
|
+
|
|
20
|
+
options = @options.merge(first: @batch_size)
|
|
21
|
+
|
|
22
|
+
loop do
|
|
23
|
+
batch = @space.select(@conditions, **options)
|
|
24
|
+
batch.each(&block)
|
|
25
|
+
break if batch.size < @batch_size
|
|
26
|
+
|
|
27
|
+
options = options.merge(after: batch.tuples.last)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tarantool
|
|
4
|
+
module Crud
|
|
5
|
+
# Operations on one sharded space; Hash rows go to the *_object crud variants.
|
|
6
|
+
class Space
|
|
7
|
+
attr_reader :name
|
|
8
|
+
|
|
9
|
+
def initialize(name, call)
|
|
10
|
+
@name = name.to_s
|
|
11
|
+
@call = call
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def insert(tuple, **) = row(variant('insert', tuple), tuple, **)
|
|
15
|
+
|
|
16
|
+
def replace(tuple, **) = row(variant('replace', tuple), tuple, **)
|
|
17
|
+
|
|
18
|
+
def upsert(tuple, operations, **) = row(variant('upsert', tuple), tuple, operations, **)
|
|
19
|
+
|
|
20
|
+
def get(key, **) = row('get', key, **)
|
|
21
|
+
|
|
22
|
+
def update(key, operations, **) = row('update', key, operations, **)
|
|
23
|
+
|
|
24
|
+
def delete(key, **) = row('delete', key, **)
|
|
25
|
+
|
|
26
|
+
def min(index = nil, **) = row('min', index, **)
|
|
27
|
+
|
|
28
|
+
def max(index = nil, **) = row('max', index, **)
|
|
29
|
+
|
|
30
|
+
def select(conditions = nil, **options)
|
|
31
|
+
options[:after] = options[:after].values if options[:after].is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
Result.from_crud(value('select', Conditions.build(conditions), **options))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def each(conditions = nil, batch_size: 1000, **options, &)
|
|
37
|
+
Scan.new(self, conditions, batch_size:, options:).each(&)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def count(conditions = nil, **) = value('count', Conditions.build(conditions), **)
|
|
41
|
+
|
|
42
|
+
def len(**) = value('len', **)
|
|
43
|
+
|
|
44
|
+
def truncate(**) = value('truncate', **)
|
|
45
|
+
|
|
46
|
+
def insert_many(rows, **) = many(many_variant('insert', rows), rows, **)
|
|
47
|
+
|
|
48
|
+
def replace_many(rows, **) = many(many_variant('replace', rows), rows, **)
|
|
49
|
+
|
|
50
|
+
def upsert_many(pairs, **) = many(many_variant('upsert', pairs.map(&:first)), pairs, **)
|
|
51
|
+
|
|
52
|
+
def insert_many!(rows, **) = successful(insert_many(rows, **))
|
|
53
|
+
|
|
54
|
+
def replace_many!(rows, **) = successful(replace_many(rows, **))
|
|
55
|
+
|
|
56
|
+
def upsert_many!(pairs, **) = successful(upsert_many(pairs, **))
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def value(function, *arguments, **options) = @call.value(function, [name, *arguments], options)
|
|
61
|
+
|
|
62
|
+
def row(...) = Result.from_crud(value(...)).first
|
|
63
|
+
|
|
64
|
+
def variant(function, tuple) = tuple.is_a?(Hash) ? "#{function}_object" : function
|
|
65
|
+
|
|
66
|
+
def many(function, rows, **options) = @call.many(function, [name, rows], options)
|
|
67
|
+
|
|
68
|
+
def many_variant(function, rows)
|
|
69
|
+
kinds = rows.map { |row| row.is_a?(Hash) }.uniq
|
|
70
|
+
raise ArgumentError, 'rows must be all Arrays or all Hashes' if kinds.size > 1
|
|
71
|
+
|
|
72
|
+
kinds.first ? "#{function}_object_many" : "#{function}_many"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def successful(result)
|
|
76
|
+
raise ManyError, result unless result.success?
|
|
77
|
+
|
|
78
|
+
result
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tarantool/client'
|
|
4
|
+
|
|
5
|
+
require_relative 'crud/version'
|
|
6
|
+
require_relative 'crud/errors'
|
|
7
|
+
require_relative 'crud/conditions'
|
|
8
|
+
require_relative 'crud/result'
|
|
9
|
+
require_relative 'crud/many_result'
|
|
10
|
+
require_relative 'crud/defaults'
|
|
11
|
+
require_relative 'crud/call'
|
|
12
|
+
require_relative 'crud/scan'
|
|
13
|
+
require_relative 'crud/space'
|
|
14
|
+
require_relative 'crud/client'
|
metadata
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: tarantool-crud
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Starlexxx
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-16 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: tarantool-client
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.1.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.1.0
|
|
27
|
+
description: Per-space CRUD, conditions, pagination and batch operations over the
|
|
28
|
+
tarantool/crud module for vshard clusters on Tarantool 2.11+ and 3.x, built on tarantool-client.
|
|
29
|
+
email:
|
|
30
|
+
- apavlovskiy088@gmail.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- CHANGELOG.md
|
|
36
|
+
- LICENSE
|
|
37
|
+
- README.md
|
|
38
|
+
- lib/tarantool/crud.rb
|
|
39
|
+
- lib/tarantool/crud/call.rb
|
|
40
|
+
- lib/tarantool/crud/client.rb
|
|
41
|
+
- lib/tarantool/crud/conditions.rb
|
|
42
|
+
- lib/tarantool/crud/defaults.rb
|
|
43
|
+
- lib/tarantool/crud/errors.rb
|
|
44
|
+
- lib/tarantool/crud/many_result.rb
|
|
45
|
+
- lib/tarantool/crud/result.rb
|
|
46
|
+
- lib/tarantool/crud/scan.rb
|
|
47
|
+
- lib/tarantool/crud/space.rb
|
|
48
|
+
- lib/tarantool/crud/version.rb
|
|
49
|
+
homepage: https://github.com/Starlexxx/tarantool-ruby
|
|
50
|
+
licenses:
|
|
51
|
+
- MIT
|
|
52
|
+
metadata:
|
|
53
|
+
source_code_uri: https://github.com/Starlexxx/tarantool-ruby/tree/master/tarantool-crud
|
|
54
|
+
changelog_uri: https://github.com/Starlexxx/tarantool-ruby/blob/master/tarantool-crud/CHANGELOG.md
|
|
55
|
+
bug_tracker_uri: https://github.com/Starlexxx/tarantool-ruby/issues
|
|
56
|
+
homepage_uri: https://github.com/Starlexxx/tarantool-ruby
|
|
57
|
+
rubygems_mfa_required: 'true'
|
|
58
|
+
post_install_message:
|
|
59
|
+
rdoc_options: []
|
|
60
|
+
require_paths:
|
|
61
|
+
- lib
|
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '3.2'
|
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '0'
|
|
72
|
+
requirements: []
|
|
73
|
+
rubygems_version: 3.5.22
|
|
74
|
+
signing_key:
|
|
75
|
+
specification_version: 4
|
|
76
|
+
summary: Ruby wrapper over tarantool/crud for sharded Tarantool clusters
|
|
77
|
+
test_files: []
|