monetdb 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ module MonetDB
2
+ class Connection
3
+ module Messages
4
+ private
5
+
6
+ def msg_chr(string)
7
+ string.empty? ? "" : string[0].chr
8
+ end
9
+
10
+ def msg?(string, msg)
11
+ msg_chr(string) == msg
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -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
- class MonetDB
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 QueryError < Error
10
- end
11
-
12
- class DataError < Error
6
+ class ConnectionError < Error
13
7
  end
14
8
 
15
- class CommandError < Error
9
+ class ProtocolError < Error
16
10
  end
17
11
 
18
- class ConnectionError < Error
12
+ class AuthenticationError < Error
19
13
  end
20
14
 
21
- class SocketError < Error
15
+ class CommandError < Error
22
16
  end
23
17
 
24
- class ProtocolError < Error
18
+ class QueryError < Error
25
19
  end
26
20
 
27
21
  end
@@ -1,7 +1,7 @@
1
- class MonetDB
1
+ module MonetDB
2
2
  MAJOR = 0
3
- MINOR = 1
4
- TINY = 3
3
+ MINOR = 2
4
+ TINY = 0
5
5
 
6
6
  VERSION = [MAJOR, MINOR, TINY].join(".")
7
7
  end
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
@@ -10,3 +10,6 @@ end
10
10
 
11
11
  require "bundler"
12
12
  Bundler.require :default, :development, :test
13
+
14
+ require_relative "test_helper/minitest"
15
+ require_relative "test_helper/simple_connection"
@@ -0,0 +1,7 @@
1
+ class MiniTest::Test
2
+ def teardown
3
+ MonetDB.instance_variables.each do |name|
4
+ MonetDB.instance_variable_set name, nil
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ class SimpleConnection
2
+ attr_reader :config
3
+
4
+ def initialize
5
+ @config = {
6
+ :host => "localhost",
7
+ :port => 50000,
8
+ :username => "monetdb",
9
+ :password => "monetdb"
10
+ }
11
+ end
12
+
13
+ end
@@ -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