monetdb 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/.travis.yml +5 -0
- data/CHANGELOG.rdoc +4 -0
- data/Gemfile +0 -15
- data/README.rdoc +2 -2
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/lib/monetdb.rb +6 -255
- data/lib/monetdb/connection.rb +94 -412
- data/lib/monetdb/connection/messages.rb +16 -0
- data/lib/monetdb/connection/query.rb +136 -0
- data/lib/monetdb/connection/setup.rb +125 -0
- data/lib/monetdb/error.rb +6 -12
- data/lib/monetdb/version.rb +3 -3
- data/monetdb.gemspec +6 -3
- data/script/console +16 -1
- data/test/test_helper.rb +3 -0
- data/test/test_helper/minitest.rb +7 -0
- data/test/test_helper/simple_connection.rb +13 -0
- data/test/unit/connection/test_messages.rb +48 -0
- data/test/unit/connection/test_query.rb +276 -0
- data/test/unit/connection/test_setup.rb +364 -0
- data/test/unit/test_connection.rb +178 -0
- data/test/unit/test_monetdb.rb +77 -1
- metadata +62 -24
- data/lib/monetdb/core_ext.rb +0 -1
- data/lib/monetdb/core_ext/string.rb +0 -67
- data/lib/monetdb/data.rb +0 -300
- data/lib/monetdb/hasher.rb +0 -40
- data/lib/monetdb/transaction.rb +0 -36
@@ -0,0 +1,136 @@
|
|
1
|
+
module MonetDB
|
2
|
+
class Connection
|
3
|
+
module Query
|
4
|
+
|
5
|
+
def query(statement)
|
6
|
+
raise ConnectionError, "Not connected to server" unless connected?
|
7
|
+
|
8
|
+
write "s#{statement};"
|
9
|
+
response = read.split("\n")
|
10
|
+
|
11
|
+
query_header, table_header = extract_headers!(response)
|
12
|
+
|
13
|
+
if query_header[:type] == Q_TABLE
|
14
|
+
unless query_header[:rows] == response.size
|
15
|
+
raise QueryError, "Amount of fetched rows does not match header value (#{response.size} instead of #{query_header[:rows]})"
|
16
|
+
end
|
17
|
+
response = parse_rows(table_header, response.join("\n"))
|
18
|
+
else
|
19
|
+
response = true
|
20
|
+
end
|
21
|
+
|
22
|
+
response
|
23
|
+
end
|
24
|
+
|
25
|
+
alias :select_rows :query
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def extract_headers!(response)
|
30
|
+
[parse_query_header!(response), parse_scheme_header!(response)]
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_query_header!(response)
|
34
|
+
header = response.shift
|
35
|
+
|
36
|
+
raise QueryError, header if header[0].chr == MSG_ERROR
|
37
|
+
|
38
|
+
unless header[0].chr == MSG_QUERY
|
39
|
+
raise QueryError, "Expected an query header (#{MSG_QUERY}) but got (#{header[0].chr})"
|
40
|
+
end
|
41
|
+
|
42
|
+
to_query_header_hash header
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_query_header_hash(header)
|
46
|
+
hash = {:type => header[1].chr}
|
47
|
+
|
48
|
+
keys = {
|
49
|
+
Q_TABLE => [:id, :rows, :columns, :returned],
|
50
|
+
Q_BLOCK => [:id, :columns, :remains, :offset]
|
51
|
+
}[hash[:type]]
|
52
|
+
|
53
|
+
if keys
|
54
|
+
values = header.split(" ")[1, 4].collect(&:to_i)
|
55
|
+
hash.merge! Hash[keys.zip(values)]
|
56
|
+
end
|
57
|
+
|
58
|
+
hash.freeze
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_scheme_header!(response)
|
62
|
+
if (count = response.take_while{|x| x[0].chr == MSG_SCHEME}.size) > 0
|
63
|
+
header = response.shift(count).collect{|x| x.gsub(/(^#{MSG_SCHEME}\s+|\s+#[^#]+$)/, "").split(/,?\s+/)}
|
64
|
+
to_scheme_header_hash header
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_scheme_header_hash(header)
|
69
|
+
table_name = header[0][0]
|
70
|
+
column_names = header[1]
|
71
|
+
column_types = header[2].collect(&:to_sym)
|
72
|
+
column_lengths = header[3].collect(&:to_i)
|
73
|
+
|
74
|
+
{:table_name => table_name, :column_names => column_names, :column_types => column_types, :column_lengths => column_lengths}.freeze
|
75
|
+
end
|
76
|
+
|
77
|
+
def parse_rows(table_header, response)
|
78
|
+
column_types = table_header[:column_types]
|
79
|
+
response.split("\t]\n").collect do |row|
|
80
|
+
parsed, values = [], row.slice(1..-1).split(",\t")
|
81
|
+
values.each_with_index do |value, index|
|
82
|
+
parsed << parse_value(column_types[index], value.strip)
|
83
|
+
end
|
84
|
+
parsed
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def parse_value(type, value)
|
89
|
+
unless value == "NULL"
|
90
|
+
case type
|
91
|
+
when :varchar, :text
|
92
|
+
parse_string_value value
|
93
|
+
when :int, :smallint, :bigint
|
94
|
+
parse_integer_value value
|
95
|
+
when :double, :float, :real
|
96
|
+
parse_float_value value
|
97
|
+
when :date
|
98
|
+
parse_date_value value
|
99
|
+
when :timestamp
|
100
|
+
parse_date_time_value value
|
101
|
+
when :tinyint
|
102
|
+
parse_boolean_value value
|
103
|
+
else
|
104
|
+
raise NotImplementedError, "Cannot parse value of type #{type}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def parse_string_value(value)
|
110
|
+
value.slice(1..-2).force_encoding("UTF-8")
|
111
|
+
end
|
112
|
+
|
113
|
+
def parse_integer_value(value)
|
114
|
+
value.to_i
|
115
|
+
end
|
116
|
+
|
117
|
+
def parse_float_value(value)
|
118
|
+
value.to_f
|
119
|
+
end
|
120
|
+
|
121
|
+
def parse_date_value(value)
|
122
|
+
Date.new *value.split("-").collect(&:to_i)
|
123
|
+
end
|
124
|
+
|
125
|
+
def parse_date_time_value(value)
|
126
|
+
date, time = value.split(" ")
|
127
|
+
Time.new *(date.split("-") + time.split(":")).collect(&:to_i)
|
128
|
+
end
|
129
|
+
|
130
|
+
def parse_boolean_value(value)
|
131
|
+
value == "1"
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module MonetDB
|
2
|
+
class Connection
|
3
|
+
module Setup
|
4
|
+
private
|
5
|
+
|
6
|
+
def setup
|
7
|
+
authenticate
|
8
|
+
set_timezone_interval
|
9
|
+
set_reply_size
|
10
|
+
end
|
11
|
+
|
12
|
+
def authenticate
|
13
|
+
obtain_server_challenge!
|
14
|
+
|
15
|
+
write authentication_string
|
16
|
+
response = read
|
17
|
+
|
18
|
+
case msg_chr(response)
|
19
|
+
when MSG_ERROR
|
20
|
+
raise MonetDB::AuthenticationError, "Authentication failed: #{response}"
|
21
|
+
when MSG_REDIRECT
|
22
|
+
authentication_redirect response
|
23
|
+
else
|
24
|
+
@authentication_redirects = nil
|
25
|
+
true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def obtain_server_challenge!
|
30
|
+
config.merge! server_challenge
|
31
|
+
assert_supported_protocol!
|
32
|
+
select_supported_auth_type!
|
33
|
+
end
|
34
|
+
|
35
|
+
def server_challenge
|
36
|
+
keys_and_values = [:salt, :server_name, :protocol, :auth_types, :server_endianness, :password_digest_method].zip read.split(":")
|
37
|
+
Hash[keys_and_values]
|
38
|
+
end
|
39
|
+
|
40
|
+
def assert_supported_protocol!
|
41
|
+
unless PROTOCOLS.include?(config[:protocol])
|
42
|
+
raise MonetDB::ProtocolError, "Protocol '#{config[:protocol]}' not supported. Only #{PROTOCOLS.collect{|x| "'#{x}'"}.join(", ")}."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def select_supported_auth_type!
|
47
|
+
unless config[:auth_type] = (AUTH_TYPES & (auth_types = config[:auth_types].split(","))).first
|
48
|
+
raise MonetDB::AuthenticationError, "Authentication types (#{auth_types.join(", ")}) not supported. Only #{AUTH_TYPES.join(", ")}."
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def authentication_string
|
53
|
+
[ENDIANNESS, config[:username], "{#{config[:auth_type]}}#{authentication_hashsum}", LANG, config[:database], ""].join(":")
|
54
|
+
end
|
55
|
+
|
56
|
+
def authentication_hashsum
|
57
|
+
auth_type, password, password_digest_method = config.values_at(:auth_type, :password, :password_digest_method)
|
58
|
+
|
59
|
+
case auth_type
|
60
|
+
when AUTH_MD5, AUTH_SHA512, AUTH_SHA384, AUTH_SHA256, AUTH_SHA1
|
61
|
+
password = hexdigest(password_digest_method, password) if config[:protocol] == MAPI_V9
|
62
|
+
hexdigest(auth_type, password + config[:salt])
|
63
|
+
when AUTH_PLAIN
|
64
|
+
config[:password] + config[:salt]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def hexdigest(method, value)
|
69
|
+
Digest.const_get(method).new.hexdigest(value)
|
70
|
+
end
|
71
|
+
|
72
|
+
def authentication_redirect(response)
|
73
|
+
unless response.split("\n").detect{|x| x.match(/^\^mapi:(.*)/)}
|
74
|
+
raise MonetDB::AuthenticationError, "Authentication redirect not supported: #{response}"
|
75
|
+
end
|
76
|
+
|
77
|
+
begin
|
78
|
+
scheme, userinfo, host, port, registry, database = URI.split(uri = $1)
|
79
|
+
rescue URI::InvalidURIError
|
80
|
+
raise MonetDB::AuthenticationError, "Invalid authentication redirect URI: #{uri}"
|
81
|
+
end
|
82
|
+
|
83
|
+
case scheme
|
84
|
+
when "merovingian"
|
85
|
+
if (@authentication_redirects ||= 0) < 5
|
86
|
+
@authentication_redirects += 1
|
87
|
+
authenticate
|
88
|
+
else
|
89
|
+
raise MonetDB::AuthenticationError, "Merovingian: Too many redirects while proxying"
|
90
|
+
end
|
91
|
+
when "monetdb"
|
92
|
+
config[:host] = host
|
93
|
+
config[:port] = port
|
94
|
+
connect
|
95
|
+
else
|
96
|
+
raise MonetDB::AuthenticationError, "Cannot authenticate"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def set_timezone_interval
|
101
|
+
return false if @timezone_interval_set
|
102
|
+
|
103
|
+
offset = Time.now.gmt_offset / 3600
|
104
|
+
interval = "'+#{offset.to_s.rjust(2, "0")}:00'"
|
105
|
+
|
106
|
+
write "sSET TIME ZONE INTERVAL #{interval} HOUR TO MINUTE;"
|
107
|
+
response = read
|
108
|
+
|
109
|
+
raise CommandError, "Unable to set timezone interval: #{response}" if msg?(response, MSG_ERROR)
|
110
|
+
@timezone_interval_set = true
|
111
|
+
end
|
112
|
+
|
113
|
+
def set_reply_size
|
114
|
+
return false if @reply_size_set
|
115
|
+
|
116
|
+
write "Xreply_size #{REPLY_SIZE}\n"
|
117
|
+
response = read
|
118
|
+
|
119
|
+
raise CommandError, "Unable to set reply size: #{response}" if msg?(response, MSG_ERROR)
|
120
|
+
@reply_size_set = true
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
data/lib/monetdb/error.rb
CHANGED
@@ -1,27 +1,21 @@
|
|
1
|
-
|
1
|
+
module MonetDB
|
2
2
|
|
3
3
|
class Error < StandardError
|
4
|
-
def initialize(e)
|
5
|
-
$stderr.puts e
|
6
|
-
end
|
7
4
|
end
|
8
5
|
|
9
|
-
class
|
10
|
-
end
|
11
|
-
|
12
|
-
class DataError < Error
|
6
|
+
class ConnectionError < Error
|
13
7
|
end
|
14
8
|
|
15
|
-
class
|
9
|
+
class ProtocolError < Error
|
16
10
|
end
|
17
11
|
|
18
|
-
class
|
12
|
+
class AuthenticationError < Error
|
19
13
|
end
|
20
14
|
|
21
|
-
class
|
15
|
+
class CommandError < Error
|
22
16
|
end
|
23
17
|
|
24
|
-
class
|
18
|
+
class QueryError < Error
|
25
19
|
end
|
26
20
|
|
27
21
|
end
|
data/lib/monetdb/version.rb
CHANGED
data/monetdb.gemspec
CHANGED
@@ -4,8 +4,8 @@ require File.expand_path("../lib/monetdb/version", __FILE__)
|
|
4
4
|
Gem::Specification.new do |gem|
|
5
5
|
gem.authors = ["Paul Engel"]
|
6
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}
|
7
|
+
gem.summary = %q{A pure Ruby database driver for MonetDB (monetdb5-sql)}
|
8
|
+
gem.description = %q{A pure Ruby database driver for MonetDB (monetdb5-sql)}
|
9
9
|
gem.homepage = "https://github.com/archan937/monetdb"
|
10
10
|
|
11
11
|
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
@@ -15,9 +15,12 @@ Gem::Specification.new do |gem|
|
|
15
15
|
gem.require_paths = ["lib"]
|
16
16
|
gem.version = MonetDB::VERSION
|
17
17
|
|
18
|
+
gem.add_dependency "activesupport"
|
19
|
+
|
18
20
|
gem.add_development_dependency "rake"
|
21
|
+
gem.add_development_dependency "yard"
|
19
22
|
gem.add_development_dependency "pry"
|
20
23
|
gem.add_development_dependency "simplecov"
|
21
24
|
gem.add_development_dependency "minitest"
|
22
25
|
gem.add_development_dependency "mocha"
|
23
|
-
end
|
26
|
+
end
|
data/script/console
CHANGED
@@ -3,5 +3,20 @@
|
|
3
3
|
require "bundler"
|
4
4
|
Bundler.require :default, :development
|
5
5
|
|
6
|
+
QUERY = "SELECT * FROM stats LIMIT 2000"
|
7
|
+
LOGIN = {
|
8
|
+
"host" => "localhost",
|
9
|
+
"port" => 50000,
|
10
|
+
"database" => "my_monetdb",
|
11
|
+
"username" => "monetdb",
|
12
|
+
"password" => "monetdb"
|
13
|
+
}
|
14
|
+
|
15
|
+
MonetDB.establish_connection LOGIN
|
16
|
+
|
17
|
+
def query
|
18
|
+
MonetDB.connection.query QUERY
|
19
|
+
end
|
20
|
+
|
6
21
|
puts "Loading MonetDB development environment (#{MonetDB::VERSION})"
|
7
|
-
Pry.start
|
22
|
+
Pry.start
|
data/test/test_helper.rb
CHANGED
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative "../../test_helper"
|
2
|
+
|
3
|
+
module Unit
|
4
|
+
module Connection
|
5
|
+
class TestSetup < MiniTest::Test
|
6
|
+
|
7
|
+
class Connection < SimpleConnection
|
8
|
+
include MonetDB::Connection::Messages
|
9
|
+
end
|
10
|
+
|
11
|
+
describe MonetDB::Connection::Messages do
|
12
|
+
before do
|
13
|
+
@connection = Connection.new
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#msg_chr" do
|
17
|
+
describe "when passing an empty string" do
|
18
|
+
it "returns an empty string" do
|
19
|
+
assert_equal "", @connection.send(:msg_chr, "")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "when passing a non-empty string" do
|
24
|
+
it "returns the first character" do
|
25
|
+
assert_equal " ", @connection.send(:msg_chr, " ")
|
26
|
+
assert_equal "%", @connection.send(:msg_chr, "%foobar")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#msg?" do
|
32
|
+
it "verifies whether the passed string matches the passed message character" do
|
33
|
+
assert_equal true , @connection.send(:msg?, "!syntax error", MonetDB::Connection::MSG_ERROR)
|
34
|
+
assert_equal false, @connection.send(:msg?, "!syntax error", MonetDB::Connection::MSG_PROMPT)
|
35
|
+
|
36
|
+
assert_equal true , @connection.send(:msg?, "", MonetDB::Connection::MSG_PROMPT)
|
37
|
+
assert_equal false, @connection.send(:msg?, "", MonetDB::Connection::MSG_ERROR)
|
38
|
+
|
39
|
+
@connection.expects(:msg_chr).with("foo").twice.returns("!")
|
40
|
+
assert_equal true , @connection.send(:msg?, "foo", MonetDB::Connection::MSG_ERROR)
|
41
|
+
assert_equal false, @connection.send(:msg?, "foo", MonetDB::Connection::MSG_PROMPT)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|