├── .gitignore ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── tarantool16.rb └── tarantool16 │ ├── connection │ ├── common.rb │ ├── dumb.rb │ └── response.rb │ ├── consts.rb │ ├── db.rb │ ├── dumb_db.rb │ ├── errors.rb │ ├── query.rb │ ├── response.rb │ ├── schema.rb │ └── version.rb ├── tarantool16.gemspec └── test ├── bench_timeout.rb ├── config.lua ├── helper.rb └── test_dumb.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /test/*.log 10 | /test/run/ 11 | /tmp/ 12 | *.bundle 13 | *.so 14 | *.o 15 | *.a 16 | mkmf.log 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in tarantool16.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2016 Sokolov Yura aka funny_falcon 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tarantool16 2 | 3 | This is adapter for [tarantool](http://tarantool.org) version 1.6 and 1.7. 4 | 5 | (adapter for version <=1.5 is called [tarantool](https://github.org/tarantoool/tarantool-ruby)) 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'tarantool16' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install tarantool16 22 | 23 | ## Usage 24 | 25 | Currently only simple single threaded one-request-at-time connection implemented. 26 | 27 | ```ruby 28 | require 'tarantool16' 29 | 30 | db = Tarantool16.new host:'localhost:33013' 31 | #db = Tarantool16.new host:'localhost:33013', user:'tester', password:'testpass' 32 | #db = Tarantool16.new host:['tcp','localhost:33013'] 33 | #db = Tarantool16.new host:['unix','path/to.sock'] 34 | #db = Tarantool16.new unix:'path/to.sock' 35 | 36 | # select from '_space' space info about 'test' table 37 | # returns array of tuples as an array 38 | tar.get(272, ['test'], index: 2) 39 | 40 | # same, but return tuples as a hashes 41 | tar.get(272, ['test'], index: 2, hash: true) 42 | 43 | # same with names 44 | # Names and index descriptions are fetched from tarantool. 45 | # Index is autodetected by key names 46 | tar.get(:_space, {name: 'test'}) 47 | 48 | # get all spaces 49 | tar.select(:_space, nil, iterator: :all) 50 | tar.select(:_space, nil, iterator: :all, hash: true) 51 | 52 | tar.select(:_space, [512], index: 0, iterator: :>=, hash: true) 53 | 54 | # override tuple field definition 55 | tar.define_fields(:test, [:id, :name, :value]) 56 | 57 | tar.insert(:test, [1, 'buddy', [1,2,3]]) 58 | tar.replace(:test, [1, 'buddy!', [2,3,4]]) 59 | tar.update(:test, [1], [[':', 1, 6, 6, '?']]) 60 | #tar.update(:test, [1], [[':', 1, 6, 6, '?']], index: 0) 61 | tar.delete(:test, [1]) 62 | #tar.delete(:test, [1], index: 0) 63 | 64 | tar.insert(:test, {id: 1, name: 'buddy', value: [1,2,3]}) 65 | tar.replace(:test, {id: 1, name: 'buddy!', value: [2,3,4]}) 66 | tar.update(:test, {id: 1}, {name: [':', 6,6,'?']}) 67 | tar.delete(:test, {id: 1}) 68 | 69 | # note: currenlty there is no documented way to store field definition in an tarantool 70 | # but actually you can do it with this 71 | tar.update(:_space, {name: 'test'}, {format: [:=, [{name: :id, type: :num}, {name: :name, type: :str}, {name: :value, type: '*'}]]}) 72 | 73 | ``` 74 | 75 | ## Changelog 76 | 77 | 0.1.0 - breaking change: call now issues request in Tarantool17 format. 78 | use call16 to issue `call` request to Tarantool16. 79 | see tests for difference in returned results. 80 | 0.0.11 - change a way of unix socket option. 81 | since previos scheme were introduced 12 hours ago, 82 | i think it is still safe to change it. 83 | 0.0.10 - a bit of fixes 84 | 0.0.9 - Fix detection of update operation shape 85 | Add unix socket connection. 86 | 0.0.8 - Fix schema read from new tarantool versions 87 | Implement UPSERT 88 | 0.0.7 - Implement EVAL, fix REPLACE 89 | 0.0.6 - DumbConnection supports timeouts 90 | 91 | ## Contributing 92 | 93 | 1. Fork it ( https://github.com/funny-falcon/tarantool16/fork ) 94 | 2. Create your feature branch (`git checkout -b my-new-feature`) 95 | 3. Commit your changes (`git commit -am 'Add some feature'`) 96 | 4. Push to the branch (`git push origin my-new-feature`) 97 | 5. Create a new Pull Request 98 | 99 | Or simply fill an issue 100 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new 5 | 6 | -------------------------------------------------------------------------------- /lib/tarantool16.rb: -------------------------------------------------------------------------------- 1 | require "tarantool16/version" 2 | require "tarantool16/db" 3 | 4 | module Tarantool16 5 | autoload :DumbDB, 'tarantool16/dumb_db' 6 | def self.new(opts = {}) 7 | opts = opts.dup 8 | if opts[:unix] && opts[:host] 9 | raise "`:host` and `:unix` options are mutually exclusive" 10 | elsif opts[:unix] 11 | hosts = ["unix", opts[:unix]] 12 | elsif opts[:host] 13 | host = opts[:host] 14 | if Array === host 15 | hosts = host 16 | else 17 | host = [host, opts[:port]].compact.join(':') 18 | hosts = ["tcp", host] 19 | end 20 | end 21 | type = opts[:type] && opts[:type].to_s || 'dumb' 22 | case type 23 | when 'dumb' 24 | DumbDB.new hosts, opts 25 | else 26 | raise "Unknown DB type" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/tarantool16/connection/common.rb: -------------------------------------------------------------------------------- 1 | require 'msgpack' 2 | require 'openssl' 3 | require 'openssl/digest' 4 | require 'tarantool16/consts' 5 | require_relative 'response' 6 | 7 | module Tarantool16 8 | module Connection 9 | class Error < ::StandardError; end 10 | class ConnectionError < Error; end 11 | class CouldNotConnect < ConnectionError; end 12 | class Disconnected < ConnectionError; end 13 | class Timeout < ConnectionError; end 14 | class Retry < ConnectionError; end 15 | class UnexpectedResponse < Error; end 16 | 17 | module Common 18 | DEFAULT_RECONNECT = 0.2 19 | attr :host, :user 20 | def _init_common(host, opts) 21 | @host = host 22 | @user = opts[:user] 23 | if opts[:password] 24 | @passwd = ::OpenSSL::Digest::SHA1.digest(opts[:password]) 25 | end 26 | if opts[:reconnect].nil? 27 | @reconnect_timeout = DEFAULT_RECONNECT 28 | @reconnect = true 29 | elsif Numeric === opts[:reconnect] 30 | @reconnect_timeout = opts[:reconnect] 31 | @reconnect = true 32 | else 33 | @reconnect = false 34 | end 35 | @timeout = opts[:timeout] 36 | @p = MessagePack::Packer.new 37 | @u = MessagePack::Unpacker.new 38 | @s = 0 39 | end 40 | 41 | if ::Process.respond_to?(:clock_gettime) 42 | if defined?(::Process::CLOCK_MONOTONIC_COARSE) 43 | CLOCK_KIND = ::Process::CLOCK_MONOTONIC_COARSE 44 | elsif defined?(::Process::CLOCK_MONOTONIC_FAST) 45 | CLOCK_KIND = ::Process::CLOCK_MONOTONIC_FAST 46 | elsif defined?(::Process::CLOCK_MONOTONIC) 47 | CLOCK_KIND = ::Process::CLOCK_MONOTONIC 48 | else 49 | CLOCK_KIND = ::Process::CLOCK_REALTIME 50 | end 51 | def now_f 52 | ::Process.clock_gettime(CLOCK_KIND) 53 | end 54 | else 55 | def now_f 56 | Time.now.to_f 57 | end 58 | end 59 | 60 | def next_sync 61 | @s = @s % 0x3fffffff + 1 62 | end 63 | 64 | def format_request(code, sync, body) 65 | @p.write(0x01020304). 66 | write_map_header(2). 67 | write(IPROTO_CODE).write(code). 68 | write(IPROTO_SYNC).write(sync). 69 | write(body) 70 | sz = @p.size - 5 71 | str = @p.to_s 72 | @p.clear 73 | # fix bigendian size 74 | str.setbyte(4, sz) 75 | str.setbyte(3, sz>>8) 76 | str.setbyte(2, sz>>16) 77 | str.setbyte(1, sz>>24) 78 | str 79 | ensure 80 | @p.clear 81 | end 82 | 83 | def format_authenticate(user, pass1, salt) 84 | pass2 = ::OpenSSL::Digest::SHA1.digest(pass1) 85 | scramble = ::OpenSSL::Digest::SHA1.new(salt).update(pass2).digest 86 | pints = pass1.unpack('L*') 87 | sints = scramble.unpack('L*') 88 | pints.size.times{|i| sints[i] ^= pints[i] } 89 | packed = sints.pack('L*') 90 | # tarantool waits packed as a string, so that force msgpack to pack as string 91 | packed.force_encoding('utf-8') 92 | format_request(REQUEST_TYPE_AUTHENTICATE, next_sync, { 93 | IPROTO_USER_NAME => user, 94 | IPROTO_TUPLE => [ 'chap-sha1', packed ] 95 | }) 96 | end 97 | 98 | def parse_greeting(greeting) 99 | @greeting = greeting[0, 64] 100 | @salt = greeting[64..-1].unpack('m')[0][0,20] 101 | end 102 | 103 | def parse_size(str) 104 | @u.feed(str) 105 | n = @u.read 106 | unless Integer === n 107 | return UnexpectedResponse.new("wanted response size, got #{n.inspect}") 108 | end 109 | n 110 | rescue ::MessagePack::UnpackError, ::MessagePack::TypeError => e 111 | @u.reset 112 | e 113 | end 114 | 115 | def parse_response(str) 116 | sync = nil 117 | @u.feed(str) 118 | n = @u.read_map_header 119 | while n > 0 120 | cd = @u.read 121 | vl = @u.read 122 | case cd 123 | when IPROTO_SYNC 124 | sync = vl 125 | when IPROTO_CODE 126 | code = vl 127 | end 128 | n -= 1 129 | end 130 | if sync == nil 131 | return Option.error(nil, UnexpectedResponse, "Mailformed response: no sync") 132 | elsif code == nil 133 | return Option.error(nil, UnexpectedResponse, "Mailformed response: no code for sync=#{sync}") 134 | end 135 | begin 136 | bmap = @u.read 137 | body = bmap[IPROTO_DATA] || bmap[IPROTO_ERROR] 138 | rescue EOFError 139 | body = nil 140 | end 141 | Option.ok(sync, code, body) 142 | rescue ::MessagePack::UnpackError, ::MessagePack::TypeError => e 143 | @u.reset 144 | Option.error(sync, e, nil) 145 | end 146 | 147 | def _unix? 148 | @host[0] == 'unix' || @host[0] == :unix 149 | end 150 | 151 | def _tcp? 152 | @host[0] == 'tcp' || @host[0] == :tcp 153 | end 154 | 155 | def _unix_sock_path 156 | @host[1] 157 | end 158 | 159 | def host_port 160 | @host[1] =~ /^(.*):([^:]+)$/ 161 | [$1, $2.to_i] 162 | end 163 | 164 | def _ipv6? 165 | @host[1].count(':') > 1 166 | end 167 | 168 | def _insert(space_no, tuple, cb) 169 | req = {IPROTO_SPACE_ID => space_no, 170 | IPROTO_TUPLE => tuple} 171 | send_request(REQUEST_TYPE_INSERT, req, cb) 172 | end 173 | 174 | def _replace(space_no, tuple, cb) 175 | req = {IPROTO_SPACE_ID => space_no, 176 | IPROTO_TUPLE => tuple} 177 | send_request(REQUEST_TYPE_REPLACE, req, cb) 178 | end 179 | 180 | def _delete(space_no, index_no, key, cb) 181 | req = {IPROTO_SPACE_ID => space_no, 182 | IPROTO_INDEX_ID => index_no, 183 | IPROTO_KEY => key} 184 | send_request(REQUEST_TYPE_DELETE, req, cb) 185 | end 186 | 187 | def _select(space_no, index_no, key, offset, limit, iterator, cb) 188 | iterator ||= ::Tarantool16::ITERATOR_EQ 189 | unless Integer === iterator 190 | iterator = ::Tarantool16.iter(iterator) 191 | end 192 | req = {IPROTO_SPACE_ID => space_no, 193 | IPROTO_INDEX_ID => index_no, 194 | IPROTO_KEY => key || [], 195 | IPROTO_OFFSET => offset, 196 | IPROTO_LIMIT => limit, 197 | IPROTO_ITERATOR => iterator} 198 | send_request(REQUEST_TYPE_SELECT, req, cb) 199 | end 200 | 201 | def _update(space_no, index_no, key, ops, cb) 202 | req = {IPROTO_SPACE_ID => space_no, 203 | IPROTO_INDEX_ID => index_no, 204 | IPROTO_KEY => key, 205 | IPROTO_TUPLE => ops} 206 | send_request(REQUEST_TYPE_UPDATE, req, cb) 207 | end 208 | 209 | def _upsert(space_no, index_no, tuple_key, ops, cb) 210 | req = {IPROTO_SPACE_ID => space_no, 211 | IPROTO_INDEX_ID => index_no, 212 | IPROTO_TUPLE => tuple_key, 213 | IPROTO_DEF_DUPLE => ops} 214 | send_request(REQUEST_TYPE_UPSERT, req, cb) 215 | end 216 | 217 | def _call(name, args, cb) 218 | req = {IPROTO_FUNCTION_NAME => name, 219 | IPROTO_TUPLE => args} 220 | send_request(REQUEST_TYPE_CALL17, req, cb) 221 | end 222 | 223 | def _call16(name, args, cb) 224 | req = {IPROTO_FUNCTION_NAME => name, 225 | IPROTO_TUPLE => args} 226 | send_request(REQUEST_TYPE_CALL16, req, cb) 227 | end 228 | 229 | def _eval(expr, args, cb) 230 | req = {IPROTO_EXPR => expr, 231 | IPROTO_TUPLE => args} 232 | send_request(REQUEST_TYPE_EVAL, req, cb) 233 | end 234 | 235 | REQ_EMPTY = {}.freeze 236 | def _ping(cb) 237 | send_request(REQUEST_TYPE_PING, REQ_EMPTY, cb) 238 | end 239 | end 240 | end 241 | end 242 | -------------------------------------------------------------------------------- /lib/tarantool16/connection/dumb.rb: -------------------------------------------------------------------------------- 1 | require 'socket' 2 | require 'io/wait' 3 | begin 4 | #require 'kgio' 5 | rescue LoadError 6 | end 7 | 8 | require_relative 'common' 9 | module Tarantool16 10 | module Connection 11 | class Dumb 12 | include Common 13 | 14 | def initialize(host, opts = {}) 15 | _init_common(host, opts) 16 | @reconnect_time = now_f - 1 17 | @socket = nil 18 | _connect 19 | end 20 | 21 | def send_request(code, body, cb) 22 | _connect 23 | syswrite format_request(code, next_sync, body) 24 | written = true 25 | response = _read_response 26 | rescue ::Errno::EPIPE, Retry => e 27 | @socket.close rescue nil 28 | @socket = nil 29 | if !written && @retry && @reconnect 30 | @retry = false 31 | retry 32 | end 33 | cb.call Option.error(nil, Disconnected, e.message) 34 | rescue StandardError => e 35 | cb.call Option.error(nil, e, nil) 36 | else 37 | cb.call response 38 | end 39 | 40 | def disconnect 41 | if @socket 42 | @socket.close rescue nil 43 | @socket = nil 44 | @s = 0 45 | end 46 | end 47 | 48 | def close 49 | @reconnect = false 50 | if @socket 51 | @socket.close rescue nil 52 | @socket = false 53 | @s = 0 54 | end 55 | end 56 | 57 | def connected? 58 | @socket 59 | end 60 | 61 | def could_be_connected? 62 | @socket || (@socket.nil? && (@reconnect || @reconnect_time < now_f)) 63 | end 64 | 65 | private 66 | def _connect 67 | return if @socket 68 | unless could_be_connected? 69 | raise Disconnected, "connection is closed" 70 | end 71 | if _unix? 72 | @socket = Socket.unix(_unix_sock_path) 73 | elsif _tcp? 74 | @socket = Socket.new((_ipv6? ? Socket::AF_INET6 : Socket::AF_INET), Socket::SOCK_STREAM) 75 | @socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1) 76 | @socket.sync = true 77 | sockaddr = Socket.pack_sockaddr_in(*host_port.reverse) 78 | @retry = @reconnect 79 | if @timeout 80 | _connect_nonblock(sockaddr) 81 | else 82 | @socket.connect(sockaddr) 83 | end 84 | else 85 | raise "unsupported host option: #{@host.inspect}" 86 | end 87 | 88 | greeting = _read(IPROTO_GREETING_SIZE) 89 | unless greeting && greeting.bytesize == IPROTO_GREETING_SIZE 90 | raise Disconnected, "mailformed greeting #{greeting.inspect}" 91 | end 92 | @nbuf = "\x00\x00\x00\x00\x00".force_encoding('BINARY') 93 | parse_greeting greeting 94 | authenticate if @user 95 | rescue ::Errno::ECONNREFUSED, ::Errno::EPIPE, Disconnected, Timeout => e 96 | @socket.close rescue nil 97 | @socket = nil 98 | if !@reconnect 99 | @socket = false 100 | @s = 0 101 | else 102 | @reconnect_time = now_f + @reconnect_timeout 103 | end 104 | raise CouldNotConnect, e.message 105 | end 106 | 107 | def _connect_nonblock(sockaddr) 108 | expire = now_f + @timeout 109 | begin 110 | @socket.connect_nonblock(sockaddr) 111 | rescue IO::WaitWritable 112 | t = [@socket] 113 | IO.select(t, t, nil, expire - now_f) 114 | begin 115 | @socket.connect_nonblock(sockaddr) 116 | rescue Errno::EISCONN 117 | end 118 | end 119 | end 120 | 121 | def syswrite(req) 122 | unless @timeout 123 | if @socket.syswrite(req) != req.bytesize 124 | raise Retry, "Could not write message" 125 | end 126 | else 127 | expire = now_f 128 | begin 129 | until req.empty? 130 | n = @socket.write_nonblock(req) 131 | req = req[n..-1] 132 | end 133 | rescue IO::WaitWritable 134 | nf = now_f 135 | if expire <= nf 136 | raise Timeout, "response timeouted" 137 | else 138 | _wait_writable(expire - nf) 139 | retry 140 | end 141 | rescue Errno::EINTR 142 | retry 143 | end 144 | end 145 | end 146 | 147 | def authenticate 148 | syswrite format_authenticate(@user, @passwd, @salt) 149 | _read_response.raise_if_error! 150 | end 151 | 152 | def _read_response 153 | str = _read(5, @nbuf) 154 | unless str && str.bytesize == 5 155 | # check if we sent request or not 156 | begin 157 | @socket.send("\x00", 0) 158 | rescue ::Errno::EPIPE 159 | # if OS knows that socket is closed, then request were not sent 160 | raise Retry 161 | else 162 | # otherwise request were sent 163 | raise Disconnected, "disconnected while read length" 164 | end 165 | end 166 | n = parse_size(str) 167 | raise n unless ::Integer === n 168 | resp = _read(n) 169 | raise Disconnected, "disconnected while read response" unless resp && resp.bytesize == n 170 | r = parse_response(resp) 171 | if r.ok? && r.sync != @s 172 | raise UnexpectedResponse, "sync mismatch: #{@s} != #{r.sync}" 173 | end 174 | r 175 | end 176 | 177 | def _read(n, buf=nil) 178 | unless @timeout 179 | buf ? @socket.read(n, buf) : @socket.read(n) 180 | else 181 | expire = now_f + @timeout 182 | rbuf = nil 183 | while n > 0 184 | case tbuf = _read_nonblock(n, buf) 185 | when String 186 | if rbuf 187 | rbuf << tbuf 188 | else 189 | rbuf = tbuf 190 | end 191 | buf = nil 192 | n -= tbuf.size 193 | when :wait_readable 194 | nf = now_f 195 | if expire <= nf 196 | raise Timeout, "response timeouted" 197 | else 198 | _wait_readable(expire - nf) 199 | end 200 | when nil 201 | raise EOFError 202 | end 203 | end 204 | return rbuf 205 | end 206 | end 207 | 208 | if (RUBY_VERSION.split('.').map(&:to_i) <=> [2,1]) >= 0 209 | EXCEPTION_FALSE = {exception: false}.freeze 210 | def _read_nonblock(n, buf) 211 | if buf 212 | @socket.read_nonblock(n, buf, **EXCEPTION_FALSE) 213 | else 214 | @socket.read_nonblock(n, **EXCEPTION_FALSE) 215 | end 216 | end 217 | elsif defined?(Kgio) 218 | def _read_nonblock(n, buf) 219 | return buf ? Kgio.tryread(@socket, n, buf) : Kgio.tryread(@socket, n) 220 | end 221 | else 222 | def _read_nonblock(n, buf) 223 | begin 224 | if buf 225 | @socket.read_nonblock(n, buf) 226 | else 227 | @socket.read_nonblock(n) 228 | end 229 | rescue IO::WaitReadable 230 | return :wait_readable 231 | rescue Errno::EINTR 232 | retry 233 | end 234 | end 235 | end 236 | 237 | if RUBY_ENGINE == 'jruby' 238 | def _wait_readable(timeout) 239 | IO.select([@socket], nil, nil, timeout) 240 | end 241 | else 242 | def _wait_readable(timeout) 243 | @socket.wait(timeout) 244 | end 245 | end 246 | if IO.instance_methods.include?(:wait_writable) 247 | def _wait_writable(timeout) 248 | @socket.wait_writable(timeout) 249 | end 250 | else 251 | def _wait_writable(timeout) 252 | t = [@socket] 253 | IO.select(t, t, nil, expire - now_f) 254 | end 255 | end 256 | end 257 | end 258 | end 259 | -------------------------------------------------------------------------------- /lib/tarantool16/connection/response.rb: -------------------------------------------------------------------------------- 1 | require 'tarantool16/errors' 2 | require 'tarantool16/response' 3 | 4 | module Tarantool16 5 | module Connection 6 | class Option 7 | attr :sync, :error, :data 8 | def initialize(sync, err, data) 9 | @sync = sync 10 | @error = err 11 | @data = data 12 | end 13 | 14 | def ok? 15 | !@error 16 | end 17 | 18 | def raise_if_error! 19 | raise @error if @error 20 | end 21 | 22 | def self.ok(sync, code, data) 23 | if code == 0 24 | new(sync, nil, data) 25 | else 26 | new(sync, ::Tarantool16::DBError.with_code_message(code&(REQUEST_TYPE_ERROR-1), data), nil) 27 | end 28 | end 29 | 30 | def self.error(sync, err, message = nil) 31 | if err.is_a? Class 32 | err = err.new message 33 | end 34 | new(sync, err, nil) 35 | end 36 | 37 | def inspect 38 | s = @sync ? " sync=#{sync}" : "" 39 | if ok? 40 | "" 41 | else 42 | "" 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/tarantool16/consts.rb: -------------------------------------------------------------------------------- 1 | module Tarantool16 2 | IPROTO_CODE = 0x00 3 | IPROTO_SYNC = 0x01 4 | IPROTO_SPACE_ID = 0x10 5 | IPROTO_INDEX_ID = 0x11 6 | IPROTO_LIMIT = 0x12 7 | IPROTO_OFFSET = 0x13 8 | IPROTO_ITERATOR = 0x14 9 | IPROTO_KEY = 0x20 10 | IPROTO_TUPLE = 0x21 11 | IPROTO_FUNCTION_NAME = 0x22 12 | IPROTO_USER_NAME = 0x23 13 | IPROTO_EXPR = 0x27 14 | IPROTO_DEF_DUPLE = 0x28 15 | IPROTO_DATA = 0x30 16 | IPROTO_ERROR = 0x31 17 | 18 | IPROTO_GREETING_SIZE = 128 19 | 20 | REQUEST_TYPE_OK = 0 21 | REQUEST_TYPE_PING = 64 22 | REQUEST_TYPE_SELECT = 1 23 | REQUEST_TYPE_INSERT = 2 24 | REQUEST_TYPE_REPLACE = 3 25 | REQUEST_TYPE_UPDATE = 4 26 | REQUEST_TYPE_DELETE = 5 27 | REQUEST_TYPE_CALL16 = 6 28 | REQUEST_TYPE_AUTHENTICATE = 7 29 | REQUEST_TYPE_EVAL = 8 30 | REQUEST_TYPE_UPSERT = 9 31 | REQUEST_TYPE_CALL17 = 10 32 | REQUEST_TYPE_ERROR = 1 << 15 33 | 34 | 35 | SPACE_SCHEMA = 272 36 | SPACE_SPACE = 280 37 | SPACE_VSPACE = 281 38 | SPACE_INDEX = 288 39 | SPACE_VINDEX = 289 40 | SPACE_FUNC = 296 41 | SPACE_USER = 304 42 | SPACE_PRIV = 312 43 | SPACE_CLUSTER = 320 44 | 45 | INDEX_SPACE_PRIMARY = 0 46 | INDEX_SPACE_NAME = 2 47 | INDEX_INDEX_PRIMARY = 0 48 | INDEX_INDEX_NAME = 2 49 | 50 | ITERATOR_EQ = 0 51 | ITERATOR_REQ = 1 52 | ITERATOR_ALL = 2 53 | ITERATOR_LT = 3 54 | ITERATOR_LE = 4 55 | ITERATOR_GE = 5 56 | ITERATOR_GT = 6 57 | ITERATOR_BITS_ALL_SET = 7 58 | ITERATOR_BITS_ANY_SET = 8 59 | ITERATOR_BITS_ALL_NOT_SET = 9 60 | ITERATOR_RTREE_OVERLAPS = 10 61 | ITERATOR_RTREE_NEIGHBOR = 11 62 | 63 | Iterators = {} 64 | [ 65 | [ITERATOR_EQ, %w[eq ==]], 66 | [ITERATOR_REQ, %w[req rev ==<]], 67 | [ITERATOR_ALL, %w[all *]], 68 | [ITERATOR_LT, %w[< lt]], 69 | [ITERATOR_LE, %w[<= le]], 70 | [ITERATOR_GE, %w[>= ge]], 71 | [ITERATOR_GT, %w[> gt]], 72 | [ITERATOR_BITS_ALL_SET, %w[ball &=]], 73 | [ITERATOR_BITS_ANY_SET, %w[bany &]], 74 | [ITERATOR_BITS_ALL_NOT_SET, %w[bnotany !&]], 75 | [ITERATOR_RTREE_OVERLAPS, %w[roverlaps here &&]], 76 | [ITERATOR_RTREE_NEIGHBOR, %w[rneighbor near <->]], 77 | ].each do |it, names| 78 | names.each do |name| 79 | Iterators[it] = it 80 | Iterators[name] = it 81 | Iterators[name.to_sym] = it 82 | end 83 | end 84 | Iterators[nil] = ITERATOR_EQ 85 | def self.iter(iter) 86 | unless it = Iterators[iter] 87 | raise "Unknown iterator #{iter.inspect}" 88 | end 89 | it 90 | end 91 | 92 | # Default value for socket timeout (seconds) 93 | SOCKET_TIMEOUT = nil 94 | # Default maximum number of attempts to reconnect 95 | RECONNECT_MAX_ATTEMPTS = 10 96 | # Default delay between attempts to reconnect (seconds) 97 | RECONNECT_DELAY = 0.1 98 | # Number of reattempts in case of server 99 | # return completion_status == 1 (try again) 100 | RETRY_MAX_ATTEMPTS = 10 101 | end 102 | -------------------------------------------------------------------------------- /lib/tarantool16/db.rb: -------------------------------------------------------------------------------- 1 | require_relative 'schema' 2 | module Tarantool16 3 | class DB 4 | attr :conn 5 | 6 | def initialize(host, opts = {}) 7 | @host = host 8 | @opts = opts.dup 9 | @future = nil 10 | @spaces = nil 11 | @defined_fields = {} 12 | @conn = self.class::Connection.new(@host, @opts) 13 | end 14 | 15 | def define_fields(sid, fields) 16 | sid = sid.to_s if sid.is_a?(Symbol) 17 | @defined_fields[sid] = fields 18 | if @spaces && (sp = @spaces[sid]) 19 | if sp.sid && sp.name && !sp.name.empty? 20 | rf1 = @defined_fields[sp.sid] 21 | rf2 = @defined_fields[sp.name] 22 | if rf1 && rf2 && rf1 != rf2 23 | raise "Misconfigured defined fields for #{sp.name_sid}" 24 | end 25 | end 26 | sp.fields = fields 27 | end 28 | end 29 | 30 | def _synchronized 31 | raise "Override #_synchronized" 32 | end 33 | 34 | UNDEF = Object.new.freeze 35 | def _with_space(name, cb) 36 | future = @future || _space_future 37 | future.then_blk do |r| 38 | unless r.ok? 39 | cb.call r 40 | else 41 | sps = r.data 42 | sp = sps[name] 43 | if sp.nil? && Symbol == name 44 | sp = sps[name.to_s] 45 | sps[name] = sp unless sp.nil? 46 | end 47 | if sp.nil? 48 | cb.call Option.error(SchemaError, "space #{name} not found") 49 | else 50 | yield sp 51 | end 52 | end 53 | end 54 | end 55 | 56 | def _space_future 57 | _synchronized do 58 | return @future if @future 59 | future = @future = self.class::SchemaFuture.new 60 | fill_indexes = nil 61 | spaces = nil 62 | fill_spaces = lambda do|r| 63 | unless r.ok? 64 | future.set r 65 | _synchronized do 66 | @future = nil 67 | end 68 | else 69 | _synchronized do 70 | _fill_spaces(r.data) 71 | spaces = @spaces 72 | _select(SPACE_VINDEX, 0, [], 0, 2**30, :all, false, fill_indexes) 73 | end 74 | end 75 | end 76 | fill_indexes = lambda do |r| 77 | unless r.ok? 78 | future.set r 79 | _synchronized do 80 | @future = nil 81 | @spaces = nil 82 | end 83 | else 84 | _synchronized do 85 | _fill_indices(spaces, r.data) 86 | future.set Option.ok(spaces) 87 | end 88 | end 89 | end 90 | _select(SPACE_VSPACE, 0, [], 0, 2**30, :all, false, fill_spaces) 91 | return future 92 | end 93 | end 94 | 95 | def _fill_spaces(rows) 96 | @spaces = {} 97 | rows.each do |row| 98 | fields = @defined_fields[row[0]] || @defined_fields[row[2]] || row[6] 99 | sp = SchemaSpace.new(row[0], row[2], fields) 100 | @spaces[row[0]] = sp 101 | @spaces[sp.name] = sp 102 | @spaces[sp.name.to_sym] = sp 103 | end 104 | end 105 | 106 | def _fill_indices(spaces, rows) 107 | rows. 108 | map{|row| 109 | if row[4].is_a? Hash 110 | # new format 111 | [row[0], [row[2], row[1], row[3], row[5].map(&:first)]] 112 | else 113 | [row[0], [row[2], row[1], row[3], 6.step(row.size-1, 2).map{|i| row[i]}]] 114 | end 115 | }. 116 | group_by{|sid, _| sid}. 117 | each do |sid, inds| 118 | sp = spaces[sid] 119 | sp.indices = inds.map{|_sid, ind| ind} 120 | end 121 | end 122 | 123 | def _insert(sno, tuple, need_hash, cb) 124 | if !need_hash && sno.is_a?(Integer) && tuple.is_a?(Array) 125 | return conn._insert(sno, tuple, cb) 126 | end 127 | _with_space(sno, cb) do |sp| 128 | _tuple = tuple.is_a?(Hash) ? sp.map_tuple(tuple) : tuple 129 | _cb = need_hash ? sp.wrap_cb(cb) : cb 130 | conn._insert(sp.sid, _tuple, _cb) 131 | end 132 | end 133 | 134 | def _replace(sno, tuple, need_hash, cb) 135 | if !need_hash && sno.is_a?(Integer) && tuple.is_a?(Array) 136 | return conn._replace(sno, tuple, cb) 137 | end 138 | _with_space(sno, cb) do |sp| 139 | _tuple = tuple.is_a?(Hash) ? sp.map_tuple(tuple) : tuple 140 | _cb = need_hash ? sp.wrap_cb(cb) : cb 141 | conn._replace(sp.sid, _tuple, _cb) 142 | end 143 | end 144 | 145 | def _delete(sno, ino, key, need_hash, cb) 146 | ino = 0 if ino.nil? && key.is_a?(Array) 147 | if !need_hash && sno.is_a?(Integer) && ino.is_a?(Integer) && key.is_a?(Array) 148 | return conn._delete(sno, ino, key, cb) 149 | end 150 | _with_space(sno, cb) do |sp| 151 | sp.get_ino(ino, key, ITERATOR_EQ, cb) do |_ino, _key| 152 | _cb = need_hash ? sp.wrap_cb(cb) : cb 153 | conn._delete(sp.sid, _ino, _key, _cb) 154 | end 155 | end 156 | end 157 | 158 | def _select(sno, ino, key, offset, limit, iterator, need_hash, cb) 159 | key = [] if key.nil? 160 | ino = 0 if ino.nil? && key.is_a?(Array) 161 | unless iterator.is_a?(Integer) 162 | if key.empty? && (Array === key || Hash === key) 163 | iterator = ITERATOR_ALL 164 | else 165 | iterator = ::Tarantool16.iter(iterator) 166 | end 167 | end 168 | if sno.is_a?(Integer) && ino.is_a?(Integer) && (key.is_a?(Array) || key.nil?) 169 | return conn._select(sno, ino, key, offset, limit, iterator, cb) 170 | end 171 | _with_space(sno, cb) do |sp| 172 | sp.get_ino(ino, key, iterator, cb) do |_ino, _key| 173 | _cb = need_hash ? sp.wrap_cb(cb) : cb 174 | conn._select(sp.sid, _ino, _key, offset, limit, iterator, _cb) 175 | end 176 | end 177 | end 178 | 179 | def _update(sno, ino, key, ops, need_hash, cb) 180 | ino = 0 if ino.nil? && key.is_a?(Array) 181 | ops_good = ops.is_a?(Array) && ops.all?{|a| a[1].is_a?(Integer)} 182 | if sno.is_a?(Integer) && ino.is_a?(Integer) && key.is_a?(Array) && ops_good 183 | return conn._update(sno, ino, key, ops, cb) 184 | end 185 | _with_space(sno, cb) do |sp| 186 | sp.get_ino(ino, key, ITERATOR_EQ, cb) do |_ino, _key| 187 | _ops = ops_good ? ops : sp.map_ops(ops) 188 | _cb = need_hash ? sp.wrap_cb(cb) : cb 189 | conn._update(sp.sid, _ino, _key, _ops, _cb) 190 | end 191 | end 192 | end 193 | 194 | def _upsert(sno, ino, tuple_key, ops, need_hash, cb) 195 | ino = 0 if ino.nil? 196 | ops_good = ops.is_a?(Array) && ops.all?{|a| a[1].is_a?(Integer)} 197 | if sno.is_a?(Integer) && ino.is_a?(Integer) && tuple_key.is_a?(Array) && ops_good 198 | return conn._upsert(sno, ino, tuple_key, ops, cb) 199 | end 200 | _with_space(sno, cb) do |sp| 201 | _tuple = tuple_key.is_a?(Hash) ? sp.map_tuple(tuple_key) : tuple_key 202 | sp.get_ino(ino, nil, ITERATOR_EQ, cb) do |_ino, _key| 203 | _ops = ops_good ? ops : sp.map_ops(ops) 204 | _cb = need_hash ? sp.wrap_cb(cb) : cb 205 | conn._upsert(sp.sid, _ino, tuple_key, _ops, _cb) 206 | end 207 | end 208 | end 209 | 210 | def _call(name, args, cb) 211 | conn._call(name, args, cb) 212 | end 213 | 214 | def _call16(name, args, cb) 215 | conn._call16(name, args, cb) 216 | end 217 | 218 | def _eval(expr, args, cb) 219 | conn._eval(expr, args, cb) 220 | end 221 | 222 | def _ping(cb) 223 | conn._ping(cb) 224 | end 225 | end 226 | end 227 | -------------------------------------------------------------------------------- /lib/tarantool16/dumb_db.rb: -------------------------------------------------------------------------------- 1 | require_relative 'db' 2 | require_relative 'connection/dumb' 3 | 4 | module Tarantool16 5 | class DumbDB < DB 6 | Connection = Tarantool16::Connection::Dumb 7 | 8 | RETURN_OR_RAISE = lambda{|r| 9 | raise r.error unless r.ok? 10 | r.data 11 | } 12 | RETURN_ONE_OR_RAISE = lambda{|r| 13 | raise r.error unless r.ok? 14 | r.data[0] 15 | } 16 | HUGE_LIMIT = 2**30 17 | 18 | def select(sno, key, opts={}) 19 | ino = opts[:index] 20 | offset = opts[:offset] || 0 21 | limit = opts[:limit] || 2**30 22 | iterator = opts[:iterator] 23 | need_hash = opts.fetch(:hash, Hash === key) 24 | key = _norm_key(key) 25 | _select(sno, ino, key, offset, limit, iterator, need_hash, RETURN_OR_RAISE) 26 | end 27 | 28 | def get(sno, key, opts={}) 29 | ino = opts[:index] 30 | iterator = opts[:iterator] 31 | need_hash = opts.fetch(:hash, Hash === key) 32 | key = _norm_key(key) 33 | _select(sno, ino, key, 0, 1, iterator, need_hash, RETURN_ONE_OR_RAISE) 34 | end 35 | 36 | def insert(sno, tuple, opts = {}) 37 | need_hash = opts[:hash] || tuple.is_a?(Hash) 38 | _insert(sno, tuple, need_hash, RETURN_OR_RAISE) 39 | end 40 | 41 | def replace(sno, tuple, opts = {}) 42 | need_hash = opts[:hash] || tuple.is_a?(Hash) 43 | _replace(sno, tuple, need_hash, RETURN_OR_RAISE) 44 | end 45 | 46 | def delete(sno, key, opts = {}) 47 | key_hash = Hash === key 48 | ino = opts[:index] || (key_hash ? nil : 0) 49 | need_hash = opts.fetch(:hash, key_hash) 50 | key = _norm_key(key) 51 | _delete(sno, ino, key, need_hash, RETURN_OR_RAISE) 52 | end 53 | 54 | def update(sno, key, ops, opts = {}) 55 | key_hash = Hash === key 56 | ino = opts[:index] || (key_hash ? nil : 0) 57 | need_hash = opts.fetch(:hash, key_hash) 58 | key = _norm_key(key) 59 | _update(sno, ino, key, ops, need_hash, RETURN_OR_RAISE) 60 | end 61 | 62 | def upsert(sno, tuple_key, ops, opts = {}) 63 | ino = opts[:index] || 0 64 | need_hash = opts.fetch(:hash, Hash === tuple_key) 65 | _upsert(sno, ino, tuple_key, ops, need_hash, RETURN_OR_RAISE) 66 | end 67 | 68 | def call(name, args) 69 | _call(name, args, RETURN_OR_RAISE) 70 | end 71 | 72 | def call16(name, args) 73 | _call16(name, args, RETURN_OR_RAISE) 74 | end 75 | 76 | def eval(expr, args) 77 | _eval(expr, args, RETURN_OR_RAISE) 78 | end 79 | 80 | def _synchronized 81 | yield 82 | end 83 | 84 | def _norm_key(key) 85 | case key 86 | when Array 87 | key 88 | when Hash 89 | key 90 | else 91 | [key] 92 | end 93 | end 94 | 95 | class SchemaFuture 96 | UNDEF = Object.new.freeze 97 | def initialize 98 | @r = UNDEF 99 | end 100 | def then(cb) 101 | unless @r.equal? UNDEF 102 | return cb.call(@r) 103 | end 104 | raise "DumbDB::ShemaFuture future is not real future :-(" 105 | end 106 | 107 | def then_blk 108 | unless @r.equal? UNDEF 109 | return yield @r 110 | end 111 | raise "DumbDB::ShemaFuture future is not real future :-(" 112 | end 113 | 114 | def set(r) 115 | @r = r 116 | end 117 | end 118 | 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/tarantool16/errors.rb: -------------------------------------------------------------------------------- 1 | 2 | module Tarantool16 3 | class Error < ::StandardError; end 4 | class ConnectionError < Error; end 5 | DBErrors = {} 6 | class SchemaError < Error; end 7 | class DBError < Error 8 | class KnownDBError < DBError 9 | class << self 10 | attr_accessor :return_code 11 | end 12 | def return_code 13 | self.class.return_code 14 | end 15 | end 16 | class UnknownDBError < DBError 17 | attr_accessor :return_code 18 | end 19 | 20 | { 21 | 1=> :ER_ILLEGAL_PARAMS, 22 | 2=> :ER_MEMORY_ISSUE, 23 | 3=> :ER_TUPLE_FOUND, 24 | 4=> :ER_TUPLE_NOT_FOUND, 25 | 5=> :ER_UNSUPPORTED, 26 | 6=> :ER_NONMASTER, 27 | 7=> :ER_READONLY, 28 | 8=> :ER_INJECTION, 29 | 9=> :ER_CREATE_SPACE, 30 | 10=> :ER_SPACE_EXISTS, 31 | 11=> :ER_DROP_SPACE, 32 | 12=> :ER_ALTER_SPACE, 33 | 13=> :ER_INDEX_TYPE, 34 | 14=> :ER_MODIFY_INDEX, 35 | 15=> :ER_LAST_DROP, 36 | 16=> :ER_TUPLE_FORMAT_LIMIT, 37 | 17=> :ER_DROP_PRIMARY_KEY, 38 | 18=> :ER_KEY_PART_TYPE, 39 | 19=> :ER_EXACT_MATCH, 40 | 20=> :ER_INVALID_MSGPACK, 41 | 21=> :ER_PROC_RET, 42 | 22=> :ER_TUPLE_NOT_ARRAY, 43 | 23=> :ER_FIELD_TYPE, 44 | 24=> :ER_FIELD_TYPE_MISMATCH, 45 | 25=> :ER_SPLICE, 46 | 26=> :ER_ARG_TYPE, 47 | 27=> :ER_TUPLE_IS_TOO_LONG, 48 | 28=> :ER_UNKNOWN_UPDATE_OP, 49 | 29=> :ER_UPDATE_FIELD, 50 | 30=> :ER_FIBER_STACK, 51 | 31=> :ER_KEY_PART_COUNT, 52 | 32=> :ER_PROC_LUA, 53 | 33=> :ER_NO_SUCH_PROC, 54 | 34=> :ER_NO_SUCH_TRIGGER, 55 | 35=> :ER_NO_SUCH_INDEX, 56 | 36=> :ER_NO_SUCH_SPACE, 57 | 37=> :ER_NO_SUCH_FIELD, 58 | 38=> :ER_SPACE_FIELD_COUNT, 59 | 39=> :ER_INDEX_FIELD_COUNT, 60 | 40=> :ER_WAL_IO, 61 | 41=> :ER_MORE_THAN_ONE_TUPLE, 62 | 42=> :ER_ACCESS_DENIED, 63 | 43=> :ER_CREATE_USER, 64 | 44=> :ER_DROP_USER, 65 | 45=> :ER_NO_SUCH_USER, 66 | 46=> :ER_USER_EXISTS, 67 | 47=> :ER_PASSWORD_MISMATCH, 68 | 48=> :ER_UNKNOWN_REQUEST_TYPE, 69 | 49=> :ER_UNKNOWN_SCHEMA_OBJECT, 70 | 50=> :ER_CREATE_FUNCTION, 71 | 51=> :ER_NO_SUCH_FUNCTION, 72 | 52=> :ER_FUNCTION_EXISTS, 73 | 53=> :ER_FUNCTION_ACCESS_DENIED, 74 | 54=> :ER_FUNCTION_MAX, 75 | 55=> :ER_SPACE_ACCESS_DENIED, 76 | 56=> :ER_USER_MAX, 77 | 57=> :ER_NO_SUCH_ENGINE, 78 | 58=> :ER_RELOAD_CFG, 79 | 59=> :ER_CFG, 80 | 60=> :ER_SOPHIA, 81 | 61=> :ER_LOCAL_SERVER_IS_NOT_ACTIVE, 82 | 62=> :ER_UNKNOWN_SERVER, 83 | 63=> :ER_CLUSTER_ID_MISMATCH, 84 | 64=> :ER_INVALID_UUID, 85 | 65=> :ER_CLUSTER_ID_IS_RO, 86 | 66=> :ER_RESERVED66, 87 | 67=> :ER_SERVER_ID_IS_RESERVED, 88 | 68=> :ER_INVALID_ORDER, 89 | 69=> :ER_MISSING_REQUEST_FIELD, 90 | 70=> :ER_IDENTIFIER, 91 | 71=> :ER_DROP_FUNCTION, 92 | 72=> :ER_ITERATOR_TYPE, 93 | 73=> :ER_REPLICA_MAX, 94 | 74=> :ER_INVALID_XLOG, 95 | 75=> :ER_INVALID_XLOG_NAME, 96 | 76=> :ER_INVALID_XLOG_ORDER, 97 | 77=> :ER_NO_CONNECTION, 98 | 78=> :ER_TIMEOUT, 99 | 79=> :ER_ACTIVE_TRANSACTION, 100 | 80=> :ER_NO_ACTIVE_TRANSACTION, 101 | 81=> :ER_CROSS_ENGINE_TRANSACTION, 102 | 82=> :ER_NO_SUCH_ROLE, 103 | 83=> :ER_ROLE_EXISTS, 104 | 84=> :ER_CREATE_ROLE, 105 | 85=> :ER_INDEX_EXISTS, 106 | 86=> :ER_TUPLE_REF_OVERFLOW, 107 | 87=> :ER_ROLE_LOOP, 108 | 88=> :ER_GRANT, 109 | 89=> :ER_PRIV_GRANTED, 110 | 90=> :ER_ROLE_GRANTED, 111 | 91=> :ER_PRIV_NOT_GRANTED, 112 | 92=> :ER_ROLE_NOT_GRANTED, 113 | 93=> :ER_MISSING_SNAPSHOT, 114 | 94=> :ER_CANT_UPDATE_PRIMARY_KEY, 115 | 95=> :ER_UPDATE_INTEGER_OVERFLOW, 116 | 96=> :ER_GUEST_USER_PASSWORD, 117 | 97=> :ER_TRANSACTION_CONFLICT, 118 | 98=> :ER_UNSUPPORTED_ROLE_PRIV, 119 | 99=> :ER_LOAD_FUNCTION, 120 | 100=> :ER_FUNCTION_LANGUAGE, 121 | 101=> :ER_RTREE_RECT, 122 | 102=> :ER_PROC_C, 123 | 103=> :ER_UNKNOWN_RTREE_INDEX_DISTANCE_TYPE, 124 | 104=> :ER_PROTOCOL, 125 | 105=> :ER_UPSERT_UNIQUE_SECONDARY_KEY, 126 | 106=> :ER_WRONG_INDEX_RECORD, 127 | 107=> :ER_WRONG_INDEX_PARTS, 128 | 108=> :ER_WRONG_INDEX_OPTIONS, 129 | 109=> :ER_WRONG_SCHEMA_VERSION, 130 | 110=> :ER_SLAB_ALLOC_MAX, 131 | 111=> :ER_WRONG_SPACE_OPTIONS, 132 | 112=> :ER_UNSUPPORTED_INDEX_FEATURE, 133 | 113=> :ER_VIEW_IS_RO, 134 | }.each do |n, s| 135 | klass = Class.new(KnownDBError) 136 | klass.return_code = n 137 | Tarantool16::DBErrors[n] = klass 138 | Tarantool16.const_set(s, klass) 139 | end 140 | def self.with_code_message(n, m="") 141 | if klass = DBErrors[n] 142 | klass.new(m) 143 | else 144 | e = UnknownDBError.new(m) 145 | e.return_code = n 146 | e 147 | end 148 | end 149 | 150 | def inspect 151 | "<#{self.class.name} return_code=#{return_code} message=#{message}>" 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /lib/tarantool16/query.rb: -------------------------------------------------------------------------------- 1 | relative_require 'consts' 2 | 3 | module Tarantool16 4 | class Query 5 | def initialize(db) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/tarantool16/response.rb: -------------------------------------------------------------------------------- 1 | module Tarantool16 2 | class Option 3 | attr :error, :data 4 | def initialize(err, data) 5 | @error = err 6 | @data = data 7 | end 8 | 9 | def ok? 10 | !@error 11 | end 12 | 13 | def raise_if_error! 14 | raise @error if @error 15 | end 16 | 17 | def self.ok(data) 18 | new(nil, data) 19 | end 20 | 21 | def self.error(err, message = nil) 22 | if err.is_a? Class 23 | err = err.new message 24 | end 25 | new(err, nil) 26 | end 27 | 28 | def inspect 29 | if ok? 30 | "