monetdb 0.1.0

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,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