nose 0.1.0pre

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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/lib/nose/backend/cassandra.rb +390 -0
  3. data/lib/nose/backend/file.rb +185 -0
  4. data/lib/nose/backend/mongo.rb +242 -0
  5. data/lib/nose/backend.rb +557 -0
  6. data/lib/nose/cost/cassandra.rb +33 -0
  7. data/lib/nose/cost/entity_count.rb +27 -0
  8. data/lib/nose/cost/field_size.rb +31 -0
  9. data/lib/nose/cost/request_count.rb +32 -0
  10. data/lib/nose/cost.rb +68 -0
  11. data/lib/nose/debug.rb +45 -0
  12. data/lib/nose/enumerator.rb +199 -0
  13. data/lib/nose/indexes.rb +239 -0
  14. data/lib/nose/loader/csv.rb +99 -0
  15. data/lib/nose/loader/mysql.rb +199 -0
  16. data/lib/nose/loader/random.rb +48 -0
  17. data/lib/nose/loader/sql.rb +105 -0
  18. data/lib/nose/loader.rb +38 -0
  19. data/lib/nose/model/entity.rb +136 -0
  20. data/lib/nose/model/fields.rb +293 -0
  21. data/lib/nose/model.rb +113 -0
  22. data/lib/nose/parser.rb +202 -0
  23. data/lib/nose/plans/execution_plan.rb +282 -0
  24. data/lib/nose/plans/filter.rb +99 -0
  25. data/lib/nose/plans/index_lookup.rb +302 -0
  26. data/lib/nose/plans/limit.rb +42 -0
  27. data/lib/nose/plans/query_planner.rb +361 -0
  28. data/lib/nose/plans/sort.rb +49 -0
  29. data/lib/nose/plans/update.rb +60 -0
  30. data/lib/nose/plans/update_planner.rb +270 -0
  31. data/lib/nose/plans.rb +135 -0
  32. data/lib/nose/proxy/mysql.rb +275 -0
  33. data/lib/nose/proxy.rb +102 -0
  34. data/lib/nose/query_graph.rb +481 -0
  35. data/lib/nose/random/barbasi_albert.rb +48 -0
  36. data/lib/nose/random/watts_strogatz.rb +50 -0
  37. data/lib/nose/random.rb +391 -0
  38. data/lib/nose/schema.rb +89 -0
  39. data/lib/nose/search/constraints.rb +143 -0
  40. data/lib/nose/search/problem.rb +328 -0
  41. data/lib/nose/search/results.rb +200 -0
  42. data/lib/nose/search.rb +266 -0
  43. data/lib/nose/serialize.rb +747 -0
  44. data/lib/nose/statements/connection.rb +160 -0
  45. data/lib/nose/statements/delete.rb +83 -0
  46. data/lib/nose/statements/insert.rb +146 -0
  47. data/lib/nose/statements/query.rb +161 -0
  48. data/lib/nose/statements/update.rb +101 -0
  49. data/lib/nose/statements.rb +645 -0
  50. data/lib/nose/timing.rb +79 -0
  51. data/lib/nose/util.rb +305 -0
  52. data/lib/nose/workload.rb +244 -0
  53. data/lib/nose.rb +37 -0
  54. data/templates/workload.erb +42 -0
  55. metadata +700 -0
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoSE
4
+ module Plans
5
+ # A simple state class to hold the cardinality for updates
6
+ class UpdateState
7
+ attr_reader :statement, :cardinality
8
+
9
+ def initialize(statement, cardinality)
10
+ @statement = statement
11
+ @cardinality = cardinality
12
+ end
13
+
14
+ # XXX This is just a placeholder since this is
15
+ # currently used by the query planner
16
+ # @return [Boolean]
17
+ def answered?
18
+ true
19
+ end
20
+ end
21
+
22
+ # A plan for executing an update
23
+ class UpdatePlan < AbstractPlan
24
+ attr_reader :statement, :index, :query_plans, :update_steps, :cost_model,
25
+ :update_fields
26
+
27
+ include Comparable
28
+
29
+ def initialize(statement, index, trees, update_steps, cost_model)
30
+ @statement = statement
31
+ @index = index
32
+ @trees = trees
33
+ @query_plans = nil # these will be set later when we pick indexes
34
+ update_steps.each { |step| step.calculate_cost cost_model }
35
+ @update_steps = update_steps
36
+ @cost_model = cost_model
37
+
38
+ # Update with fields specified in the settings and conditions
39
+ # (rewrite from foreign keys to IDs if needed)
40
+ @update_fields = if statement.is_a?(Connection) ||
41
+ statement.is_a?(Delete)
42
+ []
43
+ else
44
+ statement.settings.map(&:field)
45
+ end
46
+ @update_fields += statement.conditions.each_value.map(&:field)
47
+ @update_fields.map! do |field|
48
+ field.is_a?(Fields::ForeignKeyField) ? field.entity.id_field : field
49
+ end
50
+ end
51
+
52
+ # The weight of this query for a given workload
53
+ # @return [Fixnum]
54
+ def weight
55
+ return 1 if @workload.nil?
56
+
57
+ @workload.statement_weights[@statement]
58
+ end
59
+
60
+ # The group of the associated statement
61
+ # @return [String]
62
+ def group
63
+ @statement.group
64
+ end
65
+
66
+ # Name the plan by the statement
67
+ # @return [String]
68
+ def name
69
+ "#{@statement.text} for #{@index.key}"
70
+ end
71
+
72
+ # The steps for this plan are the update steps
73
+ # @return [Array<UpdatePlanStep>]
74
+ def steps
75
+ @update_steps
76
+ end
77
+
78
+ # Parameters to this update plan
79
+ # @return [Hash]
80
+ def params
81
+ conditions = if @statement.respond_to?(:conditions)
82
+ @statement.conditions
83
+ else
84
+ {}
85
+ end
86
+ settings = if @statement.respond_to?(:settings)
87
+ @statement.settings
88
+ else
89
+ []
90
+ end
91
+
92
+ params = conditions.merge Hash[settings.map do |setting|
93
+ [setting.field.id, Condition.new(setting.field, :'=', setting.value)]
94
+ end]
95
+
96
+ convert_param_keys params
97
+ end
98
+
99
+ # Select query plans to actually use here
100
+ # @return [void]
101
+ def select_query_plans(indexes = nil, &block)
102
+ if block_given?
103
+ @query_plans = @trees.map(&block)
104
+ else
105
+ @query_plans = @trees.map do |tree|
106
+ plan = tree.select_using_indexes(indexes).min_by(&:cost)
107
+ fail if plan.nil?
108
+ plan
109
+ end
110
+ end
111
+
112
+ update_support_fields
113
+
114
+ @trees = nil
115
+ end
116
+
117
+ # Compare all the fields for the plan for equality
118
+ # @return [Boolean]
119
+ def eql?(other)
120
+ return false unless other.is_a? UpdatePlan
121
+ fail 'plans must be resolved before checking equality' \
122
+ if @query_plans.nil? || other.query_plans.nil?
123
+
124
+ @statement == other.statement &&
125
+ @index == other.index &&
126
+ @query_plans == other.query_plans &&
127
+ @update_steps == other.update_steps &&
128
+ @cost_model == other.cost_model
129
+ end
130
+
131
+ # :nocov:
132
+ def to_color
133
+ "\n statement: " + @statement.to_color +
134
+ "\n index: " + @index.to_color +
135
+ "\n query_plans: " + @query_plans.to_color +
136
+ "\nupdate_steps: " + @update_steps.to_color +
137
+ "\n cost_model: " + @cost_model.to_color
138
+ end
139
+ # :nocov:
140
+
141
+ # Two plans are compared by their execution cost
142
+ # @return [Boolean]
143
+ def <=>(other)
144
+ cost <=> other.cost
145
+ end
146
+
147
+ # The cost of performing the update on this index
148
+ # @return [Fixnum]
149
+ def update_cost
150
+ @update_steps.sum_by(&:cost)
151
+ end
152
+
153
+ # The cost is the sum of all the query costs plus the update costs
154
+ # @return [Fixnum]
155
+ def cost
156
+ @query_plans.sum_by(&:cost) + update_cost
157
+ end
158
+
159
+ private
160
+
161
+ # Add fields from support queries to those which should be updated
162
+ # @return [void]
163
+ def update_support_fields
164
+ # Add fields fetched from support queries
165
+ @update_fields += @query_plans.flat_map do |query_plan|
166
+ query_plan.query.select.to_a
167
+ end.compact
168
+ end
169
+
170
+ # Ensure we only use primary keys for conditions
171
+ # @return [Hash]
172
+ def convert_param_keys(params)
173
+ Hash[params.each_value.map do |condition|
174
+ field = condition.field
175
+ if field.is_a?(Fields::ForeignKeyField)
176
+ field = field.entity.id_field
177
+ condition = Condition.new field, condition.operator,
178
+ condition.value
179
+ end
180
+
181
+ [field.id, condition]
182
+ end]
183
+ end
184
+ end
185
+
186
+ # A planner for update statements in the workload
187
+ class UpdatePlanner
188
+ def initialize(model, trees, cost_model, by_id_graph = false)
189
+ @logger = Logging.logger['nose::update_planner']
190
+
191
+ @model = model
192
+ @cost_model = cost_model
193
+ @by_id_graph = by_id_graph
194
+
195
+ # Remove anything not a support query then group by statement and index
196
+ @query_plans = trees.select do |tree|
197
+ tree.query.is_a? SupportQuery
198
+ end
199
+ @query_plans = @query_plans.group_by { |tree| tree.query.statement }
200
+ @query_plans.each do |plan_stmt, plan_trees|
201
+ @query_plans[plan_stmt] = plan_trees.group_by do |tree|
202
+ index = tree.query.index
203
+ index = index.to_id_path if @by_id_path
204
+
205
+ index
206
+ end
207
+ end
208
+ end
209
+
210
+ # Find the necessary update plans for a given set of indexes
211
+ # @return [Array<UpdatePlan>]
212
+ def find_plans_for_update(statement, indexes)
213
+ indexes = indexes.map(&:to_id_graph).to_set if @by_id_graph
214
+
215
+ indexes.map do |index|
216
+ next unless statement.modifies_index?(index)
217
+
218
+ if (@query_plans[statement] &&
219
+ @query_plans[statement][index]).nil?
220
+ trees = []
221
+
222
+ if statement.is_a? Insert
223
+ cardinality = 1
224
+ else
225
+ cardinality = Cardinality.filter index.entries,
226
+ statement.eq_fields,
227
+ statement.range_field
228
+ end
229
+ else
230
+ # Get the cardinality of the last step to use for the update state
231
+ trees = @query_plans[statement][index]
232
+ plans = trees.map do |tree|
233
+ tree.select_using_indexes(indexes).min_by(&:cost)
234
+ end
235
+
236
+ # Multiply the cardinalities because we are crossing multiple
237
+ # relationships and need the cross-product
238
+ cardinality = plans.product_by { |p| p.last.state.cardinality }
239
+ end
240
+
241
+ state = UpdateState.new statement, cardinality
242
+ update_steps = update_steps statement, index, state
243
+ UpdatePlan.new statement, index, trees, update_steps, @cost_model
244
+ end.compact
245
+ end
246
+
247
+ private
248
+
249
+ # Find the required update steps
250
+ # @return [Array<UpdatePlanStep>]
251
+ def update_steps(statement, index, state)
252
+ update_steps = []
253
+ update_steps << DeletePlanStep.new(index, state) \
254
+ if statement.requires_delete?(index)
255
+
256
+ if statement.requires_insert?(index)
257
+ fields = if statement.is_a?(Connect)
258
+ statement.conditions.each_value.map(&:field)
259
+ else
260
+ statement.settings.map(&:field)
261
+ end
262
+
263
+ update_steps << InsertPlanStep.new(index, state, fields)
264
+ end
265
+
266
+ update_steps
267
+ end
268
+ end
269
+ end
270
+ end
data/lib/nose/plans.rb ADDED
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoSE
4
+ # Statement planning and abstract models of execution steps
5
+ module Plans
6
+ # A single step in a statement plan
7
+ class PlanStep
8
+ include Supertype
9
+
10
+ attr_accessor :state, :parent
11
+ attr_reader :children, :cost, :fields
12
+
13
+ def initialize
14
+ @children = Set.new
15
+ @parent = nil
16
+ @fields = Set.new
17
+ end
18
+
19
+ # :nocov:
20
+ def to_color
21
+ # Split on capital letters and remove the last two parts (PlanStep)
22
+ self.class.name.split('::').last.split(/(?=[A-Z])/)[0..-3] \
23
+ .map(&:downcase).join(' ').capitalize
24
+ end
25
+ # :nocov:
26
+
27
+ # Set the children of the current plan step
28
+ # @return [void]
29
+ def children=(children)
30
+ @children = children.to_set
31
+
32
+ # Track the parent step of each step
33
+ children.each do |child|
34
+ child.instance_variable_set(:@parent, self)
35
+ fields = child.instance_variable_get(:@fields) + self.fields
36
+ child.instance_variable_set(:@fields, fields)
37
+ end
38
+ end
39
+
40
+ # Mark the fields in this index as fetched
41
+ # @return [void]
42
+ def add_fields_from_index(index)
43
+ @fields += index.all_fields
44
+ end
45
+
46
+ # Get the list of steps which led us here
47
+ # If a cost model is not provided, statement plans using
48
+ # this step cannot be evaluated on the basis of cost
49
+ #
50
+ # (this is to support PlanStep#parent_index which does not need cost)
51
+ # @return [QueryPlan]
52
+ def parent_steps(cost_model = nil)
53
+ steps = nil
54
+
55
+ if @parent.nil?
56
+ steps = QueryPlan.new state.query, cost_model
57
+ else
58
+ steps = @parent.parent_steps cost_model
59
+ steps << self
60
+ end
61
+
62
+ steps
63
+ end
64
+
65
+ # Find the closest index to this step
66
+ # @return [PlanStep]
67
+ def parent_index
68
+ step = parent_steps.to_a.reverse_each.find do |parent_step|
69
+ parent_step.is_a? IndexLookupPlanStep
70
+ end
71
+ step.index unless step.nil?
72
+ end
73
+
74
+ # Calculate the cost of executing this step in the plan
75
+ # @return [Fixnum]
76
+ def calculate_cost(cost_model)
77
+ @cost = cost_model.method((subtype_name + '_cost').to_sym).call self
78
+ end
79
+
80
+ # Add the Subtype module to all step classes
81
+ # @return [void]
82
+ def self.inherited(child_class)
83
+ child_class.send(:include, Subtype)
84
+ end
85
+ end
86
+
87
+ # A dummy step used to inspect failed statement plans
88
+ class PrunedPlanStep < PlanStep
89
+ def state
90
+ OpenStruct.new answered?: true
91
+ end
92
+ end
93
+
94
+ # The root of a tree of statement plans used as a placeholder
95
+ class RootPlanStep < PlanStep
96
+ def initialize(state)
97
+ super()
98
+ @state = state
99
+ @cost = 0
100
+ end
101
+ end
102
+
103
+ # This superclass defines what is necessary for manually defined
104
+ # and automatically generated plans to provide for execution
105
+ class AbstractPlan
106
+ attr_reader :group, :name, :weight
107
+
108
+ # @abstract Subclasses should produce the steps for executing this query
109
+ def steps
110
+ fail NotImplementedError
111
+ end
112
+
113
+ # @abstract Subclasses should produce the fields selected by this plan
114
+ def select_fields
115
+ []
116
+ end
117
+
118
+ # @abstract Subclasses should produce the parameters
119
+ # necessary for this plan
120
+ def params
121
+ fail NotImplementedError
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ require_relative 'plans/filter'
128
+ require_relative 'plans/index_lookup'
129
+ require_relative 'plans/limit'
130
+ require_relative 'plans/sort'
131
+ require_relative 'plans/update'
132
+
133
+ require_relative 'plans/query_planner'
134
+ require_relative 'plans/update_planner'
135
+ require_relative 'plans/execution_plan'
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mysql'
4
+
5
+ module NoSE
6
+ module Proxy
7
+ # A proxy which speaks the MySQL protocol and executes queries
8
+ class MysqlProxy < ProxyBase
9
+ def initialize(*args)
10
+ super
11
+
12
+ # Initialize a hash for the state of sockets
13
+ @state = {}
14
+ end
15
+
16
+ # Authenticate the client and process queries
17
+ def handle_connection(socket)
18
+ return authenticate socket if @state[socket].nil?
19
+
20
+ # Retrieve the saved state of the socket
21
+ protocol = @state[socket]
22
+
23
+ begin
24
+ protocol.process_command(&method(:process_query))
25
+ rescue ::Mysql::ClientError::ServerGoneError
26
+ # Ensure the socket is closed and remove the state
27
+ remove_connection socket
28
+ return false
29
+ end
30
+
31
+ # Keep this socket around
32
+ true
33
+ end
34
+
35
+ # Remove the state of the socket
36
+ def remove_connection(socket)
37
+ socket.close
38
+ @state.delete socket
39
+ end
40
+
41
+ private
42
+
43
+ # Auth the client and prepare for query processsing
44
+ # @return [Boolean]
45
+ def authenticate(socket)
46
+ protocol = ::Mysql::ServerProtocol.new socket
47
+
48
+ # Try to authenticate
49
+ begin
50
+ protocol.authenticate
51
+ rescue
52
+ remove_connection socket
53
+ return false
54
+ end
55
+
56
+ @state[socket] = protocol
57
+
58
+ true
59
+ end
60
+
61
+ # Execute the query on the backend and return the result
62
+ def process_query(protocol, query)
63
+ begin
64
+ @logger.debug { "Got query #{query}" }
65
+ result = query_result query
66
+ @logger.debug "Executed query with #{result.size} results"
67
+ rescue ParseFailed => exc
68
+ protocol.error ::Mysql::ServerError::ER_PARSE_ERROR, exc.message
69
+ rescue Backend::PlanNotFound => exc
70
+ protocol.error ::Mysql::ServerError::ER_UNKNOWN_STMT_HANDLER,
71
+ exc.message
72
+ end
73
+
74
+ result
75
+ end
76
+
77
+ private
78
+
79
+ # Get the result of the query from the backend
80
+ def query_result(query)
81
+ query = Statement.parse query, @result.workload.model
82
+ @backend.query(query).lazy.map do |row|
83
+ Hash[query.select.map { |field| [field.name, row[field.id]] }]
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ # Extend the client library with necessary server code
90
+ class ::Mysql
91
+ # Simple class which doesn't do connection setup
92
+ class ServerProtocol < Protocol
93
+ def initialize(socket)
94
+ # We need a much simpler initialization than the default class
95
+ @sock = socket
96
+ end
97
+
98
+ # Perform authentication
99
+ def authenticate
100
+ reset
101
+ write InitialPacket.serialize
102
+ AuthenticationPacket.parse read # TODO: Check auth
103
+ write ResultPacket.serialize 0
104
+ end
105
+
106
+ # Send an error message with the given number and text
107
+ def error(errno, message)
108
+ write ErrorPacket.serialize errno, message
109
+ end
110
+
111
+ # Process a single incoming command
112
+ def process_command(&block)
113
+ reset
114
+ pkt = read
115
+ command = pkt.utiny
116
+
117
+ case command
118
+ when COM_QUIT
119
+ # Stop processing because the client left
120
+ return
121
+ when COM_QUERY
122
+ process_query pkt.to_s, &block
123
+ when COM_PING
124
+ write ResultPacket.serialize 0
125
+ else
126
+ # Return error for invalid commands
127
+ protocol.error ::Mysql::ServerError::ER_NOT_SUPPORTED_YET,
128
+ 'Command not supported'
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ # Handle an individual query
135
+ def process_query(query)
136
+ # Execute the query on the backend
137
+ result = yield self, query
138
+ return if result.nil?
139
+
140
+ # Return the list of fields in the result
141
+ field_names = result.any? ? result.peek.keys : []
142
+ write_fields result, field_names
143
+ write_rows result, field_names
144
+ end
145
+
146
+ # Write the list of fields for the resulting rows
147
+ def write_fields(result, field_names)
148
+ write ResultPacket.serialize field_names.count
149
+ field_names.each do |field_name|
150
+ type, = Protocol.value2net result.first[field_name]
151
+
152
+ write FieldPacket.serialize '', '', '', field_name, '', 1, type,
153
+ Field::NOT_NULL_FLAG, 0, ''
154
+ end
155
+ write EOFPacket.serialize
156
+ end
157
+
158
+ # Write a packet for each row in the results
159
+ def write_rows(result, field_names)
160
+ result.each do |row|
161
+ values = field_names.map { |field_name| row[field_name] }
162
+ write(values.map do |value|
163
+ Protocol.value2net(value.to_s).last
164
+ end.inject('', &:+))
165
+ end
166
+ write EOFPacket.serialize
167
+ end
168
+ end
169
+
170
+ # Add serialization of the initial packet
171
+ class InitialPacket
172
+ # Serialize the initial server hello
173
+ # @return [String]
174
+ def self.serialize
175
+ [
176
+ ::Mysql::Protocol::VERSION,
177
+ 'nose',
178
+ 0,
179
+ 'AAAAAAAA',
180
+ 0,
181
+ CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION,
182
+ 33, # utf8_general_ci
183
+ SERVER_STATUS_AUTOCOMMIT,
184
+ 'AAAAAAAAAAAA'
185
+ ].pack('CZ*Va8CvCvx13Z*')
186
+ end
187
+ end
188
+
189
+ # Add serialization of result packets
190
+ class ResultPacket
191
+ # Serialize a simple OK response
192
+ # rubocop:disable Metrics/ParameterLists
193
+ # @return [String]
194
+ def self.serialize(field_count, affected_rows = 0, insert_id = 0,
195
+ server_status = 0, warning_count = 0, message = '')
196
+ return Packet.lcb(field_count) unless field_count.zero?
197
+
198
+ Packet.lcb(field_count) +
199
+ Packet.lcb(affected_rows) +
200
+ Packet.lcb(insert_id) +
201
+ [
202
+ server_status,
203
+ warning_count
204
+ ].pack('vv') +
205
+ Packet.lcs(message)
206
+ end
207
+ # rubocop:enable Metrics/ParameterLists
208
+ end
209
+
210
+ # Add serialization of field packets
211
+ class FieldPacket
212
+ # Serialize all the data for a field
213
+ # rubocop:disable Metrics/ParameterLists
214
+ # @return [String]
215
+ def self.serialize(db, table, org_table, name, org_name, length, type,
216
+ flags, decimals, default)
217
+ Packet.lcs('def') + # catalog
218
+ Packet.lcs(db) +
219
+ Packet.lcs(table) +
220
+ Packet.lcs(org_table) +
221
+ Packet.lcs(name) +
222
+ Packet.lcs(org_name) +
223
+ [
224
+ 0x0c,
225
+ 33, # utf8_general_ci
226
+ length,
227
+ type,
228
+ flags,
229
+ decimals,
230
+ 0
231
+ ].pack('CvVCvCv') + Packet.lcs(default)
232
+ end
233
+ # rubocop:enable Metrics/ParameterLists
234
+ end
235
+
236
+ # Add parsing of auth packets
237
+ class AuthenticationPacket
238
+ # Parse the incoming authentication packet
239
+ def self.parse(_pkt)
240
+ # XXX: Unneeded for now since we don't handle auth
241
+ # client_flags = pkt.ulong
242
+ # max_packet_size = pkt.ulong
243
+ # charset_number = pkt.lcb
244
+ # f1 = pkt.read(23)
245
+ # username = pkt.string
246
+ # scrambled_password = pkt.lcs
247
+ # databasename = pkt.string
248
+ end
249
+ end
250
+
251
+ # Simple EOF packet
252
+ class EOFPacket
253
+ # Static string to indicate EOF
254
+ # @return [String]
255
+ def self.serialize
256
+ "\xfe\x00\x00\x00\x00"
257
+ end
258
+ end
259
+
260
+ # Serialize an error message
261
+ class ErrorPacket
262
+ # Generate a packet with a given error number and message
263
+ # @return [String]
264
+ def self.serialize(errno, message)
265
+ [
266
+ 0xff,
267
+ errno,
268
+ '#',
269
+ @sqlstate,
270
+ message
271
+ ].pack('Cvaa5a*')
272
+ end
273
+ end
274
+ end
275
+ end