omf_rete 0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|