tarantool 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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