monetdb 0.1.3 → 0.2.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 +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
|