omf_rete 0.5

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