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/random.rb
ADDED
@@ -0,0 +1,391 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pickup'
|
4
|
+
|
5
|
+
module NoSE
|
6
|
+
module Random
|
7
|
+
# A simple representation of a random ER diagram
|
8
|
+
class Network
|
9
|
+
attr_reader :entities
|
10
|
+
|
11
|
+
def initialize(params = {})
|
12
|
+
@nodes_nb = params.fetch :nodes_nb, 10
|
13
|
+
@field_count = RandomGaussian.new params.fetch(:num_fields, 3), 1
|
14
|
+
@neighbours = Array.new(@nodes_nb) { Set.new }
|
15
|
+
end
|
16
|
+
|
17
|
+
# :nocov:
|
18
|
+
def inspect
|
19
|
+
@nodes.map do |node|
|
20
|
+
@entities[node].inspect
|
21
|
+
end.join "\n"
|
22
|
+
end
|
23
|
+
# :nocov:
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
# Create a random entity to use in the model
|
28
|
+
# @return [Entity]
|
29
|
+
def create_entity(node)
|
30
|
+
num_entities = RandomGaussian.new 10_000, 100
|
31
|
+
entity = Entity.new('E' + random_name(node)) * num_entities.rand
|
32
|
+
pick_fields entity
|
33
|
+
|
34
|
+
entity
|
35
|
+
end
|
36
|
+
|
37
|
+
# Probabilities of selecting various field types
|
38
|
+
FIELD_TYPES = {
|
39
|
+
Fields::IntegerField => 45,
|
40
|
+
Fields::StringField => 35,
|
41
|
+
Fields::DateField => 10,
|
42
|
+
Fields::FloatField => 10
|
43
|
+
}.freeze
|
44
|
+
|
45
|
+
# Select random fields for an entity
|
46
|
+
# @return [void]
|
47
|
+
def pick_fields(entity)
|
48
|
+
entity << Fields::IDField.new(entity.name + 'ID')
|
49
|
+
0.upto(@field_count.rand).each do |field_index|
|
50
|
+
entity << random_field(field_index)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Generate a random field to add to an entity
|
55
|
+
# @return [Fields::Field]
|
56
|
+
def random_field(field_index)
|
57
|
+
Pickup.new(FIELD_TYPES).pick.send(:new, 'F' + random_name(field_index))
|
58
|
+
end
|
59
|
+
|
60
|
+
# Add foreign key relationships for neighbouring nodes
|
61
|
+
# @return [void]
|
62
|
+
def add_foreign_keys
|
63
|
+
@neighbours.each_with_index do |other_nodes, node|
|
64
|
+
other_nodes.each do |other_node|
|
65
|
+
@neighbours[other_node].delete node
|
66
|
+
|
67
|
+
if rand > 0.5
|
68
|
+
from_node = node
|
69
|
+
to_node = other_node
|
70
|
+
else
|
71
|
+
from_node = other_node
|
72
|
+
to_node = node
|
73
|
+
end
|
74
|
+
|
75
|
+
from_field = Fields::ForeignKeyField.new(
|
76
|
+
'FK' + @entities[to_node].name + 'ID',
|
77
|
+
@entities[to_node]
|
78
|
+
)
|
79
|
+
to_field = Fields::ForeignKeyField.new(
|
80
|
+
'FK' + @entities[from_node].name + 'ID',
|
81
|
+
@entities[from_node]
|
82
|
+
)
|
83
|
+
|
84
|
+
from_field.reverse = to_field
|
85
|
+
to_field.reverse = from_field
|
86
|
+
|
87
|
+
@entities[from_node] << from_field
|
88
|
+
@entities[to_node] << to_field
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Add a new link between two nodes
|
94
|
+
# @return [void]
|
95
|
+
def add_link(node, other_node)
|
96
|
+
@neighbours[node] << other_node
|
97
|
+
@neighbours[other_node] << node
|
98
|
+
end
|
99
|
+
|
100
|
+
# Remove a link between two nodes
|
101
|
+
# @return [void]
|
102
|
+
def remove_link(node, other_node)
|
103
|
+
@neighbours[node].delete other_node
|
104
|
+
@neighbours[other_node].delete node
|
105
|
+
end
|
106
|
+
|
107
|
+
# Find a new neighbour for a node
|
108
|
+
def new_neighbour(node, neighbour)
|
109
|
+
unlinkable_nodes = [node, neighbour] + @neighbours[node].to_a
|
110
|
+
(@nodes.to_a - unlinkable_nodes).sample
|
111
|
+
end
|
112
|
+
|
113
|
+
# Random names of variables combined to create random names
|
114
|
+
VARIABLE_NAMES = %w(Foo Bar Baz Quux Corge Grault
|
115
|
+
Garply Waldo Fred Plugh).freeze
|
116
|
+
|
117
|
+
# Generate a random name for an attribute
|
118
|
+
# @return [String]
|
119
|
+
def random_name(index)
|
120
|
+
index.to_s.chars.map(&:to_i).map { |digit| VARIABLE_NAMES[digit] }.join
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Generates random queries over entities in a given model
|
125
|
+
class StatementGenerator
|
126
|
+
def initialize(model)
|
127
|
+
@model = model
|
128
|
+
end
|
129
|
+
|
130
|
+
# Generate a new random insertion to entities in the model
|
131
|
+
# @return [Insert]
|
132
|
+
def random_insert(connect = true)
|
133
|
+
entity = @model.entities.values.sample
|
134
|
+
settings = entity.fields.each_value.map do |field|
|
135
|
+
"#{field.name}=?"
|
136
|
+
end.join ', '
|
137
|
+
insert = "INSERT INTO #{entity.name} SET #{settings} "
|
138
|
+
|
139
|
+
# Optionally add connections to other entities
|
140
|
+
insert += random_connection(entity) if connect
|
141
|
+
|
142
|
+
Statement.parse insert, @model
|
143
|
+
end
|
144
|
+
|
145
|
+
# Generate a random connection for an Insert
|
146
|
+
def random_connection(entity)
|
147
|
+
connections = entity.foreign_keys.values.sample(2)
|
148
|
+
'AND CONNECT TO ' + connections.map do |connection|
|
149
|
+
"#{connection.name}(?)"
|
150
|
+
end.join(', ')
|
151
|
+
end
|
152
|
+
|
153
|
+
# Generate a new random update of entities in the model
|
154
|
+
# @return [Update]
|
155
|
+
def random_update(path_length = 1, updated_fields = 2,
|
156
|
+
condition_count = 1)
|
157
|
+
path = random_path(path_length)
|
158
|
+
settings = random_settings path, updated_fields
|
159
|
+
from = [path.first.parent.name] + path.entries[1..-1].map(&:name)
|
160
|
+
update = "UPDATE #{from.first} FROM #{from.join '.'} " \
|
161
|
+
"SET #{settings} " +
|
162
|
+
random_where_clause(path, condition_count)
|
163
|
+
|
164
|
+
Statement.parse update, @model
|
165
|
+
end
|
166
|
+
|
167
|
+
# Get random settings for an update
|
168
|
+
# @return [String]
|
169
|
+
def random_settings(path, updated_fields)
|
170
|
+
# Don't update key fields
|
171
|
+
update_fields = path.entities.first.fields.values
|
172
|
+
update_fields.reject! { |field| field.is_a? Fields::IDField }
|
173
|
+
|
174
|
+
update_fields.sample(updated_fields).map do |field|
|
175
|
+
"#{field.name}=?"
|
176
|
+
end.join ', '
|
177
|
+
end
|
178
|
+
|
179
|
+
# Generate a new random deletion of entities in the model
|
180
|
+
# @return [Delete]
|
181
|
+
def random_delete
|
182
|
+
path = random_path(1)
|
183
|
+
|
184
|
+
from = [path.first.parent.name] + path.entries[1..-1].map(&:name)
|
185
|
+
delete = "DELETE #{from.first} FROM #{from.join '.'} " +
|
186
|
+
random_where_clause(path, 1)
|
187
|
+
|
188
|
+
Statement.parse delete, @model
|
189
|
+
end
|
190
|
+
|
191
|
+
# Generate a new random query from entities in the model
|
192
|
+
# @return [Query]
|
193
|
+
def random_query(path_length = 3, selected_fields = 2,
|
194
|
+
condition_count = 2, order = false)
|
195
|
+
path = random_path path_length
|
196
|
+
graph = QueryGraph::Graph.from_path path
|
197
|
+
|
198
|
+
conditions = [
|
199
|
+
Condition.new(path.entities.first.fields.values.sample, :'=', nil)
|
200
|
+
]
|
201
|
+
condition_count -= 1
|
202
|
+
|
203
|
+
new_fields = random_where_conditions(path, condition_count,
|
204
|
+
conditions.map(&:field).to_set)
|
205
|
+
conditions += new_fields.map do |field|
|
206
|
+
Condition.new(field, :'>', nil)
|
207
|
+
end
|
208
|
+
|
209
|
+
conditions = Hash[conditions.map do |condition|
|
210
|
+
[condition.field.id, condition]
|
211
|
+
end]
|
212
|
+
|
213
|
+
params = {
|
214
|
+
select: random_select(path, selected_fields),
|
215
|
+
model: @model,
|
216
|
+
graph: graph,
|
217
|
+
key_path: graph.longest_path,
|
218
|
+
entity: graph.longest_path.first.parent,
|
219
|
+
conditions: conditions,
|
220
|
+
order: order ? [graph.entities.to_a.sample.fields.values.sample] : []
|
221
|
+
}
|
222
|
+
|
223
|
+
query = Query.new params, nil
|
224
|
+
query.hash
|
225
|
+
|
226
|
+
query
|
227
|
+
end
|
228
|
+
|
229
|
+
# Get random fields to select for a Query
|
230
|
+
# @return [Set<Fields::Field>]
|
231
|
+
def random_select(path, selected_fields)
|
232
|
+
fields = Set.new
|
233
|
+
while fields.length < selected_fields
|
234
|
+
fields.add path.entities.sample.fields.values.sample
|
235
|
+
end
|
236
|
+
|
237
|
+
fields
|
238
|
+
end
|
239
|
+
|
240
|
+
# Produce a random statement according to a given set of weights
|
241
|
+
# @return [Statement]
|
242
|
+
def random_statement(weights = { query: 80, insert: 10, update: 5,
|
243
|
+
delete: 5 })
|
244
|
+
pick = Pickup.new(weights)
|
245
|
+
type = pick.pick
|
246
|
+
send(('random_' + type.to_s).to_sym)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Return a random path through the entity graph
|
250
|
+
# @return [KeyPath]
|
251
|
+
def random_path(max_length)
|
252
|
+
# Pick the start of path weighted based on
|
253
|
+
# the number of deges from each entity
|
254
|
+
pick = Pickup.new(Hash[@model.entities.each_value.map do |entity|
|
255
|
+
[entity, entity.foreign_keys.length]
|
256
|
+
end])
|
257
|
+
path = [pick.pick.id_field]
|
258
|
+
|
259
|
+
while path.length < max_length
|
260
|
+
# Find a list of keys to entities we have not seen before
|
261
|
+
last_entity = path.last.entity
|
262
|
+
keys = last_entity.foreign_keys.values
|
263
|
+
keys.reject! { |key| path.map(&:entity).include? key.entity }
|
264
|
+
break if keys.empty?
|
265
|
+
|
266
|
+
# Add a random new key to the path
|
267
|
+
path << keys.sample
|
268
|
+
end
|
269
|
+
|
270
|
+
KeyPath.new path
|
271
|
+
end
|
272
|
+
|
273
|
+
# Produce a random query graph over the entity graph
|
274
|
+
def random_graph(max_nodes)
|
275
|
+
graph = QueryGraph::Graph.new
|
276
|
+
last_node = graph.add_node @model.entities.values.sample
|
277
|
+
while graph.size < max_nodes
|
278
|
+
# Get the possible foreign keys to use
|
279
|
+
keys = last_node.entity.foreign_keys.values
|
280
|
+
keys.reject! { |key| graph.nodes.map(&:entity).include? key.entity }
|
281
|
+
break if keys.empty?
|
282
|
+
|
283
|
+
# Pick a random foreign key to traverse
|
284
|
+
next_key = keys.sample
|
285
|
+
graph.add_edge last_node, next_key.entity, next_key
|
286
|
+
|
287
|
+
# Select a new node to start from, making sure we pick one
|
288
|
+
# that still has valid outgoing edges
|
289
|
+
last_node = graph.nodes.reject do |node|
|
290
|
+
(node.entity.foreign_keys.each_value.map(&:entity) -
|
291
|
+
graph.nodes.map(&:entity)).empty?
|
292
|
+
end.sample
|
293
|
+
break if last_node.nil?
|
294
|
+
end
|
295
|
+
|
296
|
+
graph
|
297
|
+
end
|
298
|
+
|
299
|
+
private
|
300
|
+
|
301
|
+
# Produce a random where clause using fields along a given path
|
302
|
+
# @return [String]
|
303
|
+
def random_where_clause(path, count = 2)
|
304
|
+
# Ensure we have at least one condition at the beginning of the path
|
305
|
+
conditions = [path.entities.first.fields.values.sample]
|
306
|
+
conditions += random_where_conditions path, count - 1
|
307
|
+
|
308
|
+
return '' if conditions.empty?
|
309
|
+
"WHERE #{conditions.map do |field|
|
310
|
+
"#{path.find_field_parent(field).name}.#{field.name} = ?"
|
311
|
+
end.join ' AND '}"
|
312
|
+
end
|
313
|
+
|
314
|
+
# Produce a random set of fields for a where clause
|
315
|
+
# @return [Array<Fields::Field>]
|
316
|
+
def random_where_conditions(path, count, exclude = Set.new)
|
317
|
+
1.upto(count).map do
|
318
|
+
field = path.entities.sample.fields.values.sample
|
319
|
+
next nil if field.name == '**' || exclude.include?(field)
|
320
|
+
|
321
|
+
field
|
322
|
+
end.compact
|
323
|
+
end
|
324
|
+
|
325
|
+
# Get the name to be used in the query for a condition field
|
326
|
+
# @return [String]
|
327
|
+
def condition_field_name(field, path)
|
328
|
+
field_path = path.first.name
|
329
|
+
path_end = path.index(field.parent)
|
330
|
+
last_entity = path.first
|
331
|
+
path[1..path_end].each do |entity|
|
332
|
+
fk = last_entity.foreign_keys.each_value.find do |key|
|
333
|
+
key.entity == entity
|
334
|
+
end
|
335
|
+
field_path += '.' << fk.name
|
336
|
+
last_entity = entity
|
337
|
+
end
|
338
|
+
|
339
|
+
field_path
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# Generate random numbers according to a Guassian distribution
|
345
|
+
class RandomGaussian
|
346
|
+
def initialize(mean, stddev, integer = true, min = 1)
|
347
|
+
@mean = mean
|
348
|
+
@stddev = stddev
|
349
|
+
@valid = false
|
350
|
+
@next = 0
|
351
|
+
@integer = integer
|
352
|
+
@min = min
|
353
|
+
end
|
354
|
+
|
355
|
+
# Return the next valid random number
|
356
|
+
# @return [Fixnum]
|
357
|
+
def rand
|
358
|
+
if @valid
|
359
|
+
@valid = false
|
360
|
+
clamp @next
|
361
|
+
else
|
362
|
+
@valid = true
|
363
|
+
x, y = self.class.gaussian(@mean, @stddev)
|
364
|
+
@next = y
|
365
|
+
clamp x
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
# Return a random number for the given distribution
|
370
|
+
# @return [Array<Fixnum>]
|
371
|
+
def self.gaussian(mean, stddev)
|
372
|
+
theta = 2 * Math::PI * rand
|
373
|
+
rho = Math.sqrt(-2 * Math.log(1 - rand))
|
374
|
+
scale = stddev * rho
|
375
|
+
x = mean + scale * Math.cos(theta)
|
376
|
+
y = mean + scale * Math.sin(theta)
|
377
|
+
[x, y]
|
378
|
+
end
|
379
|
+
|
380
|
+
private
|
381
|
+
|
382
|
+
# Clamp the value to the given minimum
|
383
|
+
def clamp(value)
|
384
|
+
value = value.to_i if @integer
|
385
|
+
[@min, value].max unless @min.nil?
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
require_relative 'random/barbasi_albert'
|
391
|
+
require_relative 'random/watts_strogatz'
|
data/lib/nose/schema.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
# Simple DSL for constructing indexes
|
5
|
+
class Schema
|
6
|
+
attr_reader :model, :indexes
|
7
|
+
|
8
|
+
def initialize(&block)
|
9
|
+
@indexes = {}
|
10
|
+
instance_eval(&block) if block_given?
|
11
|
+
end
|
12
|
+
|
13
|
+
# Find the schema with the given name
|
14
|
+
def self.load(name)
|
15
|
+
filename = File.expand_path "../../../schemas/#{name}.rb", __FILE__
|
16
|
+
contents = File.read(filename)
|
17
|
+
binding.eval contents, filename
|
18
|
+
end
|
19
|
+
|
20
|
+
# rubocop:disable MethodName
|
21
|
+
|
22
|
+
# Set the model to be used by the schema
|
23
|
+
# @return [void]
|
24
|
+
def Model(name)
|
25
|
+
@model = Model.load name
|
26
|
+
NoSE::DSL.mixin_fields @model.entities, IndexDSL
|
27
|
+
end
|
28
|
+
|
29
|
+
# Add a simple index for an entity
|
30
|
+
# @return [void]
|
31
|
+
def SimpleIndex(entity)
|
32
|
+
@indexes[entity] = @model[entity].simple_index
|
33
|
+
end
|
34
|
+
|
35
|
+
# Wrap commands for defining index attributes
|
36
|
+
# @return [void]
|
37
|
+
def Index(key, &block)
|
38
|
+
# Apply the DSL
|
39
|
+
dsl = IndexDSL.new(self)
|
40
|
+
dsl.instance_eval(&block) if block_given?
|
41
|
+
index = Index.new dsl.hash_fields, dsl.order_fields, dsl.extra,
|
42
|
+
QueryGraph::Graph.from_path(dsl.path_keys), key
|
43
|
+
@indexes[index.key] = index
|
44
|
+
end
|
45
|
+
|
46
|
+
# rubocop:enable MethodName
|
47
|
+
end
|
48
|
+
|
49
|
+
# DSL for index creation within a schema
|
50
|
+
class IndexDSL
|
51
|
+
attr_reader :hash_fields, :order_fields, :extra, :path_keys
|
52
|
+
|
53
|
+
def initialize(schema)
|
54
|
+
@schema = schema
|
55
|
+
@hash_fields = []
|
56
|
+
@order_fields = []
|
57
|
+
@extra = []
|
58
|
+
@path_keys = []
|
59
|
+
end
|
60
|
+
|
61
|
+
# rubocop:disable MethodName
|
62
|
+
|
63
|
+
# Define a list of hash fields
|
64
|
+
# @return [void]
|
65
|
+
def Hash(*fields)
|
66
|
+
@hash_fields += fields.flatten
|
67
|
+
end
|
68
|
+
|
69
|
+
# Define a list of ordered fields
|
70
|
+
# @return [void]
|
71
|
+
def Ordered(*fields)
|
72
|
+
@order_fields += fields.flatten
|
73
|
+
end
|
74
|
+
|
75
|
+
# Define a list of extra fields
|
76
|
+
# @return [void]
|
77
|
+
def Extra(*fields)
|
78
|
+
@extra += fields.flatten
|
79
|
+
end
|
80
|
+
|
81
|
+
# Define the keys for the index path
|
82
|
+
# @return [void]
|
83
|
+
def Path(*keys)
|
84
|
+
@path_keys += keys
|
85
|
+
end
|
86
|
+
|
87
|
+
# rubocop:enable MethodName
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
module Search
|
5
|
+
# Base class for constraints
|
6
|
+
class Constraint
|
7
|
+
# If this is not overridden, apply query-specific constraints
|
8
|
+
# @return [void]
|
9
|
+
def self.apply(problem)
|
10
|
+
problem.queries.each_with_index do |query, q|
|
11
|
+
apply_query query, q, problem
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# To be implemented in subclasses for query-specific constraints
|
16
|
+
# @return [void]
|
17
|
+
def self.apply_query(*_args)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Constraints which force indexes to be present to be used
|
22
|
+
class IndexPresenceConstraints < Constraint
|
23
|
+
# Add constraint for indices being present
|
24
|
+
def self.apply(problem)
|
25
|
+
problem.indexes.each do |index|
|
26
|
+
problem.queries.each_with_index do |query, q|
|
27
|
+
name = "q#{q}_#{index.key}_avail" if ENV['NOSE_LOG'] == 'debug'
|
28
|
+
constr = MIPPeR::Constraint.new problem.query_vars[index][query] +
|
29
|
+
problem.index_vars[index] * -1,
|
30
|
+
:<=, 0, name
|
31
|
+
problem.model << constr
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# The single constraint used to enforce a maximum storage cost
|
38
|
+
class SpaceConstraint < Constraint
|
39
|
+
# Add space constraint if needed
|
40
|
+
def self.apply(problem)
|
41
|
+
return unless problem.data[:max_space].finite?
|
42
|
+
|
43
|
+
fail 'Space constraint not supported when grouping by ID graph' \
|
44
|
+
if problem.data[:by_id_graph]
|
45
|
+
|
46
|
+
space = problem.total_size
|
47
|
+
constr = MIPPeR::Constraint.new space, :<=,
|
48
|
+
problem.data[:max_space] * 1.0,
|
49
|
+
'max_space'
|
50
|
+
problem.model << constr
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Constraints that force each query to have an available plan
|
55
|
+
class CompletePlanConstraints < Constraint
|
56
|
+
# Add the discovered constraints to the problem
|
57
|
+
def self.add_query_constraints(query, q, constraints, problem)
|
58
|
+
constraints.each do |entities, constraint|
|
59
|
+
name = "q#{q}_#{entities.map(&:name).join '_'}" \
|
60
|
+
if ENV['NOSE_LOG'] == 'debug'
|
61
|
+
|
62
|
+
# If this is a support query, then we might not need a plan
|
63
|
+
if query.is_a? SupportQuery
|
64
|
+
# Find the index associated with the support query and make
|
65
|
+
# the requirement of a plan conditional on this index
|
66
|
+
index_var = if problem.data[:by_id_graph]
|
67
|
+
problem.index_vars[query.index.to_id_graph]
|
68
|
+
else
|
69
|
+
problem.index_vars[query.index]
|
70
|
+
end
|
71
|
+
next if index_var.nil?
|
72
|
+
|
73
|
+
constr = MIPPeR::Constraint.new constraint + index_var * -1.0,
|
74
|
+
:==, 0, name
|
75
|
+
else
|
76
|
+
constr = MIPPeR::Constraint.new constraint, :==, 1, name
|
77
|
+
end
|
78
|
+
|
79
|
+
problem.model << constr
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Add complete query plan constraints
|
84
|
+
def self.apply_query(query, q, problem)
|
85
|
+
entities = query.join_order
|
86
|
+
query_constraints = Hash[entities.each_cons(2).map do |e, next_e|
|
87
|
+
[[e, next_e], MIPPeR::LinExpr.new]
|
88
|
+
end]
|
89
|
+
|
90
|
+
# Add the sentinel entities at the end and beginning
|
91
|
+
last = Entity.new '__LAST__'
|
92
|
+
query_constraints[[entities.last, last]] = MIPPeR::LinExpr.new
|
93
|
+
first = Entity.new '__FIRST__'
|
94
|
+
query_constraints[[entities.first, first]] = MIPPeR::LinExpr.new
|
95
|
+
|
96
|
+
problem.data[:costs][query].each do |index, (steps, _)|
|
97
|
+
# All indexes should advance a step if possible unless
|
98
|
+
# this is either the last step from IDs to entity
|
99
|
+
# data or the first step going from data to IDs
|
100
|
+
index_step = steps.first
|
101
|
+
fail if entities.length > 1 && index.graph.size == 1 && \
|
102
|
+
!(steps.last.state.answered? ||
|
103
|
+
index_step.parent.is_a?(Plans::RootPlanStep))
|
104
|
+
|
105
|
+
# Join each step in the query graph
|
106
|
+
index_var = problem.query_vars[index][query]
|
107
|
+
index_entities = index.graph.entities.sort_by do |entity|
|
108
|
+
entities.index entity
|
109
|
+
end
|
110
|
+
index_entities.each_cons(2) do |entity, next_entity|
|
111
|
+
# Make sure the constraints go in the correct direction
|
112
|
+
if query_constraints.key?([entity, next_entity])
|
113
|
+
query_constraints[[entity, next_entity]] += index_var
|
114
|
+
else
|
115
|
+
query_constraints[[next_entity, entity]] += index_var
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# If this query has been answered, add the jump to the last step
|
120
|
+
query_constraints[[entities.last, last]] += index_var \
|
121
|
+
if steps.last.state.answered?
|
122
|
+
|
123
|
+
# If this index is the first step, add this index to the beginning
|
124
|
+
query_constraints[[entities.first, first]] += index_var \
|
125
|
+
if index_step.parent.is_a?(Plans::RootPlanStep)
|
126
|
+
|
127
|
+
# Ensure the previous index is available
|
128
|
+
parent_index = index_step.parent.parent_index
|
129
|
+
next if parent_index.nil?
|
130
|
+
|
131
|
+
parent_var = problem.query_vars[parent_index][query]
|
132
|
+
name = "q#{q}_#{index.key}_parent" if ENV['NOSE_LOG'] == 'debug'
|
133
|
+
constr = MIPPeR::Constraint.new index_var * 1.0 + parent_var * -1.0,
|
134
|
+
:<=, 0, name
|
135
|
+
problem.model << constr
|
136
|
+
end
|
137
|
+
|
138
|
+
# Ensure we have exactly one index on each component of the query graph
|
139
|
+
add_query_constraints query, q, query_constraints, problem
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|