nose 0.1.0pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/nose/backend/cassandra.rb +390 -0
- data/lib/nose/backend/file.rb +185 -0
- data/lib/nose/backend/mongo.rb +242 -0
- data/lib/nose/backend.rb +557 -0
- data/lib/nose/cost/cassandra.rb +33 -0
- data/lib/nose/cost/entity_count.rb +27 -0
- data/lib/nose/cost/field_size.rb +31 -0
- data/lib/nose/cost/request_count.rb +32 -0
- data/lib/nose/cost.rb +68 -0
- data/lib/nose/debug.rb +45 -0
- data/lib/nose/enumerator.rb +199 -0
- data/lib/nose/indexes.rb +239 -0
- data/lib/nose/loader/csv.rb +99 -0
- data/lib/nose/loader/mysql.rb +199 -0
- data/lib/nose/loader/random.rb +48 -0
- data/lib/nose/loader/sql.rb +105 -0
- data/lib/nose/loader.rb +38 -0
- data/lib/nose/model/entity.rb +136 -0
- data/lib/nose/model/fields.rb +293 -0
- data/lib/nose/model.rb +113 -0
- data/lib/nose/parser.rb +202 -0
- data/lib/nose/plans/execution_plan.rb +282 -0
- data/lib/nose/plans/filter.rb +99 -0
- data/lib/nose/plans/index_lookup.rb +302 -0
- data/lib/nose/plans/limit.rb +42 -0
- data/lib/nose/plans/query_planner.rb +361 -0
- data/lib/nose/plans/sort.rb +49 -0
- data/lib/nose/plans/update.rb +60 -0
- data/lib/nose/plans/update_planner.rb +270 -0
- data/lib/nose/plans.rb +135 -0
- data/lib/nose/proxy/mysql.rb +275 -0
- data/lib/nose/proxy.rb +102 -0
- data/lib/nose/query_graph.rb +481 -0
- data/lib/nose/random/barbasi_albert.rb +48 -0
- data/lib/nose/random/watts_strogatz.rb +50 -0
- data/lib/nose/random.rb +391 -0
- data/lib/nose/schema.rb +89 -0
- data/lib/nose/search/constraints.rb +143 -0
- data/lib/nose/search/problem.rb +328 -0
- data/lib/nose/search/results.rb +200 -0
- data/lib/nose/search.rb +266 -0
- data/lib/nose/serialize.rb +747 -0
- data/lib/nose/statements/connection.rb +160 -0
- data/lib/nose/statements/delete.rb +83 -0
- data/lib/nose/statements/insert.rb +146 -0
- data/lib/nose/statements/query.rb +161 -0
- data/lib/nose/statements/update.rb +101 -0
- data/lib/nose/statements.rb +645 -0
- data/lib/nose/timing.rb +79 -0
- data/lib/nose/util.rb +305 -0
- data/lib/nose/workload.rb +244 -0
- data/lib/nose.rb +37 -0
- data/templates/workload.erb +42 -0
- 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
|