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/lib/MonetDBData.rb
ADDED
@@ -0,0 +1,460 @@
|
|
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
|
+
# Models a MonetDB RecordSet
|
19
|
+
require 'time'
|
20
|
+
require 'ostruct'
|
21
|
+
|
22
|
+
require "bigdecimal"
|
23
|
+
|
24
|
+
require 'MonetDBConnection'
|
25
|
+
|
26
|
+
require 'logger'
|
27
|
+
|
28
|
+
class MonetDBData
|
29
|
+
@@DEBUG = false
|
30
|
+
|
31
|
+
def initialize(connection)
|
32
|
+
@connection = connection
|
33
|
+
@lang = @connection.lang
|
34
|
+
|
35
|
+
# Structure containing the header+results set for a fired Q_TABLE query
|
36
|
+
@header = []
|
37
|
+
@query = {}
|
38
|
+
|
39
|
+
@record_set = []
|
40
|
+
@index = 0 # Position of the last returned record
|
41
|
+
|
42
|
+
|
43
|
+
@row_count = 0
|
44
|
+
@row_offset = 10
|
45
|
+
@row_index = Integer(REPLY_SIZE)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Fire a query and return the server response
|
49
|
+
def execute(q)
|
50
|
+
# fire a query and get ready to receive the data
|
51
|
+
@connection.send(format_query(q))
|
52
|
+
data = @connection.receive
|
53
|
+
|
54
|
+
return if data == nil
|
55
|
+
|
56
|
+
record_set = "" # temporarly store retrieved rows
|
57
|
+
record_set = receive_record_set(data)
|
58
|
+
|
59
|
+
if (@lang == LANG_SQL) or (@lang == LANG_XQUERY and XQUERY_OUTPUT_SEQ)
|
60
|
+
rows = receive_record_set(data)
|
61
|
+
# the fired query is a SELECT; store and return the whole record set
|
62
|
+
if @action == Q_TABLE
|
63
|
+
@header = parse_header_table(@header)
|
64
|
+
@header.freeze
|
65
|
+
|
66
|
+
if @row_index.to_i < @row_count.to_i
|
67
|
+
block_rows = ""
|
68
|
+
while next_block
|
69
|
+
data = @connection.receive
|
70
|
+
block_rows += receive_record_set(data)
|
71
|
+
end
|
72
|
+
record_set += block_rows
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# ruby string management seems to not properly understand the MSG_PROMPT escape character.
|
77
|
+
# In order to avoid data loss the @record_set array is built once that all tuples have been retrieved
|
78
|
+
@record_set = record_set.split("\t]\n")
|
79
|
+
|
80
|
+
if @record_set.length != @query['rows'].to_i
|
81
|
+
raise MonetDBQueryError, "Warning: Query #{@query['id']} declared to result in #{@query['rows']} but #{@record_set.length} returned instead"
|
82
|
+
end
|
83
|
+
elsif (@lang == XQUERY and ! XQUERY_OUTPUT_SEQ)
|
84
|
+
return data # return an xml file
|
85
|
+
end
|
86
|
+
@record_set.freeze
|
87
|
+
end
|
88
|
+
|
89
|
+
# Free memory used to store the record set
|
90
|
+
def free()
|
91
|
+
@connection = nil
|
92
|
+
|
93
|
+
@header = []
|
94
|
+
@query = {}
|
95
|
+
|
96
|
+
@record_set = []
|
97
|
+
@index = 0 # Position of the last returned record
|
98
|
+
|
99
|
+
|
100
|
+
@row_index = Integer(REPLY_SIZE)
|
101
|
+
@row_count = 0
|
102
|
+
@row_offset = 10
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns the record set entries hashed by column name orderd by column position
|
107
|
+
def fetch_all_hash()
|
108
|
+
columns = {}
|
109
|
+
@header["columns_name"].each do |col_name|
|
110
|
+
columns[col_name] = fetch_column_name(col_name)
|
111
|
+
end
|
112
|
+
|
113
|
+
return columns
|
114
|
+
end
|
115
|
+
|
116
|
+
def fetch_hash()
|
117
|
+
if @index >= @query['rows'].to_i
|
118
|
+
return false
|
119
|
+
else
|
120
|
+
columns = {}
|
121
|
+
@header["columns_name"].each do |col_name|
|
122
|
+
position = @header["columns_order"].fetch(col_name)
|
123
|
+
row = parse_tuple(@record_set[@index])
|
124
|
+
columns[col_name] = row[position]
|
125
|
+
end
|
126
|
+
@index += 1
|
127
|
+
return columns
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns the values for the column 'field'
|
132
|
+
def fetch_column_name(field="")
|
133
|
+
position = @header["columns_order"].fetch(field)
|
134
|
+
|
135
|
+
col = Array.new
|
136
|
+
# Scan the record set by row
|
137
|
+
@record_set.each do |row|
|
138
|
+
col << parse_tuple(row[position])
|
139
|
+
end
|
140
|
+
|
141
|
+
return col
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
def fetch()
|
146
|
+
@index
|
147
|
+
if @index > @query['rows'].to_i
|
148
|
+
false
|
149
|
+
else
|
150
|
+
parse_tuple(@record_set[@index])
|
151
|
+
@index += 1
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Cursor method that retrieves all the records present in a table and stores them in a cache.
|
156
|
+
def fetch_all()
|
157
|
+
if @query['type'] == Q_TABLE
|
158
|
+
rows = Array.new
|
159
|
+
@record_set.each do |row|
|
160
|
+
rows << parse_tuple(row)
|
161
|
+
end
|
162
|
+
@index = Integer(rows.length)
|
163
|
+
else
|
164
|
+
raise MonetDBDataError, "There is no record set currently available"
|
165
|
+
end
|
166
|
+
|
167
|
+
return rows
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns the number of rows in the record set
|
171
|
+
def num_rows()
|
172
|
+
return @query['rows'].to_i
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns the number of fields in the record set
|
176
|
+
def num_fields()
|
177
|
+
return @query['columns'].to_i
|
178
|
+
end
|
179
|
+
|
180
|
+
# Returns the (ordered) name of the columns in the record set
|
181
|
+
def name_fields()
|
182
|
+
return @header['columns_name']
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns the (ordered) name of the columns in the record set
|
186
|
+
def type_fields
|
187
|
+
return @header['columns_type']
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
# store block of data, parse it and store it.
|
193
|
+
def receive_record_set(response)
|
194
|
+
rows = ""
|
195
|
+
response.each_line do |row|
|
196
|
+
if row[0].chr == MSG_QUERY
|
197
|
+
if row[1].chr == Q_TABLE
|
198
|
+
@action = Q_TABLE
|
199
|
+
@query = parse_header_query(row)
|
200
|
+
@query.freeze
|
201
|
+
@row_count = @query['rows'].to_i #total number of rows in table
|
202
|
+
elsif row[1].chr == Q_BLOCK
|
203
|
+
# strip the block header from data
|
204
|
+
@action = Q_BLOCK
|
205
|
+
@block = parse_header_query(row)
|
206
|
+
elsif row[1].chr == Q_TRANSACTION
|
207
|
+
@action = Q_TRANSACTION
|
208
|
+
elsif row[1].chr == Q_CREATE
|
209
|
+
@action = Q_CREATE
|
210
|
+
end
|
211
|
+
elsif row[0].chr == MSG_INFO
|
212
|
+
raise MonetDBQueryError, row
|
213
|
+
elsif row[0].chr == MSG_SCHEMA_HEADER
|
214
|
+
# process header data
|
215
|
+
@header << row
|
216
|
+
elsif row[0].chr == MSG_TUPLE
|
217
|
+
rows += row
|
218
|
+
elsif row[0] == MSG_PROMPT
|
219
|
+
return rows
|
220
|
+
end
|
221
|
+
end
|
222
|
+
return rows # return an array of unparsed tuples
|
223
|
+
end
|
224
|
+
|
225
|
+
def next_block
|
226
|
+
if @row_index == @row_count
|
227
|
+
return false
|
228
|
+
else
|
229
|
+
# The increment step is small to better deal with ruby socket's performance.
|
230
|
+
# For larger values of the step performance drop;
|
231
|
+
#
|
232
|
+
@row_offset = [@row_offset, (@row_count - @row_index)].min
|
233
|
+
|
234
|
+
# export offset amount
|
235
|
+
@connection.set_export(@query['id'], @row_index.to_s, @row_offset.to_s)
|
236
|
+
@row_index += @row_offset
|
237
|
+
@row_offset += 1
|
238
|
+
end
|
239
|
+
return true
|
240
|
+
|
241
|
+
end
|
242
|
+
|
243
|
+
# Formats a query <i>string</i> so that it can be parsed by the server
|
244
|
+
def format_query(q)
|
245
|
+
if @lang.downcase == LANG_SQL
|
246
|
+
return "s" + q + ";"
|
247
|
+
elsif @lang.downcase == LANG_XQUERY
|
248
|
+
return "s" + q
|
249
|
+
else
|
250
|
+
raise LanguageNotSupported, @lang
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Parses the data returned by the server and stores the content of header and record set
|
255
|
+
# for a Q_TABLE query in two (immutable) arrays. The Q_TABLE instance is then reperesented
|
256
|
+
# by the OpenStruct variable @Q_TABLE_instance with separate fields for 'header' and 'record_set'.
|
257
|
+
#
|
258
|
+
#def parse_tuples(record_set)
|
259
|
+
# processed_record_set = Array.new
|
260
|
+
|
261
|
+
# record_set.split("]\n").each do |row|
|
262
|
+
|
263
|
+
# remove trailing and ending "[ ]"
|
264
|
+
# row = row.gsub(/^\[\s+/,'')
|
265
|
+
# row = row.gsub(/\t\]\n$/,'')
|
266
|
+
|
267
|
+
# row = row.split(/\t/)
|
268
|
+
|
269
|
+
# processed_row = Array.new
|
270
|
+
|
271
|
+
# index the field position
|
272
|
+
# position = 0
|
273
|
+
# while position < row.length
|
274
|
+
# field = row[position].gsub(/,$/, '')
|
275
|
+
|
276
|
+
# if @type_cast == true
|
277
|
+
# if @header["columns_type"] != nil
|
278
|
+
# name = @header["columns_name"][position]
|
279
|
+
# if @header["columns_type"][name] != nil
|
280
|
+
# type = @header["columns_type"].fetch(name)
|
281
|
+
# end
|
282
|
+
|
283
|
+
# field = self.type_cast(field, type)
|
284
|
+
# field = type_cast(field, type)
|
285
|
+
|
286
|
+
# end
|
287
|
+
# end
|
288
|
+
|
289
|
+
# processed_row << field.gsub(/\\/, '').gsub(/^"/,'').gsub(/"$/,'').gsub(/\"/, '')
|
290
|
+
# position += 1
|
291
|
+
# end
|
292
|
+
# processed_record_set << processed_row
|
293
|
+
# end
|
294
|
+
# return processed_record_set
|
295
|
+
#end
|
296
|
+
|
297
|
+
# parse one tuple as returned from the server
|
298
|
+
def parse_tuple(tuple)
|
299
|
+
fields = Array.new
|
300
|
+
# remove trailing "["
|
301
|
+
tuple = tuple.gsub(/^\[\s+/,'')
|
302
|
+
|
303
|
+
tuple.split(/,\t/).each do |f|
|
304
|
+
fields << f.gsub(/\\/, '').gsub(/^"/,'').gsub(/"$/,'').gsub(/\"/, '')
|
305
|
+
end
|
306
|
+
|
307
|
+
return fields.freeze
|
308
|
+
end
|
309
|
+
|
310
|
+
# Parses a query header and returns information about the query.
|
311
|
+
def parse_header_query(row)
|
312
|
+
type = row[1].chr
|
313
|
+
if type == Q_TABLE
|
314
|
+
# Performing a SELECT: store informations about the table size, query id, total number of records and returned.
|
315
|
+
id = row.split(' ')[1]
|
316
|
+
rows = row.split(' ')[2]
|
317
|
+
columns = row.split(' ')[3]
|
318
|
+
returned = row.split(' ')[4]
|
319
|
+
|
320
|
+
header = { "id" => id, "type" => type, "rows" => rows, "columns" => columns, "returned" => returned }
|
321
|
+
elsif type == Q_BLOCK
|
322
|
+
# processing block header
|
323
|
+
|
324
|
+
id = row.split(' ')[1]
|
325
|
+
columns = row.split(' ')[2]
|
326
|
+
remains = row.split(' ')[3]
|
327
|
+
offset = row.split(' ')[4]
|
328
|
+
|
329
|
+
header = { "id" => id, "type" => type, "remains" => remains, "columns" => columns, "offset" => offset }
|
330
|
+
else
|
331
|
+
header = {"type" => type}
|
332
|
+
end
|
333
|
+
|
334
|
+
return header.freeze
|
335
|
+
end
|
336
|
+
|
337
|
+
# Parses a Q_TABLE header and returns information about the schema.
|
338
|
+
def parse_header_table(header_t)
|
339
|
+
if @query["type"] == Q_TABLE
|
340
|
+
if header_t != nil
|
341
|
+
name_t = header_t[0].split(' ')[1].gsub(/,$/, '')
|
342
|
+
name_cols = Array.new
|
343
|
+
|
344
|
+
header_t[1].split('%')[1].gsub(/'^\%'/, '').split('#')[0].split(' ').each do |col|
|
345
|
+
name_cols << col.gsub(/,$/, '')
|
346
|
+
end
|
347
|
+
|
348
|
+
type_cols = { }
|
349
|
+
header_t[2].split('%')[1].gsub(/'^\%'/, '').split('#')[0].split(' ').each_with_index do |col, i|
|
350
|
+
if col.gsub(/,$/, '') != nil
|
351
|
+
type_cols[ name_cols[i] ] = col.gsub(/,$/, '')
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
length_cols = { }
|
356
|
+
header_t[3].split('%')[1].gsub(/'^\%'/, '').split('#')[0].split(' ').each_with_index do |col, i|
|
357
|
+
length_cols[ name_cols[i] ] = col.gsub(/,$/, '')
|
358
|
+
end
|
359
|
+
|
360
|
+
columns_order = {}
|
361
|
+
name_cols.each_with_index do |col, i|
|
362
|
+
columns_order[col] = i
|
363
|
+
end
|
364
|
+
|
365
|
+
return {"table_name" => name_t, "columns_name" => name_cols, "columns_type" => type_cols,
|
366
|
+
"columns_length" => length_cols, "columns_order" => columns_order}.freeze
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# Overload the class string to convert monetdb to ruby types.
|
373
|
+
class String
|
374
|
+
def getInt
|
375
|
+
self.to_i
|
376
|
+
end
|
377
|
+
|
378
|
+
def getFloat
|
379
|
+
self.to_f
|
380
|
+
end
|
381
|
+
|
382
|
+
def getString
|
383
|
+
#data = self.reverse
|
384
|
+
# parse the string starting from the end;
|
385
|
+
#escape = false
|
386
|
+
#position = 0
|
387
|
+
#for i in data
|
388
|
+
# if i == '\\' and escape == true
|
389
|
+
# if data[position+1] == '\\'
|
390
|
+
# data[position+1] = ''
|
391
|
+
# escape = true
|
392
|
+
# else
|
393
|
+
# escape = false
|
394
|
+
# end
|
395
|
+
# end
|
396
|
+
# position += 1
|
397
|
+
#end
|
398
|
+
#data.reverse
|
399
|
+
self.gsub(/^"/,'').gsub(/"$/,'')
|
400
|
+
end
|
401
|
+
|
402
|
+
def getBlob
|
403
|
+
# first strip trailing and leading " characters
|
404
|
+
self.gsub(/^"/,'').gsub(/"$/,'')
|
405
|
+
|
406
|
+
# convert from HEX to the origianl binary data.
|
407
|
+
blob = ""
|
408
|
+
self.scan(/../) { |tuple| blob += tuple.hex.chr }
|
409
|
+
return blob
|
410
|
+
end
|
411
|
+
|
412
|
+
# ruby currently supports only time + date frommatted timestamps;
|
413
|
+
# treat TIME and DATE as strings.
|
414
|
+
def getTime
|
415
|
+
# HH:MM:SS
|
416
|
+
self.gsub(/^"/,'').gsub(/"$/,'')
|
417
|
+
end
|
418
|
+
|
419
|
+
def getDate
|
420
|
+
self.gsub(/^"/,'').gsub(/"$/,'')
|
421
|
+
end
|
422
|
+
|
423
|
+
def getDateTime
|
424
|
+
#YYYY-MM-DD HH:MM:SS
|
425
|
+
date = self.split(' ')[0].split('-')
|
426
|
+
time = self.split(' ')[1].split(':')
|
427
|
+
|
428
|
+
Time.gm(date[0], date[1], date[2], time[0], time[1], time[2])
|
429
|
+
end
|
430
|
+
|
431
|
+
def getChar
|
432
|
+
# ruby < 1.9 does not have a Char datatype
|
433
|
+
begin
|
434
|
+
c = self.ord
|
435
|
+
rescue
|
436
|
+
c = self
|
437
|
+
end
|
438
|
+
|
439
|
+
return c
|
440
|
+
end
|
441
|
+
|
442
|
+
def getBool
|
443
|
+
if ['1', 'y', 't', 'true'].include?(self)
|
444
|
+
return true
|
445
|
+
elsif ['0','n','f', 'false'].include?(self)
|
446
|
+
return false
|
447
|
+
else
|
448
|
+
# unknown
|
449
|
+
return nil
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
def getNull
|
454
|
+
if self.upcase == 'NONE'
|
455
|
+
return nil
|
456
|
+
else
|
457
|
+
raise "Unknown value"
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|