activefacts 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/Manifest.txt +7 -2
  2. data/examples/CQL/Address.cql +0 -2
  3. data/examples/CQL/Blog.cql +2 -2
  4. data/examples/CQL/CompanyDirectorEmployee.cql +1 -1
  5. data/examples/CQL/Death.cql +1 -1
  6. data/examples/CQL/Metamodel.cql +5 -5
  7. data/examples/CQL/MultiInheritance.cql +2 -0
  8. data/examples/CQL/PersonPlaysGame.cql +1 -1
  9. data/lib/activefacts/cql/Concepts.treetop +17 -8
  10. data/lib/activefacts/cql/Language/English.treetop +1 -2
  11. data/lib/activefacts/generate/absorption.rb +1 -1
  12. data/lib/activefacts/generate/null.rb +8 -1
  13. data/lib/activefacts/generate/oo.rb +174 -0
  14. data/lib/activefacts/generate/ruby.rb +49 -208
  15. data/lib/activefacts/generate/sql/server.rb +137 -72
  16. data/lib/activefacts/generate/text.rb +1 -1
  17. data/lib/activefacts/input/orm.rb +12 -2
  18. data/lib/activefacts/persistence.rb +5 -1
  19. data/lib/activefacts/persistence/columns.rb +324 -0
  20. data/lib/activefacts/persistence/foreignkey.rb +87 -0
  21. data/lib/activefacts/persistence/index.rb +171 -0
  22. data/lib/activefacts/persistence/reference.rb +326 -0
  23. data/lib/activefacts/persistence/tables.rb +307 -0
  24. data/lib/activefacts/support.rb +1 -1
  25. data/lib/activefacts/version.rb +1 -1
  26. data/lib/activefacts/vocabulary/extensions.rb +42 -5
  27. data/spec/absorption_spec.rb +8 -6
  28. data/spec/cql_cql_spec.rb +1 -0
  29. data/spec/cql_sql_spec.rb +2 -1
  30. data/spec/cql_unit_spec.rb +0 -6
  31. data/spec/norma_cql_spec.rb +1 -0
  32. data/spec/norma_sql_spec.rb +1 -1
  33. data/spec/norma_tables_spec.rb +41 -43
  34. metadata +9 -4
  35. data/lib/activefacts/persistence/composition.rb +0 -653
@@ -0,0 +1,87 @@
1
+ module ActiveFacts
2
+ module Metamodel
3
+
4
+ class ForeignKey
5
+ attr_reader :from, :to, :reference, :from_columns, :to_columns
6
+ def initialize(from, to, fk_ref, from_columns, to_columns)
7
+ @from, @to, @fk_ref, @from_columns, @to_columns =
8
+ from, to, fk_ref, from_columns, to_columns
9
+ end
10
+ end
11
+
12
+ class Concept
13
+ def all_absorbed_foreign_key_reference_path
14
+ references_from.inject([]) do |array, ref|
15
+ if ref.is_simple_reference
16
+ array << [ref]
17
+ elsif ref.is_absorbing
18
+ ref.to.all_absorbed_foreign_key_reference_path.each{|aref|
19
+ array << aref.insert(0, ref)
20
+ }
21
+ end
22
+ array
23
+ end
24
+ end
25
+
26
+ def foreign_keys
27
+ fk_ref_paths = all_absorbed_foreign_key_reference_path
28
+
29
+ # Get the ForeignKey object for each absorbed reference path
30
+ fk_ref_paths.map do |fk_ref_path|
31
+ debug :fk, "\nFK: " + fk_ref_path.map{|fk_ref| fk_ref.reading }*" and " do
32
+
33
+ from_columns = columns.select{|column|
34
+ column.references[0...fk_ref_path.size] == fk_ref_path
35
+ }
36
+ debug :fk, "from_columns = #{from_columns.map { |column| column.name }*", "}"
37
+
38
+ absorption_path = []
39
+ to = fk_ref_path.last.to
40
+ # REVISIT: There should be a better way to find where it's absorbed (especially since this fails for absorbed subtypes having their own identification!)
41
+ while (r = to.absorbed_via)
42
+ absorption_path << r
43
+ to = r.to == to ? r.from : r.to
44
+ end
45
+ raise "REVISIT: #{fk_ref_path.inspect} is bad" unless to and to.columns
46
+
47
+ unless absorption_path.empty?
48
+ debug :fk, "Reference target #{fk_ref_path.last.to.name} is absorbed into #{to.name} via:" do
49
+ debug :fk, "#{absorption_path.map(&:reading)*" and "}"
50
+ end
51
+ end
52
+
53
+ debug :fk, "Looking at absorption depth of #{absorption_path.size} in #{to.name} for to_columns for #{from_columns.map(&:name)*", "}:"
54
+ to_supertypes = to.supertypes_transitive
55
+ to_columns = from_columns.map do |from_column|
56
+ debug :fk, "\tLooking for counterpart of #{from_column.name}: #{from_column.comment}" do
57
+ target_path = absorption_path + from_column.references[fk_ref_path.size..-1]
58
+ debug :fk, "\tcounterpart MUST MATCH #{target_path.map(&:reading)*" and "}"
59
+ c = to.columns.detect do |column|
60
+ debug :fk, "Considering #{column.references.map(&:reading) * " and "}"
61
+ debug :fk, "exact match: #{column.name}: #{column.comment}" if column.references == target_path
62
+ # Column may be inherited into "to", in which case target_path is too long.
63
+ cr = column.references
64
+ allowed_type = fk_ref_path.last.to
65
+ #debug :fk, "Check for absorption, need #{allowed_type.name}" if cr != target_path
66
+ cr == target_path or
67
+ cr == target_path[-cr.size..-1] &&
68
+ !target_path[0...-cr.size].detect do |ref|
69
+ ft = ref.fact_type
70
+ next true if allowed_type.absorbed_via != ref # Problems if it doesn't match
71
+ allowed_type = ref.from
72
+ false
73
+ end
74
+ end
75
+ raise "REVISIT: Failed to find conterpart column for #{from_column.name}" unless c
76
+ c
77
+ end
78
+ end
79
+ debug :fk, "to_columns in #{to.name}: #{to_columns.map { |column| column ? column.name : "OOPS!" }*", "}"
80
+
81
+ ForeignKey.new(self, to, fk_ref_path[-1], from_columns, to_columns)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,171 @@
1
+ #
2
+ # An Index on a Concept is used to represent a unique constraint across roles absorbed
3
+ # into that concept's table.
4
+ #
5
+ # Reference objects update each concept's list of the references *to* and *from* that concept.
6
+ #
7
+ # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
8
+ #
9
+
10
+ module ActiveFacts
11
+ module Metamodel
12
+ class Index
13
+ attr_reader :uniqueness_constraint, :on, :over, :columns, :is_primary, :is_unique
14
+
15
+ # An Index arises from a uniqueness constraint and applies to a table,
16
+ # but because the UC may actually be over an object absorbed into the table,
17
+ # we must record that object also.
18
+ # We record the columns it's over, whether it's primary (for 'over'),
19
+ # and whether it's unique (always, at present)
20
+ def initialize(uc, on, over, columns, is_primary, is_unique = true)
21
+ @uniqueness_constraint, @on, @over, @columns, @is_primary, @is_unique =
22
+ uc, on, over, columns, is_primary, is_unique
23
+ end
24
+
25
+ def real_name
26
+ @uniqueness_constraint.name && @uniqueness_constraint.name != '' ? @uniqueness_constraint.name : nil
27
+ end
28
+
29
+ def name
30
+ uc = @uniqueness_constraint
31
+ r = real_name
32
+ return r if r && r !~ /^(Ex|In)ternalUniquenessConstraint[0-9]+$/
33
+ (uc.is_preferred_identifier ? "PK_" : "IX_") +
34
+ view_name +
35
+ (uc.is_preferred_identifier ? "" : "By"+column_names*"")
36
+ end
37
+
38
+ def abbreviated_column_names
39
+ columns.map{|column| column.name.sub(/^#{over.name}/,'')}
40
+ end
41
+
42
+ def column_names
43
+ columns.map{|column| column.name}
44
+ end
45
+
46
+ def view_name
47
+ "#{over.name}#{on == over ? "" : "In"+on.name}"
48
+ end
49
+
50
+ def to_s
51
+ name = @uniqueness_constraint.name
52
+ colnames = @columns.map(&:name)*", "
53
+ preferred = @uniqueness_constraint.is_preferred_identifier ? " (preferred)" : ""
54
+ "Index #{name} on #{@on.name} over #{@over.name}(#{colnames})#{preferred}"
55
+ end
56
+ end
57
+
58
+ class Concept
59
+ attr_reader :indices
60
+
61
+ def clear_indices
62
+ # Clear any previous indices
63
+ @indices = nil
64
+ end
65
+
66
+ def populate_indices
67
+ # The absorption path of a column indicates how it came to be in this table.
68
+ # It might be a direct many:one valuetype relationship, or it might be in such
69
+ # a relationship to an entity that was absorbed into this table (and so on).
70
+ # The reference path is the set of absorption references and one past it.
71
+ # Stopping here means we don't dig into the definitions of FK column counterparts.
72
+ # Note that many columns of an object may have the same ref_path.
73
+ all_column_by_ref_path =
74
+ debug :index2, "Indexing columns by ref_path" do
75
+ columns.inject({}) do |hash, column|
76
+ debug :index2, "References in column #{name}#{column.name}" do
77
+ ref_path = column.absorption_references
78
+ raise "No absorption_references for #{column.name} from #{column.references.map(&:to_s)*" and "}" if !ref_path || ref_path.empty?
79
+ (hash[ref_path] ||= []) << column
80
+ debug :index2, "#{column.name} involves #{ref_path.map(&:to_s)*" and "}"
81
+ end
82
+ hash
83
+ end
84
+ end
85
+
86
+ columns_by_unique_constraint = {}
87
+ all_column_by_role_ref =
88
+ all_column_by_ref_path.
89
+ keys. # Go through all refpaths and find uniqueness constraints
90
+ inject({}) do |hash, ref_path|
91
+ ref_path.each do |ref|
92
+ next unless ref.to_role
93
+ ref.to_role.all_role_ref.each do |role_ref|
94
+ pcs = role_ref.role_sequence.all_presence_constraint.
95
+ reject do |pc|
96
+ !pc.max_frequency or # No maximum freq; cannot be a uniqueness constraint
97
+ pc.max_frequency != 1 or # maximum is not 1
98
+ pc.role_sequence.all_role_ref.size == 1 && # UniquenessConstraint is over one role
99
+ (pc.role_sequence.all_role_ref[0].role.fact_type.is_a?(TypeInheritance) || # Inheritance
100
+ pc.role_sequence.all_role_ref[0].role.fact_type.all_role.size == 1) # Unary
101
+ # The preceeeding two restrictions exclude the internal UCs created within NORMA.
102
+ end
103
+ next unless pcs.size > 0
104
+ # The columns for this ref_path support the UCs in "pcs".
105
+ pcs.each do |pc|
106
+ (columns_by_unique_constraint[pc] ||= []).concat(all_column_by_ref_path[ref_path])
107
+ end
108
+ hash[role_ref] = all_column_by_ref_path[ref_path]
109
+ end
110
+ end
111
+ hash
112
+ end
113
+
114
+ debug :index, "All Indices in #{name}:" do
115
+ @indices = columns_by_unique_constraint.map do |uc, columns|
116
+ absorption_level = columns.map(&:absorption_level).min
117
+ over = columns[0].references[absorption_level].from
118
+
119
+ # Absorption through a one-to-one forms a UC that we don't need to enforce using an index:
120
+ next if over != self and
121
+ over.absorbed_via == columns[0].references[absorption_level-1] and
122
+ (rrs = uc.role_sequence.all_role_ref).size == 1 and
123
+ over.absorbed_via.fact_type.all_role.include?(rrs[0].role)
124
+
125
+ index = Index.new(
126
+ uc,
127
+ self,
128
+ over,
129
+ columns,
130
+ uc.is_preferred_identifier
131
+ )
132
+ debug :index, index
133
+ index
134
+ end
135
+ end
136
+ end
137
+
138
+ end
139
+
140
+ class Vocabulary
141
+ def populate_all_indices
142
+ debug :index, "Populating all concept indices" do
143
+ all_feature.each do |feature|
144
+ next unless feature.is_a? Concept
145
+ feature.clear_indices
146
+ end
147
+ all_feature.each do |feature|
148
+ next unless feature.is_a? Concept
149
+ next unless feature.is_table
150
+ debug :index, "Populating indices for #{feature.name}" do
151
+ feature.populate_indices
152
+ end
153
+ end
154
+ end
155
+ debug :index, "Finished concept indices" do
156
+ all_feature.each do |feature|
157
+ next unless feature.is_a? Concept
158
+ next unless feature.is_table
159
+ next unless feature.indices.size > 0
160
+ debug :index, "#{feature.name}:" do
161
+ feature.indices.each do |index|
162
+ debug :index, index
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ end
171
+ end
@@ -0,0 +1,326 @@
1
+ #
2
+ # A Reference from one Concept to another is created for each many-1 or 1-1 relationship
3
+ # (including subtyping), and also for a unary role (implicitly to Boolean concept).
4
+ # A 1-1 or subtyping reference should be created in only one direction, and may be flipped
5
+ # if needed.
6
+ #
7
+ # A reference to a concept that's a table or is fully absorbed into a table will
8
+ # become a foreign key, otherwise it will absorb all that concept's references.
9
+ #
10
+ # Reference objects update each concept's list of the references *to* and *from* that concept.
11
+ #
12
+ # Copyright (c) 2008 Clifford Heath. Read the LICENSE file.
13
+ #
14
+ # REVISIT: References need is_mandatory
15
+ # REVISIT: Need to index References by to_role, to help in finding PK references etc.
16
+
17
+ module ActiveFacts
18
+ module Metamodel
19
+
20
+ class Reference
21
+ attr_reader :from, :to # A "from" instance is related to one "to" instance
22
+ attr_reader :from_role, :to_role # For objectified facts, one role will be nil (a phantom)
23
+ attr_reader :fact_type
24
+
25
+ # A Reference is created from a concept in regard to a role it plays
26
+ def initialize(from, role)
27
+ @from = from
28
+ return unless role # All done if it's a self-value reference for a ValueType
29
+ @fact_type = role.fact_type
30
+ if @fact_type.all_role.size == 1
31
+ # @from_role is nil for a unary
32
+ @to_role = role
33
+ @to = role.fact_type.entity_type # nil unless the unary is objectified
34
+ elsif (role.fact_type.entity_type == @from) # role is in "from", an objectified fact type
35
+ @from_role = nil # Phantom role
36
+ @to_role = role
37
+ @to = @to_role.concept
38
+ else
39
+ @from_role = role
40
+ @to = role.fact_type.entity_type # If set, to_role is a phantom
41
+ unless @to
42
+ raise "Illegal reference through >binary fact type" if @fact_type.all_role.size >2
43
+ @to_role = (role.fact_type.all_role-[role])[0]
44
+ @to = @to_role.concept
45
+ end
46
+ end
47
+ end
48
+
49
+ def role_type
50
+ role = @from_role||@to_role
51
+ role && role.role_type
52
+ end
53
+
54
+ def is_mandatory
55
+ !@from_role || # All phantom roles of fact types are mandatory
56
+ is_unary || # Unary fact types become booleans, which must be true or false
57
+ @from_role.is_mandatory
58
+ end
59
+
60
+ def is_unary
61
+ !@to && @to_role && @to_role.fact_type.all_role.size == 1
62
+ end
63
+
64
+ # This case is the only one that cannot be used in the preferred identifier of @from
65
+ def is_to_objectified_fact
66
+ @to && !@to_role && @from_role
67
+ end
68
+
69
+ def is_from_objectified_fact
70
+ @to && @to_role && !@from_role
71
+ end
72
+
73
+ def is_self_value
74
+ !@to && !@to_role
75
+ end
76
+
77
+ def is_absorbing
78
+ @to && @to.absorbed_via == self
79
+ end
80
+
81
+ def is_simple_reference
82
+ # It's a simple reference to a thing if that thing is a table,
83
+ # or is fully absorbed into another table but not via this reference.
84
+ @to && (@to.is_table or @to.absorbed_via && !is_absorbing)
85
+ end
86
+
87
+ def to_names
88
+ case
89
+ when is_unary
90
+ @to_role.fact_type.preferred_reading.reading_text.gsub(/\{[0-9]\}/,'').strip.split(/\s/)
91
+ when @to && !@to_role # @to is an objectified fact type so @to_role is a phantom
92
+ [@to.name]
93
+ when !@to_role # Self-value role of an independent ValueType
94
+ ["#{@from.name}Value"]
95
+ when @to_role.role_name # Named role
96
+ [@to_role.role_name]
97
+ else # Use the name from the preferred reading
98
+ role_ref = @to_role.preferred_reference
99
+ [role_ref.leading_adjective, @to_role.concept.name, role_ref.trailing_adjective].compact.map{|w| w.split(/\s/)}.flatten.reject{|s| s == ''}
100
+ end
101
+ end
102
+
103
+ # For a one-to-one (or a subtyping fact type), reverse the direction:
104
+ def flip
105
+ raise "Illegal flip of #{self}" unless @to and [:one_one, :subtype, :supertype].include?(role_type)
106
+
107
+ detabulate
108
+
109
+ if @to.absorbed_via == self
110
+ @to.absorbed_via = nil
111
+ @from.absorbed_via = self
112
+ end
113
+
114
+ # Flip the reference
115
+ @to, @from = @from, @to
116
+ @to_role, @from_role = @from_role, @to_role
117
+
118
+ tabulate
119
+ end
120
+
121
+ def tabulate
122
+ # Add to @to and @from's reference lists
123
+ @from.references_from << self
124
+ @to.references_to << self if @to # Guard against self-values
125
+
126
+ debug :references, "Adding #{to_s}"
127
+ self
128
+ end
129
+
130
+ def detabulate
131
+ # Remove from @to and @from's reference lists if present
132
+ return unless @from.references_from.delete(self)
133
+ @to.references_to.delete self if @to # Guard against self-values
134
+ debug :references, "Dropping #{to_s}"
135
+ self
136
+ end
137
+
138
+ def to_s
139
+ "reference from #{@from.name}#{@to ? " to #{@to.name}" : ""}" + (@fact_type ? " in '#{@fact_type.default_reading}'" : "")
140
+ end
141
+
142
+ def reading
143
+ is_self_value ? "#{from.name} has value" : @fact_type.default_reading
144
+ end
145
+
146
+ def inspect; to_s; end
147
+ end
148
+
149
+ class Concept
150
+ # Say whether the independence of this object is still under consideration
151
+ # This is used in detecting dependency cycles, such as occurs in the Metamodel
152
+ attr_accessor :tentative
153
+ attr_writer :is_table # The two Concept subclasses provide the reader
154
+
155
+ def show_tabular
156
+ (tentative ? "tentatively " : "") +
157
+ (is_table ? "" : "not ")+"a table"
158
+ end
159
+
160
+ def definitely_table
161
+ @is_table = true
162
+ @tentative = false
163
+ end
164
+
165
+ def definitely_not_table
166
+ @is_table = false
167
+ @tentative = false
168
+ end
169
+
170
+ def probably_table
171
+ @is_table = true
172
+ @tentative = true
173
+ end
174
+
175
+ def probably_not_table
176
+ @is_table = false
177
+ @tentative = true
178
+ end
179
+
180
+ def references_from
181
+ @references_from ||= []
182
+ end
183
+
184
+ def references_to
185
+ @references_to ||= []
186
+ end
187
+
188
+ def has_references
189
+ @references_from || @references_to
190
+ end
191
+
192
+ def clear_references
193
+ # Clear any previous references:
194
+ @references_to = nil
195
+ @references_from = nil
196
+ end
197
+
198
+ def populate_references
199
+ all_role.each do |role|
200
+ populate_reference role
201
+ end
202
+ end
203
+
204
+ def populate_reference role
205
+ role_type = role.role_type
206
+ debug :references, "#{name} has #{role_type} role in '#{role.fact_type.describe}'"
207
+ case role_type
208
+ when :many_one
209
+ Reference.new(self, role).tabulate # A simple reference
210
+
211
+ when :one_many
212
+ if role.fact_type.entity_type == self # A Role of this objectified FactType
213
+ Reference.new(self, role).tabulate # A simple reference; check that
214
+ else
215
+ # Can't absorb many of these into one of those
216
+ #debug :references, "Ignoring #{role_type} reference from #{name} to #{Reference.new(self, role).to.name}"
217
+ end
218
+
219
+ when :unary
220
+ Reference.new(self, role).tabulate # A simple reference
221
+
222
+ when :supertype # A subtype absorbs a reference to its supertype when separate, or all when partitioned
223
+ # REVISIT: Or when partitioned
224
+ if role.fact_type.subtype.is_independent
225
+ debug :references, "supertype #{name} doesn't absorb a reference to separate subtype #{role.fact_type.subtype.name}"
226
+ else
227
+ r = Reference.new(self, role)
228
+ r.to.absorbed_via = r
229
+ debug :references, "supertype #{name} absorbs subtype #{r.to.name}"
230
+ r.tabulate
231
+ end
232
+
233
+ when :subtype # This object is a supertype, which can absorb the subtype unless that's independent
234
+ if is_independent # REVISIT: Or when partitioned
235
+ Reference.new(self, role).tabulate
236
+ # If partitioned, the supertype is absorbed into *each* subtype; a reference to the supertype needs to know which
237
+ else
238
+ # debug :references, "subtype #{name} is absorbed into #{role.fact_type.supertype.name}"
239
+ end
240
+
241
+ when :one_one
242
+ r = Reference.new(self, role)
243
+
244
+ # Decide which way the one-to-one is likely to go; it will be flipped later if necessary.
245
+ # Force the decision if just one is independent:
246
+ r.tabulate and return if is_independent and !r.to.is_independent
247
+ return if !is_independent and r.to.is_independent
248
+
249
+ if is_a?(ValueType)
250
+ # Never absorb an entity type into a value type
251
+ return if r.to.is_a?(EntityType) # Don't tabulate it
252
+ else
253
+ if r.to.is_a?(ValueType)
254
+ r.tabulate # Always absorb a value type into an entity type
255
+ return
256
+ end
257
+
258
+ # Force the decision if one EntityType identifies another:
259
+ if preferred_identifier.role_sequence.all_role_ref.detect{|rr| rr.role == r.to_role}
260
+ debug :references, "EntityType #{name} is identified by EntityType #{r.to.name}, so gets absorbed elsewhere"
261
+ return
262
+ end
263
+ if r.to.preferred_identifier.role_sequence.all_role_ref.detect{|rr| rr.role == role}
264
+ debug :references, "EntityType #{name} identifies EntityType #{r.to.name}, so absorbs it"
265
+ r.to.absorbed_via = r
266
+ r.tabulate
267
+ return
268
+ end
269
+ end
270
+
271
+ # Either both EntityTypes, or both ValueTypes.
272
+ # Make an arbitrary (but stable) decision which way to go. We might flip it later.
273
+ unless r.from.name < r.to.name or
274
+ (r.from == r.to && references_to.detect{|ref| ref.to_role == role}) # one-to-one self reference, done already
275
+ r.tabulate
276
+ end
277
+ else
278
+ raise "Illegal role type, #{role.fact_type.describe(role)} no uniqueness constraint"
279
+ end
280
+ end
281
+ end
282
+
283
+ class EntityType
284
+ def populate_references
285
+ if fact_type && fact_type.all_role.size > 1
286
+ # NOT: fact_type.all_role.each do |role| # Place roles in the preferred order instead:
287
+ fact_type.preferred_reading.role_sequence.all_role_ref.map(&:role).each do |role|
288
+ populate_reference role # Objectified fact role, handled specially
289
+ end
290
+ end
291
+ super
292
+ end
293
+ end
294
+
295
+ class Vocabulary
296
+ def populate_all_references
297
+ debug :references, "Populating all concept references" do
298
+ all_feature.each do |feature|
299
+ next unless feature.is_a? Concept
300
+ feature.clear_references
301
+ feature.is_table = nil # Undecided; force an attempt to decide
302
+ feature.tentative = true # Uncertain
303
+ end
304
+ all_feature.each do |feature|
305
+ next unless feature.is_a? Concept
306
+ debug :references, "Populating references for #{feature.name}" do
307
+ feature.populate_references
308
+ end
309
+ end
310
+ end
311
+ debug :references, "Finished concept references" do
312
+ all_feature.each do |feature|
313
+ next unless feature.is_a? Concept
314
+ next unless feature.references_from.size > 0
315
+ debug :references, "#{feature.name}:" do
316
+ feature.references_from.each do |ref|
317
+ debug :references, "#{ref}"
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
324
+
325
+ end
326
+ end