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