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.
- checksums.yaml +15 -0
- data/.gitignore +8 -0
- data/CHANGELOG.rdoc +5 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +40 -0
- data/LICENSE +17 -0
- data/README.rdoc +154 -0
- data/Rakefile +10 -0
- data/VERSION +1 -0
- data/lib/monetdb.rb +263 -0
- data/lib/monetdb/connection.rb +443 -0
- data/lib/monetdb/core_ext.rb +1 -0
- data/lib/monetdb/core_ext/string.rb +67 -0
- data/lib/monetdb/data.rb +300 -0
- data/lib/monetdb/error.rb +27 -0
- data/lib/monetdb/hasher.rb +40 -0
- data/lib/monetdb/transaction.rb +36 -0
- data/lib/monetdb/version.rb +7 -0
- data/monetdb.gemspec +23 -0
- data/script/console +7 -0
- data/test/test_helper.rb +12 -0
- data/test/test_helper/coverage.rb +8 -0
- data/test/unit/test_monetdb.rb +15 -0
- metadata +139 -0
data/lib/monetdb/data.rb
ADDED
@@ -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
|
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
|