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.
@@ -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