ruby-monetdb-sql 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +154 -0
- data/lib/MonetDB.rb +274 -0
- data/lib/MonetDBConnection.rb +550 -0
- data/lib/MonetDBData.rb +460 -0
- data/lib/MonetDBExceptions.rb +55 -0
- data/lib/hasher.rb +56 -0
- metadata +51 -0
data/README
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
== Standalone driver ==
|
2
|
+
This directory contains the a ruby interface to monetdb5
|
3
|
+
written in pure ruby.
|
4
|
+
|
5
|
+
lib/MonetDB.rb
|
6
|
+
lib/MonetDBConnection.rb
|
7
|
+
lib/MonetDBStatements.rb
|
8
|
+
lib/MonetDBData.rb
|
9
|
+
lib/MonetDBExceptions.rb
|
10
|
+
lib/hasher.rb
|
11
|
+
lib/demo.rb: demo application how to interact with the database
|
12
|
+
|
13
|
+
ruby-monetdb-sql-0.1.gemspec: make file for rubygems
|
14
|
+
|
15
|
+
doc/: rubydoc in HTML format
|
16
|
+
|
17
|
+
== Installation ==
|
18
|
+
|
19
|
+
The standalone monetdb driver can be installed using the RubyGems Package Manager.
|
20
|
+
|
21
|
+
First build a gem file starting from the gemspec configuration:
|
22
|
+
|
23
|
+
$ gem build ruby-monetdb-sql-0.1.gemspec
|
24
|
+
|
25
|
+
Then install with the command:
|
26
|
+
|
27
|
+
$ gem install ruby-monetdb-sql-0.1.gem
|
28
|
+
|
29
|
+
== Usage ==
|
30
|
+
To use the standalone driver import the 'MonetDB' class and 'rubygems' (in case you installed it using gems).
|
31
|
+
|
32
|
+
A typical sequence of events is as follows:
|
33
|
+
Invoke query using the database handle to send the statement to the server and get back a result set object.
|
34
|
+
|
35
|
+
A result set object has methods for fetching rows, moving around in the result set, obtaining column metadata, and releasing the result set.
|
36
|
+
Use a row fetching method such as fetch_row or an iterator such as each to access the rows of the result set.
|
37
|
+
If you want a count of the number of rows in the result set: invoke 'num_rows' method.
|
38
|
+
Invoke 'free' to release the result set.
|
39
|
+
|
40
|
+
== Example ==
|
41
|
+
|
42
|
+
require 'MonetDB'
|
43
|
+
|
44
|
+
db = MonetDB.new
|
45
|
+
db.connect(user = "monetdb", passwd = "monetdb", lang = "sql", host="127.0.0.1", port = 50000, db_name = "demo", auth_type = "SHA1")
|
46
|
+
|
47
|
+
# set type_cast=true to enable MonetDB to Ruby type mapping
|
48
|
+
res = db.query("SELECT * from tables;", type_cast = false)
|
49
|
+
|
50
|
+
#puts res.debug_columns_type
|
51
|
+
|
52
|
+
puts "Number of rows returned: " + res.num_rows.to_s
|
53
|
+
puts "Number of fields: " + res.num_fields.to_s
|
54
|
+
|
55
|
+
|
56
|
+
# Get the columns' name
|
57
|
+
col_names = res.name_fields
|
58
|
+
|
59
|
+
|
60
|
+
# Iterate over the record set and retrieve on row at a time
|
61
|
+
puts res.fetch
|
62
|
+
while row = res.fetch do
|
63
|
+
printf "%s \n", row
|
64
|
+
end
|
65
|
+
|
66
|
+
# Release the result set.
|
67
|
+
res.free
|
68
|
+
|
69
|
+
# Disconnect from server
|
70
|
+
db.close
|
71
|
+
|
72
|
+
See lib/demo.rb and the MonetDBDatar class documentation for more examples.
|
73
|
+
|
74
|
+
|
75
|
+
|
76
|
+
== ActiveRecord connector adapter ==
|
77
|
+
Active Record connects business objects and database tables to create a persistable domain model where logic and data are presented in one wrapping. It‘s an implementation of the object-relational mapping (ORM) pattern.
|
78
|
+
|
79
|
+
Required files:
|
80
|
+
|
81
|
+
adapter/lib/active_record/monetdb_adapter.rb
|
82
|
+
|
83
|
+
Usage example follows:
|
84
|
+
require 'active_record'
|
85
|
+
|
86
|
+
ActiveRecord::Base.logger = Logger.new(STDERR)
|
87
|
+
ActiveRecord::Base.colorize_logging = true
|
88
|
+
|
89
|
+
ActiveRecord::Base.establish_connection(
|
90
|
+
:adapter => "monetdb",
|
91
|
+
:host => "localhost",
|
92
|
+
:database => "demo"
|
93
|
+
)
|
94
|
+
|
95
|
+
# Create a new table
|
96
|
+
class AddTests < ActiveRecord::Migration
|
97
|
+
def self.up
|
98
|
+
create_table :tests do |table|
|
99
|
+
table.column :name, :string
|
100
|
+
table.column :surname, :string
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.down
|
105
|
+
drop_table :tests
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
AddTests.up
|
111
|
+
|
112
|
+
# Migration: add a column name with a default value
|
113
|
+
class AddAge < ActiveRecord::Migration
|
114
|
+
def self.up
|
115
|
+
add_column :tests, :age, :smallint, :default => 18
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.down
|
119
|
+
remove_column :tests, :age
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
class Test < ActiveRecord::Base
|
125
|
+
end
|
126
|
+
|
127
|
+
# Insert an entry in the table
|
128
|
+
Test.create(:name => 'X', :surname => 'Y')
|
129
|
+
|
130
|
+
# add a column
|
131
|
+
AddAge.up
|
132
|
+
|
133
|
+
# return the first result of the query SELECT * from tables
|
134
|
+
row = Test.find(:first)
|
135
|
+
printf "SELECT * from tests LIMIT 1:\n"
|
136
|
+
printf "Name: %s, Surname: %s, Age: %s\n", row.name, row.surname, row.age
|
137
|
+
|
138
|
+
# Drop the table
|
139
|
+
AddTests.down
|
140
|
+
|
141
|
+
== Rubygem ==
|
142
|
+
|
143
|
+
The standalone ruby driver can be distributed as a ruby gem.
|
144
|
+
A gem file is already available; however, it can be generated
|
145
|
+
starting from the ruby-monetdb-sql-0.1.gemspec file:
|
146
|
+
|
147
|
+
$ gem build ruby-monetdb-sql-0.1.gemspec
|
148
|
+
|
149
|
+
To install the file run the command:
|
150
|
+
|
151
|
+
$ gem install ruby-monetdb-sql-0.1.gem
|
152
|
+
|
153
|
+
Documentation in ri and html format will be generated and installed as well
|
154
|
+
|
data/lib/MonetDB.rb
ADDED
@@ -0,0 +1,274 @@
|
|
1
|
+
# The contents of this file are subject to the MonetDB Public License
|
2
|
+
# Version 1.1 (the "License"); you may not use this file except in
|
3
|
+
# compliance with the License. You may obtain a copy of the License at
|
4
|
+
# http://monetdb.cwi.nl/Legal/MonetDBLicense-1.1.html
|
5
|
+
#
|
6
|
+
# Software distributed under the License is distributed on an "AS IS"
|
7
|
+
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
|
8
|
+
# License for the specific language governing rights and limitations
|
9
|
+
# under the License.
|
10
|
+
#
|
11
|
+
# The Original Code is the MonetDB Database System.
|
12
|
+
#
|
13
|
+
# The Initial Developer of the Original Code is CWI.
|
14
|
+
# Portions created by CWI are Copyright (C) 1997-July 2008 CWI.
|
15
|
+
# Copyright August 2008-2011 MonetDB B.V.
|
16
|
+
# All Rights Reserved.
|
17
|
+
|
18
|
+
|
19
|
+
# A typical sequence of events is as follows:
|
20
|
+
# Fire a query using the database handle to send the statement to the server and get back a result set object.
|
21
|
+
|
22
|
+
# A result set object has methods for fetching rows, moving around in the result set, obtaining column metadata, and releasing the result set.
|
23
|
+
# Use a row fetching method such as 'fetch_row' or an iterator such as each to access the rows of the result set.
|
24
|
+
# Call 'free' to release the result set.
|
25
|
+
|
26
|
+
|
27
|
+
#module MonetDB
|
28
|
+
|
29
|
+
require 'MonetDBConnection'
|
30
|
+
require 'MonetDBData'
|
31
|
+
require 'MonetDBExceptions'
|
32
|
+
|
33
|
+
# = Introduction
|
34
|
+
#
|
35
|
+
# A typical sequence of events is as follows:
|
36
|
+
# Create a database instance (handle), invoke query using the database handle to send the statement to the server and get back a result set object.
|
37
|
+
#
|
38
|
+
# A result set object has methods for fetching rows, moving around in the result set, obtaining column metadata, and releasing the result set.
|
39
|
+
# A result set object is an instance of the MonetDBData class.
|
40
|
+
#
|
41
|
+
# Records can be returneds as arrays and iterators over the set.
|
42
|
+
#
|
43
|
+
# A database handler (dbh) is and instance of the MonetDB class.
|
44
|
+
#
|
45
|
+
# = Connection management
|
46
|
+
#
|
47
|
+
# connect - establish a new connection
|
48
|
+
# * user: username (default is monetdb)
|
49
|
+
# * passwd: password (default is monetdb)
|
50
|
+
# * lang: language (default is sql)
|
51
|
+
# * host: server hostanme or ip (default is localhost)
|
52
|
+
# * port: server port (default is 50000)
|
53
|
+
# * db_name: name of the database to connect to
|
54
|
+
# * auth_type: hashing function to use during authentication (default is SHA1)
|
55
|
+
#
|
56
|
+
# is_connected? - returns true if there is an active connection to a server, false otherwise
|
57
|
+
# reconnect - recconnect to a server
|
58
|
+
# close - terminate a connection
|
59
|
+
# auto_commit? - returns ture if the session is running in auto commit mode, false otherwise
|
60
|
+
# auto_commit - enable/disable auto commit mode.
|
61
|
+
#
|
62
|
+
# query - fire a query
|
63
|
+
#
|
64
|
+
# Currently MAPI protocols 8 and 9 are supported.
|
65
|
+
#
|
66
|
+
# = Managing record sets
|
67
|
+
#
|
68
|
+
#
|
69
|
+
# A record set is represented as an instance of the MonetDBData class; the class provides methods to manage retrieved data.
|
70
|
+
#
|
71
|
+
#
|
72
|
+
# The following methods allow to iterate over data:
|
73
|
+
#
|
74
|
+
# fetch - iterates over the record set and retrieves on row at a time. Each row is returned as an array.
|
75
|
+
# fetch_hash - iterates over columns (on cell at a time).
|
76
|
+
# fetch_all_hash - returns record set entries hashed by column name orderd by column position.
|
77
|
+
#
|
78
|
+
# To return the record set as an array (with each tuple stored as array of fields) the following method can be used:
|
79
|
+
#
|
80
|
+
# fetch_all - fetch all rows and store them
|
81
|
+
#
|
82
|
+
#
|
83
|
+
# Information about the retrieved record set can be obtained via the following methods:
|
84
|
+
#
|
85
|
+
# num_rows - returns the number of rows present in the record set
|
86
|
+
# num_fields - returns the number of fields (columns) that compose the schema
|
87
|
+
# name_fields - returns the (ordered) name of the schema's columns
|
88
|
+
# type_fields - returns the (ordered) types list of the schema's columns
|
89
|
+
#
|
90
|
+
# To release a record set MonetDBData#free can be used.
|
91
|
+
#
|
92
|
+
# = Type conversion
|
93
|
+
#
|
94
|
+
# A mapping between SQL and ruby type is supported. Each retrieved field can be converted to a ruby datatype via
|
95
|
+
# a getTYPE method.
|
96
|
+
#
|
97
|
+
# The currently supported cast methods are:
|
98
|
+
#
|
99
|
+
# getInt - convert to an integer value
|
100
|
+
# getFloat - convert to a floating point value
|
101
|
+
# getString - return a string representation of the value, with trailing and leading " characters removed
|
102
|
+
# getBlob - convert an SQL stored HEX string to its binary representation
|
103
|
+
# getTime - return a string representation of a TIME field
|
104
|
+
# getDate - return a string representation of a DATE field
|
105
|
+
# getDateTime - convert a TIMESTAMP field to a ruby Time object
|
106
|
+
# getChar - on ruby >= 1.9, convert a CHAR field to char
|
107
|
+
# getBool - convert a BOOLEAN field to a ruby bool object. If the value of the field is unknown, nil is returned
|
108
|
+
# getNull - convert a NULL value to a nil object
|
109
|
+
#
|
110
|
+
#
|
111
|
+
# = Transactions
|
112
|
+
#
|
113
|
+
# By default monetdb works in auto_commit mode. To turn this feature off MonetDB#auto_commit(flag=false) can be used.
|
114
|
+
#
|
115
|
+
# Once auto_commit has been disable it is possible to start transactions, create/delete savepoints, rollback and commit with
|
116
|
+
# the usual SQL statements.
|
117
|
+
#
|
118
|
+
# Savepoints IDs can be generated using the MonetDB#save method. To release a savepoint ID use MonetDB#release.
|
119
|
+
#
|
120
|
+
# Savepoints can be accessed (as a stack) with the MonetDB#transactions method.
|
121
|
+
#
|
122
|
+
# example/standalone.rb contains usage example of the above mentioned methods.
|
123
|
+
|
124
|
+
# = Introduction
|
125
|
+
#
|
126
|
+
# A typical sequence of events is as follows:
|
127
|
+
# Create a database instance (handle), invoke query using the database handle to send the statement to the server and get back a result set object.
|
128
|
+
#
|
129
|
+
# A result set object has methods for fetching rows, moving around in the result set, obtaining column metadata, and releasing the result set.
|
130
|
+
# A result set object is an instance of the MonetDBData class.
|
131
|
+
#
|
132
|
+
# Records can be returneds as arrays and iterators over the set.
|
133
|
+
#
|
134
|
+
# A database handler (dbh) is and instance of the MonetDB class.
|
135
|
+
#
|
136
|
+
# = Connection management
|
137
|
+
#
|
138
|
+
# connect - establish a new connection
|
139
|
+
# * user: username (default is monetdb)
|
140
|
+
# * passwd: password (default is monetdb)
|
141
|
+
# * lang: language (default is sql)
|
142
|
+
# * host: server hostanme or ip (default is localhost)
|
143
|
+
# * port: server port (default is 50000)
|
144
|
+
# * db_name: name of the database to connect to
|
145
|
+
# * auth_type: hashing function to use during authentication (default is SHA1)
|
146
|
+
#
|
147
|
+
# is_connected? - returns true if there is an active connection to a server, false otherwise
|
148
|
+
# reconnect - recconnect to a server
|
149
|
+
# close - terminate a connection
|
150
|
+
# auto_commit? - returns ture if the session is running in auto commit mode, false otherwise
|
151
|
+
# auto_commit - enable/disable auto commit mode.
|
152
|
+
#
|
153
|
+
# query - fire a query
|
154
|
+
#
|
155
|
+
# Currently MAPI protocols 8 and 9 are supported.
|
156
|
+
#
|
157
|
+
# = Managing record sets
|
158
|
+
#
|
159
|
+
#
|
160
|
+
# A record set is represented as an instance of the MonetDBData class; the class provides methods to manage retrieved data.
|
161
|
+
#
|
162
|
+
#
|
163
|
+
# The following methods allow to iterate over data:
|
164
|
+
#
|
165
|
+
# fetch - iterates over the record set and retrieves on row at a time. Each row is returned as an array.
|
166
|
+
# fetch_hash - iterates over columns (on cell at a time).
|
167
|
+
# fetch_all_hash - returns record set entries hashed by column name orderd by column position.
|
168
|
+
#
|
169
|
+
# To return the record set as an array (with each tuple stored as array of fields) the following method can be used:
|
170
|
+
#
|
171
|
+
# fetch_all - fetch all rows and store them
|
172
|
+
#
|
173
|
+
#
|
174
|
+
# Information about the retrieved record set can be obtained via the following methods:
|
175
|
+
#
|
176
|
+
# num_rows - returns the number of rows present in the record set
|
177
|
+
# num_fields - returns the number of fields (columns) that compose the schema
|
178
|
+
# name_fields - returns the (ordered) name of the schema's columns
|
179
|
+
#
|
180
|
+
# To release a record set MonetDBData#free can be used.
|
181
|
+
#
|
182
|
+
# = Type conversion
|
183
|
+
#
|
184
|
+
# Invoking MonetDB#query with the flag type_conversion=true will result in a type cast of the record set fields from SQL types to ruby types
|
185
|
+
#
|
186
|
+
# demo.rb contains usage example of the above mentioned methods.
|
187
|
+
|
188
|
+
class MonetDB
|
189
|
+
def initalize()
|
190
|
+
@connection = nil
|
191
|
+
end
|
192
|
+
|
193
|
+
# Establish a new connection.
|
194
|
+
# * user: username (default is monetdb)
|
195
|
+
# * passwd: password (default is monetdb)
|
196
|
+
# * lang: language (default is sql)
|
197
|
+
# * host: server hostanme or ip (default is localhost)
|
198
|
+
# * port: server port (default is 50000)
|
199
|
+
# * db_name: name of the database to connect to
|
200
|
+
# * auth_type: hashing function to use during authentication (default is SHA1)
|
201
|
+
def connect(username = "monetdb", password = "monetdb", lang = "sql", host="127.0.0.1", port = "50000", db_name = "test", auth_type = "SHA1")
|
202
|
+
# TODO: handle pools of connections
|
203
|
+
@username = username
|
204
|
+
@password = password
|
205
|
+
@lang = lang
|
206
|
+
@host = host
|
207
|
+
@port = port
|
208
|
+
@db_name = db_name
|
209
|
+
@auth_type = auth_type
|
210
|
+
@connection = MonetDBConnection.new(user = @username, passwd = @password, lang = @lang, host = @host, port = @port)
|
211
|
+
@connection.connect(@db_name, @auth_type)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Send a <b> user submitted </b> query to the server and store the response.
|
215
|
+
# Returns and instance of MonetDBData.
|
216
|
+
def query(q = "")
|
217
|
+
if @connection != nil
|
218
|
+
@data = MonetDBData.new(@connection)
|
219
|
+
@data.execute(q)
|
220
|
+
end
|
221
|
+
return @data
|
222
|
+
end
|
223
|
+
|
224
|
+
# Return true if there exists a "connection" object
|
225
|
+
def is_connected?
|
226
|
+
if @connection == nil
|
227
|
+
return false
|
228
|
+
else
|
229
|
+
return true
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Reconnect to the server
|
234
|
+
def reconnect
|
235
|
+
if @connection != nil
|
236
|
+
self.close
|
237
|
+
|
238
|
+
@connection = MonetDBConnection.new(user = @username, passwd = @password, lang = @lang, host = @host, port = @port)
|
239
|
+
@connection.connect(db_name = @db_name, auth_type = @auth_type)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Turn auto commit on/off
|
244
|
+
def auto_commit(flag=true)
|
245
|
+
@connection.set_auto_commit(flag)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Returns the current auto commit (on/off) settings.
|
249
|
+
def auto_commit?
|
250
|
+
@connection.auto_commit?
|
251
|
+
end
|
252
|
+
|
253
|
+
# Returns the name of the last savepoint in a transactions pool
|
254
|
+
def transactions
|
255
|
+
@connection.savepoint
|
256
|
+
end
|
257
|
+
|
258
|
+
# Create a new savepoint ID
|
259
|
+
def save
|
260
|
+
@connection.transactions.save
|
261
|
+
end
|
262
|
+
|
263
|
+
# Release a savepoint ID
|
264
|
+
def release
|
265
|
+
@connection.transactions.release
|
266
|
+
end
|
267
|
+
|
268
|
+
# Close an active connection
|
269
|
+
def close()
|
270
|
+
@connection.disconnect
|
271
|
+
@connection = nil
|
272
|
+
end
|
273
|
+
end
|
274
|
+
#end
|
@@ -0,0 +1,550 @@
|
|
1
|
+
# The contents of this file are subject to the MonetDB Public License
|
2
|
+
# Version 1.1 (the "License"); you may not use this file except in
|
3
|
+
# compliance with the License. You may obtain a copy of the License at
|
4
|
+
# http://monetdb.cwi.nl/Legal/MonetDBLicense-1.1.html
|
5
|
+
#
|
6
|
+
# Software distributed under the License is distributed on an "AS IS"
|
7
|
+
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
|
8
|
+
# License for the specific language governing rights and limitations
|
9
|
+
# under the License.
|
10
|
+
#
|
11
|
+
# The Original Code is the MonetDB Database System.
|
12
|
+
#
|
13
|
+
# The Initial Developer of the Original Code is CWI.
|
14
|
+
# Portions created by CWI are Copyright (C) 1997-July 2008 CWI.
|
15
|
+
# Copyright August 2008-2011 MonetDB B.V.
|
16
|
+
# All Rights Reserved.
|
17
|
+
|
18
|
+
# Implements the MAPI communication protocol
|
19
|
+
|
20
|
+
|
21
|
+
require 'socket'
|
22
|
+
require 'time'
|
23
|
+
require 'hasher'
|
24
|
+
require 'MonetDBExceptions'
|
25
|
+
require 'iconv' # utf-8 support
|
26
|
+
require 'uri' # parse merovingian redirects
|
27
|
+
|
28
|
+
Q_TABLE = "1" # SELECT operation
|
29
|
+
Q_UPDATE = "2" # INSERT/UPDATE operations
|
30
|
+
Q_CREATE = "3" # CREATE/DROP TABLE operations
|
31
|
+
Q_TRANSACTION = "4" # TRANSACTION
|
32
|
+
Q_PREPARE = "5"
|
33
|
+
Q_BLOCK = "6" # QBLOCK message
|
34
|
+
|
35
|
+
MSG_REDIRECT = '^' # auth redirection through merovingian
|
36
|
+
MSG_QUERY = '&'
|
37
|
+
MSG_SCHEMA_HEADER = '%'
|
38
|
+
MSG_INFO = '!' # info response from mserver
|
39
|
+
MSG_TUPLE = '['
|
40
|
+
MSG_PROMPT = ""
|
41
|
+
|
42
|
+
|
43
|
+
REPLY_SIZE = '-1'
|
44
|
+
|
45
|
+
MAX_AUTH_ITERATION = 10 # maximum number of atuh iterations (thorough merovingian) allowed
|
46
|
+
|
47
|
+
MONET_ERROR = -1
|
48
|
+
|
49
|
+
LANG_SQL = "sql"
|
50
|
+
|
51
|
+
# Xquery support
|
52
|
+
LANG_XQUERY = "xquery"
|
53
|
+
XQUERY_OUTPUT_SEQ = true # use monetdb xquery's output seq
|
54
|
+
|
55
|
+
class MonetDBConnection
|
56
|
+
|
57
|
+
|
58
|
+
# enable debug output
|
59
|
+
@@DEBUG = false
|
60
|
+
|
61
|
+
# hour in seconds, used for timezone calculation
|
62
|
+
@@HOUR = 3600
|
63
|
+
|
64
|
+
# maximum size (in bytes) for a monetdb message to be sent
|
65
|
+
@@MAX_MESSAGE_SIZE = 32766
|
66
|
+
|
67
|
+
# endianness of a message sent to the server
|
68
|
+
@@CLIENT_ENDIANNESS = "BIG"
|
69
|
+
|
70
|
+
# MAPI protocols supported by the driver
|
71
|
+
@@SUPPORTED_PROTOCOLS = [ 8, 9 ]
|
72
|
+
|
73
|
+
attr_reader :socket, :auto_commit, :transactions, :lang
|
74
|
+
|
75
|
+
# Instantiates a new MonetDBConnection object
|
76
|
+
# * user: username (default is monetdb)
|
77
|
+
# * passwd: password (default is monetdb)
|
78
|
+
# * lang: language (default is sql)
|
79
|
+
# * host: server hostanme or ip (default is localhost)
|
80
|
+
# * port: server port (default is 50000)
|
81
|
+
|
82
|
+
def initialize(user = "monetdb", passwd = "monetdb", lang = "sql", host="127.0.0.1", port = "50000")
|
83
|
+
@user = user
|
84
|
+
@passwd = passwd
|
85
|
+
@lang = lang.downcase
|
86
|
+
@host = host
|
87
|
+
@port = port
|
88
|
+
|
89
|
+
@client_endianness = @@CLIENT_ENDIANNESS
|
90
|
+
|
91
|
+
@auth_iteration = 0
|
92
|
+
@connection_established = false
|
93
|
+
|
94
|
+
@transactions = MonetDBTransaction.new # handles a pool of transactions (generates and keeps track of savepoints)
|
95
|
+
|
96
|
+
if @@DEBUG == true
|
97
|
+
require 'logger'
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
# Connect to the database, creates a new socket
|
103
|
+
def connect(db_name = 'demo', auth_type = 'SHA1')
|
104
|
+
@database = db_name
|
105
|
+
@auth_type = auth_type
|
106
|
+
|
107
|
+
@socket = TCPSocket.new(@host, @port.to_i)
|
108
|
+
if real_connect
|
109
|
+
if @lang == LANG_SQL
|
110
|
+
set_timezone
|
111
|
+
set_reply_size
|
112
|
+
elsif (@lang == LANG_XQUERY) and XQUERY_OUTPUT_SEQ
|
113
|
+
# require xquery output to be in seq format
|
114
|
+
send(format_command("output seq"))
|
115
|
+
end
|
116
|
+
true
|
117
|
+
end
|
118
|
+
false
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
# perform a real connection; retrieve challenge, proxy through merovinginan, build challenge and set the timezone
|
123
|
+
def real_connect
|
124
|
+
|
125
|
+
server_challenge = retrieve_server_challenge()
|
126
|
+
print "CHALLENGE: " + server_challenge.to_s
|
127
|
+
if server_challenge != nil
|
128
|
+
salt = server_challenge.split(':')[0]
|
129
|
+
@server_name = server_challenge.split(':')[1]
|
130
|
+
@protocol = server_challenge.split(':')[2].to_i
|
131
|
+
@supported_auth_types = server_challenge.split(':')[3].split(',')
|
132
|
+
@server_endianness = server_challenge.split(':')[4]
|
133
|
+
if @protocol == 9
|
134
|
+
@pwhash = server_challenge.split(':')[5]
|
135
|
+
end
|
136
|
+
else
|
137
|
+
raise MonetDBConnectionError, "Error: server returned an empty challenge string."
|
138
|
+
end
|
139
|
+
|
140
|
+
# The server supports only RIPMED168 or crypt as an authentication hash function, but the driver does not.
|
141
|
+
if @supported_auth_types.length == 1
|
142
|
+
auth = @supported_auth_types[0]
|
143
|
+
if auth.upcase == "RIPEMD160" or auth.upcase == "CRYPT"
|
144
|
+
raise MonetDBConnectionError, auth.upcase + " " + ": algorithm not supported by ruby-monetdb."
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
# If the server protocol version is not 8: abort and notify the user.
|
150
|
+
if @@SUPPORTED_PROTOCOLS.include?(@protocol) == false
|
151
|
+
raise MonetDBProtocolError, "Protocol not supported. The current implementation of ruby-monetdb works with MAPI protocols #{@@SUPPORTED_PROTOCOLS} only."
|
152
|
+
|
153
|
+
elsif mapi_proto_v8?
|
154
|
+
reply = build_auth_string_v8(@auth_type, salt, @database)
|
155
|
+
elsif mapi_proto_v9?
|
156
|
+
reply = build_auth_string_v9(@auth_type, salt, @database)
|
157
|
+
end
|
158
|
+
|
159
|
+
if @socket != nil
|
160
|
+
@connection_established = true
|
161
|
+
|
162
|
+
send(reply)
|
163
|
+
monetdb_auth = receive
|
164
|
+
|
165
|
+
if monetdb_auth.length == 0
|
166
|
+
# auth succedeed
|
167
|
+
true
|
168
|
+
else
|
169
|
+
if monetdb_auth[0].chr == MSG_REDIRECT
|
170
|
+
#redirection
|
171
|
+
|
172
|
+
redirects = [] # store a list of possible redirects
|
173
|
+
|
174
|
+
monetdb_auth.split('\n').each do |m|
|
175
|
+
# strip the trailing ^mapi:
|
176
|
+
# if the redirect string start with something != "^mapi:" or is empty, the redirect is invalid and shall not be included.
|
177
|
+
if m[0..5] == "^mapi:"
|
178
|
+
redir = m[6..m.length]
|
179
|
+
# url parse redir
|
180
|
+
redirects.push(redir)
|
181
|
+
else
|
182
|
+
$stderr.print "Warning: Invalid Redirect #{m}"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
if redirects.size == 0
|
187
|
+
raise MonetDBConnectionError, "No valid redirect received"
|
188
|
+
else
|
189
|
+
begin
|
190
|
+
uri = URI.split(redirects[0])
|
191
|
+
# Splits the string on following parts and returns array with result:
|
192
|
+
#
|
193
|
+
# * Scheme
|
194
|
+
# * Userinfo
|
195
|
+
# * Host
|
196
|
+
# * Port
|
197
|
+
# * Registry
|
198
|
+
# * Path
|
199
|
+
# * Opaque
|
200
|
+
# * Query
|
201
|
+
# * Fragment
|
202
|
+
server_name = uri[0]
|
203
|
+
host = uri[2]
|
204
|
+
port = uri[3]
|
205
|
+
database = uri[5].gsub(/^\//, '') if uri[5] != nil
|
206
|
+
rescue URI::InvalidURIError
|
207
|
+
raise MonetDBConnectionError, "Invalid redirect: #{redirects[0]}"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
if server_name == "merovingian"
|
212
|
+
if @auth_iteration <= 10
|
213
|
+
@auth_iteration += 1
|
214
|
+
real_connect
|
215
|
+
else
|
216
|
+
raise MonetDBConnectionError, "Merovingian: too many iterations while proxying."
|
217
|
+
end
|
218
|
+
elsif server_name == "monetdb"
|
219
|
+
begin
|
220
|
+
@socket.close
|
221
|
+
rescue
|
222
|
+
raise MonetDBConnectionError, "I/O error while closing connection to #{@socket}"
|
223
|
+
end
|
224
|
+
# reinitialize a connection
|
225
|
+
@host = host
|
226
|
+
@port = port
|
227
|
+
|
228
|
+
connect(database, @auth_type)
|
229
|
+
else
|
230
|
+
@connection_established = false
|
231
|
+
raise MonetDBConnectionError, monetdb_auth
|
232
|
+
end
|
233
|
+
elsif monetdb_auth[0].chr == MSG_INFO
|
234
|
+
raise MonetDBConnectionError, monetdb_auth
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
def savepoint
|
240
|
+
@transactions.savepoint
|
241
|
+
end
|
242
|
+
|
243
|
+
# Formats a <i>command</i> string so that it can be parsed by the server
|
244
|
+
def format_command(x)
|
245
|
+
return "X" + x + "\n"
|
246
|
+
end
|
247
|
+
|
248
|
+
|
249
|
+
# send an 'export' command to the server
|
250
|
+
def set_export(id, idx, offset)
|
251
|
+
send(format_command("export " + id.to_s + " " + idx.to_s + " " + offset.to_s ))
|
252
|
+
end
|
253
|
+
|
254
|
+
# send a 'reply_size' command to the server
|
255
|
+
def set_reply_size
|
256
|
+
send(format_command(("reply_size " + REPLY_SIZE)))
|
257
|
+
|
258
|
+
response = receive
|
259
|
+
|
260
|
+
if response == MSG_PROMPT
|
261
|
+
true
|
262
|
+
elsif response[0] == MSG_INFO
|
263
|
+
raise MonetDBCommandError, "Unable to set reply_size: #{response}"
|
264
|
+
end
|
265
|
+
|
266
|
+
end
|
267
|
+
|
268
|
+
def set_output_seq
|
269
|
+
send(format_command("output seq"))
|
270
|
+
end
|
271
|
+
|
272
|
+
# Disconnect from server
|
273
|
+
def disconnect()
|
274
|
+
if @connection_established
|
275
|
+
begin
|
276
|
+
@socket.close
|
277
|
+
rescue => e
|
278
|
+
$stderr.print e
|
279
|
+
end
|
280
|
+
else
|
281
|
+
raise MonetDBConnectionError, "No connection established."
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# send data to a monetdb5 server instance and returns server's response
|
286
|
+
def send(data)
|
287
|
+
encode_message(data).each do |m|
|
288
|
+
@socket.write(m)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# receive data from a monetdb5 server instance
|
293
|
+
def receive
|
294
|
+
is_final, chunk_size = recv_decode_hdr
|
295
|
+
|
296
|
+
if chunk_size == 0
|
297
|
+
return "" # needed on ruby-1.8.6 linux/64bit; recv(0) hangs on this configuration.
|
298
|
+
end
|
299
|
+
|
300
|
+
data = @socket.recv(chunk_size)
|
301
|
+
|
302
|
+
if is_final == false
|
303
|
+
while is_final == false
|
304
|
+
is_final, chunk_size = recv_decode_hdr
|
305
|
+
data += @socket.recv(chunk_size)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
return data
|
310
|
+
end
|
311
|
+
|
312
|
+
# Builds and authentication string given the parameters submitted by the user (MAPI protocol v8).
|
313
|
+
#
|
314
|
+
def build_auth_string_v8(auth_type, salt, db_name)
|
315
|
+
# seed = password + salt
|
316
|
+
if (auth_type.upcase == "MD5" or auth_type.upcase == "SHA1") and @supported_auth_types.include?(auth_type.upcase)
|
317
|
+
auth_type = auth_type.upcase
|
318
|
+
digest = Hasher.new(auth_type, @passwd+salt)
|
319
|
+
hashsum = digest.hashsum
|
320
|
+
elsif auth_type.downcase == "plain" or not @supported_auth_types.include?(auth_type.upcase)
|
321
|
+
auth_type = 'plain'
|
322
|
+
hashsum = @passwd + salt
|
323
|
+
|
324
|
+
elsif auth_type.downcase == "crypt"
|
325
|
+
auth_type = @supported_auth_types[@supported_auth_types.index(auth_type)+1]
|
326
|
+
$stderr.print "The selected hashing algorithm is not supported by the Ruby driver. #{auth_type} will be used instead."
|
327
|
+
digest = Hasher.new(auth_type, @passwd+salt)
|
328
|
+
hashsum = digest.hashsum
|
329
|
+
else
|
330
|
+
# The user selected an auth type not supported by the server.
|
331
|
+
raise MonetDBConnectionError, "#{auth_type} not supported by the server. Please choose one from #{@supported_auth_types}"
|
332
|
+
|
333
|
+
end
|
334
|
+
# Build the reply message with header
|
335
|
+
reply = @client_endianness + ":" + @user + ":{" + auth_type + "}" + hashsum + ":" + @lang + ":" + db_name + ":"
|
336
|
+
end
|
337
|
+
|
338
|
+
#
|
339
|
+
# Builds and authentication string given the parameters submitted by the user (MAPI protocol v9).
|
340
|
+
#
|
341
|
+
def build_auth_string_v9(auth_type, salt, db_name)
|
342
|
+
if (auth_type.upcase == "MD5" or auth_type.upcase == "SHA1") and @supported_auth_types.include?(auth_type.upcase)
|
343
|
+
auth_type = auth_type.upcase
|
344
|
+
# Hash the password
|
345
|
+
pwhash = Hasher.new(@pwhash, @passwd)
|
346
|
+
|
347
|
+
digest = Hasher.new(auth_type, pwhash.hashsum + salt)
|
348
|
+
hashsum = digest.hashsum
|
349
|
+
|
350
|
+
elsif auth_type.downcase == "plain" # or not @supported_auth_types.include?(auth_type.upcase)
|
351
|
+
# Keep it for compatibility with merovingian
|
352
|
+
auth_type = 'plain'
|
353
|
+
hashsum = @passwd + salt
|
354
|
+
elsif @supported_auth_types.include?(auth_type.upcase)
|
355
|
+
if auth_type.upcase == "RIPEMD160"
|
356
|
+
auth_type = @supported_auth_types[@supported_auth_types.index(auth_type)+1]
|
357
|
+
$stderr.print "The selected hashing algorithm is not supported by the Ruby driver. #{auth_type} will be used instead."
|
358
|
+
end
|
359
|
+
# Hash the password
|
360
|
+
pwhash = Hasher.new(@pwhash, @passwd)
|
361
|
+
|
362
|
+
digest = Hasher.new(auth_type, pwhash.hashsum + salt)
|
363
|
+
hashsum = digest.hashsum
|
364
|
+
else
|
365
|
+
# The user selected an auth type not supported by the server.
|
366
|
+
raise MonetDBConnectionError, "#{auth_type} not supported by the server. Please choose one from #{@supported_auth_types}"
|
367
|
+
end
|
368
|
+
# Build the reply message with header
|
369
|
+
reply = @client_endianness + ":" + @user + ":{" + auth_type + "}" + hashsum + ":" + @lang + ":" + db_name + ":"
|
370
|
+
end
|
371
|
+
|
372
|
+
# builds a message to be sent to the server
|
373
|
+
def encode_message(msg = "")
|
374
|
+
message = Array.new
|
375
|
+
data = ""
|
376
|
+
|
377
|
+
hdr = 0 # package header
|
378
|
+
pos = 0
|
379
|
+
is_final = false # last package in the stream
|
380
|
+
|
381
|
+
while (! is_final)
|
382
|
+
data = msg[pos..pos+[@@MAX_MESSAGE_SIZE.to_i, (msg.length - pos).to_i].min]
|
383
|
+
pos += data.length
|
384
|
+
|
385
|
+
if (msg.length - pos) == 0
|
386
|
+
last_bit = 1
|
387
|
+
is_final = true
|
388
|
+
else
|
389
|
+
last_bit = 0
|
390
|
+
end
|
391
|
+
|
392
|
+
hdr = [(data.length << 1) | last_bit].pack('v')
|
393
|
+
|
394
|
+
message << hdr + data.to_s # Short Little Endian Encoding
|
395
|
+
end
|
396
|
+
|
397
|
+
message.freeze # freeze and return the encode message
|
398
|
+
end
|
399
|
+
|
400
|
+
# Used as the first step in the authentication phase; retrives a challenge string from the server.
|
401
|
+
def retrieve_server_challenge()
|
402
|
+
server_challenge = receive
|
403
|
+
end
|
404
|
+
|
405
|
+
# reads and decodes the header of a server message
|
406
|
+
def recv_decode_hdr()
|
407
|
+
if @socket != nil
|
408
|
+
fb = @socket.recv(1)
|
409
|
+
sb = @socket.recv(1)
|
410
|
+
|
411
|
+
# Use execeptions handling to keep compatibility between different ruby
|
412
|
+
# versions.
|
413
|
+
#
|
414
|
+
# Chars are treated differently in ruby 1.8 and 1.9
|
415
|
+
# try do to ascii to int conversion using ord (ruby 1.9)
|
416
|
+
# and if it fail fallback to character.to_i (ruby 1.8)
|
417
|
+
begin
|
418
|
+
fb = fb[0].ord
|
419
|
+
sb = sb[0].ord
|
420
|
+
rescue NoMethodError => one_eight
|
421
|
+
fb = fb[0].to_i
|
422
|
+
sb = sb[0].to_i
|
423
|
+
end
|
424
|
+
|
425
|
+
chunk_size = (sb << 7) | (fb >> 1)
|
426
|
+
|
427
|
+
is_final = false
|
428
|
+
if ( (fb & 1) == 1 )
|
429
|
+
is_final = true
|
430
|
+
|
431
|
+
end
|
432
|
+
# return the size of the chunk (in bytes)
|
433
|
+
return is_final, chunk_size
|
434
|
+
else
|
435
|
+
raise MonetDBSocketError, "Error while receiving data\n"
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
# Sets the time zone according to the Operating System settings
|
440
|
+
def set_timezone()
|
441
|
+
tz = Time.new
|
442
|
+
tz_offset = tz.gmt_offset / @@HOUR
|
443
|
+
|
444
|
+
if tz_offset <= 9 # verify minute count!
|
445
|
+
tz_offset = "'+0" + tz_offset.to_s + ":00'"
|
446
|
+
else
|
447
|
+
tz_offset = "'+" + tz_offset.to_s + ":00'"
|
448
|
+
end
|
449
|
+
query_tz = "sSET TIME ZONE INTERVAL " + tz_offset + " HOUR TO MINUTE;"
|
450
|
+
|
451
|
+
# Perform the query directly within the method
|
452
|
+
send(query_tz)
|
453
|
+
response = receive
|
454
|
+
|
455
|
+
if response == MSG_PROMPT
|
456
|
+
true
|
457
|
+
elsif response[0].chr == MSG_INFO
|
458
|
+
raise MonetDBQueryError, response
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
# Turns auto commit on/off
|
463
|
+
def set_auto_commit(flag=true)
|
464
|
+
if flag == false
|
465
|
+
ac = " 0"
|
466
|
+
else
|
467
|
+
ac = " 1"
|
468
|
+
end
|
469
|
+
|
470
|
+
send(format_command("auto_commit " + ac))
|
471
|
+
|
472
|
+
response = receive
|
473
|
+
if response == MSG_PROMPT
|
474
|
+
@auto_commit = flag
|
475
|
+
elsif response[0].chr == MSG_INFO
|
476
|
+
raise MonetDBCommandError, response
|
477
|
+
return
|
478
|
+
end
|
479
|
+
|
480
|
+
end
|
481
|
+
|
482
|
+
# Check the auto commit status (on/off)
|
483
|
+
def auto_commit?
|
484
|
+
@auto_commit
|
485
|
+
end
|
486
|
+
|
487
|
+
# Check if monetdb is running behind the merovingian proxy and forward the connection in case
|
488
|
+
def merovingian?
|
489
|
+
if @server_name.downcase == 'merovingian'
|
490
|
+
true
|
491
|
+
else
|
492
|
+
false
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
def mserver?
|
497
|
+
if @server_name.downcase == 'monetdb'
|
498
|
+
true
|
499
|
+
else
|
500
|
+
false
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
# Check which protocol is spoken by the server
|
505
|
+
def mapi_proto_v8?
|
506
|
+
if @protocol == 8
|
507
|
+
true
|
508
|
+
else
|
509
|
+
false
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
def mapi_proto_v9?
|
514
|
+
if @protocol == 9
|
515
|
+
true
|
516
|
+
else
|
517
|
+
false
|
518
|
+
end
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
# handles transactions and savepoints. Can be used to simulate nested transactions.
|
523
|
+
class MonetDBTransaction
|
524
|
+
def initialize
|
525
|
+
@id = 0
|
526
|
+
@savepoint = ""
|
527
|
+
end
|
528
|
+
|
529
|
+
def savepoint
|
530
|
+
@savepoint = "monetdbsp" + @id.to_s
|
531
|
+
end
|
532
|
+
|
533
|
+
def release
|
534
|
+
prev_id
|
535
|
+
end
|
536
|
+
|
537
|
+
def save
|
538
|
+
next_id
|
539
|
+
end
|
540
|
+
|
541
|
+
private
|
542
|
+
def next_id
|
543
|
+
@id += 1
|
544
|
+
end
|
545
|
+
|
546
|
+
def prev_id
|
547
|
+
@id -= 1
|
548
|
+
end
|
549
|
+
|
550
|
+
end
|