nose 0.1.0pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/lib/nose/backend/cassandra.rb +390 -0
  3. data/lib/nose/backend/file.rb +185 -0
  4. data/lib/nose/backend/mongo.rb +242 -0
  5. data/lib/nose/backend.rb +557 -0
  6. data/lib/nose/cost/cassandra.rb +33 -0
  7. data/lib/nose/cost/entity_count.rb +27 -0
  8. data/lib/nose/cost/field_size.rb +31 -0
  9. data/lib/nose/cost/request_count.rb +32 -0
  10. data/lib/nose/cost.rb +68 -0
  11. data/lib/nose/debug.rb +45 -0
  12. data/lib/nose/enumerator.rb +199 -0
  13. data/lib/nose/indexes.rb +239 -0
  14. data/lib/nose/loader/csv.rb +99 -0
  15. data/lib/nose/loader/mysql.rb +199 -0
  16. data/lib/nose/loader/random.rb +48 -0
  17. data/lib/nose/loader/sql.rb +105 -0
  18. data/lib/nose/loader.rb +38 -0
  19. data/lib/nose/model/entity.rb +136 -0
  20. data/lib/nose/model/fields.rb +293 -0
  21. data/lib/nose/model.rb +113 -0
  22. data/lib/nose/parser.rb +202 -0
  23. data/lib/nose/plans/execution_plan.rb +282 -0
  24. data/lib/nose/plans/filter.rb +99 -0
  25. data/lib/nose/plans/index_lookup.rb +302 -0
  26. data/lib/nose/plans/limit.rb +42 -0
  27. data/lib/nose/plans/query_planner.rb +361 -0
  28. data/lib/nose/plans/sort.rb +49 -0
  29. data/lib/nose/plans/update.rb +60 -0
  30. data/lib/nose/plans/update_planner.rb +270 -0
  31. data/lib/nose/plans.rb +135 -0
  32. data/lib/nose/proxy/mysql.rb +275 -0
  33. data/lib/nose/proxy.rb +102 -0
  34. data/lib/nose/query_graph.rb +481 -0
  35. data/lib/nose/random/barbasi_albert.rb +48 -0
  36. data/lib/nose/random/watts_strogatz.rb +50 -0
  37. data/lib/nose/random.rb +391 -0
  38. data/lib/nose/schema.rb +89 -0
  39. data/lib/nose/search/constraints.rb +143 -0
  40. data/lib/nose/search/problem.rb +328 -0
  41. data/lib/nose/search/results.rb +200 -0
  42. data/lib/nose/search.rb +266 -0
  43. data/lib/nose/serialize.rb +747 -0
  44. data/lib/nose/statements/connection.rb +160 -0
  45. data/lib/nose/statements/delete.rb +83 -0
  46. data/lib/nose/statements/insert.rb +146 -0
  47. data/lib/nose/statements/query.rb +161 -0
  48. data/lib/nose/statements/update.rb +101 -0
  49. data/lib/nose/statements.rb +645 -0
  50. data/lib/nose/timing.rb +79 -0
  51. data/lib/nose/util.rb +305 -0
  52. data/lib/nose/workload.rb +244 -0
  53. data/lib/nose.rb +37 -0
  54. data/templates/workload.erb +42 -0
  55. metadata +700 -0
@@ -0,0 +1,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