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
data/lib/nose/backend.rb
ADDED
@@ -0,0 +1,557 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
# Communication with backends for index creation and statement execution
|
5
|
+
module Backend
|
6
|
+
# Superclass of all database backends
|
7
|
+
class BackendBase
|
8
|
+
def initialize(model, indexes, plans, update_plans, _config)
|
9
|
+
@model = model
|
10
|
+
@indexes = indexes
|
11
|
+
@plans = plans
|
12
|
+
@update_plans = update_plans
|
13
|
+
end
|
14
|
+
|
15
|
+
# By default, do not use ID graphs
|
16
|
+
# @return [Boolean]
|
17
|
+
def by_id_graph
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
# @abstract Subclasses implement to check if an index is empty
|
22
|
+
# @return [Boolean]
|
23
|
+
def index_empty?(_index)
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
# @abstract Subclasses implement to check if an index already exists
|
28
|
+
# @return [Boolean]
|
29
|
+
def index_exists?(_index)
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
# @abstract Subclasses implement to remove existing indexes
|
34
|
+
# @return [void]
|
35
|
+
def drop_index
|
36
|
+
end
|
37
|
+
|
38
|
+
# @abstract Subclasses implement to allow inserting
|
39
|
+
# data into the backend database
|
40
|
+
# :nocov:
|
41
|
+
# @return [void]
|
42
|
+
def index_insert_chunk(_index, _chunk)
|
43
|
+
fail NotImplementedError
|
44
|
+
end
|
45
|
+
# :nocov:
|
46
|
+
|
47
|
+
# @abstract Subclasses implement to generate a new random ID
|
48
|
+
# :nocov:
|
49
|
+
# @return [Object]
|
50
|
+
def generate_id
|
51
|
+
fail NotImplementedError
|
52
|
+
end
|
53
|
+
# :nocov:
|
54
|
+
|
55
|
+
# @abstract Subclasses should create indexes
|
56
|
+
# :nocov:
|
57
|
+
# @return [Enumerable]
|
58
|
+
def indexes_ddl(_execute = false, _skip_existing = false,
|
59
|
+
_drop_existing = false)
|
60
|
+
fail NotImplementedError
|
61
|
+
end
|
62
|
+
# :nocov:
|
63
|
+
|
64
|
+
# @abstract Subclasses should return sample values from the index
|
65
|
+
# :nocov:
|
66
|
+
# @return [Array<Hash>]
|
67
|
+
def indexes_sample(_index, _count)
|
68
|
+
fail NotImplementedError
|
69
|
+
end
|
70
|
+
# :nocov:
|
71
|
+
|
72
|
+
# Prepare a query to be executed with the given plans
|
73
|
+
# @return [PreparedQuery]
|
74
|
+
def prepare_query(query, fields, conditions, plans = [])
|
75
|
+
plan = plans.empty? ? find_query_plan(query) : plans.first
|
76
|
+
|
77
|
+
state = Plans::QueryState.new(query, @model) unless query.nil?
|
78
|
+
first_step = Plans::RootPlanStep.new state
|
79
|
+
steps = [first_step] + plan.to_a + [nil]
|
80
|
+
PreparedQuery.new query, prepare_query_steps(steps, fields, conditions)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Prepare a statement to be executed with the given plans
|
84
|
+
def prepare(statement, plans = [])
|
85
|
+
if statement.is_a? Query
|
86
|
+
prepare_query statement, statement.all_fields,
|
87
|
+
statement.conditions, plans
|
88
|
+
elsif statement.is_a? Delete
|
89
|
+
prepare_update statement, plans
|
90
|
+
elsif statement.is_a? Disconnect
|
91
|
+
prepare_update statement, plans
|
92
|
+
elsif statement.is_a? Connection
|
93
|
+
prepare_update statement, plans
|
94
|
+
else
|
95
|
+
prepare_update statement, plans
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Execute a query with the stored plans
|
100
|
+
# @return [Array<Hash>]
|
101
|
+
def query(query, plans = [])
|
102
|
+
prepared = prepare query, plans
|
103
|
+
prepared.execute query.conditions
|
104
|
+
end
|
105
|
+
|
106
|
+
# Prepare an update for execution
|
107
|
+
# @return [PreparedUpdate]
|
108
|
+
def prepare_update(update, plans)
|
109
|
+
# Search for plans if they were not given
|
110
|
+
plans = find_update_plans(update) if plans.empty?
|
111
|
+
fail PlanNotFound if plans.empty?
|
112
|
+
|
113
|
+
# Prepare each plan
|
114
|
+
plans.map do |plan|
|
115
|
+
delete = false
|
116
|
+
insert = false
|
117
|
+
plan.update_steps.each do |step|
|
118
|
+
delete = true if step.is_a?(Plans::DeletePlanStep)
|
119
|
+
insert = true if step.is_a?(Plans::InsertPlanStep)
|
120
|
+
end
|
121
|
+
|
122
|
+
steps = []
|
123
|
+
add_delete_step(plan, steps) if delete
|
124
|
+
add_insert_step(plan, steps, plan.update_fields) if insert
|
125
|
+
|
126
|
+
PreparedUpdate.new update, prepare_support_plans(plan), steps
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Execute an update with the stored plans
|
131
|
+
# @return [void]
|
132
|
+
def update(update, plans = [])
|
133
|
+
prepared = prepare_update update, plans
|
134
|
+
prepared.each { |p| p.execute update.settings, update.conditions }
|
135
|
+
end
|
136
|
+
|
137
|
+
# Superclass for all statement execution steps
|
138
|
+
class StatementStep
|
139
|
+
include Supertype
|
140
|
+
attr_reader :index
|
141
|
+
end
|
142
|
+
|
143
|
+
# Look up data on an index in the backend
|
144
|
+
class IndexLookupStatementStep < StatementStep
|
145
|
+
def initialize(client, _select, _conditions,
|
146
|
+
step, next_step, prev_step)
|
147
|
+
@client = client
|
148
|
+
@step = step
|
149
|
+
@index = step.index
|
150
|
+
@prev_step = prev_step
|
151
|
+
@next_step = next_step
|
152
|
+
|
153
|
+
@eq_fields = step.eq_filter
|
154
|
+
@range_field = step.range_filter
|
155
|
+
end
|
156
|
+
|
157
|
+
protected
|
158
|
+
|
159
|
+
# Get lookup values from the query for the first step
|
160
|
+
def initial_results(conditions)
|
161
|
+
[Hash[conditions.map do |field_id, condition|
|
162
|
+
fail if condition.value.nil?
|
163
|
+
[field_id, condition.value]
|
164
|
+
end]]
|
165
|
+
end
|
166
|
+
|
167
|
+
# Construct a list of conditions from the results
|
168
|
+
def result_conditions(conditions, results)
|
169
|
+
results.map do |result|
|
170
|
+
result_condition = @eq_fields.map do |field|
|
171
|
+
Condition.new field, :'=', result[field.id]
|
172
|
+
end
|
173
|
+
|
174
|
+
unless @range_field.nil?
|
175
|
+
operator = conditions.each_value.find(&:range?).operator
|
176
|
+
result_condition << Condition.new(@range_field, operator,
|
177
|
+
result[@range_field.id])
|
178
|
+
end
|
179
|
+
|
180
|
+
result_condition
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Decide which fields should be selected
|
185
|
+
def expand_selected_fields(select)
|
186
|
+
# We just pick whatever is contained in the index that is either
|
187
|
+
# mentioned in the query or required for the next lookup
|
188
|
+
# TODO: Potentially try query.all_fields for those not required
|
189
|
+
# It should be sufficient to check what is needed for future
|
190
|
+
# filtering and sorting and use only those + query.select
|
191
|
+
select += @next_step.index.hash_fields \
|
192
|
+
unless @next_step.nil? ||
|
193
|
+
!@next_step.is_a?(Plans::IndexLookupPlanStep)
|
194
|
+
select &= @step.index.all_fields
|
195
|
+
|
196
|
+
select
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Insert data into an index on the backend
|
201
|
+
class InsertStatementStep < StatementStep
|
202
|
+
def initialize(client, index, _fields)
|
203
|
+
@client = client
|
204
|
+
@index = index
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Delete data from an index on the backend
|
209
|
+
class DeleteStatementStep < StatementStep
|
210
|
+
def initialize(client, index)
|
211
|
+
@client = client
|
212
|
+
@index = index
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Perform filtering external to the backend
|
217
|
+
class FilterStatementStep < StatementStep
|
218
|
+
def initialize(_client, _fields, _conditions,
|
219
|
+
step, _next_step, _prev_step)
|
220
|
+
@step = step
|
221
|
+
end
|
222
|
+
|
223
|
+
# Filter results by a list of fields given in the step
|
224
|
+
# @return [Array<Hash>]
|
225
|
+
def process(conditions, results)
|
226
|
+
# Extract the equality conditions
|
227
|
+
eq_conditions = conditions.values.select do |condition|
|
228
|
+
!condition.range? && @step.eq.include?(condition.field)
|
229
|
+
end
|
230
|
+
|
231
|
+
# XXX: This assumes that the range filter step is the same as
|
232
|
+
# the one in the query, which is always true for now
|
233
|
+
range = @step.range && conditions.each_value.find(&:range?)
|
234
|
+
|
235
|
+
results.select! { |row| include_row?(row, eq_conditions, range) }
|
236
|
+
|
237
|
+
results
|
238
|
+
end
|
239
|
+
|
240
|
+
private
|
241
|
+
|
242
|
+
# Check if the row should be included in the result
|
243
|
+
# @return [Boolean]
|
244
|
+
def include_row?(row, eq_conditions, range)
|
245
|
+
select = eq_conditions.all? do |condition|
|
246
|
+
row[condition.field.id] == condition.value
|
247
|
+
end
|
248
|
+
|
249
|
+
if range
|
250
|
+
range_check = row[range.field.id].method(range.operator)
|
251
|
+
select &&= range_check.call range.value
|
252
|
+
end
|
253
|
+
|
254
|
+
select
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Perform sorting external to the backend
|
259
|
+
class SortStatementStep < StatementStep
|
260
|
+
def initialize(_client, _fields, _conditions,
|
261
|
+
step, _next_step, _prev_step)
|
262
|
+
@step = step
|
263
|
+
end
|
264
|
+
|
265
|
+
# Sort results by a list of fields given in the step
|
266
|
+
# @return [Array<Hash>]
|
267
|
+
def process(_conditions, results)
|
268
|
+
results.sort_by! do |row|
|
269
|
+
@step.sort_fields.map do |field|
|
270
|
+
row[field.id]
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# Perform a client-side limit of the result set size
|
277
|
+
class LimitStatementStep < StatementStep
|
278
|
+
def initialize(_client, _fields, _conditions,
|
279
|
+
step, _next_step, _prev_step)
|
280
|
+
@limit = step.limit
|
281
|
+
end
|
282
|
+
|
283
|
+
# Remove results past the limit
|
284
|
+
# @return [Array<Hash>]
|
285
|
+
def process(_conditions, results)
|
286
|
+
results[0..@limit - 1]
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
private
|
291
|
+
|
292
|
+
# Find plans for a given query
|
293
|
+
# @return [Plans::QueryPlan]
|
294
|
+
def find_query_plan(query)
|
295
|
+
plan = @plans.find do |possible_plan|
|
296
|
+
possible_plan.query == query
|
297
|
+
end unless query.nil?
|
298
|
+
fail PlanNotFound if plan.nil?
|
299
|
+
|
300
|
+
plan
|
301
|
+
end
|
302
|
+
|
303
|
+
# Prepare all the steps for executing a given query
|
304
|
+
# @return [Array<StatementStep>]
|
305
|
+
def prepare_query_steps(steps, fields, conditions)
|
306
|
+
steps.each_cons(3).map do |prev_step, step, next_step|
|
307
|
+
step_class = StatementStep.subtype_class step.subtype_name
|
308
|
+
|
309
|
+
# Check if the subclass has overridden this step
|
310
|
+
subclass_step_name = step_class.name.sub \
|
311
|
+
'NoSE::Backend::BackendBase', self.class.name
|
312
|
+
step_class = Object.const_get subclass_step_name
|
313
|
+
step_class.new client, fields, conditions,
|
314
|
+
step, next_step, prev_step
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# Find plans for a given update
|
319
|
+
# @return [Array<Plans::UpdatePlan>]
|
320
|
+
def find_update_plans(update)
|
321
|
+
@update_plans.select do |possible_plan|
|
322
|
+
possible_plan.statement == update
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# Add a delete step to a prepared update plan
|
327
|
+
# @return [void]
|
328
|
+
def add_delete_step(plan, steps)
|
329
|
+
step_class = DeleteStatementStep
|
330
|
+
subclass_step_name = step_class.name.sub \
|
331
|
+
'NoSE::Backend::BackendBase', self.class.name
|
332
|
+
step_class = Object.const_get subclass_step_name
|
333
|
+
steps << step_class.new(client, plan.index)
|
334
|
+
end
|
335
|
+
|
336
|
+
# Add an insert step to a prepared update plan
|
337
|
+
# @return [void]
|
338
|
+
def add_insert_step(plan, steps, fields)
|
339
|
+
step_class = InsertStatementStep
|
340
|
+
subclass_step_name = step_class.name.sub \
|
341
|
+
'NoSE::Backend::BackendBase', self.class.name
|
342
|
+
step_class = Object.const_get subclass_step_name
|
343
|
+
steps << step_class.new(client, plan.index, fields)
|
344
|
+
end
|
345
|
+
|
346
|
+
# Prepare plans for each support query
|
347
|
+
# @return [Array<PreparedQuery>]
|
348
|
+
def prepare_support_plans(plan)
|
349
|
+
plan.query_plans.map do |query_plan|
|
350
|
+
query = query_plan.instance_variable_get(:@query)
|
351
|
+
prepare_query query, query_plan.select_fields, query_plan.params,
|
352
|
+
[query_plan.steps]
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
# A prepared query which can be executed against the backend
|
358
|
+
class PreparedQuery
|
359
|
+
attr_reader :query, :steps
|
360
|
+
|
361
|
+
def initialize(query, steps)
|
362
|
+
@query = query
|
363
|
+
@steps = steps
|
364
|
+
end
|
365
|
+
|
366
|
+
# Execute the query for the given set of conditions
|
367
|
+
# @return [Array<Hash>]
|
368
|
+
def execute(conditions)
|
369
|
+
results = nil
|
370
|
+
|
371
|
+
@steps.each do |step|
|
372
|
+
if step.is_a?(BackendBase::IndexLookupStatementStep)
|
373
|
+
field_ids = step.index.all_fields.map(&:id)
|
374
|
+
field_conds = conditions.select { |key| field_ids.include? key }
|
375
|
+
else
|
376
|
+
field_conds = conditions
|
377
|
+
end
|
378
|
+
results = step.process field_conds, results
|
379
|
+
|
380
|
+
# The query can't return any results at this point, so we're done
|
381
|
+
break if results.empty?
|
382
|
+
end
|
383
|
+
|
384
|
+
# Only return fields selected by the query if one is given
|
385
|
+
# (we have no query to refer to for manually-defined plans)
|
386
|
+
unless @query.nil?
|
387
|
+
select_ids = @query.select.map(&:id).to_set
|
388
|
+
results.map { |row| row.select! { |k, _| select_ids.include? k } }
|
389
|
+
end
|
390
|
+
|
391
|
+
results
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# An update prepared with a backend which is ready to execute
|
396
|
+
class PreparedUpdate
|
397
|
+
attr_reader :statement, :steps
|
398
|
+
|
399
|
+
def initialize(statement, support_plans, steps)
|
400
|
+
@statement = statement
|
401
|
+
@support_plans = support_plans
|
402
|
+
@delete_step = steps.find do |step|
|
403
|
+
step.is_a? BackendBase::DeleteStatementStep
|
404
|
+
end
|
405
|
+
@insert_step = steps.find do |step|
|
406
|
+
step.is_a? BackendBase::InsertStatementStep
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
# Execute the statement for the given set of conditions
|
411
|
+
# @return [void]
|
412
|
+
def execute(update_settings, update_conditions)
|
413
|
+
# Execute all the support queries
|
414
|
+
settings = initial_update_settings update_settings, update_conditions
|
415
|
+
|
416
|
+
# Execute the support queries for this update
|
417
|
+
support = support_results update_conditions
|
418
|
+
|
419
|
+
# Perform the deletion
|
420
|
+
@delete_step.process support unless support.empty? || @delete_step.nil?
|
421
|
+
return if @insert_step.nil?
|
422
|
+
|
423
|
+
# Get the fields which should be used from the original statement
|
424
|
+
# If we didn't delete old entries, then we just need the primary key
|
425
|
+
# attributes of the index, otherwise we need everything
|
426
|
+
index = @insert_step.index
|
427
|
+
include_fields = if @delete_step.nil?
|
428
|
+
index.hash_fields + index.order_fields
|
429
|
+
else
|
430
|
+
index.all_fields
|
431
|
+
end
|
432
|
+
|
433
|
+
# Add fields from the original statement
|
434
|
+
update_conditions.each_value do |condition|
|
435
|
+
next unless include_fields.include? condition.field
|
436
|
+
settings.merge! condition.field.id => condition.value
|
437
|
+
end
|
438
|
+
|
439
|
+
if support.empty?
|
440
|
+
support = [settings]
|
441
|
+
else
|
442
|
+
support.each do |row|
|
443
|
+
row.merge!(settings) { |_, value, _| value }
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
# Stop if we have nothing to insert, otherwise insert
|
448
|
+
return if support.empty?
|
449
|
+
@insert_step.process support
|
450
|
+
end
|
451
|
+
|
452
|
+
private
|
453
|
+
|
454
|
+
# Get the initial values which will be used in the first plan step
|
455
|
+
# @return [Hash]
|
456
|
+
def initial_update_settings(update_settings, update_conditions)
|
457
|
+
if !@insert_step.nil? && @delete_step.nil?
|
458
|
+
# Populate the data to insert for Insert statements
|
459
|
+
settings = Hash[update_settings.map do |setting|
|
460
|
+
[setting.field.id, setting.value]
|
461
|
+
end]
|
462
|
+
else
|
463
|
+
# Get values for updates and deletes
|
464
|
+
settings = Hash[update_conditions.map do |field_id, condition|
|
465
|
+
[field_id, condition.value]
|
466
|
+
end]
|
467
|
+
end
|
468
|
+
|
469
|
+
settings
|
470
|
+
end
|
471
|
+
|
472
|
+
# Execute all the support queries
|
473
|
+
# @return [Array<Hash>]
|
474
|
+
def support_results(settings)
|
475
|
+
return [] if @support_plans.empty?
|
476
|
+
|
477
|
+
# Get a hash of values used in settings, first
|
478
|
+
# resolving any settings which specify foreign keys
|
479
|
+
settings = Hash[settings.map do |k, v|
|
480
|
+
new_condition = v.resolve_foreign_key
|
481
|
+
[new_condition.field.id, new_condition]
|
482
|
+
end]
|
483
|
+
setting_values = Hash[settings.map { |k, v| [k, v.value] }]
|
484
|
+
|
485
|
+
# If we have no query for IDs on the first entity, we must
|
486
|
+
# have the fields we need to execute the other support queries
|
487
|
+
if !@statement.nil? &&
|
488
|
+
@support_plans.first.query.entity != @statement.entity
|
489
|
+
support = @support_plans.map do |plan|
|
490
|
+
plan.execute settings
|
491
|
+
end
|
492
|
+
|
493
|
+
# Combine the results from multiple support queries
|
494
|
+
unless support.empty?
|
495
|
+
support = support.first.product(*support[1..-1])
|
496
|
+
support.map! do |results|
|
497
|
+
results.reduce(&:merge!).merge!(setting_values)
|
498
|
+
end
|
499
|
+
end
|
500
|
+
else
|
501
|
+
# Execute the first support query to get a list of IDs
|
502
|
+
first_query = @support_plans.first.query
|
503
|
+
|
504
|
+
# We may not have a statement if this is manually defined
|
505
|
+
if @statement.nil?
|
506
|
+
select_key = false
|
507
|
+
entity_fields = nil
|
508
|
+
else
|
509
|
+
id = @statement.entity.id_field
|
510
|
+
select_key = first_query.select.include? id
|
511
|
+
|
512
|
+
# Select any fields from the entity being modified if required
|
513
|
+
entity_fields = @support_plans.first.execute settings \
|
514
|
+
if first_query.graph.size == 1 && \
|
515
|
+
first_query.graph.entities.first == @statement.entity
|
516
|
+
end
|
517
|
+
|
518
|
+
if select_key
|
519
|
+
# Pull the IDs from the first support query
|
520
|
+
conditions = entity_fields.map do |row|
|
521
|
+
{ id.id => Condition.new(id, :'=', row[id.id]) }
|
522
|
+
end
|
523
|
+
else
|
524
|
+
# Use the ID specified in the statement conditions
|
525
|
+
conditions = [settings]
|
526
|
+
end
|
527
|
+
|
528
|
+
# Execute the support queries for each ID
|
529
|
+
support = conditions.each_with_index.flat_map do |condition, i|
|
530
|
+
results = @support_plans[(select_key ? 1 : 0)..-1].map do |plan|
|
531
|
+
plan.execute condition
|
532
|
+
end
|
533
|
+
|
534
|
+
# Combine the results of the different support queries
|
535
|
+
results[0].product(*results[1..-1]).map do |result|
|
536
|
+
row = result.reduce(&:merge!)
|
537
|
+
row.merge!(entity_fields[i]) unless entity_fields.nil?
|
538
|
+
row.merge!(setting_values)
|
539
|
+
|
540
|
+
row
|
541
|
+
end
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
support
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
# Raised when a statement is executed that we have no plan for
|
550
|
+
class PlanNotFound < StandardError
|
551
|
+
end
|
552
|
+
|
553
|
+
# Raised when a backend attempts to create an index that already exists
|
554
|
+
class IndexAlreadyExists < StandardError
|
555
|
+
end
|
556
|
+
end
|
557
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
module Cost
|
5
|
+
# A cost model which estimates the number of requests to the backend
|
6
|
+
class CassandraCost < Cost
|
7
|
+
include Subtype
|
8
|
+
|
9
|
+
# Rough cost estimate as the number of requests made
|
10
|
+
# @return [Numeric]
|
11
|
+
def index_lookup_cost(step)
|
12
|
+
return nil if step.state.nil?
|
13
|
+
rows = step.state.cardinality
|
14
|
+
parts = step.state.hash_cardinality
|
15
|
+
|
16
|
+
@options[:index_cost] + parts * @options[:partition_cost] +
|
17
|
+
rows * @options[:row_cost]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Cost estimate as number of entities deleted
|
21
|
+
def delete_cost(step)
|
22
|
+
return nil if step.state.nil?
|
23
|
+
step.state.cardinality * @options[:delete_cost]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Cost estimate as number of entities inserted
|
27
|
+
def insert_cost(step)
|
28
|
+
return nil if step.state.nil?
|
29
|
+
step.state.cardinality * @options[:insert_cost]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
module Cost
|
5
|
+
# A cost model which estimates the number of entities transferred
|
6
|
+
class EntityCountCost < Cost
|
7
|
+
include Subtype
|
8
|
+
|
9
|
+
# Rough cost estimate as the number of entities retrieved at each step
|
10
|
+
# @return [Numeric]
|
11
|
+
def index_lookup_cost(step)
|
12
|
+
# Simply count the number of entities at each step
|
13
|
+
step.state.cardinality
|
14
|
+
end
|
15
|
+
|
16
|
+
# Cost estimate as number of entities deleted
|
17
|
+
def delete_cost(step)
|
18
|
+
step.state.cardinality
|
19
|
+
end
|
20
|
+
|
21
|
+
# Cost estimate as number of entities inserted
|
22
|
+
def insert_cost(step)
|
23
|
+
step.state.cardinality
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
module Cost
|
5
|
+
# A cost model which estimates the total size of data transferred
|
6
|
+
class FieldSizeCost < Cost
|
7
|
+
include Subtype
|
8
|
+
|
9
|
+
# Rough cost estimate as the size of data returned
|
10
|
+
# @return [Numeric]
|
11
|
+
def index_lookup_cost(step)
|
12
|
+
# If we have an answer to the query, we only need
|
13
|
+
# to fetch the data fields which are selected
|
14
|
+
fields = step.index.all_fields
|
15
|
+
fields &= step.state.query.select if step.state.answered?
|
16
|
+
|
17
|
+
step.state.cardinality * fields.sum_by(&:size)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Cost estimate as the size of an index entry
|
21
|
+
def delete_cost(step)
|
22
|
+
step.index.entry_size
|
23
|
+
end
|
24
|
+
|
25
|
+
# Cost estimate as the size of an index entry
|
26
|
+
def insert_cost(step)
|
27
|
+
step.index.entry_size
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
module Cost
|
5
|
+
# A cost model which estimates the number of requests to the backend
|
6
|
+
class RequestCountCost < Cost
|
7
|
+
include Subtype
|
8
|
+
|
9
|
+
# Rough cost estimate as the number of requests made
|
10
|
+
# @return [Numeric]
|
11
|
+
def index_lookup_cost(step)
|
12
|
+
# We always start with a single lookup, then the number
|
13
|
+
# of lookups is determined by the cardinality at the preceding step
|
14
|
+
if step.parent.is_a?(Plans::RootPlanStep)
|
15
|
+
1
|
16
|
+
else
|
17
|
+
step.state.cardinality
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Cost estimate as number of entities deleted
|
22
|
+
def delete_cost(step)
|
23
|
+
step.state.cardinality
|
24
|
+
end
|
25
|
+
|
26
|
+
# Cost estimate as number of entities inserted
|
27
|
+
def insert_cost(step)
|
28
|
+
step.state.cardinality
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|