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