omf_rete 0.5
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.
- data/.gitignore +4 -0
- data/README.md +182 -0
- data/Rakefile +14 -0
- data/lib/omf_rete/abstract_tuple_set.rb +68 -0
- data/lib/omf_rete/indexed_tuple_set.rb +129 -0
- data/lib/omf_rete/join_op.rb +113 -0
- data/lib/omf_rete/planner/abstract_plan.rb +57 -0
- data/lib/omf_rete/planner/filter_plan.rb +49 -0
- data/lib/omf_rete/planner/join_plan.rb +94 -0
- data/lib/omf_rete/planner/plan_builder.rb +302 -0
- data/lib/omf_rete/planner/plan_level_builder.rb +94 -0
- data/lib/omf_rete/planner/plan_set.rb +82 -0
- data/lib/omf_rete/planner/source_plan.rb +81 -0
- data/lib/omf_rete/store/alpha/alpha_element.rb +95 -0
- data/lib/omf_rete/store/alpha/alpha_inner_element.rb +96 -0
- data/lib/omf_rete/store/alpha/alpha_leaf_element.rb +41 -0
- data/lib/omf_rete/store/alpha/alpha_store.rb +197 -0
- data/lib/omf_rete/store.rb +57 -0
- data/lib/omf_rete/tuple_stream.rb +241 -0
- data/lib/omf_rete/version.rb +9 -0
- data/lib/omf_rete.rb +35 -0
- data/omf_rete.gemspec +24 -0
- data/tests/test.rb +8 -0
- data/tests/test_backtracking.rb +42 -0
- data/tests/test_filter.rb +77 -0
- data/tests/test_indexed_tuple_set.rb +58 -0
- data/tests/test_join_op.rb +50 -0
- data/tests/test_planner.rb +232 -0
- data/tests/test_store.rb +157 -0
- metadata +74 -0
@@ -0,0 +1,94 @@
|
|
1
|
+
|
2
|
+
require 'omf_rete/tuple_stream'
|
3
|
+
|
4
|
+
module OMF::Rete
|
5
|
+
module Planner
|
6
|
+
|
7
|
+
|
8
|
+
# This class represents a planned join op.
|
9
|
+
#
|
10
|
+
#
|
11
|
+
class JoinPlan < AbstractPlan
|
12
|
+
|
13
|
+
# stream1 - first stream to join
|
14
|
+
# stream2 - second stream to join
|
15
|
+
# joinSet - set of bindings to join on
|
16
|
+
# resultSet - set of bindings in result
|
17
|
+
# coverSet - set of leaf nodes contributing to this result
|
18
|
+
#
|
19
|
+
def initialize(stream1, stream2, joinSet, resultSet, coverSet, planBuilder)
|
20
|
+
super coverSet, resultSet
|
21
|
+
|
22
|
+
@planBuilder = planBuilder
|
23
|
+
@left = stream1
|
24
|
+
@right = stream2
|
25
|
+
@join_set = joinSet
|
26
|
+
end
|
27
|
+
|
28
|
+
# Materialize the plan. Create all the relevant operations and tuple sets
|
29
|
+
# to realize a configuration for the respective query. Returns the result
|
30
|
+
# set.
|
31
|
+
#
|
32
|
+
def materialize(indexPattern, resultSet, opts, &block)
|
33
|
+
unless resultSet
|
34
|
+
description = @result_set.to_a.sort
|
35
|
+
resultSet = IndexedTupleSet.new(description, indexPattern, nil, opts)
|
36
|
+
end
|
37
|
+
|
38
|
+
indexPattern = @join_set.to_a
|
39
|
+
leftSet = @left.materialize(indexPattern, nil, opts)
|
40
|
+
rightSet = @right.materialize(indexPattern, nil, opts)
|
41
|
+
op = JoinOP.new(leftSet, rightSet, resultSet)
|
42
|
+
resultSet.source = op
|
43
|
+
resultSet
|
44
|
+
end
|
45
|
+
|
46
|
+
# Create a hash for this plan which allows us to
|
47
|
+
# to identify identical plans.
|
48
|
+
#
|
49
|
+
# Please note, that there is most likely a mroe efficient way to
|
50
|
+
# calculate a hash with the above properties
|
51
|
+
#
|
52
|
+
def hash()
|
53
|
+
unless @hash
|
54
|
+
s = StringIO.new
|
55
|
+
describe(s, 0, 0, '|')
|
56
|
+
str = s.string
|
57
|
+
@hash = str.hash
|
58
|
+
end
|
59
|
+
@hash
|
60
|
+
end
|
61
|
+
|
62
|
+
# Return the cost of this plan.
|
63
|
+
#
|
64
|
+
# TODO: Some more meaningful heuristic will be nice
|
65
|
+
#
|
66
|
+
def cost()
|
67
|
+
unless @cost
|
68
|
+
lcost = @left.cost()
|
69
|
+
rcost = @right.cost()
|
70
|
+
#@cost = 1 + 1.2 * (lcost > rcost ? lcost : rcost)
|
71
|
+
@cost = 1 + 1.2 * (lcost + rcost)
|
72
|
+
|
73
|
+
end
|
74
|
+
@cost
|
75
|
+
end
|
76
|
+
|
77
|
+
def describe(out = STDOUT, offset = 0, incr = 2, sep = "\n")
|
78
|
+
out.write(" " * offset)
|
79
|
+
result = @result_set.to_a.sort
|
80
|
+
join = @join_set.to_a.sort
|
81
|
+
out.write("join: [#{join.join(', ')}] => [#{result.join(', ')}] cost: #{cost}#{sep}")
|
82
|
+
@left.describe(out, offset + incr, incr, sep)
|
83
|
+
@right.describe(out, offset + incr, incr, sep)
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_s
|
87
|
+
result = @result_set.to_a.sort
|
88
|
+
join = @join_set.to_a.sort
|
89
|
+
"JoinPlan [#{join.join(', ')}] out: [#{result.join(', ')}]"
|
90
|
+
end
|
91
|
+
end # PlanBuilder
|
92
|
+
|
93
|
+
end # Planner
|
94
|
+
end # module
|
@@ -0,0 +1,302 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
|
4
|
+
# Monkey patch symbol to allow consistent ordering of set keys
|
5
|
+
unless (:test).respond_to? '<=>'
|
6
|
+
class Symbol
|
7
|
+
def <=>(o)
|
8
|
+
self.to_s <=> o.to_s
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
module OMF::Rete
|
15
|
+
module Planner
|
16
|
+
|
17
|
+
# The base exception for all errors related
|
18
|
+
class PlannerException < Exception; end
|
19
|
+
|
20
|
+
require 'omf_rete/planner/source_plan'
|
21
|
+
require 'omf_rete/planner/plan_level_builder'
|
22
|
+
require 'omf_rete/planner/plan_set'
|
23
|
+
require 'omf_rete/planner/filter_plan'
|
24
|
+
|
25
|
+
# This class builds all the possible plans for a given
|
26
|
+
# query
|
27
|
+
#
|
28
|
+
class PlanBuilder
|
29
|
+
|
30
|
+
attr_reader :plan, :store
|
31
|
+
#
|
32
|
+
# query -- query consists of an array of tuple paterns with binding declarations
|
33
|
+
# store -- store to attach source sets to
|
34
|
+
#
|
35
|
+
def initialize(query, store, opts = {})
|
36
|
+
@store = store
|
37
|
+
@opts = opts
|
38
|
+
|
39
|
+
_parse_query(query)
|
40
|
+
|
41
|
+
@complete_plans = []
|
42
|
+
if (@source_cnt == 1)
|
43
|
+
# only one source means a trivial plan, the source itself
|
44
|
+
@complete_plans = @sources.to_a
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
def build()
|
50
|
+
level = 0
|
51
|
+
maxLevels = @source_cnt + 10 # pull the emergency breaks sometimes
|
52
|
+
while (@complete_plans.empty? && level < maxLevels) do
|
53
|
+
_iterate()
|
54
|
+
level += 1
|
55
|
+
end
|
56
|
+
if (@complete_plans.empty?)
|
57
|
+
raise PlannerException.new("Can't create plan")
|
58
|
+
end
|
59
|
+
@complete_plans
|
60
|
+
end
|
61
|
+
|
62
|
+
def each_plan()
|
63
|
+
@complete_plans.each do |p| yield(p) end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return plan with lowest cost
|
67
|
+
#
|
68
|
+
def best_plan()
|
69
|
+
# best_plan = nil
|
70
|
+
# lowest_cost = 9999999999
|
71
|
+
#
|
72
|
+
# each_plan do |plan|
|
73
|
+
# cost = plan.cost
|
74
|
+
# if (cost < lowest_cost)
|
75
|
+
# lowest_cost = cost
|
76
|
+
# best_plan = plan
|
77
|
+
# end
|
78
|
+
# end
|
79
|
+
best_plan = @complete_plans.min do |p1, p2|
|
80
|
+
p1.cost <=> p2.cost
|
81
|
+
end
|
82
|
+
best_plan
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
# Materialize the plan. Create all the relevant operations and tuple sets
|
87
|
+
# to realize a configuration for the respective query. Returns the result
|
88
|
+
# set.
|
89
|
+
#
|
90
|
+
def materialize(projectPattern = nil, plan = nil, opts = nil, &block)
|
91
|
+
unless plan
|
92
|
+
plan = best_plan()
|
93
|
+
end
|
94
|
+
unless plan
|
95
|
+
raise PlannerException.new("No plan to materialize");
|
96
|
+
end
|
97
|
+
if (plan.is_a?(SourcePlan))
|
98
|
+
# This is really just a simple pattern on the store
|
99
|
+
_materialize_simple_plan(projectPattern, plan, opts, &block)
|
100
|
+
else
|
101
|
+
# this is the root of the plan
|
102
|
+
if projectPattern
|
103
|
+
description = projectPattern
|
104
|
+
else
|
105
|
+
description = plan.result_set.to_a.sort
|
106
|
+
end
|
107
|
+
frontS, endS = _materialize_result_stream(plan, projectPattern, opts, &block)
|
108
|
+
plan.materialize(nil, frontS, opts, &block)
|
109
|
+
endS
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
def describe(out = STDOUT, offset = 0, incr = 2, sep = "\n")
|
115
|
+
out << "\n=========\n"
|
116
|
+
@complete_plans.each do |p|
|
117
|
+
out << "------\n"
|
118
|
+
p.describe(out, offset, incr, sep)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# Parse +query+ which is an array of query tuples or filters.
|
125
|
+
#
|
126
|
+
# This method create a new +SourcePlan+ (to be attached to a store)
|
127
|
+
# for every query tuple in the +query+ array.
|
128
|
+
#
|
129
|
+
def _parse_query(query)
|
130
|
+
@query = query
|
131
|
+
@sources = Set.new
|
132
|
+
@filters = []
|
133
|
+
@plans = PlanSet.new
|
134
|
+
query.each do |sp|
|
135
|
+
|
136
|
+
if sp.is_a? FilterPlan
|
137
|
+
@filters << sp
|
138
|
+
elsif sp.is_a? SourcePlan
|
139
|
+
@sources << sp
|
140
|
+
@plans << sp
|
141
|
+
elsif sp.is_a? Array
|
142
|
+
unless sp.length == @store.length
|
143
|
+
raise PlannerException.new("SubPlan: Expected array of store size, but got '#{sp}'")
|
144
|
+
end
|
145
|
+
begin
|
146
|
+
p = SourcePlan.new(sp, @store)
|
147
|
+
@sources << p
|
148
|
+
@plans << p
|
149
|
+
rescue NoBindingException
|
150
|
+
# ignore sources with no bindings in them
|
151
|
+
end
|
152
|
+
else
|
153
|
+
raise PlannerException.new("SubPlan: Unknown sub goal definition '#{sp}'")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
@source_cnt = @sources.size
|
157
|
+
if @sources.empty?
|
158
|
+
raise PlannerException.new("Query '#{query}' seems to be empty")
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
#
|
163
|
+
# Array of sources from lower levels to build new plans from
|
164
|
+
#
|
165
|
+
# plans -- array of plans to combine
|
166
|
+
# fullCoverSet -- set containing all initial sources
|
167
|
+
#
|
168
|
+
def _iterate()
|
169
|
+
# puts ">>>>>>>> LEVEL >>>>>"
|
170
|
+
plans = @plans.to_a.dup
|
171
|
+
plans.each_with_index do |plan, i|
|
172
|
+
unless (plan.complete?) # don't combine complete plans anymore
|
173
|
+
unless (rem = plans[i + 1 .. -1]).empty?
|
174
|
+
_build_for_plan(plan, rem)
|
175
|
+
end
|
176
|
+
unless plan.used?
|
177
|
+
_add_plan(plan)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
@plans
|
182
|
+
end
|
183
|
+
|
184
|
+
# Compare +plan+ with all remaining plans and create
|
185
|
+
# a new plan if it can be combined. If no new plan
|
186
|
+
# is created for +plan+ elevated it to this level.
|
187
|
+
#
|
188
|
+
def _build_for_plan(plan, otherPlans)
|
189
|
+
otherPlans.each do |other|
|
190
|
+
unless (other.complete?) # don't combine complete plans anymore
|
191
|
+
_build_for(plan, other)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
def _build_for(left, right)
|
198
|
+
# STDOUT.puts "CHECKING"
|
199
|
+
# STDOUT.puts " LEFT"
|
200
|
+
# left.describe
|
201
|
+
# STDOUT.puts " RIGHT"
|
202
|
+
# right.describe
|
203
|
+
|
204
|
+
lcover = left.cover_set
|
205
|
+
rcover = right.cover_set
|
206
|
+
combinedCover = lcover + rcover
|
207
|
+
combinedSize = combinedCover.size
|
208
|
+
if (lcover.size == combinedSize || rcover.size == combinedSize)
|
209
|
+
return nil # doesn't get us closer to a solution
|
210
|
+
end
|
211
|
+
|
212
|
+
joinSet = left.result_set.intersection(right.result_set)
|
213
|
+
if (joinSet.empty?)
|
214
|
+
return nil # nothing to join
|
215
|
+
end
|
216
|
+
|
217
|
+
resultSet = left.result_set + right.result_set
|
218
|
+
left.used
|
219
|
+
right.used
|
220
|
+
jp = JoinPlan.new(left, right, joinSet, resultSet, combinedCover, self)
|
221
|
+
_add_plan(jp)
|
222
|
+
end
|
223
|
+
|
224
|
+
def _add_plan(plan)
|
225
|
+
action = 'DUPLICATE: '
|
226
|
+
if (@plans << plan)
|
227
|
+
action = 'ADDED: '
|
228
|
+
if (plan.cover_set.size == @source_cnt)
|
229
|
+
action = 'COMPLETE: '
|
230
|
+
@complete_plans << plan
|
231
|
+
plan.complete()
|
232
|
+
end
|
233
|
+
end
|
234
|
+
# STDOUT << action
|
235
|
+
# plan.describe
|
236
|
+
end
|
237
|
+
|
238
|
+
# The +plan+ consists only of a source plan. Create
|
239
|
+
# a processing stream and attach a block which extracts
|
240
|
+
# the 'bound' elements from the incoming tuple.
|
241
|
+
#
|
242
|
+
def _materialize_simple_plan(projectPattern, plan, opts, &block)
|
243
|
+
|
244
|
+
unless projectPattern
|
245
|
+
# create one from the binding varibales in plan.description
|
246
|
+
projectPattern = []
|
247
|
+
plan.description.each do |name|
|
248
|
+
if name.to_s.end_with?('?')
|
249
|
+
projectPattern << name.to_sym
|
250
|
+
end
|
251
|
+
end
|
252
|
+
if (projectPattern.empty?)
|
253
|
+
raise NoBindingException.new("No binding declaration in source plan '#{plan.description.join(', ')}'")
|
254
|
+
end
|
255
|
+
end
|
256
|
+
description = projectPattern
|
257
|
+
|
258
|
+
#src = plan.materialize(nil, projectPattern, opts)
|
259
|
+
src = ProcessingTupleStream.new(projectPattern, projectPattern, plan.description)
|
260
|
+
frontS, endS = _materialize_result_stream(plan, projectPattern, opts, &block)
|
261
|
+
|
262
|
+
src.receiver = frontS
|
263
|
+
frontS.source = src
|
264
|
+
|
265
|
+
@store.registerTSet(src, plan.description) if @store
|
266
|
+
|
267
|
+
endS
|
268
|
+
end
|
269
|
+
|
270
|
+
# This creates the result stream and stacks all filters on top (if any)
|
271
|
+
# It returns the first and last element as an array.
|
272
|
+
#
|
273
|
+
def _materialize_result_stream(plan, projectPattern, opts, &block)
|
274
|
+
plan_description = plan.result_description
|
275
|
+
description = projectPattern || plan.result_description
|
276
|
+
rs = ResultTupleStream.new(description, &block)
|
277
|
+
|
278
|
+
# This is a very naive plan to add filters. It simple stacks them all at the end.
|
279
|
+
# It would be much better to put them right after each source or join which produces
|
280
|
+
# the matching binding stream.
|
281
|
+
#
|
282
|
+
first_filter = nil
|
283
|
+
last_filter = nil
|
284
|
+
@filters.each do |f|
|
285
|
+
fs = f.materialize(plan_description, last_filter, opts)
|
286
|
+
if (last_filter)
|
287
|
+
last_filter.receiver = fs
|
288
|
+
end
|
289
|
+
first_filter ||= fs
|
290
|
+
last_filter = fs
|
291
|
+
end
|
292
|
+
if (last_filter)
|
293
|
+
last_filter.receiver = rs
|
294
|
+
rs.source = last_filter
|
295
|
+
end
|
296
|
+
[first_filter || rs, rs]
|
297
|
+
end
|
298
|
+
|
299
|
+
|
300
|
+
end # PlanBuilder
|
301
|
+
end # Planner
|
302
|
+
end # module
|
@@ -0,0 +1,94 @@
|
|
1
|
+
|
2
|
+
require 'omf_rete/planner/join_plan'
|
3
|
+
|
4
|
+
module OMF::Rete
|
5
|
+
module Planner
|
6
|
+
|
7
|
+
|
8
|
+
# This class builds all the possible plans for a given
|
9
|
+
# level of the plan forest.
|
10
|
+
#
|
11
|
+
class PlanLevelBuilder
|
12
|
+
|
13
|
+
attr_reader :plans, :complete_plans
|
14
|
+
|
15
|
+
# fullCover -- Set of all sources to cover
|
16
|
+
#
|
17
|
+
def initialize(sources)
|
18
|
+
@fullCover = sources
|
19
|
+
@complete_plans = []
|
20
|
+
@plans = sources.clone
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Array of sources from lower levels to build new plans from
|
25
|
+
#
|
26
|
+
# plans -- array of plans to combine
|
27
|
+
# fullCoverSet -- set containing all initial sources
|
28
|
+
#
|
29
|
+
def build()
|
30
|
+
plans = @plans.to_a
|
31
|
+
plans.each_with_index do |plan, i|
|
32
|
+
unless (rem = plans[i + 1 .. -1]).nil?
|
33
|
+
build_for_plan(plan, rem)
|
34
|
+
end
|
35
|
+
unless plan.used?
|
36
|
+
add_plan(plan)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
@plans
|
40
|
+
end
|
41
|
+
|
42
|
+
def describe(out = STDOUT, offset = 0, incr = 2, sep = "\n")
|
43
|
+
@plans.each do |p|
|
44
|
+
p.describe(out, offset, incr, sep)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
|
52
|
+
# Compare +plan+ with all remaining plans and create
|
53
|
+
# a new plan if it can be combined. If no new plan
|
54
|
+
# is created for +plan+ elevated it to this level.
|
55
|
+
#
|
56
|
+
def build_for_plan(plan, otherPlans)
|
57
|
+
otherPlans.each do |other|
|
58
|
+
build_for(plan, other)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def build_for(left, right)
|
64
|
+
lcover = left.cover_set
|
65
|
+
rcover = right.cover_set
|
66
|
+
combinedCover = lcover + rcover
|
67
|
+
combinedSize = combinedCover.size
|
68
|
+
if (lcover.size == combinedSize || rcover.size == combinedSize)
|
69
|
+
return nil # doesn't get us closer to a solution
|
70
|
+
end
|
71
|
+
|
72
|
+
joinSet = left.result_set.intersection(right.result_set)
|
73
|
+
if (joinSet.empty?)
|
74
|
+
return nil # nothing to join
|
75
|
+
end
|
76
|
+
|
77
|
+
resultSet = left.result_set + right.result_set
|
78
|
+
left.used
|
79
|
+
right.used
|
80
|
+
jp = JoinPlan.new(left, right, joinSet, resultSet, combinedCover)
|
81
|
+
add_plan(jp)
|
82
|
+
end
|
83
|
+
|
84
|
+
def add_plan(plan)
|
85
|
+
if (plan.cover_set == @fullCover)
|
86
|
+
@complete_plans << plan
|
87
|
+
end
|
88
|
+
@plans << plan
|
89
|
+
end
|
90
|
+
|
91
|
+
end # PlanBuilder
|
92
|
+
|
93
|
+
end # Planner
|
94
|
+
end # module
|
@@ -0,0 +1,82 @@
|
|
1
|
+
#
|
2
|
+
#
|
3
|
+
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
module OMF::Rete
|
7
|
+
module Planner
|
8
|
+
|
9
|
+
# This is a specialisation of the Set class which uses the
|
10
|
+
# hash of an object to determine identity.
|
11
|
+
#
|
12
|
+
class PlanSet
|
13
|
+
|
14
|
+
def initialize()
|
15
|
+
@hash = {}
|
16
|
+
@plans = []
|
17
|
+
end
|
18
|
+
|
19
|
+
# Converts the set to an array. The order of elements is uncertain.
|
20
|
+
def to_a
|
21
|
+
@plans
|
22
|
+
end
|
23
|
+
|
24
|
+
def empty?
|
25
|
+
@plans.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
def length
|
29
|
+
@plans.length
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
# Returns true if two sets are equal. The equality of each couple
|
34
|
+
# of elements is defined according to Object#eql?.
|
35
|
+
def ==(set)
|
36
|
+
equal?(set) and return true
|
37
|
+
|
38
|
+
set.is_a?(PlanSet) && size == set.size or return false
|
39
|
+
|
40
|
+
hash = @hash.dup
|
41
|
+
set.all? { |o| hash.include?(o) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def eql?(o) # :nodoc:
|
45
|
+
return false unless o.is_a?(PlanSet)
|
46
|
+
@plans.eql?(o.instance_eval{@plans})
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns true if the set contains the given object.
|
50
|
+
def include?(o)
|
51
|
+
@hash.has_value?(o)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Calls the given block once for each element in the set, passing
|
55
|
+
# the element as parameter. Returns an enumerator if no block is
|
56
|
+
# given.
|
57
|
+
def each
|
58
|
+
block_given? or return enum_for(__method__)
|
59
|
+
@plans.each do |o| yield(o) end
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
# Adds the given object to the set and returns false if object is already
|
64
|
+
# in the set, true otherwise.
|
65
|
+
#
|
66
|
+
def add(o)
|
67
|
+
really_added = false
|
68
|
+
oh = o.hash
|
69
|
+
unless @hash.key?(oh)
|
70
|
+
really_added = true
|
71
|
+
@hash[oh] = o
|
72
|
+
@plans << o
|
73
|
+
end
|
74
|
+
really_added
|
75
|
+
end
|
76
|
+
alias << add
|
77
|
+
|
78
|
+
end # class
|
79
|
+
end # module
|
80
|
+
end # module
|
81
|
+
|
82
|
+
|
@@ -0,0 +1,81 @@
|
|
1
|
+
|
2
|
+
require 'omf_rete/planner/plan_builder'
|
3
|
+
require 'omf_rete/planner/abstract_plan'
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
module OMF::Rete
|
7
|
+
module Planner
|
8
|
+
|
9
|
+
# Thrown if the plan doesn't contain any bindings
|
10
|
+
class NoBindingException < PlannerException; end
|
11
|
+
|
12
|
+
# This class represents a planned join op.
|
13
|
+
#
|
14
|
+
#
|
15
|
+
class SourcePlan < AbstractPlan
|
16
|
+
attr_reader :description
|
17
|
+
attr_reader :source_set # tuple set created by this plan
|
18
|
+
|
19
|
+
#
|
20
|
+
# description - description of tuples contained in set
|
21
|
+
# store - store to attach +source_set+ to
|
22
|
+
#
|
23
|
+
def initialize(description, store = nil)
|
24
|
+
@description = description
|
25
|
+
# the result set consists of all the binding declarations
|
26
|
+
# which are symbols with trailing '?'
|
27
|
+
resultSet = Set.new
|
28
|
+
description.each do |name|
|
29
|
+
if name.to_s.end_with?('?')
|
30
|
+
resultSet << name.to_sym
|
31
|
+
end
|
32
|
+
end
|
33
|
+
if (resultSet.empty?)
|
34
|
+
raise NoBindingException.new("No binding declaration in sub plan '#{description.join(', ')}'")
|
35
|
+
end
|
36
|
+
coverSet = Set.new([self])
|
37
|
+
super coverSet, resultSet
|
38
|
+
|
39
|
+
#raise Exception unless store.kind_of?(Moana::Filter::Store)
|
40
|
+
@store = store
|
41
|
+
end
|
42
|
+
|
43
|
+
# Materialize the plan. Returns a tuple set.
|
44
|
+
#
|
45
|
+
def materialize(indexPattern, projectPattern, opts)
|
46
|
+
unless indexPattern
|
47
|
+
# this plan only consists of a single source
|
48
|
+
projectPattern ||= result_description
|
49
|
+
@source_set = ProcessingTupleStream.new(projectPattern, projectPattern, @description)
|
50
|
+
else
|
51
|
+
@source_set = OMF::Rete::IndexedTupleSet.new(@description, indexPattern)
|
52
|
+
end
|
53
|
+
@store.registerTSet(@source_set, @description) if @store
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return the cost of this plan.
|
57
|
+
#
|
58
|
+
# TODO: Some more meaningful heuristic will be nice
|
59
|
+
#
|
60
|
+
def cost()
|
61
|
+
unless @cost
|
62
|
+
@cost = @description.inject(0) do |val, el|
|
63
|
+
val + ((el.nil? || el.to_s.end_with?('?')) ? 1 : 0.1)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
@cost
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def describe(out = STDOUT, offset = 0, incr = 2, sep = "\n")
|
71
|
+
out.write(" " * offset)
|
72
|
+
desc = @description.collect do |e| e || '*' end
|
73
|
+
# index = @result_set.to_a.sort
|
74
|
+
# out.write("src: [#{desc.join(', ')}] index: [#{index.join(', ')}] cost: #{cost}#{sep}")
|
75
|
+
out.write("src: [#{desc.join(', ')}] cost: #{cost}#{sep}")
|
76
|
+
end
|
77
|
+
|
78
|
+
end # SourcePlan
|
79
|
+
|
80
|
+
end # module
|
81
|
+
end # module
|