vertica 0.9.0.beta3 → 0.9.0.beta4

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