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.
@@ -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