tarantool 0.1

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.
@@ -0,0 +1,19 @@
1
+ module Tarantool
2
+ require 'tarantool/request'
3
+ module Requests
4
+ REQUEST_TYPES = {
5
+ insert: 13,
6
+ select: 17,
7
+ update: 19,
8
+ delete: 21,
9
+ call: 22,
10
+ ping: 65280
11
+ }
12
+ BOX_RETURN_TUPLE = 1
13
+ BOX_ADD = 2
14
+
15
+ %w{insert select update delete call ping}.each do |v|
16
+ require "tarantool/requests/#{v}"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ module Tarantool
2
+ module Requests
3
+ class Call < Request
4
+ request_type :call
5
+
6
+ attr_reader :flags, :proc_name, :tuple
7
+ def parse_args
8
+ @flags = params[:return_tuple] ? 1 : 0
9
+ @proc_name = params[:proc_name]
10
+ @tuple = params[:args] || []
11
+ end
12
+
13
+ def make_body
14
+ [flags].pack('L') +
15
+ self.class.pack_field(proc_name) +
16
+ self.class.pack_tuple(*tuple)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ module Tarantool
2
+ module Requests
3
+ class Delete < Request
4
+ request_type :delete
5
+
6
+ attr_reader :flags, :key
7
+ def parse_args
8
+ @flags = params[:return_tuple] ? 1 : 0
9
+ @key = params[:key] || args.first
10
+ end
11
+
12
+ def make_body
13
+ [space_no, flags].pack('LL') +
14
+ self.class.pack_tuple(key)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module Tarantool
2
+ module Requests
3
+ class Insert < Request
4
+ request_type :insert
5
+
6
+ attr_reader :flags, :values
7
+ def parse_args
8
+ @flags = BOX_ADD
9
+ @flags |= BOX_RETURN_TUPLE if params[:return_tuple]
10
+ @values = params[:values] || args
11
+ end
12
+
13
+ def make_body
14
+ [space_no, flags].pack('LL') +
15
+ self.class.pack_tuple(*values)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ module Tarantool
2
+ module Requests
3
+ class Ping < Request
4
+ request_type :ping
5
+
6
+ def make_body
7
+ @start_time = Time.now
8
+ ''
9
+ end
10
+
11
+ def make_response(data)
12
+ succeed(Time.now - @start_time)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ module Tarantool
2
+ module Requests
3
+ class Select < Request
4
+ request_type :select
5
+
6
+ attr_reader :index_no, :offset, :limit, :count, :tuples
7
+ def parse_args
8
+ @index_no = params[:index_no] || 0
9
+ @offset = params[:offset] || 0
10
+ @limit = params[:limit] || -1
11
+ @tuples = params[:values] || args
12
+ raise(ArgumentError.new('values are required')) if tuples.empty?
13
+ params[:return_tuple] = true
14
+ end
15
+
16
+ def make_body
17
+ [space_no, index_no, offset, limit, tuples.size].pack('LLLLL') +
18
+ tuples.map { |tuple| self.class.pack_tuple(*tuple) }.join
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ module Tarantool
2
+ module Requests
3
+ class Update < Request
4
+ request_type :update
5
+
6
+ OP_CODES = { set: 0, add: 1, and: 2, or: 3, xor: 4, splice: 5 }
7
+
8
+ def self.pack_ops(ops)
9
+ ops.map do |op|
10
+ raise ArgumentError.new('Operation should be array of size 3') unless op.size == 3
11
+
12
+ field_no, op_symbol, op_arg = op
13
+ op_code = OP_CODES[op_symbol] || raise(ArgumentError.new("Unsupported operation symbol '#{op_symbol}'"))
14
+
15
+ [field_no, op_code].pack('LC') + self.pack_field(op_arg)
16
+ end.join
17
+ end
18
+
19
+ attr_reader :flags, :key, :ops
20
+ def parse_args
21
+ @flags = params[:return_tuple] ? 1 : 0
22
+ @key = params[:key] || args.first
23
+ @ops = params[:ops]
24
+ raise ArgumentError.new('Key is required') unless key
25
+ end
26
+
27
+ def make_body
28
+ [space_no, flags].pack('LL') +
29
+ self.class.pack_tuple(key) +
30
+ [ops.size].pack('L') +
31
+ self.class.pack_ops(ops)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,58 @@
1
+ module Tarantool
2
+ class Field
3
+ attr_reader :data
4
+ def initialize(data)
5
+ @data = data
6
+ end
7
+
8
+ def to_i
9
+ if data.bytesize == 4
10
+ data.unpack('L')[0]
11
+ elsif data.bytesize == 8
12
+ data.unpack('Q')[0]
13
+ else
14
+ raise ValueError.new("Unable to cast field to int: length must be 4 or 8 bytes, field length is #{data.size}")
15
+ end
16
+ end
17
+
18
+ def to_s
19
+ data.dup.force_encoding('utf-8')
20
+ end
21
+ end
22
+ class Response
23
+ attr_reader :tuples_affected, :offset, :tuples
24
+ def initialize(data, params = {})
25
+ @offset = 0
26
+ @tuples_affected, = data[0, 4].unpack('L')
27
+ @offset += 4
28
+ if params[:return_tuple]
29
+ @tuples = (1..tuples_affected).map do
30
+ unpack_tuple(data)
31
+ end
32
+ else
33
+ tuples_affected
34
+ end
35
+ end
36
+
37
+ # Only select request can return many tuples
38
+ def tuple
39
+ tuples.first
40
+ end
41
+
42
+ def unpack_tuple(data)
43
+ byte_size, cardinality = data[offset, 8].unpack("LL")
44
+ @offset += 8
45
+ tuple_data = data[offset, byte_size]
46
+ @offset += byte_size
47
+ (1..cardinality).map do
48
+ Field.new unpack_field(tuple_data)
49
+ end
50
+ end
51
+
52
+ def unpack_field(data)
53
+ byte_size, = data.unpack('w')
54
+ data.slice!(0, [byte_size].pack('w').bytesize) # ololo
55
+ data.slice!(0, byte_size)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,9 @@
1
+ module Tarantool
2
+ module Serializers
3
+ MAP = {}
4
+ %w{string integer}.each do |v|
5
+ require "tarantool/serializers/#{v}"
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ require 'bson'
2
+ module Tarantool
3
+ module Serializers
4
+ class BSON
5
+ Serializers::MAP[:bson] = self
6
+ def self.encode(value)
7
+ ::BSON.serialize(value).to_s
8
+ end
9
+
10
+ def self.decode(field)
11
+ ::BSON.deserialize(field.to_s)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ module Tarantool
2
+ module Serializers
3
+ class Integer
4
+ Serializers::MAP[:integer] = self
5
+ def self.encode(value)
6
+ value.to_i
7
+ end
8
+
9
+ def self.decode(field)
10
+ field.to_i
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Tarantool
2
+ module Serializers
3
+ class String
4
+ Serializers::MAP[:string] = self
5
+ def self.encode(value)
6
+ value.to_s
7
+ end
8
+
9
+ def self.decode(field)
10
+ field.to_s
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ module Tarantool
2
+ class Space
3
+ attr_accessor :space_no
4
+ attr_reader :connection
5
+ def initialize(connection, space_no = nil)
6
+ @connection = connection
7
+ @space_no = space_no
8
+ end
9
+
10
+ def select(*args)
11
+ request Requests::Select, args
12
+ end
13
+
14
+ def call(*args)
15
+ request Requests::Call, args
16
+ end
17
+
18
+ def insert(*args)
19
+ request Requests::Insert, args
20
+ end
21
+
22
+
23
+ def delete(*args)
24
+ request Requests::Delete, args
25
+ end
26
+
27
+ def update(*args)
28
+ request Requests::Update, args
29
+ end
30
+
31
+ def ping(*args)
32
+ request Requests::Ping, args
33
+ end
34
+
35
+ def request(cls, args)
36
+ cls.new(self, *args).perform
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ require 'tarantool'
2
+ require 'em-synchrony'
3
+
4
+ module Tarantool
5
+ class Space
6
+ alias :deffered_request :request
7
+ def request(*args)
8
+ EM::Synchrony.sync(deffered_request(*args)).tap do |v|
9
+ raise v if v.is_a?(Exception)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module Helpers
2
+ module Let
3
+ def let(name, &blk)
4
+ define_method name do
5
+ @let_assigments ||= {}
6
+ @let_assigments[name] ||= send(:"original_#{name}")
7
+ end
8
+ define_method "original_#{name}", &blk
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module Helpers
2
+ module Truncate
3
+ def teardown
4
+ while (res = Tarantool.call(proc_name: 'box.select_range', args: [Tarantool.singleton_space.space_no.to_s, '0', '100'], return_tuple: true)) && res.tuples.size > 0
5
+ res.tuples.each do |k, *_|
6
+ Tarantool.delete key: k
7
+ end
8
+ end
9
+ super
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,28 @@
1
+ require 'bundler'
2
+ ENV['BUNDLE_GEMFILE'] = File.expand_path('../../Gemfile', __FILE__)
3
+ Bundler.setup
4
+
5
+ require 'minitest/spec'
6
+
7
+ require 'helpers/let'
8
+ require 'helpers/truncate'
9
+ require 'rr'
10
+
11
+ require 'tarantool/synchrony'
12
+
13
+ config = { host: '10.211.55.3', port: 33013, space_no: 0 }
14
+
15
+ Tarantool.configure config
16
+
17
+ class MiniTest::Unit::TestCase
18
+ extend Helpers::Let
19
+ include RR::Adapters::MiniTest
20
+ end
21
+
22
+ at_exit {
23
+ EM.synchrony do
24
+ exit_code = MiniTest::Unit.new.run(ARGV)
25
+ EM.stop
26
+ exit_code
27
+ end
28
+ }
@@ -0,0 +1,32 @@
1
+ slab_alloc_arena = 0.1
2
+ pid_file = "box.pid"
3
+
4
+ logger="cat - >> tarantool.log"
5
+
6
+ primary_port = 33013
7
+ secondary_port = 33014
8
+ admin_port = 33015
9
+
10
+ rows_per_wal = 50
11
+
12
+ space[0].enabled = 1
13
+
14
+ space[0].index[0].type = "HASH"
15
+ space[0].index[0].unique = 1
16
+ space[0].index[0].key_field[0].fieldno = 0
17
+ space[0].index[0].key_field[0].type = "STR"
18
+
19
+ space[0].index[1].type = "TREE"
20
+ space[0].index[1].unique = 1
21
+ space[0].index[1].key_field[0].fieldno = 1
22
+ space[0].index[1].key_field[0].type = "STR"
23
+ space[0].index[1].key_field[1].fieldno = 2
24
+ space[0].index[1].key_field[1].type = "STR"
25
+
26
+
27
+ space[1].enabled = 1
28
+
29
+ space[1].index[0].type = "HASH"
30
+ space[1].index[0].unique = 1
31
+ space[1].index[0].key_field[0].fieldno = 0
32
+ space[1].index[0].key_field[0].type = "NUM"
@@ -0,0 +1,247 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'spec_helper'
3
+ require 'tarantool/record'
4
+ require 'yajl'
5
+ require 'tarantool/serializers/bson'
6
+ describe Tarantool::Record do
7
+ include Helpers::Truncate
8
+ before do
9
+ Tarantool.singleton_space.space_no = 0
10
+ end
11
+ let(:user_class) do
12
+ Class.new(Tarantool::Record) do
13
+ def self.name # For naming
14
+ "User"
15
+ end
16
+
17
+ field :login, :string
18
+ field :name, :string
19
+ field :email, :string
20
+ field :apples_count, :integer, default: 0
21
+ index :name, :email
22
+ end
23
+ end
24
+
25
+ let(:user) { user_class.new }
26
+ it "should set and get attributes" do
27
+ user.name = 'Andrew'
28
+ user.name.must_equal 'Andrew'
29
+ end
30
+
31
+ describe "inheritance" do
32
+ let(:author_class) do
33
+ Class.new(user_class) do
34
+ field :best_book, Integer
35
+ end
36
+ end
37
+ let(:artist_class) do
38
+ Class.new(user_class) do
39
+ field :imdb_id, Integer
40
+ end
41
+ end
42
+
43
+ describe "Artist from User" do
44
+ it "should has only itself field" do
45
+ artist_class.fields.keys.must_equal [:login, :name, :email, :apples_count, :imdb_id]
46
+ end
47
+ end
48
+
49
+ it "should has same space no as parent" do
50
+ user_class.space_no = 1
51
+ artist_class.space_no.must_equal 1
52
+ end
53
+
54
+ it "should has different space no to parent if setted" do
55
+ artist_class.space_no = 1
56
+ user_class.space_no.must_equal 0
57
+ artist_class.space_no.must_equal 1
58
+ end
59
+ end
60
+
61
+ describe "detect_index_no" do
62
+ let(:select) { user_class.select }
63
+ it "should return 0 for :login" do
64
+ select.detect_index_no([:login]).must_equal 0
65
+ end
66
+ it "should return 1 for :name" do
67
+ select.detect_index_no([:name]).must_equal 1
68
+ end
69
+ it "should return 1 for :name, :email" do
70
+ select.detect_index_no([:name, :email]).must_equal 1
71
+ end
72
+ it "should return nil for :email" do
73
+ select.detect_index_no([:email]).must_be_nil
74
+ end
75
+ it "should return nil for :login, :name" do
76
+ select.detect_index_no([:login, :name]).must_be_nil
77
+ end
78
+ end
79
+
80
+ describe "save" do
81
+ it "should save and select record" do
82
+ u = user_class.new login: 'prepor', name: 'Andrew', email: 'ceo@prepor.ru'
83
+ u.save
84
+ u = user_class.find 'prepor'
85
+ u.id.must_equal 'prepor'
86
+ u.email.must_equal 'ceo@prepor.ru'
87
+ u.name.must_equal 'Andrew'
88
+ u.apples_count.must_equal 0
89
+ end
90
+
91
+ it "should update dirty attributes" do
92
+ u = user_class.create login: 'prepor', name: 'Andrew', email: 'ceo@prepor.ru'
93
+ u.name = 'Petr'
94
+ u.save
95
+ u = user_class.find 'prepor'
96
+ u.email.must_equal 'ceo@prepor.ru'
97
+ u.name.must_equal 'Petr'
98
+ end
99
+
100
+ describe "with nils" do
101
+ before do
102
+ user_class.field :info, :bson
103
+ end
104
+ it "should work properly with nils values" do
105
+ u = user_class.create login: 'prepor', name: 'Andrew', email: 'ceo@prepor.ru', apples_count: nil
106
+ u.info.must_be_nil
107
+ u.apples_count.must_be_nil
108
+ u = u.reload
109
+ u.info.must_be_nil
110
+ u.apples_count.must_be_nil
111
+ u.info = {'bio' => 'hi!'}
112
+ u.apples_count = 1
113
+ u.save
114
+ u = u.reload
115
+ u.info.must_equal({ 'bio' => 'hi!' })
116
+ u.apples_count.must_equal 1
117
+ end
118
+ end
119
+ end
120
+
121
+ describe "reload" do
122
+ it "should reload current record" do
123
+ u = user_class.create login: 'prepor', name: 'Andrew', email: 'ceo@prepor.ru'
124
+ u.name = 'Petr'
125
+ u.reload.name.must_equal 'Andrew'
126
+ end
127
+ end
128
+
129
+ describe "increment" do
130
+ let(:user) { user_class.create login: 'prepor', name: 'Andrew', email: 'ceo@prepor.ru' }
131
+ it "should increment apples count by 1" do
132
+ user.increment :apples_count
133
+ user.reload.apples_count.must_equal 1
134
+ end
135
+
136
+ it "should increment apples count by 3" do
137
+ user.increment :apples_count, 3
138
+ user.reload.apples_count.must_equal 3
139
+ end
140
+ end
141
+
142
+ describe "destroy" do
143
+ it "should destroy record" do
144
+ u = user_class.create login: 'prepor', name: 'Andrew', email: 'ceo@prepor.ru'
145
+ u.destroy
146
+ u.reload.must_be_nil
147
+ end
148
+ end
149
+
150
+ describe "validations" do
151
+ describe "with validator on login size" do
152
+ before do
153
+ user_class.validates_length_of(:login, minimum: 3)
154
+ end
155
+ it "should invalidate all records with login less then 3 chars" do
156
+ u = user_class.new login: 'pr', name: 'Andrew', email: 'ceo@prepor.ru'
157
+ u.save.must_equal false
158
+ u.valid?.must_equal false
159
+ u.errors.size.must_equal 1
160
+ u.login = 'prepor'
161
+ u.save.must_equal true
162
+ u.valid?.must_equal true
163
+ u.errors.size.must_equal 0
164
+ end
165
+ end
166
+ end
167
+
168
+ describe "callbacks" do
169
+ it "should run before / after create callbackss in right places" do
170
+ user_class.before_create :action_before_create
171
+ user_class.after_create :action_after_create
172
+
173
+ u = user_class.new login: 'prepor', name: 'Andrew', email: 'ceo@prepor.ru'
174
+ mock(u).action_before_create { u.new_record?.must_equal true }
175
+ mock(u).action_after_create { u.new_record?.must_equal false }
176
+ u.save
177
+ end
178
+ end
179
+
180
+ describe "serialization" do
181
+ it "should support AM serialization API" do
182
+ h = { login: 'prepor', name: 'Andrew', email: 'ceo@prepor.ru' }
183
+ u = user_class.create h
184
+ u.as_json.must_equal({ 'user' => h.merge(apples_count: 0) })
185
+ end
186
+
187
+ describe "fields serilizers" do
188
+ before do
189
+ user_class.field :info, :bson
190
+ end
191
+
192
+ it "should serialise and deserialize info field" do
193
+ info = { 'bio' => "hi!", 'age' => 23, 'hobbies' => ['mufa', 'tuka'] }
194
+ u = user_class.create login: 'prepor', name: 'Andrew', email: 'ceo@prepor.ru', info: info
195
+ u.info['hobbies'].must_equal ['mufa', 'tuka']
196
+ u = u.reload
197
+ u.info['hobbies'].must_equal ['mufa', 'tuka']
198
+ end
199
+ end
200
+ end
201
+
202
+ describe "select" do
203
+ describe "by name Andrew" do
204
+ let(:select) { user_class.where(name: 'Andrew') }
205
+ before do
206
+ user_class.create login: 'prepor', name: 'Andrew', email: 'ceo@prepor.ru'
207
+ user_class.create login: 'petro', name: 'Petr', email: 'petro@gmail.com'
208
+ user_class.create login: 'ruden', name: 'Andrew', email: 'rudenkoco@gmail.com'
209
+ end
210
+ it "should select all records with name == 'Andrew'" do
211
+ select.all.map(&:login).must_equal ['prepor', 'ruden']
212
+ end
213
+
214
+ it "should select first record with name == 'Andrew'" do
215
+ select.first.login.must_equal 'prepor'
216
+ end
217
+
218
+ it "should select 1 record by name and email" do
219
+ user_class.where(name: 'Andrew', email: 'rudenkoco@gmail.com').map(&:login).must_equal ['ruden']
220
+ end
221
+
222
+ it "should select 2 record by name and email" do
223
+ user_class.where(name: ['Andrew', 'Andrew'], email: ['ceo@prepor.ru', 'rudenkoco@gmail.com']).map(&:login).must_equal ['prepor', 'ruden']
224
+ end
225
+
226
+ it "should select 3 record by names" do
227
+ user_class.where(name: ['Andrew', 'Petr']).map(&:login).must_equal ['prepor', 'ruden', 'petro']
228
+ end
229
+
230
+ describe "with limit 1" do
231
+ let(:select) { super().limit(1) }
232
+ it "should select first record with name == 'Andrew'" do
233
+ select.map(&:login).must_equal ['prepor']
234
+ end
235
+
236
+ describe "with offset 1" do
237
+ let(:select) { super().offset(1) }
238
+ it "should select last record with name == 'Andrew'" do
239
+ select.map(&:login).must_equal ['ruden']
240
+ end
241
+ end
242
+ end
243
+
244
+ end
245
+ end
246
+
247
+ end