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