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.
data/lib/tarantool.rb ADDED
@@ -0,0 +1,44 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'eventmachine'
3
+ require 'em-synchrony'
4
+
5
+ module Tarantool
6
+ VERSION = '0.1'
7
+ extend self
8
+ require 'tarantool/space'
9
+ require 'tarantool/connection'
10
+ require 'tarantool/requests'
11
+ require 'tarantool/response'
12
+ require 'tarantool/exceptions'
13
+ require 'tarantool/serializers'
14
+
15
+ def singleton_space
16
+ @singleton_space ||= Space.new connection, @config[:space_no]
17
+ end
18
+
19
+ def connection
20
+ @connection ||= begin
21
+ raise "Tarantool.configure before connect" unless @config
22
+ EM.connect @config[:host], @config[:port], Tarantool::Connection
23
+ end
24
+ end
25
+
26
+ def space(no = nil)
27
+ Space.new connection, no || @config[:space_no]
28
+ end
29
+
30
+ def configure(config = {})
31
+ @config = config
32
+ end
33
+
34
+ def hexdump(string)
35
+ string.unpack('C*').map{ |c| "%02x" % c }.join(' ')
36
+ end
37
+
38
+ [:select, :update, :insert, :delete, :call, :ping].each do |v|
39
+ define_method v do |*params|
40
+ singleton_space.send v, *params
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,54 @@
1
+ require 'em/protocols/fixed_header_and_body'
2
+ module Tarantool
3
+ class Connection < EM::Connection
4
+ include EM::Protocols::FixedHeaderAndBody
5
+
6
+ header_size 12
7
+
8
+ def next_request_id
9
+ @request_id ||= 0
10
+ @request_id += 1
11
+ if @request_id > 0xffffffff
12
+ @request_id = 0
13
+ end
14
+ @request_id
15
+ end
16
+
17
+ def connection_completed
18
+ @connected = true
19
+ end
20
+
21
+ # begin FixedHeaderAndBody API
22
+ def body_size
23
+ @body_size
24
+ end
25
+
26
+ def receive_header(header)
27
+ @type, @body_size, @request_id = header.unpack('L3')
28
+ end
29
+
30
+ def receive_body(data)
31
+ clb = waiting_requests[@request_id]
32
+ raise UnexpectedResponse.new("For request id #{request_id}") unless clb
33
+ clb.call data
34
+ end
35
+ # end FixedHeaderAndBody API
36
+
37
+ def waiting_requests
38
+ @waiting_requests ||= {}
39
+ end
40
+
41
+ def send_packet(request_id, data, &clb)
42
+ send_data data
43
+ waiting_requests[request_id] = clb
44
+ end
45
+
46
+ def close_connection(*args)
47
+ super(*args)
48
+ end
49
+
50
+ def unbind
51
+ raise CouldNotConnect.new unless @connected
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,11 @@
1
+ module Tarantool
2
+ class TarantoolError < StandardError; end
3
+ class UndefinedRequestType < TarantoolError; end
4
+ class CouldNotConnect < TarantoolError; end
5
+ class BadReturnCode < TarantoolError; end
6
+ class StringTooLong < TarantoolError; end
7
+ class ArgumentError < TarantoolError; end
8
+ class UnexpectedResponse < TarantoolError; end
9
+ class UndefinedSpace < TarantoolError; end
10
+ class ValueError < TarantoolError; end
11
+ end
@@ -0,0 +1,316 @@
1
+ require 'active_model'
2
+ require 'tarantool/synchrony'
3
+ module Tarantool
4
+ class Select
5
+ include Enumerable
6
+ attr_reader :record
7
+ def initialize(record)
8
+ @record = record
9
+ end
10
+
11
+ def space_no
12
+ record.space_no
13
+ end
14
+
15
+ def each(&blk)
16
+ res = Tarantool.select(*@tuples, index_no: @index_no, limit: @limit, offset: @offset).tuples
17
+ res.each do |tuple|
18
+ blk.call record.from_server(tuple)
19
+ end
20
+ end
21
+
22
+ def limit(limit)
23
+ @limit = limit
24
+ self
25
+ end
26
+
27
+ def offset(offset)
28
+ @offset = offset
29
+ self
30
+ end
31
+
32
+ # id: 1
33
+ # id: [1, 2, 3]
34
+ # [{ name: 'a', email: 'a'}, { name: 'b', email: 'b'}]
35
+ def where(params)
36
+ raise SelectError.new('Where condition already setted') if @index_no # todo?
37
+ keys, @tuples = case params
38
+ when Hash
39
+ ordered_keys = record.ordered_keys params.keys
40
+ # name: ['a', 'b'], email: ['c', 'd'] => [['a', 'c'], ['b', 'd']]
41
+ if params.values.first.is_a?(Array)
42
+ [ordered_keys, params[ordered_keys.first].zip(*ordered_keys[1, ordered_keys.size].map { |k| params[k] })]
43
+ else
44
+ [ordered_keys, [record.hash_to_tuple(params)]]
45
+ end
46
+ when Array
47
+ [params.first.keys, params.map { |v| record.hash_to_tuple(v) }]
48
+ end
49
+ @index_no = detect_index_no keys
50
+ raise ArgumentError.new("Undefined index for keys #{keys}") unless @index_no
51
+ self
52
+ end
53
+
54
+ # # works fine only on TREE index
55
+ # def batches(count = 1000, &blk)
56
+ # raise ArgumentError.new("Only one tuple provided in batch selects") if @tuples.size > 1
57
+
58
+ # end
59
+
60
+ # def _batch_exec
61
+ # Tarantool.call proc_name: 'box.select_range', args: [space_no.to_s, @index_no.to_s, count.to_s] + @tuples.first.map(&:to_s), return_tuple: true
62
+ # end
63
+
64
+ def batches_each(&blk)
65
+ batches { |records| records.each(&blk) }
66
+ end
67
+
68
+ def all
69
+ to_a
70
+ end
71
+
72
+ def first
73
+ limit(1).all.first
74
+ end
75
+
76
+ def detect_index_no(keys)
77
+ index_no = nil
78
+ record.indexes.each_with_index do |v, i|
79
+ keys_inst = keys.dup
80
+ v.each do |index_part|
81
+ unless keys_inst.delete(index_part)
82
+ break
83
+ end
84
+ if keys_inst.size == 0
85
+ index_no = i
86
+ end
87
+ end
88
+ break if index_no
89
+ end
90
+ index_no
91
+ end
92
+ end
93
+ class Record
94
+ extend ActiveModel::Naming
95
+ include ActiveModel::AttributeMethods
96
+ include ActiveModel::Validations
97
+ include ActiveModel::Serialization
98
+ extend ActiveModel::Callbacks
99
+ include ActiveModel::Dirty
100
+
101
+ include ActiveModel::Serializers::JSON
102
+ include ActiveModel::Serializers::Xml
103
+
104
+ define_model_callbacks :save, :create, :update, :destroy
105
+
106
+ class_attribute :fields
107
+ self.fields = {}
108
+
109
+ class_attribute :default_values
110
+ self.default_values = {}
111
+
112
+ class_attribute :primary_index
113
+ class_attribute :indexes
114
+ self.indexes = []
115
+
116
+ class_attribute :space_no
117
+ define_attr_method :space_no do
118
+ original_space_no || 0
119
+ end
120
+ class << self
121
+ def field(name, type, params = {})
122
+ define_attribute_method name
123
+ self.fields = fields.merge name => { type: type, field_no: fields.size, params: params }
124
+ unless self.primary_index
125
+ self.primary_index = name
126
+ index name
127
+ end
128
+ if params[:default]
129
+ self.default_values = default_values.merge name => params[:default]
130
+ end
131
+ define_method name do
132
+ attributes[name]
133
+ end
134
+ define_method "#{name}=" do |v|
135
+ send("#{name}_will_change!") unless v == attributes[name]
136
+ attributes[name] = v
137
+ end
138
+ end
139
+
140
+ def index(*fields)
141
+ self.indexes = (indexes.dup << fields).sort_by { |v| v.size }
142
+ end
143
+
144
+ def find(*keys)
145
+ res = space.select(*keys)
146
+ if keys.size == 1
147
+ if res.tuple
148
+ from_server res.tuple
149
+ else
150
+ nil
151
+ end
152
+ else
153
+ res.tuples.map { |tuple| from_server tuple }
154
+ end
155
+ end
156
+
157
+ def select
158
+ Select.new(self)
159
+ end
160
+
161
+ %w{where limit offset}.each do |v|
162
+ define_method v do |*args|
163
+ select.send(v, *args)
164
+ end
165
+ end
166
+
167
+ def create(attribites = {})
168
+ new(attribites).tap { |o| o.save }
169
+ end
170
+
171
+ def from_server(tuple)
172
+ new(tuple_to_hash(tuple)).tap { |v| v.old_record! }
173
+ end
174
+
175
+ def space
176
+ @space ||= Tarantool.space space_no
177
+ end
178
+
179
+ def tuple_to_hash(tuple)
180
+ fields.keys.zip(tuple).inject({}) do |memo, (k, v)|
181
+ memo[k] = _cast(k, v) unless v.nil?
182
+ memo
183
+ end
184
+ end
185
+
186
+ def hash_to_tuple(hash, with_nils = false)
187
+ res = []
188
+ fields.keys.each do |k|
189
+ v = hash[k]
190
+ res << _cast(k, v) if with_nils || !v.nil?
191
+ end
192
+ res
193
+ end
194
+
195
+ def ordered_keys(keys)
196
+ fields.keys.inject([]) do |memo, k|
197
+ keys.each do |k2|
198
+ memo << k2 if k2 == k
199
+ end
200
+ memo
201
+ end
202
+ end
203
+
204
+ def _cast(name, value)
205
+ type = self.fields[name][:type]
206
+ serializer = _get_serializer(type)
207
+ if value.is_a?(Field)
208
+ return nil if value.data == "\0"
209
+ serializer.decode(value)
210
+ else
211
+ return "\0" if value.nil?
212
+ serializer.encode(value)
213
+ end
214
+ end
215
+
216
+ def _get_serializer(type)
217
+ Serializers::MAP[type] || raise(TarantoolError.new("Undefind serializer #{type}"))
218
+ end
219
+ end
220
+
221
+ attr_accessor :new_record
222
+ def initialize(attributes = {})
223
+ attributes.each do |k, v|
224
+ send("#{k}=", v)
225
+ end
226
+ @new_record = true
227
+ end
228
+
229
+ def id
230
+ attributes[self.class.primary_index]
231
+ end
232
+
233
+ def space
234
+ self.class.space
235
+ end
236
+
237
+ def new_record?
238
+ @new_record
239
+ end
240
+
241
+ def attributes
242
+ @attributes ||= self.class.default_values.dup
243
+ end
244
+
245
+ def new_record!
246
+ @new_record = true
247
+ end
248
+
249
+ def old_record!
250
+ @new_record = false
251
+ end
252
+
253
+ def save
254
+ def in_callbacks(&blk)
255
+ run_callbacks(:save) { run_callbacks(new_record? ? :create : :update, &blk)}
256
+ end
257
+ in_callbacks do
258
+ if valid?
259
+ if new_record?
260
+ space.insert(*to_tuple)
261
+ else
262
+ ops = changed.inject([]) do |memo, k|
263
+ k = k.to_sym
264
+ memo << [field_no(k), :set, self.class._cast(k, attributes[k])] if attributes[k]
265
+ memo
266
+ end
267
+ space.update id, ops: ops
268
+ end
269
+ @previously_changed = changes
270
+ @changed_attributes.clear
271
+ old_record!
272
+ true
273
+ else
274
+ false
275
+ end
276
+ end
277
+ end
278
+
279
+ def update_attribute(field, value)
280
+ self.send("#{field}=", value)
281
+ save
282
+ end
283
+
284
+ def update_attributes(attributes)
285
+ attributes.each do |k, v|
286
+ self.send("#{k}=", v)
287
+ end
288
+ save
289
+ end
290
+
291
+ def increment(field, by = 1)
292
+ space.update id, ops: [[field_no(field), :add, by]]
293
+ end
294
+
295
+ def destroy
296
+ run_callbacks :destroy do
297
+ space.delete id
298
+ true
299
+ end
300
+ end
301
+
302
+ def to_tuple
303
+ self.class.hash_to_tuple attributes, true
304
+ end
305
+
306
+ def field_no(name)
307
+ self.class.fields[name][:field_no]
308
+ end
309
+
310
+ # return new object, not reloading itself as AR-model
311
+ def reload
312
+ self.class.find(id)
313
+ end
314
+
315
+ end
316
+ end
@@ -0,0 +1,94 @@
1
+ module Tarantool
2
+ class Request
3
+ include EM::Deferrable
4
+
5
+ class << self
6
+ def request_type(name = nil)
7
+ if name
8
+ @request_type = Tarantool::Requests::REQUEST_TYPES[name] || raise(UndefinedRequestType)
9
+ else
10
+ @request_type
11
+ end
12
+ end
13
+
14
+ def pack_tuple(*values)
15
+ [values.size].pack('L') + values.map { |v| pack_field(v) }.join
16
+ end
17
+
18
+ def pack_field(value)
19
+ if String === value
20
+ raise StringTooLong.new if value.bytesize > 1024 * 1024
21
+ [value.bytesize, value].pack('wa*')
22
+ elsif Integer === value
23
+ if value < 4294967296 # 2 ^ 32
24
+ [4, value].pack('wL')
25
+ else
26
+ [8, value].pack('wQ')
27
+ end
28
+ elsif value.is_a?(Tarantool::Field)
29
+ [value.data.bytesize].pack('w') + value.data
30
+ else
31
+ raise ArgumentError.new("Field should be integer or string")
32
+ end
33
+ end
34
+ end
35
+
36
+ attr_reader :space, :params, :args
37
+ attr_reader :space_no
38
+ def initialize(space, *args)
39
+ @space = space
40
+ @args = args
41
+ @params = if args.last.is_a? Hash
42
+ args.pop
43
+ else
44
+ {}
45
+ end
46
+ @space_no = params.delete(:space_no) || space.space_no || raise(UndefinedSpace.new)
47
+ parse_args
48
+ end
49
+
50
+ def perform
51
+ send_packet(make_packet(make_body))
52
+ self
53
+ end
54
+
55
+ def parse_args
56
+
57
+ end
58
+
59
+ def request_id
60
+ @request_id ||= connection.next_request_id
61
+ end
62
+
63
+ def make_packet(body)
64
+ [self.class.request_type, body.size, request_id].pack('LLL') +
65
+ body
66
+ end
67
+
68
+ def send_packet(packet)
69
+ connection.send_packet request_id, packet do |data|
70
+ make_response data
71
+ end
72
+ end
73
+
74
+ def make_response(data)
75
+ return_code, = data[0,4].unpack('L')
76
+ if return_code == 0
77
+ succeed Response.new(data[4, data.size], response_params)
78
+ else
79
+ msg = data[4, data.size].unpack('a*')
80
+ fail BadReturnCode.new("Error code #{return_code}: #{msg}")
81
+ end
82
+ end
83
+
84
+ def response_params
85
+ res = {}
86
+ res[:return_tuple] = true if params[:return_tuple]
87
+ res
88
+ end
89
+
90
+ def connection
91
+ space.connection
92
+ end
93
+ end
94
+ end