monetdb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +8 -0
- data/CHANGELOG.rdoc +5 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +40 -0
- data/LICENSE +17 -0
- data/README.rdoc +154 -0
- data/Rakefile +10 -0
- data/VERSION +1 -0
- data/lib/monetdb.rb +263 -0
- data/lib/monetdb/connection.rb +443 -0
- data/lib/monetdb/core_ext.rb +1 -0
- data/lib/monetdb/core_ext/string.rb +67 -0
- data/lib/monetdb/data.rb +300 -0
- data/lib/monetdb/error.rb +27 -0
- data/lib/monetdb/hasher.rb +40 -0
- data/lib/monetdb/transaction.rb +36 -0
- data/lib/monetdb/version.rb +7 -0
- data/monetdb.gemspec +23 -0
- data/script/console +7 -0
- data/test/test_helper.rb +12 -0
- data/test/test_helper/coverage.rb +8 -0
- data/test/unit/test_monetdb.rb +15 -0
- metadata +139 -0
@@ -0,0 +1,443 @@
|
|
1
|
+
require "socket"
|
2
|
+
require "time"
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
#
|
6
|
+
# Implements the MAPI communication protocol
|
7
|
+
#
|
8
|
+
|
9
|
+
class MonetDB
|
10
|
+
class Connection
|
11
|
+
|
12
|
+
# enable debug output
|
13
|
+
@@DEBUG = false
|
14
|
+
|
15
|
+
# hour in seconds, used for timezone calculation
|
16
|
+
@@HOUR = 3600
|
17
|
+
|
18
|
+
# maximum size (in bytes) for a monetdb message to be sent
|
19
|
+
@@MAX_MESSAGE_SIZE = 32766
|
20
|
+
|
21
|
+
# endianness of a message sent to the server
|
22
|
+
@@CLIENT_ENDIANNESS = "BIG"
|
23
|
+
|
24
|
+
# MAPI protocols supported by the driver
|
25
|
+
@@SUPPORTED_PROTOCOLS = [8, 9]
|
26
|
+
|
27
|
+
attr_reader :socket, :auto_commit, :transactions, :lang
|
28
|
+
|
29
|
+
# A new instance of MonetDB::Connection.
|
30
|
+
# * user : username (default is monetdb)
|
31
|
+
# * passwd : password (default is monetdb)
|
32
|
+
# * lang : language (default is sql)
|
33
|
+
# * host : server hostanme or ip (default is localhost)
|
34
|
+
# * port : server port (default is 50000)
|
35
|
+
def initialize(user = "monetdb", passwd = "monetdb", lang = "sql", host = "127.0.0.1", port = "50000")
|
36
|
+
@user = user
|
37
|
+
@passwd = passwd
|
38
|
+
@lang = lang.downcase
|
39
|
+
@host = host
|
40
|
+
@port = port
|
41
|
+
|
42
|
+
@client_endianness = @@CLIENT_ENDIANNESS
|
43
|
+
@auth_iteration = 0
|
44
|
+
@connection_established = false
|
45
|
+
@transactions = MonetDB::Transaction.new
|
46
|
+
end
|
47
|
+
|
48
|
+
# Connect to the database, creates a new socket.
|
49
|
+
def connect(db_name = "demo", auth_type = "SHA1")
|
50
|
+
@database = db_name
|
51
|
+
@auth_type = auth_type
|
52
|
+
@socket = TCPSocket.new(@host, @port.to_i)
|
53
|
+
|
54
|
+
if real_connect
|
55
|
+
if @lang == LANG_SQL
|
56
|
+
set_timezone
|
57
|
+
set_reply_size
|
58
|
+
elsif (@lang == LANG_XQUERY) and XQUERY_OUTPUT_SEQ
|
59
|
+
# require xquery output to be in seq format
|
60
|
+
send(format_command("output seq"))
|
61
|
+
end
|
62
|
+
true
|
63
|
+
else
|
64
|
+
false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Perform a real connection; retrieve challenge, proxy through merovinginan, build challenge and set the timezone.
|
69
|
+
def real_connect
|
70
|
+
server_challenge = retrieve_server_challenge()
|
71
|
+
|
72
|
+
if server_challenge != nil
|
73
|
+
salt = server_challenge.split(':')[0]
|
74
|
+
@server_name = server_challenge.split(':')[1]
|
75
|
+
@protocol = server_challenge.split(':')[2].to_i
|
76
|
+
@supported_auth_types = server_challenge.split(':')[3].split(',')
|
77
|
+
@server_endianness = server_challenge.split(':')[4]
|
78
|
+
if @protocol == 9
|
79
|
+
@pwhash = server_challenge.split(':')[5]
|
80
|
+
end
|
81
|
+
else
|
82
|
+
raise MonetDB::ConnectionError, "Error: server returned an empty challenge string."
|
83
|
+
end
|
84
|
+
|
85
|
+
# The server supports only RIPMED168 or crypt as an authentication hash function, but the driver does not.
|
86
|
+
if @supported_auth_types.length == 1
|
87
|
+
auth = @supported_auth_types[0]
|
88
|
+
if auth.upcase == "RIPEMD160" or auth.upcase == "CRYPT"
|
89
|
+
raise MonetDB::ConnectionError, auth.upcase + " " + ": algorithm not supported by ruby-monetdb."
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# If the server protocol version is not 8: abort and notify the user.
|
94
|
+
if @@SUPPORTED_PROTOCOLS.include?(@protocol) == false
|
95
|
+
raise MonetDB::ProtocolError, "Protocol not supported. The current implementation of ruby-monetdb works with MAPI protocols #{@@SUPPORTED_PROTOCOLS} only."
|
96
|
+
|
97
|
+
elsif mapi_proto_v8?
|
98
|
+
reply = build_auth_string_v8(@auth_type, salt, @database)
|
99
|
+
elsif mapi_proto_v9?
|
100
|
+
reply = build_auth_string_v9(@auth_type, salt, @database)
|
101
|
+
end
|
102
|
+
|
103
|
+
if @socket != nil
|
104
|
+
@connection_established = true
|
105
|
+
|
106
|
+
send(reply)
|
107
|
+
monetdb_auth = receive
|
108
|
+
|
109
|
+
if monetdb_auth.length == 0
|
110
|
+
# auth succedeed
|
111
|
+
true
|
112
|
+
else
|
113
|
+
if monetdb_auth[0].chr == MSG_REDIRECT
|
114
|
+
#redirection
|
115
|
+
|
116
|
+
redirects = [] # store a list of possible redirects
|
117
|
+
|
118
|
+
monetdb_auth.split('\n').each do |m|
|
119
|
+
# strip the trailing ^mapi:
|
120
|
+
# if the redirect string start with something != "^mapi:" or is empty, the redirect is invalid and shall not be included.
|
121
|
+
if m[0..5] == "^mapi:"
|
122
|
+
redir = m[6..m.length]
|
123
|
+
# url parse redir
|
124
|
+
redirects.push(redir)
|
125
|
+
else
|
126
|
+
$stderr.print "Warning: Invalid Redirect #{m}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
if redirects.size == 0
|
131
|
+
raise MonetDB::ConnectionError, "No valid redirect received"
|
132
|
+
else
|
133
|
+
begin
|
134
|
+
uri = URI.split(redirects[0])
|
135
|
+
# Splits the string on following parts and returns array with result:
|
136
|
+
#
|
137
|
+
# * Scheme
|
138
|
+
# * Userinfo
|
139
|
+
# * Host
|
140
|
+
# * Port
|
141
|
+
# * Registry
|
142
|
+
# * Path
|
143
|
+
# * Opaque
|
144
|
+
# * Query
|
145
|
+
# * Fragment
|
146
|
+
server_name = uri[0]
|
147
|
+
host = uri[2]
|
148
|
+
port = uri[3]
|
149
|
+
database = uri[5].gsub(/^\//, '') if uri[5] != nil
|
150
|
+
rescue URI::InvalidURIError
|
151
|
+
raise MonetDB::ConnectionError, "Invalid redirect: #{redirects[0]}"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
if server_name == "merovingian"
|
156
|
+
if @auth_iteration <= 10
|
157
|
+
@auth_iteration += 1
|
158
|
+
real_connect
|
159
|
+
else
|
160
|
+
raise MonetDB::ConnectionError, "Merovingian: too many iterations while proxying."
|
161
|
+
end
|
162
|
+
elsif server_name == "monetdb"
|
163
|
+
begin
|
164
|
+
@socket.close
|
165
|
+
rescue
|
166
|
+
raise MonetDB::ConnectionError, "I/O error while closing connection to #{@socket}"
|
167
|
+
end
|
168
|
+
# reinitialize a connection
|
169
|
+
@host = host
|
170
|
+
@port = port
|
171
|
+
|
172
|
+
connect(database, @auth_type)
|
173
|
+
else
|
174
|
+
@connection_established = false
|
175
|
+
raise MonetDB::ConnectionError, monetdb_auth
|
176
|
+
end
|
177
|
+
elsif monetdb_auth[0].chr == MSG_INFO
|
178
|
+
raise MonetDB::ConnectionError, monetdb_auth
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def savepoint
|
185
|
+
@transactions.savepoint
|
186
|
+
end
|
187
|
+
|
188
|
+
# Formats a <i>command</i> string so that it can be parsed by the server.
|
189
|
+
def format_command(x)
|
190
|
+
"X" + x + "\n"
|
191
|
+
end
|
192
|
+
|
193
|
+
# Send an 'export' command to the server.
|
194
|
+
def set_export(id, idx, offset)
|
195
|
+
send(format_command("export " + id.to_s + " " + idx.to_s + " " + offset.to_s ))
|
196
|
+
end
|
197
|
+
|
198
|
+
# Send a 'reply_size' command to the server.
|
199
|
+
def set_reply_size
|
200
|
+
send(format_command(("reply_size " + REPLY_SIZE)))
|
201
|
+
response = receive
|
202
|
+
|
203
|
+
if response == MSG_PROMPT
|
204
|
+
true
|
205
|
+
elsif response[0] == MSG_INFO
|
206
|
+
raise MonetDB::CommandError, "Unable to set reply_size: #{response}"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def set_output_seq
|
211
|
+
send(format_command("output seq"))
|
212
|
+
end
|
213
|
+
|
214
|
+
# Disconnect from server.
|
215
|
+
def disconnect
|
216
|
+
if @connection_established
|
217
|
+
begin
|
218
|
+
@socket.close
|
219
|
+
rescue => e
|
220
|
+
$stderr.print e
|
221
|
+
end
|
222
|
+
else
|
223
|
+
raise MonetDB::ConnectionError, "No connection established."
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Send data to a monetdb5 server instance and returns server response.
|
228
|
+
def send(data)
|
229
|
+
encode_message(data).each do |m|
|
230
|
+
@socket.write(m)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Receive data from a monetdb5 server instance.
|
235
|
+
def receive
|
236
|
+
is_final, chunk_size = recv_decode_hdr
|
237
|
+
|
238
|
+
if chunk_size == 0
|
239
|
+
return "" # needed on ruby-1.8.6 linux/64bit; recv(0) hangs on this configuration.
|
240
|
+
end
|
241
|
+
|
242
|
+
data = @socket.recv(chunk_size)
|
243
|
+
|
244
|
+
if is_final == false
|
245
|
+
while is_final == false
|
246
|
+
is_final, chunk_size = recv_decode_hdr
|
247
|
+
data += @socket.recv(chunk_size)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
data
|
252
|
+
end
|
253
|
+
|
254
|
+
# Build and authentication string given the parameters submitted by the user (MAPI protocol v8).
|
255
|
+
def build_auth_string_v8(auth_type, salt, db_name)
|
256
|
+
# seed = password + salt
|
257
|
+
if (auth_type.upcase == "MD5" or auth_type.upcase == "SHA1") and @supported_auth_types.include?(auth_type.upcase)
|
258
|
+
auth_type = auth_type.upcase
|
259
|
+
digest = Hasher.new(auth_type, @passwd+salt)
|
260
|
+
hashsum = digest.hashsum
|
261
|
+
elsif auth_type.downcase == "plain" or not @supported_auth_types.include?(auth_type.upcase)
|
262
|
+
auth_type = 'plain'
|
263
|
+
hashsum = @passwd + salt
|
264
|
+
|
265
|
+
elsif auth_type.downcase == "crypt"
|
266
|
+
auth_type = @supported_auth_types[@supported_auth_types.index(auth_type)+1]
|
267
|
+
$stderr.print "The selected hashing algorithm is not supported by the Ruby driver. #{auth_type} will be used instead."
|
268
|
+
digest = Hasher.new(auth_type, @passwd+salt)
|
269
|
+
hashsum = digest.hashsum
|
270
|
+
else
|
271
|
+
# The user selected an auth type not supported by the server.
|
272
|
+
raise MonetDB::ConnectionError, "#{auth_type} not supported by the server. Please choose one from #{@supported_auth_types}"
|
273
|
+
|
274
|
+
end
|
275
|
+
# Build the reply message with header
|
276
|
+
reply = @client_endianness + ":" + @user + ":{" + auth_type + "}" + hashsum + ":" + @lang + ":" + db_name + ":"
|
277
|
+
end
|
278
|
+
|
279
|
+
# Build and authentication string given the parameters submitted by the user (MAPI protocol v9).
|
280
|
+
def build_auth_string_v9(auth_type, salt, db_name)
|
281
|
+
if (auth_type.upcase == "MD5" or auth_type.upcase == "SHA1") and @supported_auth_types.include?(auth_type.upcase)
|
282
|
+
auth_type = auth_type.upcase
|
283
|
+
# Hash the password
|
284
|
+
pwhash = Hasher.new(@pwhash, @passwd)
|
285
|
+
|
286
|
+
digest = Hasher.new(auth_type, pwhash.hashsum + salt)
|
287
|
+
hashsum = digest.hashsum
|
288
|
+
|
289
|
+
elsif auth_type.downcase == "plain" # or not @supported_auth_types.include?(auth_type.upcase)
|
290
|
+
# Keep it for compatibility with merovingian
|
291
|
+
auth_type = 'plain'
|
292
|
+
hashsum = @passwd + salt
|
293
|
+
elsif @supported_auth_types.include?(auth_type.upcase)
|
294
|
+
if auth_type.upcase == "RIPEMD160"
|
295
|
+
auth_type = @supported_auth_types[@supported_auth_types.index(auth_type)+1]
|
296
|
+
$stderr.print "The selected hashing algorithm is not supported by the Ruby driver. #{auth_type} will be used instead."
|
297
|
+
end
|
298
|
+
# Hash the password
|
299
|
+
pwhash = Hasher.new(@pwhash, @passwd)
|
300
|
+
|
301
|
+
digest = Hasher.new(auth_type, pwhash.hashsum + salt)
|
302
|
+
hashsum = digest.hashsum
|
303
|
+
else
|
304
|
+
# The user selected an auth type not supported by the server.
|
305
|
+
raise MonetDB::ConnectionError, "#{auth_type} not supported by the server. Please choose one from #{@supported_auth_types}"
|
306
|
+
end
|
307
|
+
# Build the reply message with header
|
308
|
+
reply = @client_endianness + ":" + @user + ":{" + auth_type + "}" + hashsum + ":" + @lang + ":" + db_name + ":"
|
309
|
+
end
|
310
|
+
|
311
|
+
# Build a message to be sent to the server.
|
312
|
+
def encode_message(msg = "")
|
313
|
+
message = Array.new
|
314
|
+
data = ""
|
315
|
+
|
316
|
+
hdr = 0 # package header
|
317
|
+
pos = 0
|
318
|
+
is_final = false # last package in the stream
|
319
|
+
|
320
|
+
while (! is_final)
|
321
|
+
data = msg[pos..pos+[@@MAX_MESSAGE_SIZE.to_i, (msg.length - pos).to_i].min]
|
322
|
+
pos += data.length
|
323
|
+
|
324
|
+
if (msg.length - pos) == 0
|
325
|
+
last_bit = 1
|
326
|
+
is_final = true
|
327
|
+
else
|
328
|
+
last_bit = 0
|
329
|
+
end
|
330
|
+
|
331
|
+
hdr = [(data.length << 1) | last_bit].pack('v')
|
332
|
+
|
333
|
+
message << hdr + data.to_s # Short Little Endian Encoding
|
334
|
+
end
|
335
|
+
|
336
|
+
message.freeze # freeze and return the encode message
|
337
|
+
end
|
338
|
+
|
339
|
+
# Used as the first step in the authentication phase; retrieves a challenge string from the server.
|
340
|
+
def retrieve_server_challenge
|
341
|
+
server_challenge = receive
|
342
|
+
end
|
343
|
+
|
344
|
+
# Reads and decodes the header of a server message.
|
345
|
+
def recv_decode_hdr
|
346
|
+
if @socket != nil
|
347
|
+
fb = @socket.recv(1)
|
348
|
+
sb = @socket.recv(1)
|
349
|
+
|
350
|
+
# Use execeptions handling to keep compatibility between different ruby
|
351
|
+
# versions.
|
352
|
+
#
|
353
|
+
# Chars are treated differently in ruby 1.8 and 1.9
|
354
|
+
# try do to ascii to int conversion using ord (ruby 1.9)
|
355
|
+
# and if it fail fallback to character.to_i (ruby 1.8)
|
356
|
+
begin
|
357
|
+
fb = fb[0].ord
|
358
|
+
sb = sb[0].ord
|
359
|
+
rescue NoMethodError => one_eight
|
360
|
+
fb = fb[0].to_i
|
361
|
+
sb = sb[0].to_i
|
362
|
+
end
|
363
|
+
|
364
|
+
chunk_size = (sb << 7) | (fb >> 1)
|
365
|
+
|
366
|
+
is_final = false
|
367
|
+
if ( (fb & 1) == 1 )
|
368
|
+
is_final = true
|
369
|
+
|
370
|
+
end
|
371
|
+
# return the size of the chunk (in bytes)
|
372
|
+
return is_final, chunk_size
|
373
|
+
else
|
374
|
+
raise MonetDB::SocketError, "Error while receiving data\n"
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
# Sets the time zone according to the Operating System settings.
|
379
|
+
def set_timezone
|
380
|
+
tz = Time.new
|
381
|
+
tz_offset = tz.gmt_offset / @@HOUR
|
382
|
+
|
383
|
+
if tz_offset <= 9 # verify minute count!
|
384
|
+
tz_offset = "'+0" + tz_offset.to_s + ":00'"
|
385
|
+
else
|
386
|
+
tz_offset = "'+" + tz_offset.to_s + ":00'"
|
387
|
+
end
|
388
|
+
query_tz = "sSET TIME ZONE INTERVAL " + tz_offset + " HOUR TO MINUTE;"
|
389
|
+
|
390
|
+
# Perform the query directly within the method
|
391
|
+
send(query_tz)
|
392
|
+
response = receive
|
393
|
+
|
394
|
+
if response == MSG_PROMPT
|
395
|
+
true
|
396
|
+
elsif response[0].chr == MSG_INFO
|
397
|
+
raise MonetDB::QueryError, response
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
# Turns auto commit on/off.
|
402
|
+
def set_auto_commit(flag = true)
|
403
|
+
if flag == false
|
404
|
+
ac = " 0"
|
405
|
+
else
|
406
|
+
ac = " 1"
|
407
|
+
end
|
408
|
+
|
409
|
+
send(format_command("auto_commit " + ac))
|
410
|
+
response = receive
|
411
|
+
|
412
|
+
if response == MSG_PROMPT
|
413
|
+
@auto_commit = flag
|
414
|
+
elsif response[0].chr == MSG_INFO
|
415
|
+
raise MonetDB::CommandError, response
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# Check the auto commit status (on/off).
|
420
|
+
def auto_commit?
|
421
|
+
!!@auto_commit
|
422
|
+
end
|
423
|
+
|
424
|
+
# Check if monetdb is running behind the merovingian proxy and forward the connection in case.
|
425
|
+
def merovingian?
|
426
|
+
@server_name.downcase == "merovingian"
|
427
|
+
end
|
428
|
+
|
429
|
+
def mserver?
|
430
|
+
@server_name.downcase == "monetdb"
|
431
|
+
end
|
432
|
+
|
433
|
+
# Check which protocol is spoken by the server.
|
434
|
+
def mapi_proto_v8?
|
435
|
+
@protocol == 8
|
436
|
+
end
|
437
|
+
|
438
|
+
def mapi_proto_v9?
|
439
|
+
@protocol == 9
|
440
|
+
end
|
441
|
+
|
442
|
+
end
|
443
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require "monetdb/core_ext/string"
|
@@ -0,0 +1,67 @@
|
|
1
|
+
#
|
2
|
+
# Overload the class string to convert monetdb to ruby types.
|
3
|
+
#
|
4
|
+
|
5
|
+
class String
|
6
|
+
|
7
|
+
def getInt
|
8
|
+
to_i
|
9
|
+
end
|
10
|
+
|
11
|
+
def getFloat
|
12
|
+
to_f
|
13
|
+
end
|
14
|
+
|
15
|
+
def getString
|
16
|
+
gsub(/^"/, "").gsub(/"$/, "")
|
17
|
+
end
|
18
|
+
|
19
|
+
# Convert from HEX to the origianl binary data.
|
20
|
+
def getBlob
|
21
|
+
blob = ""
|
22
|
+
scan(/../) { |tuple| blob += tuple.hex.chr }
|
23
|
+
blob
|
24
|
+
end
|
25
|
+
|
26
|
+
# Ruby currently supports only time formatted timestamps; treat TIME as string.
|
27
|
+
def getTime
|
28
|
+
gsub(/^"/, "").gsub(/"$/, "")
|
29
|
+
end
|
30
|
+
|
31
|
+
# Ruby currently supports only date formatted timestamps; treat DATE as string.
|
32
|
+
def getDate
|
33
|
+
gsub(/^"/, "").gsub(/"$/, "")
|
34
|
+
end
|
35
|
+
|
36
|
+
def getDateTime
|
37
|
+
date = split(" ")[0].split("-")
|
38
|
+
time = split(" ")[1].split(":")
|
39
|
+
Time.gm(date[0], date[1], date[2], time[0], time[1], time[2])
|
40
|
+
end
|
41
|
+
|
42
|
+
def getChar
|
43
|
+
# Ruby < 1.9 does not have a char datatype
|
44
|
+
begin
|
45
|
+
ord
|
46
|
+
rescue
|
47
|
+
self
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def getBool
|
52
|
+
if %w(1 y t true).include?(self)
|
53
|
+
true
|
54
|
+
elsif %w(0 n f false).include?(self)
|
55
|
+
false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def getNull
|
60
|
+
if upcase == "NONE"
|
61
|
+
nil
|
62
|
+
else
|
63
|
+
raise "Unknown value"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|