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