vertica 0.7.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/LICENSE +19 -0
  2. data/README.textile +69 -0
  3. data/Rakefile +44 -0
  4. data/lib/vertica/bit_helper.rb +51 -0
  5. data/lib/vertica/column.rb +68 -0
  6. data/lib/vertica/connection.rb +247 -0
  7. data/lib/vertica/messages/authentication.rb +33 -0
  8. data/lib/vertica/messages/backend_key_data.rb +16 -0
  9. data/lib/vertica/messages/bind.rb +36 -0
  10. data/lib/vertica/messages/bind_complete.rb +8 -0
  11. data/lib/vertica/messages/cancel_request.rb +25 -0
  12. data/lib/vertica/messages/close.rb +30 -0
  13. data/lib/vertica/messages/close_complete.rb +8 -0
  14. data/lib/vertica/messages/command_complete.rb +16 -0
  15. data/lib/vertica/messages/data_row.rb +23 -0
  16. data/lib/vertica/messages/describe.rb +30 -0
  17. data/lib/vertica/messages/empty_query_response.rb +8 -0
  18. data/lib/vertica/messages/error_response.rb +59 -0
  19. data/lib/vertica/messages/execute.rb +24 -0
  20. data/lib/vertica/messages/flush.rb +15 -0
  21. data/lib/vertica/messages/message.rb +85 -0
  22. data/lib/vertica/messages/no_data.rb +8 -0
  23. data/lib/vertica/messages/notice_response.rb +21 -0
  24. data/lib/vertica/messages/notification_response.rb +18 -0
  25. data/lib/vertica/messages/parameter_description.rb +19 -0
  26. data/lib/vertica/messages/parameter_status.rb +17 -0
  27. data/lib/vertica/messages/parse.rb +31 -0
  28. data/lib/vertica/messages/parse_complete.rb +8 -0
  29. data/lib/vertica/messages/password.rb +33 -0
  30. data/lib/vertica/messages/portal_suspended.rb +8 -0
  31. data/lib/vertica/messages/query.rb +20 -0
  32. data/lib/vertica/messages/ready_for_query.rb +14 -0
  33. data/lib/vertica/messages/row_description.rb +29 -0
  34. data/lib/vertica/messages/ssl_request.rb +14 -0
  35. data/lib/vertica/messages/startup.rb +38 -0
  36. data/lib/vertica/messages/sync.rb +15 -0
  37. data/lib/vertica/messages/terminate.rb +14 -0
  38. data/lib/vertica/messages/unknown.rb +11 -0
  39. data/lib/vertica/notice.rb +11 -0
  40. data/lib/vertica/notification.rb +13 -0
  41. data/lib/vertica/result.rb +28 -0
  42. data/lib/vertica/vertica_socket.rb +8 -0
  43. data/lib/vertica.rb +19 -0
  44. data/test/connection_test.rb +191 -0
  45. data/test/create_schema.sql +4 -0
  46. data/test/test_helper.rb +25 -0
  47. data/vertica.gemspec +64 -0
  48. metadata +112 -0
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009 Matt Bauer
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,69 @@
1
+ h1. Vertica
2
+
3
+ by Matt Bauer
4
+
5
+ h2. 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 and
9
+ prepared statements.
10
+
11
+ h2. Install
12
+
13
+ $ gem install mattbauer-vertica --source http://gems.github.com
14
+
15
+ h2. Source
16
+
17
+ Vertica's git repo is available on GitHub, which can be browsed at:
18
+
19
+ http://github.com/mattbauer/vertica
20
+
21
+ and cloned from:
22
+
23
+ git://github.com/mattbauer/vertica.git
24
+
25
+ h2. Usage
26
+
27
+ h4. Example Query
28
+
29
+ <pre>
30
+ <code>
31
+ c = Vertica::Connection.new(
32
+ :user => 'user',
33
+ :password => 'password',
34
+ :host => 'db_server',
35
+ :port => '5433',
36
+ :database => 'db
37
+ )
38
+ r = c.query("SELECT * FROM my_table")
39
+ puts r.row_count
40
+ puts r.columns[0].name
41
+ puts r.rows
42
+ c.close
43
+ </code>
44
+ </pre>
45
+
46
+ h4. Example Prepared Statement
47
+
48
+ <pre>
49
+ <code>
50
+ c = Vertica::Connection.new(
51
+ :user => 'user',
52
+ :password => 'password',
53
+ :host => 'db_server',
54
+ :port => '5433',
55
+ :database => 'db
56
+ )
57
+ c.prepare("my_prepared_statement", "SELECT * FROM my_table WHERE id = ?", 1)
58
+ r = c.execute_prepared("my_prepared_statement", 13)
59
+ puts r.row_count
60
+ puts r.columns[0].name
61
+ puts r.rows
62
+ c.close
63
+ </code>
64
+ </pre>
65
+
66
+ h2. Running The Tests
67
+
68
+ To run the tests, change the values in test_helper.rb to match your db configuration. Then
69
+ execute the create_schema.sql on the database. Then you may run the tests.
data/Rakefile ADDED
@@ -0,0 +1,44 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/testtask'
5
+
6
+ load 'vertica.gemspec'
7
+
8
+ Rake::GemPackageTask.new(VERTICA_SPEC) do |pkg|
9
+ pkg.need_tar = true
10
+ end
11
+
12
+ task :default => "test"
13
+
14
+ desc "Clean"
15
+ task :clean do
16
+ include FileUtils
17
+ rm_rf 'pkg'
18
+ end
19
+
20
+ desc "Run tests"
21
+ Rake::TestTask.new("test") do |t|
22
+ t.libs << ["test", "ext"]
23
+ t.pattern = 'test/*_test.rb'
24
+ t.verbose = true
25
+ t.warning = true
26
+ end
27
+
28
+ task :doc => [:rdoc]
29
+ namespace :doc do
30
+ Rake::RDocTask.new do |rdoc|
31
+ files = ["README", "lib/**/*.rb"]
32
+ rdoc.rdoc_files.add(files)
33
+ rdoc.main = "README.textile"
34
+ rdoc.title = "Vertica Docs"
35
+ rdoc.rdoc_dir = "doc"
36
+ rdoc.options << "--line-numbers" << "--inline-source"
37
+ end
38
+ end
39
+
40
+ desc "Run rcov on current app"
41
+ task :rcov do
42
+ system "rm -rf coverage && rcov -o coverage -x rcov.rb test/*_test.rb"
43
+ system("open coverage/index.html") if PLATFORM['darwin']
44
+ end
@@ -0,0 +1,51 @@
1
+ module Vertica
2
+ module BitHelper
3
+
4
+ def readn(n)
5
+ s = read(n)
6
+ raise "couldn't read #{n} characters" if s.nil? or s.size != n # TODO make into a Vertica Exception
7
+ s
8
+ end
9
+
10
+ def write_byte(value)
11
+ write [value].pack('C')
12
+ end
13
+
14
+ def read_byte
15
+ readn(1).unpack('C').first
16
+ end
17
+
18
+ def write_network_int16(value)
19
+ write [value].pack('n')
20
+ end
21
+
22
+ def read_network_int16
23
+ readn(2).unpack('n').first
24
+ end
25
+
26
+ def write_network_int32(value)
27
+ write [value].pack('N')
28
+ end
29
+
30
+ def read_network_int32
31
+ handle_endian_flavor(readn(4)).unpack('l').first
32
+ end
33
+
34
+ def write_cstring(value)
35
+ raise ArgumentError, "Invalid cstring" if value.include?("\000")
36
+ write "#{value}\000"
37
+ end
38
+
39
+ def read_cstring
40
+ readline("\000")[0..-2]
41
+ end
42
+
43
+ def handle_endian_flavor(s)
44
+ little_endian? ? s.reverse : s
45
+ end
46
+
47
+ def little_endian?
48
+ @little_endian ||= ([0x12345678].pack("L") == "\x12\x34\x56\x78" ? false : true)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,68 @@
1
+ module Vertica
2
+ class Column
3
+ attr_reader :name
4
+ attr_reader :table_oid
5
+ attr_reader :type_modifier
6
+ attr_reader :size
7
+ attr_reader :data_type
8
+
9
+ DATA_TYPES = [
10
+ :unspecified,
11
+ :tuple,
12
+ :pos,
13
+ :record,
14
+ :unknown,
15
+ :bool,
16
+ :in,
17
+ :float,
18
+ :char,
19
+ :varchar,
20
+ :date,
21
+ :time,
22
+ :timestamp,
23
+ :timestamp_tz,
24
+ :interval,
25
+ :time_tz,
26
+ :numberic,
27
+ :bytea,
28
+ :rle_tuple
29
+ ]
30
+
31
+ DATA_TYPE_CONVERSIONS = {
32
+ :unspecified => nil,
33
+ :tuple => nil,
34
+ :pos => nil,
35
+ :record => nil,
36
+ :unknown => nil,
37
+ :bool => lambda { |s| s == 't' },
38
+ :in => lambda { |s| s.to_i },
39
+ :float => lambda { |s| s.to_f },
40
+ :char => nil,
41
+ :varchar => nil,
42
+ :date => lambda { |s| Date.new(*s.split("-").map{|x| x.to_i}) },
43
+ :time => lambda { |s| Time.parse(s) },
44
+ :timestamp => lambda { |s| DateTime.parse(s, true) },
45
+ :timestamp_tz => lambda { |s| DateTime.parse(s, true) },
46
+ :interval => nil,
47
+ :time_tz => lambda { |s| Time.parse(s) },
48
+ :numberic => lambda { |s| s.to_d },
49
+ :bytea => nil,
50
+ :rle_tuple => nil
51
+ }
52
+
53
+ def initialize(type_modifier, format_code, table_oid, name, attribute_number, data_type_oid, size)
54
+ @type_modifier = type_modifier
55
+ @format = (format_code == 0 ? :text : :binary)
56
+ @table_oid = table_oid
57
+ @name = name
58
+ @attribute_number = attribute_number
59
+ @data_type = DATA_TYPES[data_type_oid]
60
+ @size = size
61
+ end
62
+
63
+ def convert(s)
64
+ l = DATA_TYPE_CONVERSIONS[@data_type]
65
+ l ? l.call(s) : s
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,247 @@
1
+ require 'uri'
2
+ require 'stringio'
3
+ require 'vertica/vertica_socket'
4
+ require 'vertica/messages/message'
5
+ require 'openssl/ssl'
6
+
7
+ module Vertica
8
+
9
+ class Connection
10
+
11
+ def initialize(options = {})
12
+ reset_values
13
+
14
+ @options = options
15
+ establish_connection
16
+
17
+ unless options[:skip_startup]
18
+ Messages::Startup.new(@options[:user], @options[:database]).to_bytes(@conn)
19
+ process
20
+ end
21
+ end
22
+
23
+ def close
24
+ raise_if_not_open
25
+ Messages::Terminate.new.to_bytes(@conn)
26
+ @conn.shutdown
27
+ rescue Errno::ENOTCONN
28
+ # the backend closed the connection already
29
+ ensure
30
+ reset_values
31
+ end
32
+
33
+ def reset
34
+ close if opened?
35
+ reset_values
36
+ establish_connection
37
+ end
38
+
39
+ def options
40
+ @options.dup
41
+ end
42
+
43
+ def transaction_status
44
+ @transaction_status
45
+ end
46
+
47
+ def backend_pid
48
+ @backend_pid
49
+ end
50
+
51
+ def backend_key
52
+ @backend_key
53
+ end
54
+
55
+ def notifications
56
+ @notifications
57
+ end
58
+
59
+ def parameters
60
+ @parameters.dup
61
+ end
62
+
63
+ def put_copy_data; raise NotImplementedError.new; end
64
+ def put_copy_end; raise NotImplementedError.new; end
65
+ def get_copy_data; raise NotImplementedError.new; end
66
+
67
+ def opened?
68
+ @conn && @backend_pid && @transaction_status
69
+ end
70
+
71
+ def closed?
72
+ !opened?
73
+ end
74
+
75
+ def query(query_string)
76
+ raise ArgumentError.new("Query string cannot be blank or empty.") if query_string.nil? || query_string.empty?
77
+ raise_if_not_open
78
+ reset_result
79
+
80
+ Messages::Query.new(query_string).to_bytes(@conn)
81
+ process(true)
82
+ end
83
+
84
+ def prepare(name, query, params_count = 0)
85
+ raise_if_not_open
86
+
87
+ param_types = Array.new(params_count).fill(0)
88
+
89
+ Messages::Parse.new(name, query, param_types).to_bytes(@conn)
90
+ Messages::Describe.new(:prepared_statement, name).to_bytes(@conn)
91
+ Messages::Sync.new.to_bytes(@conn)
92
+ Messages::Flush.new.to_bytes(@conn)
93
+
94
+ process
95
+ end
96
+
97
+ def execute_prepared(name, *param_values)
98
+ raise_if_not_open
99
+
100
+ portal_name = "" # use the unnamed portal
101
+ max_rows = 0 # return all rows
102
+
103
+ reset_result
104
+
105
+ Messages::Bind.new(portal_name, name, param_values).to_bytes(@conn)
106
+ Messages::Execute.new(portal_name, max_rows).to_bytes(@conn)
107
+ Messages::Sync.new.to_bytes(@conn)
108
+
109
+ result = process(true)
110
+
111
+ # Close the portal
112
+ Messages::Close.new(:portal, portal_name).to_bytes(@conn)
113
+ Messages::Flush.new.to_bytes(@conn)
114
+
115
+ process
116
+
117
+ # Return the result from the prepared statement
118
+ result
119
+ end
120
+
121
+ def self.cancel(existing_conn)
122
+ conn = new(existing_conn.options.merge(:skip_startup => true))
123
+ Messages::CancelRequest.new(existing_conn.backend_pid, existing_conn.backend_key).to_bytes(conn.send(:conn))
124
+ Messages::Flush.new.to_bytes(conn.send(:conn))
125
+ conn.close
126
+ end
127
+
128
+ protected
129
+
130
+ def establish_connection
131
+ @conn = VerticaSocket.new(@options[:host], @options[:port].to_s)
132
+
133
+ if @options[:ssl]
134
+ Messages::SslRequest.new.to_bytes(@conn)
135
+ if @conn.read_byte == ?S
136
+ @conn = OpenSSL::SSL::SSLSocket.new(@conn, OpenSSL::SSL::SSLContext.new)
137
+ @conn.sync = true
138
+ @conn.connect
139
+ else
140
+ raise Error::ConnectionError.new("SSL requested but server doesn't support it.")
141
+ end
142
+ end
143
+ end
144
+
145
+ def process(return_result = false)
146
+ loop do
147
+ message = Messages::BackendMessage.read(@conn)
148
+
149
+ case message
150
+ when Messages::Authentication
151
+ if message.code != Messages::Authentication::OK
152
+ Messages::Password.new(@options[:password], message.code, {:user => @options[:user], :salt => message.salt}).to_bytes(@conn)
153
+ end
154
+ when Messages::BackendKeyData
155
+ @backend_pid = message.pid
156
+ @backend_key = message.key
157
+ when Messages::BindComplete
158
+ :nothing
159
+ when Messages::CloseComplete
160
+ break
161
+ when Messages::CommandComplete
162
+ break
163
+ # when Messages::CopyData
164
+ # # nothing
165
+ # when Messages::CopyDone
166
+ # # nothing
167
+ # when Messages::CopyInResponse
168
+ # raise 'not done'
169
+ # when Messages::CopyOutResponse
170
+ # raise 'not done'
171
+ when Messages::DataRow
172
+ @field_values << message.fields
173
+ when Messages::EmptyQueryResponse
174
+ break
175
+ when Messages::ErrorResponse
176
+ raise Error::MessageError.new(message.error)
177
+ when Messages::NoData
178
+ :nothing
179
+ when Messages::NoticeResponse
180
+ message.notices.each do |notice|
181
+ @notices << Notice.new(notice[0], notice[1])
182
+ end
183
+ when Messages::NotificationResponse
184
+ @notifications << Notification.new(message.pid, message.condition, message.additional_info)
185
+ when Messages::ParameterDescription
186
+ :nothing
187
+ when Messages::ParameterStatus
188
+ @parameters[message.name] = message.value
189
+ when Messages::ParseComplete
190
+ break
191
+ when Messages::PortalSuspended
192
+ break
193
+ when Messages::ReadyForQuery
194
+ @transaction_status = convert_transaction_status_to_sym(message.transaction_status)
195
+ break unless return_result
196
+ when Messages::RowDescription
197
+ @field_descriptions = message.fields
198
+ when Messages::Unknown
199
+ raise Error::MessageError.new("Unknown message type: #{message.message_id}")
200
+ end
201
+ end
202
+
203
+ return_result ? Result.new(@field_descriptions, @field_values) : nil
204
+ end
205
+
206
+ def raise_if_not_open
207
+ raise ConnectionError.new("connection doesn't exist or is already closed") if @conn.nil?
208
+ end
209
+
210
+ def reset_values
211
+ reset_notifications
212
+ reset_result
213
+ @parameters = {}
214
+ @backend_pid = nil
215
+ @backend_key = nil
216
+ @transaction_status = nil
217
+ @conn = nil
218
+ end
219
+
220
+ def reset_notifications
221
+ @notifications = []
222
+ end
223
+
224
+ def reset_result
225
+ @field_descriptions = []
226
+ @field_values = []
227
+ end
228
+
229
+ def convert_transaction_status_to_sym(status)
230
+ case status
231
+ when ?I
232
+ :no_transaction
233
+ when ?T
234
+ :in_transaction
235
+ when ?E
236
+ :failed_transaction
237
+ else
238
+ nil
239
+ end
240
+ end
241
+
242
+ def conn
243
+ @conn
244
+ end
245
+
246
+ end
247
+ end
@@ -0,0 +1,33 @@
1
+ module Vertica
2
+ module Messages
3
+ class Authentication < BackendMessage
4
+ message_id ?R
5
+
6
+ OK = 0
7
+ KERBEROS_V5 = 2
8
+ CLEARTEXT_PASSWORD = 3
9
+ CRYPT_PASSWORD = 4
10
+ MD5_PASSWORD = 5
11
+ SCM_CREDENTIAL = 6
12
+ GSS = 7
13
+ GSS_CONTINUE = 8
14
+ SSPI = 9
15
+
16
+ attr_reader :code
17
+ attr_reader :salt
18
+ attr_reader :auth_data
19
+
20
+ def initialize(stream, size)
21
+ super
22
+ @code = stream.read_network_int32
23
+ if @code == CRYPT_PASSWORD
24
+ @salt = stream.readn(2)
25
+ elsif @code == MD5_PASSWORD
26
+ @salt = stream.readn(4)
27
+ elsif @code == GSS_CONTINUE
28
+ @auth_data = stream.readn(size - 9)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ module Vertica
2
+ module Messages
3
+ class BackendKeyData < BackendMessage
4
+ message_id ?K
5
+
6
+ attr_reader :pid
7
+ attr_reader :key
8
+
9
+ def initialize(stream, size)
10
+ super
11
+ @pid = stream.read_network_int32
12
+ @key = stream.read_network_int32
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,36 @@
1
+ module Vertica
2
+ module Messages
3
+ class Bind < FrontendMessage
4
+ message_id ?B
5
+
6
+ def initialize(portal_name, prepared_statement_name, parameter_values)
7
+ @portal_name = portal_name
8
+ @prepared_statement_name = prepared_statement_name
9
+ @parameter_values = parameter_values.map { |pv| pv.to_s }
10
+ end
11
+
12
+ def to_bytes(stream)
13
+ size = LENGTH_SIZE
14
+ size += @portal_name.length + 1
15
+ size += @prepared_statement_name.length + 1
16
+ size += 2 # parameter format code (0)
17
+ size += 2 # number of parameter values
18
+ size += @parameter_values.inject(0) { |sum, e| sum += (e.length + 4) }
19
+ size += 2
20
+
21
+ stream.write_byte(message_id)
22
+ stream.write_network_int32(size) # size
23
+ stream.write_cstring(@portal_name) # portal name ("")
24
+ stream.write_cstring(@prepared_statement_name) # prep
25
+ stream.write_network_int16(0) # format codes (0 - default text format)
26
+ stream.write_network_int16(@parameter_values.length) # number of parameters
27
+ @parameter_values.each do |parameter_value|
28
+ stream.write_network_int32(parameter_value.length) # parameter value (which is represented as a string) length
29
+ stream.write(parameter_value) # parameter value written out in text representation
30
+ end
31
+ stream.write_network_int16(0)
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,8 @@
1
+ module Vertica
2
+ module Messages
3
+ class BindComplete < BackendMessage
4
+ message_id ?2
5
+
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,25 @@
1
+ module Vertica
2
+ module Messages
3
+ class CancelRequest < FrontendMessage
4
+ message_id nil
5
+
6
+ def initialize(backend_pid, backend_key)
7
+ @backend_pid = backend_pid
8
+ @backend_key = backend_key
9
+ end
10
+
11
+ def to_bytes(stream)
12
+ size = LENGTH_SIZE
13
+ size += 4
14
+ size += 4
15
+ size += 4
16
+
17
+ stream.write_network_int32(size) # size
18
+ stream.write_network_int32(80877102)
19
+ stream.write_network_int32(@backend_pid)
20
+ stream.write_network_int32(@backend_key)
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ module Vertica
2
+ module Messages
3
+ class Close < FrontendMessage
4
+ message_id ?C
5
+
6
+ def initialize(close_type, close_name)
7
+ if close_type == :portal
8
+ @close_type = ?P
9
+ elsif close_type == :prepared_statement
10
+ @close_type = ?S
11
+ else
12
+ raise ArgumentError.new("#{close_type} is not a valid close_type. Must be either :portal or :prepared_statement.")
13
+ end
14
+ @close_name = close_name
15
+ end
16
+
17
+ def to_bytes(stream)
18
+ size = LENGTH_SIZE
19
+ size += 1
20
+ size += @close_name.length + 1
21
+
22
+ stream.write_byte(message_id)
23
+ stream.write_network_int32(size) # size
24
+ stream.write_byte(@close_type)
25
+ stream.write_cstring(@close_name)
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,8 @@
1
+ module Vertica
2
+ module Messages
3
+ class CloseComplete < BackendMessage
4
+ message_id ?3
5
+
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ module Vertica
2
+ module Messages
3
+ class CommandComplete < BackendMessage
4
+ message_id ?C
5
+
6
+ attr_reader :tag
7
+ attr_reader :rows
8
+
9
+ def initialize(stream, size)
10
+ @tag = stream.read_cstring
11
+ @rows = @tag.split[-1].to_i
12
+ end
13
+
14
+ end
15
+ end
16
+ end