vertica 0.9.0.beta3 → 0.9.0.beta4

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -1,9 +1,3 @@
1
- PATH
2
- remote: .
3
- specs:
4
- vertica (0.9.0.beta2)
5
- vertica
6
-
7
1
  GEM
8
2
  remote: http://rubygems.org/
9
3
  specs:
@@ -21,4 +15,3 @@ PLATFORMS
21
15
  DEPENDENCIES
22
16
  jeweler
23
17
  rake
24
- vertica!
data/README.md CHANGED
@@ -1,12 +1,11 @@
1
1
  # Vertica
2
2
 
3
- **WARNING:** This is not well tested software (yet) use at your own risk.
3
+ Vertica is a pure Ruby library for connecting to Vertica databases. You can learn more
4
+ about Vertica at http://www.vertica.com.
4
5
 
5
- ## Description
6
-
7
- Vertica is a pure Ruby library for connecting to Vertica databases. You can learn more
8
- about Vertica at http://www.vertica.com. This library currently supports queries. Prepared
9
- statements still need a bit of work.
6
+ This library currently supports simple queries. Prepared statements are currently not supported
7
+ but are being worked on. The gem is tested against Vertica version 5.0 and should work in both
8
+ Ruby 1.8 and Ruby 1.9.
10
9
 
11
10
  # Install
12
11
 
@@ -24,62 +23,89 @@ and cloned from:
24
23
 
25
24
  # Usage
26
25
 
27
- ## Example Query
26
+ ## Connecting
28
27
 
29
- ### Connecting
28
+ The <code>Vertica.connect</code> methods takes a connection parameter hash and returns a
29
+ connection object. For most options, the gem will use a default value if no value is provided.
30
30
 
31
- vertica = Vertica.connect({
31
+ connection = Vertica.connect({
32
+ :host => 'db_server',
32
33
  :user => 'user',
33
34
  :password => 'password',
34
- :host => 'db_server',
35
- :port => '5433',
36
- :database => 'db
35
+ # :ssl => false, # use SSL for the connection
36
+ # :port => 5433, # default Vertica port: 5433
37
+ # :database => 'db', # there is only one database
38
+ # :role => nil, # the (additional) role(s) to enable for the user.
39
+ # :search_path => nil, # default: <user>,public,v_catalog
40
+ # :row_style => :hash # can also be :array (see below)
37
41
  })
42
+
43
+ To close the connection when you're done with it, run <code>connection.close</code>.
44
+
45
+ ## Querying
38
46
 
39
- ### Buffered Rows
47
+ You can run simple queries using the <code>query</code> method, either in buffered and
48
+ unbuffered mode. For large result sets, you probably do not want to use buffered results.
40
49
 
41
- All rows will first be fetched and buffered into a result object. Probably shouldn't use
42
- this for large result sets.
50
+ ### Unbuffered result
43
51
 
44
- result = vertica.query("SELECT id, name FROM my_table")
45
- result.each_row |row|
52
+ Get all the result rows without buffering by providing a block:
53
+
54
+ connection.query("SELECT id, name FROM my_table") do |row|
46
55
  puts row # => {:id => 123, :name => "Jim Bob"}
47
56
  end
57
+
58
+ connection.close
59
+
60
+ ### Buffered result
61
+
62
+ Store the result of the query method as a variable to get a buffered resultset:
48
63
 
64
+ result = connection.query("SELECT id, name FROM my_table")
65
+ connection.close
66
+
49
67
  result.rows # => [{:id => 123, :name => "Jim Bob"}, {:id => 456, :name => "Joe Jack"}]
50
68
  result.row_count # => 2
69
+
70
+ result.each do |row|
71
+ puts row # => {:id => 123, :name => "Jim Bob"}
72
+ end
51
73
 
52
- vertica.close
53
-
54
- ### Unbuffered Rows
74
+ ### Row format
55
75
 
56
- The vertica gem will not buffer incoming results. The gem will read a result off the
57
- socket and pass it to the provided block.
76
+ By default, rows are returned as hashes, using symbols for the column names. Rows can also
77
+ be returned as arrays by providing a row_style:
58
78
 
59
- vertica.query("SELECT id, name FROM my_table") do |row|
60
- puts row # => {:id => 123, :name => "Jim Bob"}
79
+ connection.query("SELECT id, name FROM my_table", :row_style => :array) do |row|
80
+ puts row # => [123, "Jim Bob"]
61
81
  end
62
- vertica.close
82
+
83
+ By adding <code>:row_style => :array</code> to the connection hash, all results will be
84
+ returned as array.
63
85
 
64
- ### Example Prepared Statement
86
+ # About
65
87
 
66
- This is flaky at best right now and needs some work. This will probably fail and destroy
67
- your connection. You'll need to throw the connection away and start over.
88
+ This package is MIT licensed. See the LICENSE file for more information.
68
89
 
69
- vertica.prepare("my_prepared_statement", "SELECT * FROM my_table WHERE id = ?", 1)
70
- result = vertica.execute_prepared("my_prepared_statement", 13)
71
- result.each_rows |row|
72
- puts row # => {:id => 123, :name => "Jim Bob"}
73
- end
74
- result.rows # => [{:id => 123, :name => "Jim Bob"}, {:id => 456, :name => "Joe Jack"}]
75
- vertica.close
90
+ ## Development
91
+
92
+ This project comes with a test suite. The unit tests in <tt>/test/unit</tt> do not need a database
93
+ connection to run, the functional tests in <tt>/test/functional</tt> do need a working
94
+ database connection. You can specify the connection parameters by copying the file
95
+ <tt>/test/connection.yml.example</tt> to <tt>/test/connection.yml</tt> and filling out the
96
+ necessary fields.
97
+
98
+ Note that the test suite requires write access to the default schema of the provided connection,
99
+ although it tries to be as little invasive as possible: all tables it creates (and drops) are
100
+ prefixed with <tt>test_ruby_vertica_</tt>.
76
101
 
77
- # TODO
102
+ ### TODO
78
103
 
79
- * Tests.
80
- * Lots of tests.
104
+ * Prepared statements
105
+ * More tests
81
106
 
82
- # Authors
107
+ ## Authors
83
108
 
84
109
  * [Matt Bauer](http://github.com/mattbauer) all the hard work
85
110
  * [Jeff Smick](http://github.com/sprsquish) current maintainer
111
+ * [Willem van Bergen](http://github.com/wvanbergen) contributor
data/Rakefile CHANGED
@@ -26,7 +26,7 @@ end
26
26
 
27
27
  require 'rake/testtask'
28
28
  Rake::TestTask.new(:test) do |test|
29
- test.libs << 'test'
29
+ test.libs << 'test' << 'lib'
30
30
  test.pattern = 'test/**/*_test.rb'
31
31
  test.verbose = true
32
32
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.0.beta3
1
+ 0.9.0.beta4
data/lib/vertica.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  require 'date'
2
2
  require 'bigdecimal'
3
3
 
4
+ # Main module for this library. It contains the {.connect} method to return a
5
+ # {Vertica::Connection} instance, and methods to quote values ({.quote}) and
6
+ # identifiers ({.quote_identifier}) to safely include those in SQL strings to
7
+ # prevent SQL injection.
4
8
  module Vertica
5
9
 
6
10
  class Error < StandardError
@@ -9,13 +13,26 @@ module Vertica
9
13
  class QueryError < Error; end
10
14
  end
11
15
 
12
- PROTOCOL_VERSION = 3 << 16
16
+ # The version number of this library.
13
17
  VERSION = File.read(File.join(File.dirname(__FILE__), *%w[.. VERSION])).strip
14
18
 
15
- def self.connect(*args)
16
- Connection.new(*args)
19
+ # The protocol version (3.0.0) implemented in this library.
20
+ PROTOCOL_VERSION = 3 << 16
21
+
22
+ # Opens a new connection to a Vertica database.
23
+ # @param (see Vertica::Connection#initialize)
24
+ # @return [Vertica::Connection] The created connection to Vertica, ready for queries.
25
+ def self.connect(options)
26
+ Vertica::Connection.new(options)
17
27
  end
18
28
 
29
+ # Properly quotes a value for safe usage in SQL queries.
30
+ #
31
+ # This method has quoting rules for common types. Any other object will be converted to
32
+ # a string using +:to_s+ and then quoted as a string.
33
+ #
34
+ # @param [Object] value The value to quote.
35
+ # @return [String] The quoted value that can be safely included in SQL queries.
19
36
  def self.quote(value)
20
37
  case value
21
38
  when nil then 'NULL'
@@ -32,6 +49,9 @@ module Vertica
32
49
  end
33
50
  end
34
51
 
52
+ # Quotes an identifier for safe use within SQL queries, using double quotes.
53
+ # @param [:to_s] identifier The identifier to quote.
54
+ # @return [String] The quoted identifier that can be safely included in SQL queries.
35
55
  def self.quote_identifier(identifier)
36
56
  "\"#{identifier.to_s.gsub(/"/, '""')}\""
37
57
  end
@@ -1,236 +1,187 @@
1
1
  require 'socket'
2
2
 
3
- module Vertica
4
- class Connection
3
+ class Vertica::Connection
5
4
 
6
- STATUSES = {
7
- 'I' => :no_transaction,
8
- 'T' => :in_transaction,
9
- 'E' => :failed_transaction
10
- }
5
+ attr_reader :options, :notices, :transaction_status, :backend_pid, :backend_key, :parameters, :notice_handler
11
6
 
12
- attr_reader :options, :notices, :transaction_status, :backend_pid, :backend_key, :notifications, :parameters
7
+ attr_accessor :row_style, :debug
13
8
 
14
- attr_accessor :row_style, :debug
9
+ def self.cancel(existing_conn)
10
+ conn = self.new(existing_conn.options.merge(:skip_startup => true))
11
+ conn.write Vertica::Messages::CancelRequest.new(existing_conn.backend_pid, existing_conn.backend_key)
12
+ conn.write Vertica::Messages::Flush.new
13
+ conn.socket.close
14
+ end
15
15
 
16
- def self.cancel(existing_conn)
17
- conn = self.new(existing_conn.options.merge(:skip_startup => true))
18
- conn.write Messages::CancelRequest.new(existing_conn.backend_pid, existing_conn.backend_key)
19
- conn.write Messages::Flush.new
20
- conn.socket.close
21
- end
16
+ # Opens a connectio the a Vertica server
17
+ # @param [Hash] options The connection options to use.
18
+ def initialize(options = {})
19
+ reset_values
22
20
 
23
- def initialize(options = {})
24
- reset_values
21
+ @options = {}
22
+ options.each { |key, value| @options[key.to_s.to_sym] = value }
23
+ @options[:port] ||= 5433
25
24
 
26
- @options = {}
27
- options.each { |key, value| @options[key.to_s.to_sym] = value }
28
-
29
- @notices = []
30
-
31
- @row_style = @options[:row_style] ? @options[:row_style] : :hash
32
-
33
- unless options[:skip_startup]
34
- write Messages::Startup.new(@options[:user], @options[:database])
35
- process
36
-
37
- query("SET SEARCH_PATH TO #{options[:search_path]}") if options[:search_path]
38
- query("SET ROLE #{options[:role]}") if options[:role]
39
- end
25
+ @row_style = @options[:row_style] ? @options[:row_style] : :hash
26
+ unless options[:skip_startup]
27
+ startup_connection
28
+ initialize_connection
40
29
  end
41
-
42
- def socket
43
- @socket ||= begin
44
- raw_socket = TCPSocket.new(@options[:host], @options[:port].to_s)
45
- if @options[:ssl]
46
- require 'openssl/ssl'
47
- raw_socket.write Messages::SslRequest.new.to_bytes
48
- if raw_socket.read(1) == 'S'
49
- raw_socket = OpenSSL::SSL::SSLSocket.new(raw_socket, OpenSSL::SSL::SSLContext.new)
50
- raw_socket.sync = true
51
- raw_socket.connect
52
- else
53
- raise Error::ConnectionError.new("SSL requested but server doesn't support it.")
54
- end
30
+ end
31
+
32
+ def on_notice(&block)
33
+ @notice_handler = block
34
+ end
35
+
36
+ def socket
37
+ @socket ||= begin
38
+ raw_socket = TCPSocket.new(@options[:host], @options[:port].to_i)
39
+ if @options[:ssl]
40
+ require 'openssl/ssl'
41
+ raw_socket.write Vertica::Messages::SslRequest.new.to_bytes
42
+ if raw_socket.read(1) == 'S'
43
+ raw_socket = OpenSSL::SSL::SSLSocket.new(raw_socket, OpenSSL::SSL::SSLContext.new)
44
+ raw_socket.sync = true
45
+ raw_socket.connect
46
+ else
47
+ raise Vertica::Error::ConnectionError.new("SSL requested but server doesn't support it.")
55
48
  end
56
-
57
- raw_socket
58
49
  end
50
+
51
+ raw_socket
59
52
  end
53
+ end
60
54
 
61
- def ssl?
62
- socket.kind_of?(OpenSSL::SSL::SSLSocket)
63
- end
64
-
65
- def opened?
66
- @socket && @backend_pid && @transaction_status
67
- end
68
-
69
- def closed?
70
- !opened?
71
- end
72
-
73
- def write(message)
74
- raise ArgumentError, "invalid message: (#{message.inspect})" unless message.respond_to?(:to_bytes)
75
- puts "=> #{message.inspect}" if @debug
76
- socket.write message.to_bytes
77
- end
78
-
79
- def close
80
- write Messages::Terminate.new
81
- socket.close
82
- @socket = nil
83
- rescue Errno::ENOTCONN # the backend closed the socket already
84
- ensure
85
- reset_values
86
- end
87
-
88
- def reset
89
- close if opened?
90
- reset_values
91
- end
92
-
93
- def query(query_string, &block)
94
- raise ArgumentError.new("Query string cannot be blank or empty.") if query_string.nil? || query_string.empty?
95
- reset_result
96
- write Messages::Query.new(query_string)
97
- @process_row = block
98
- result = process(true)
99
- result unless @process_row
100
- end
101
-
102
- def prepare(name, query, params_count = 0)
103
- param_types = Array.new(params_count).fill(0)
104
-
105
- write Messages::Parse.new(name, query, param_types)
106
- write Messages::Describe.new(:prepared_statement, name)
107
- write Messages::Sync.new
108
- write Messages::Flush.new
109
-
110
- process
111
- end
112
-
113
- def execute_prepared(name, *param_values)
114
- portal_name = "" # use the unnamed portal
115
- max_rows = 0 # return all rows
116
-
117
- reset_result
55
+ def ssl?
56
+ socket.kind_of?(OpenSSL::SSL::SSLSocket)
57
+ end
118
58
 
119
- write Messages::Bind.new(portal_name, name, param_values)
120
- write Messages::Execute.new(portal_name, max_rows)
121
- write Messages::Sync.new
59
+ def opened?
60
+ @socket && @backend_pid && @transaction_status
61
+ end
122
62
 
123
- result = process(true)
63
+ def closed?
64
+ !opened?
65
+ end
124
66
 
125
- # Close the portal
126
- write Messages::Close.new(:portal, portal_name)
127
- write Messages::Flush.new
67
+ def write(message)
68
+ raise ArgumentError, "invalid message: (#{message.inspect})" unless message.respond_to?(:to_bytes)
69
+ puts "=> #{message.inspect}" if @debug
70
+ socket.write message.to_bytes
71
+ end
128
72
 
129
- process
73
+ def close
74
+ write Vertica::Messages::Terminate.new
75
+ socket.close
76
+ @socket = nil
77
+ rescue Errno::ENOTCONN # the backend closed the socket already
78
+ ensure
79
+ reset_values
80
+ end
130
81
 
131
- # Return the result from the prepared statement
132
- result
82
+ def reset
83
+ close if opened?
84
+ reset_values
85
+ end
86
+
87
+ def read_message
88
+ type = read_bytes(1)
89
+ size = read_bytes(4).unpack('N').first
90
+ raise Vertica::Error::MessageError.new("Bad message size: #{size}.") unless size >= 4
91
+ message = Vertica::Messages::BackendMessage.factory type, read_bytes(size - 4)
92
+ puts "<= #{message.inspect}" if @debug
93
+ return message
94
+ end
95
+
96
+ def process_message(message)
97
+ case message
98
+ when Vertica::Messages::ErrorResponse
99
+ raise Vertica::Error::ConnectionError.new(message.error_message)
100
+ when Vertica::Messages::NoticeResponse
101
+ @notice_handler.call(message) if @notice_handler
102
+ when Vertica::Messages::BackendKeyData
103
+ @backend_pid = message.pid
104
+ @backend_key = message.key
105
+ when Vertica::Messages::ParameterStatus
106
+ @parameters[message.name] = message.value
107
+ when Vertica::Messages::ReadyForQuery
108
+ @transaction_status = message.transaction_status
109
+ else
110
+ raise Vertica::Error::MessageError, "Unhandled message: #{message.inspect}"
133
111
  end
112
+ end
113
+
134
114
 
135
- protected
136
-
137
-
138
- def read_bytes(n)
139
- bytes = socket.read(n)
140
- raise Vertica::Error::ConnectionError.new("Couldn't read #{n} characters from socket.") if bytes.nil? || bytes.size != n
141
- return bytes
115
+ def query(sql, options = {}, &block)
116
+ job = Vertica::Query.new(self, sql, { :row_style => @row_style }.merge(options))
117
+ job.row_handler = block if block_given?
118
+ return job.run
119
+ end
120
+
121
+ def copy(sql, source = nil, &block)
122
+ job = Vertica::Query.new(self, sql, :row_style => @row_style)
123
+ if block_given?
124
+ job.copy_handler = block
125
+ elsif source && File.exists?(source.to_s)
126
+ job.copy_handler = lambda { |data| file_copy_handler(source, data) }
127
+ elsif source.respond_to?(:read) && source.respond_to?(:eof?)
128
+ job.copy_handler = lambda { |data| io_copy_handler(source, data) }
129
+ end
130
+ return job.run
131
+ end
132
+
133
+ protected
134
+
135
+ def file_copy_handler(input_file, output)
136
+ File.open(input_file, 'r') do |input|
137
+ while data = input.read(4096)
138
+ output << data
139
+ end
142
140
  end
143
-
144
- def read_message
145
- type = read_bytes(1)
146
- size = read_bytes(4).unpack('N').first
147
- raise Vertica::Error::MessageError.new("Bad message size: #{size}.") unless size >= 4
148
- msg = Messages::BackendMessage.factory type, read_bytes(size - 4)
149
- puts "<= #{msg.inspect}" if @debug
150
- return msg
141
+ end
142
+
143
+ def io_copy_handler(input, output)
144
+ until input.eof?
145
+ output << input.read(4096)
151
146
  end
147
+ end
152
148
 
153
-
154
- def process(return_result = false)
155
- result = return_result ? Result.new(row_style) : nil
156
- loop do
157
- case message = read_message
158
- when Messages::Authentication
159
- if message.code != Messages::Authentication::OK
160
- write Messages::Password.new(@options[:password], message.code, {:user => @options[:user], :salt => message.salt})
161
- end
162
-
163
- when Messages::BackendKeyData
164
- @backend_pid = message.pid
165
- @backend_key = message.key
166
-
167
- when Messages::DataRow
168
- @process_row.call(result.format_row(message)) if @process_row && result
169
- result.add_row(message) if result && !@process_row
170
-
171
- when Messages::ErrorResponse
172
- error_class = result ? Vertica::Error::QueryError : Vertica::Error::ConnectionError
173
- raise error_class.new(message.error_message)
149
+ def read_bytes(n)
150
+ bytes = socket.read(n)
151
+ raise Vertica::Error::ConnectionError.new("Couldn't read #{n} characters from socket.") if bytes.nil? || bytes.size != n
152
+ return bytes
153
+ end
174
154
 
175
-
176
- when Messages::NoticeResponse
177
- @notices << message.values
178
-
179
- when Messages::NotificationResponse
180
- @notifications << Notification.new(message.pid, message.condition, message.additional_info)
181
-
182
- when Messages::ParameterStatus
183
- @parameters[message.name] = message.value
184
-
185
- when Messages::ReadyForQuery
186
- @transaction_status = STATUSES[message.transaction_status]
187
- break unless return_result
188
-
189
- when Messages::RowDescription
190
- result.descriptions = message if result
191
-
192
- when Messages::Unknown
193
- raise Error::MessageError.new("Unknown message type: #{message.message_id}")
194
-
195
- when Messages::BindComplete,
196
- Messages::NoData,
197
- Messages::EmptyQueryResponse,
198
- Messages::ParameterDescription
199
- :nothing
200
-
201
- when Messages::CloseComplete,
202
- Messages::CommandComplete,
203
- Messages::ParseComplete,
204
- Messages::PortalSuspended
205
- break
155
+ def startup_connection
156
+ write Vertica::Messages::Startup.new(@options[:user], @options[:database])
157
+ message = nil
158
+ begin
159
+ case message = read_message
160
+ when Vertica::Messages::Authentication
161
+ if message.code != Vertica::Messages::Authentication::OK
162
+ write Vertica::Messages::Password.new(@options[:password], message.code, {:user => @options[:user], :salt => message.salt})
206
163
  end
164
+ else
165
+ process_message(message)
207
166
  end
167
+ end until message.kind_of?(Vertica::Messages::ReadyForQuery)
168
+ end
169
+
170
+ def initialize_connection
171
+ query("SET SEARCH_PATH TO #{options[:search_path]}") if options[:search_path]
172
+ query("SET ROLE #{options[:role]}") if options[:role]
173
+ end
208
174
 
209
- result
210
- end
211
-
212
- def reset_values
213
- reset_notifications
214
- reset_result
215
- @parameters = {}
216
- @backend_pid = nil
217
- @backend_key = nil
218
- @transaction_status = nil
219
- @socket = nil
220
- @process_row = nil
221
- end
222
-
223
- def reset_notifications
224
- @notifications = []
225
- end
226
-
227
- def reset_result
228
- @field_descriptions = []
229
- @field_values = []
230
- end
175
+ def reset_values
176
+ @parameters = {}
177
+ @backend_pid = nil
178
+ @backend_key = nil
179
+ @transaction_status = nil
180
+ @socket = nil
231
181
  end
232
182
  end
233
183
 
184
+ require 'vertica/query'
234
185
  require 'vertica/column'
235
186
  require 'vertica/result'
236
187
  require 'vertica/messages/message'