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