monetdb 0.1.0
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.
- 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
|