ruby-monetdb-sql 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/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
|