monetdb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,300 @@
1
+ require "time"
2
+ require "ostruct"
3
+ require "bigdecimal"
4
+ require "logger"
5
+
6
+ #
7
+ # Models a MonetDB RecordSet
8
+ #
9
+
10
+ class MonetDB
11
+ class Data
12
+
13
+ @@DEBUG = false
14
+
15
+ def initialize(connection)
16
+ @connection = connection
17
+ @lang = @connection.lang
18
+
19
+ @header = []
20
+ @query = {}
21
+
22
+ @record_set = []
23
+ @index = 0 # Position of the last returned record
24
+
25
+ @row_count = 0
26
+ @row_offset = 10
27
+ @row_index = Integer(REPLY_SIZE)
28
+ end
29
+
30
+ # Fire a query and return the server response.
31
+ def execute(q)
32
+ # fire a query and get ready to receive the data
33
+ @connection.send(format_query(q))
34
+ data = @connection.receive
35
+
36
+ return if data == nil
37
+
38
+ record_set = "" # temporarly store retrieved rows
39
+ record_set = receive_record_set(data)
40
+
41
+ if (@lang == LANG_SQL) or (@lang == LANG_XQUERY and XQUERY_OUTPUT_SEQ)
42
+ rows = receive_record_set(data)
43
+ # the fired query is a SELECT; store and return the whole record set
44
+ if @action == Q_TABLE
45
+ @header = parse_header_table(@header)
46
+ @header.freeze
47
+
48
+ if @row_index.to_i < @row_count.to_i
49
+ block_rows = ""
50
+ while next_block
51
+ data = @connection.receive
52
+ block_rows += receive_record_set(data)
53
+ end
54
+ record_set += block_rows
55
+ end
56
+ end
57
+
58
+ # ruby string management seems to not properly understand the MSG_PROMPT escape character.
59
+ # In order to avoid data loss the @record_set array is built once that all tuples have been retrieved
60
+ @record_set = record_set.split("\t]\n")
61
+
62
+ if @record_set.length != @query['rows'].to_i
63
+ raise MonetDBQueryError, "Warning: Query #{@query['id']} declared to result in #{@query['rows']} but #{@record_set.length} returned instead"
64
+ end
65
+ elsif (@lang == XQUERY and ! XQUERY_OUTPUT_SEQ)
66
+ return data # return an xml file
67
+ end
68
+ @record_set.freeze
69
+ end
70
+
71
+ # Free memory used to store the record set.
72
+ def free
73
+ @connection = nil
74
+
75
+ @header = []
76
+ @query = {}
77
+
78
+ @record_set = []
79
+ @index = 0 # Position of the last returned record
80
+
81
+ @row_index = Integer(REPLY_SIZE)
82
+ @row_count = 0
83
+ @row_offset = 10
84
+ end
85
+
86
+ # Returns the record set entries hashed by column name orderd by column position.
87
+ def fetch_all_hash
88
+ columns = {}
89
+ @header["columns_name"].each do |col_name|
90
+ columns[col_name] = fetch_column_name(col_name)
91
+ end
92
+ columns
93
+ end
94
+
95
+ def fetch_hash
96
+ if @index >= @query["rows"].to_i
97
+ false
98
+ else
99
+ columns = {}
100
+ @header["columns_name"].each do |col_name|
101
+ position = @header["columns_order"].fetch(col_name)
102
+ row = parse_tuple(@record_set[@index])
103
+ columns[col_name] = row[position]
104
+ end
105
+ @index += 1
106
+ columns
107
+ end
108
+ end
109
+
110
+ # Returns the values for the column 'field'.
111
+ def fetch_column_name(field = "")
112
+ position = @header["columns_order"].fetch(field)
113
+ col = Array.new
114
+ @record_set.each do |row|
115
+ col << parse_tuple(row[position])
116
+ end
117
+ col
118
+ end
119
+
120
+ def fetch
121
+ @index
122
+ if @index > @query["rows"].to_i
123
+ false
124
+ else
125
+ parse_tuple(@record_set[@index])
126
+ @index += 1
127
+ end
128
+ end
129
+
130
+ # Cursor method that retrieves all the records present in a table and stores them in a cache.
131
+ def fetch_all
132
+ if @query['type'] == Q_TABLE
133
+ rows = Array.new
134
+ @record_set.each do |row|
135
+ rows << parse_tuple(row)
136
+ end
137
+ @index = Integer(rows.length)
138
+ else
139
+ raise MonetDBDataError, "There is no record set currently available"
140
+ end
141
+ rows
142
+ end
143
+
144
+ # Returns the number of rows in the record set.
145
+ def num_rows()
146
+ @query["rows"].to_i
147
+ end
148
+
149
+ # Returns the number of fields in the record set.
150
+ def num_fields()
151
+ @query["columns"].to_i
152
+ end
153
+
154
+ # Returns the (ordered) name of the columns in the record set.
155
+ def name_fields()
156
+ @header["columns_name"]
157
+ end
158
+
159
+ # Returns the (ordered) name of the columns in the record set.
160
+ def type_fields
161
+ @header["columns_type"]
162
+ end
163
+
164
+ private
165
+
166
+ # Store block of data, parse it and store it.
167
+ def receive_record_set(response)
168
+ rows = ""
169
+ response.each_line do |row|
170
+ if row[0].chr == MSG_QUERY
171
+ if row[1].chr == Q_TABLE
172
+ @action = Q_TABLE
173
+ @query = parse_header_query(row)
174
+ @query.freeze
175
+ @row_count = @query['rows'].to_i #total number of rows in table
176
+ elsif row[1].chr == Q_BLOCK
177
+ # strip the block header from data
178
+ @action = Q_BLOCK
179
+ @block = parse_header_query(row)
180
+ elsif row[1].chr == Q_TRANSACTION
181
+ @action = Q_TRANSACTION
182
+ elsif row[1].chr == Q_CREATE
183
+ @action = Q_CREATE
184
+ end
185
+ elsif row[0].chr == MSG_INFO
186
+ raise MonetDBQueryError, row
187
+ elsif row[0].chr == MSG_SCHEMA_HEADER
188
+ # process header data
189
+ @header << row
190
+ elsif row[0].chr == MSG_TUPLE
191
+ rows += row
192
+ elsif row[0] == MSG_PROMPT
193
+ return rows
194
+ end
195
+ end
196
+ rows # return an array of unparsed tuples
197
+ end
198
+
199
+ def next_block
200
+ if @row_index == @row_count
201
+ return false
202
+ else
203
+ # The increment step is small to better deal with ruby socket's performance.
204
+ # For larger values of the step performance drop;
205
+ #
206
+ @row_offset = [@row_offset, (@row_count - @row_index)].min
207
+
208
+ # export offset amount
209
+ @connection.set_export(@query['id'], @row_index.to_s, @row_offset.to_s)
210
+ @row_index += @row_offset
211
+ @row_offset += 1
212
+ end
213
+ true
214
+ end
215
+
216
+ # Formats a query <i>string</i> so that it can be parsed by the server.
217
+ def format_query(q)
218
+ if @lang.downcase == LANG_SQL
219
+ "s" + q + ";"
220
+ elsif @lang.downcase == LANG_XQUERY
221
+ "s" + q
222
+ else
223
+ raise LanguageNotSupported, @lang
224
+ end
225
+ end
226
+
227
+ # Parse one tuple as returned from the server.
228
+ def parse_tuple(tuple)
229
+ fields = Array.new
230
+ # remove trailing "["
231
+ tuple = tuple.gsub(/^\[\s+/,'')
232
+
233
+ tuple.split(/,\t/).each do |f|
234
+ fields << f.gsub(/\\/, '').gsub(/^"/,'').gsub(/"$/,'').gsub(/\"/, '')
235
+ end
236
+
237
+ return fields.freeze
238
+ end
239
+
240
+ # Parses a query header and returns information about the query.
241
+ def parse_header_query(row)
242
+ type = row[1].chr
243
+ if type == Q_TABLE
244
+ # Performing a SELECT: store informations about the table size, query id, total number of records and returned.
245
+ id = row.split(' ')[1]
246
+ rows = row.split(' ')[2]
247
+ columns = row.split(' ')[3]
248
+ returned = row.split(' ')[4]
249
+
250
+ header = { "id" => id, "type" => type, "rows" => rows, "columns" => columns, "returned" => returned }
251
+ elsif type == Q_BLOCK
252
+ # processing block header
253
+
254
+ id = row.split(' ')[1]
255
+ columns = row.split(' ')[2]
256
+ remains = row.split(' ')[3]
257
+ offset = row.split(' ')[4]
258
+
259
+ header = { "id" => id, "type" => type, "remains" => remains, "columns" => columns, "offset" => offset }
260
+ else
261
+ header = {"type" => type}
262
+ end
263
+ header.freeze
264
+ end
265
+
266
+ # Parses a Q_TABLE header and returns information about the schema.
267
+ def parse_header_table(header_t)
268
+ if @query["type"] == Q_TABLE
269
+ if header_t != nil
270
+ name_t = header_t[0].split(' ')[1].gsub(/,$/, '')
271
+ name_cols = Array.new
272
+
273
+ header_t[1].split('%')[1].gsub(/'^\%'/, '').split('#')[0].split(' ').each do |col|
274
+ name_cols << col.gsub(/,$/, '')
275
+ end
276
+
277
+ type_cols = { }
278
+ header_t[2].split('%')[1].gsub(/'^\%'/, '').split('#')[0].split(' ').each_with_index do |col, i|
279
+ if col.gsub(/,$/, '') != nil
280
+ type_cols[ name_cols[i] ] = col.gsub(/,$/, '')
281
+ end
282
+ end
283
+
284
+ length_cols = { }
285
+ header_t[3].split('%')[1].gsub(/'^\%'/, '').split('#')[0].split(' ').each_with_index do |col, i|
286
+ length_cols[ name_cols[i] ] = col.gsub(/,$/, '')
287
+ end
288
+
289
+ columns_order = {}
290
+ name_cols.each_with_index do |col, i|
291
+ columns_order[col] = i
292
+ end
293
+
294
+ {"table_name" => name_t, "columns_name" => name_cols, "columns_type" => type_cols, "columns_length" => length_cols, "columns_order" => columns_order}.freeze
295
+ end
296
+ end
297
+ end
298
+ end
299
+
300
+ end
@@ -0,0 +1,27 @@
1
+ class MonetDB
2
+
3
+ class Error < StandardError
4
+ def initialize(e)
5
+ $stderr.puts e
6
+ end
7
+ end
8
+
9
+ class QueryError < Error
10
+ end
11
+
12
+ class DataError < Error
13
+ end
14
+
15
+ class CommandError < Error
16
+ end
17
+
18
+ class ConnectionError < Error
19
+ end
20
+
21
+ class SocketError < Error
22
+ end
23
+
24
+ class ProtocolError < Error
25
+ end
26
+
27
+ end
@@ -0,0 +1,40 @@
1
+ require "digest/md5"
2
+ require "digest/sha1"
3
+ require "digest/sha2"
4
+
5
+ class MonetDB
6
+ class Hasher
7
+
8
+ def initialize(method, pwd)
9
+ case method.upcase
10
+ when "SHA1"
11
+ @hashfunc = Digest::SHA1.new
12
+ @hashname = method.upcase
13
+ when "SHA256"
14
+ @hashfunc = Digest::SHA256.new
15
+ @hashname = method.upcase
16
+ when "SHA384"
17
+ @hashfunc = Digest::SHA384.new
18
+ @hashname = method.upcase
19
+ when "SHA512"
20
+ @hashfunc = Digest::SHA512.new
21
+ @hashname = method.upcase
22
+ else
23
+ @hashfunc = Digest::MD5.new
24
+ @hashname = "MD5"
25
+ end
26
+ @pwd = pwd
27
+ end
28
+
29
+ # Returns the hash method
30
+ def hashname
31
+ @hashname
32
+ end
33
+
34
+ # Compute hash code
35
+ def hashsum
36
+ @hashfunc.hexdigest(@pwd)
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,36 @@
1
+ #
2
+ # Handles transactions and savepoints. Can be used to simulate nested transactions.
3
+ #
4
+
5
+ class MonetDB
6
+ class Transaction
7
+
8
+ def initialize
9
+ @id = 0
10
+ @savepoint = ""
11
+ end
12
+
13
+ def savepoint
14
+ @savepoint = "monetdbsp#{@id}"
15
+ end
16
+
17
+ def release
18
+ prev_id
19
+ end
20
+
21
+ def save
22
+ next_id
23
+ end
24
+
25
+ private
26
+
27
+ def next_id
28
+ @id += 1
29
+ end
30
+
31
+ def prev_id
32
+ @id -= 1
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,7 @@
1
+ class MonetDB
2
+ MAJOR = 0
3
+ MINOR = 1
4
+ TINY = 0
5
+
6
+ VERSION = [MAJOR, MINOR, TINY].join(".")
7
+ end
data/monetdb.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/monetdb/version", __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Paul Engel"]
6
+ gem.email = ["pm_engel@icloud.com"]
7
+ gem.summary = %q{A pure Ruby database driver for MonetDB}
8
+ gem.description = %q{A pure Ruby database driver for MonetDB}
9
+ gem.homepage = "https://github.com/archan937/monetdb"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "monetdb"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = MonetDB::VERSION
17
+
18
+ gem.add_development_dependency "rake"
19
+ gem.add_development_dependency "pry"
20
+ gem.add_development_dependency "simplecov"
21
+ gem.add_development_dependency "minitest"
22
+ gem.add_development_dependency "mocha"
23
+ end
data/script/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler"
4
+ Bundler.require :default, :development
5
+
6
+ puts "Loading MonetDB development environment (#{MonetDB::VERSION})"
7
+ Pry.start