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 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tarantool
4
+ module Crud
5
+ VERSION = '0.1.0'
6
+ end
7
+ 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: []