activefacts 0.7.2 → 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/Manifest.txt +1 -0
  2. data/Rakefile +3 -0
  3. data/bin/afgen +9 -3
  4. data/bin/cql +0 -0
  5. data/examples/CQL/Address.cql +7 -7
  6. data/examples/CQL/Blog.cql +8 -8
  7. data/examples/CQL/CompanyDirectorEmployee.cql +3 -3
  8. data/examples/CQL/Death.cql +2 -2
  9. data/examples/CQL/Genealogy.cql +21 -21
  10. data/examples/CQL/Marriage.cql +1 -1
  11. data/examples/CQL/Metamodel.cql +34 -29
  12. data/examples/CQL/MultiInheritance.cql +3 -3
  13. data/examples/CQL/OilSupply.cql +9 -9
  14. data/examples/CQL/Orienteering.cql +27 -27
  15. data/examples/CQL/PersonPlaysGame.cql +2 -2
  16. data/examples/CQL/SchoolActivities.cql +3 -3
  17. data/examples/CQL/SimplestUnary.cql +1 -1
  18. data/examples/CQL/SubtypePI.cql +4 -4
  19. data/examples/CQL/Warehousing.cql +12 -12
  20. data/examples/CQL/WindowInRoomInBldg.cql +4 -4
  21. data/lib/activefacts/api/concept.rb +3 -2
  22. data/lib/activefacts/api/constellation.rb +1 -1
  23. data/lib/activefacts/api/entity.rb +12 -1
  24. data/lib/activefacts/api/instance.rb +1 -1
  25. data/lib/activefacts/api/role.rb +1 -1
  26. data/lib/activefacts/api/standard_types.rb +9 -1
  27. data/lib/activefacts/api/support.rb +4 -0
  28. data/lib/activefacts/api/value.rb +1 -0
  29. data/lib/activefacts/api/vocabulary.rb +2 -59
  30. data/lib/activefacts/cql/DataTypes.treetop +10 -1
  31. data/lib/activefacts/cql/Expressions.treetop +1 -1
  32. data/lib/activefacts/cql/FactTypes.treetop +1 -1
  33. data/lib/activefacts/cql/Language/English.treetop +2 -2
  34. data/lib/activefacts/generate/absorption.rb +0 -2
  35. data/lib/activefacts/generate/cql.rb +6 -8
  36. data/lib/activefacts/generate/cql/html.rb +1 -1
  37. data/lib/activefacts/generate/oo.rb +60 -40
  38. data/lib/activefacts/generate/ordered.rb +30 -21
  39. data/lib/activefacts/generate/ruby.rb +38 -15
  40. data/lib/activefacts/generate/sql/mysql.rb +257 -0
  41. data/lib/activefacts/generate/sql/server.rb +0 -1
  42. data/lib/activefacts/input/cql.rb +0 -2
  43. data/lib/activefacts/persistence/columns.rb +51 -24
  44. data/lib/activefacts/persistence/concept.rb +158 -36
  45. data/lib/activefacts/persistence/reference.rb +13 -8
  46. data/lib/activefacts/support.rb +40 -2
  47. data/lib/activefacts/version.rb +1 -1
  48. data/lib/activefacts/vocabulary/extensions.rb +5 -6
  49. data/spec/absorption_spec.rb +8 -11
  50. data/spec/api/autocounter.rb +1 -1
  51. data/spec/api/constellation.rb +1 -1
  52. data/spec/api/entity_type.rb +1 -1
  53. data/spec/api/instance.rb +1 -1
  54. data/spec/api/roles.rb +1 -1
  55. data/spec/api/value_type.rb +1 -1
  56. data/spec/cql_cql_spec.rb +2 -4
  57. data/spec/cql_parse_spec.rb +2 -4
  58. data/spec/cql_ruby_spec.rb +2 -4
  59. data/spec/cql_sql_spec.rb +4 -4
  60. data/spec/cql_symbol_tables_spec.rb +1 -1
  61. data/spec/cql_unit_spec.rb +6 -6
  62. data/spec/cqldump_spec.rb +6 -6
  63. data/spec/norma_cql_spec.rb +2 -4
  64. data/spec/norma_ruby_spec.rb +2 -4
  65. data/spec/norma_sql_spec.rb +2 -4
  66. data/spec/norma_tables_spec.rb +4 -7
  67. metadata +29 -6
@@ -18,7 +18,6 @@ module ActiveFacts
18
18
  # * norma Translate datatypes from NORMA to SQL Server
19
19
  class SERVER
20
20
  private
21
- include Metamodel
22
21
  include Persistence
23
22
  ColumnNameMax = 40
24
23
 
@@ -5,8 +5,6 @@
5
5
  require 'activefacts/vocabulary'
6
6
  require 'activefacts/cql/parser'
7
7
 
8
- require 'ruby-debug'
9
-
10
8
  module ActiveFacts
11
9
  module Input #:nodoc:
12
10
  # Compile CQL to an ActiveFacts vocabulary.
@@ -20,8 +20,6 @@ module ActiveFacts
20
20
  module Persistence #:nodoc:
21
21
 
22
22
  class Column
23
- include Metamodel
24
-
25
23
  def initialize(reference = nil) #:nodoc:
26
24
  references << reference if reference
27
25
  end
@@ -58,40 +56,59 @@ module ActiveFacts
58
56
 
59
57
  # A Column name is a sequence of names (derived from the to_roles of the References)
60
58
  # joined by a joiner string (pass nil to get the original array of names)
59
+ # The names to use is derived from the to_names of each Reference,
60
+ # modified by these rules:
61
+ # * A reference after the first one which is not a TypeInheritance but where the _from_ object plays the sole role in the preferred identifier of the _to_ entity is ignored,
62
+ # * A reference (after a name has been retained) which is a TypeInheritance retains the names of the subtype,
63
+ # * If the names retained so far end in XYZ and the to_names start with XYZ, remove the duplication
64
+ # * If we have retained the name of an entity, and this reference is the sole identifying role of an entity, and the identifying object has a name that is prefixed by the name of the object it identifies, remove the prefix and use just the suffix.
61
65
  def name(joiner = "")
62
66
  last_names = []
63
67
  names = @references.
64
- reject do |ref|
65
- # Skip any object after the first which is identified by this reference
66
- ref != @references[0] and
67
- !ref.fact_type.is_a?(TypeInheritance) and
68
- ref.to and
69
- ref.to.is_a?(EntityType) and
70
- (role_ref = ref.to.preferred_identifier.role_sequence.all_role_ref.single) and
71
- role_ref.role == ref.from_role
72
- end.
73
68
  inject([]) do |a, ref|
69
+
70
+ # Skip any object after the first which is identified by this reference
71
+ if ref != @references[0] and
72
+ !ref.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) and
73
+ ref.to and
74
+ ref.to.is_a?(ActiveFacts::Metamodel::EntityType) and
75
+ (role_ref = ref.to.preferred_identifier.role_sequence.all_role_ref.single) and
76
+ role_ref.role == ref.from_role
77
+ debug :columns, "Skipping #{ref}, identifies non-initial object"
78
+ next a
79
+ end
80
+
74
81
  names = ref.to_names
75
82
 
76
83
  # When traversing type inheritances, keep the subtype name, not the supertype names as well:
77
- if a.size > 0 && ref.fact_type.is_a?(TypeInheritance)
78
- next a if ref.to != ref.fact_type.subtype # Did we already have the subtype?
84
+ if a.size > 0 && ref.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
85
+ if ref.to != ref.fact_type.subtype # Did we already have the subtype?
86
+ debug :columns, "Skipping supertype #{ref}"
87
+ next a
88
+ end
89
+ debug :columns, "Eliding supertype in #{ref}"
79
90
  last_names.size.times { a.pop } # Remove the last names added
80
91
  elsif last_names.last && last_names.last == names[0][0...last_names.last.size]
81
92
  # When Xyz is followed by XyzID, truncate that to just ID
93
+ debug :columns, "truncating repeated #{last_names.last} in #{names[0]}"
82
94
  names[0] = names[0][last_names.last.size..-1]
83
95
  elsif last_names.last == names[0]
84
96
  # Same, but where an underscore split up the words
97
+ debug :columns, "truncating repeated name in #{names.inspect}"
85
98
  names.shift
86
99
  end
87
100
 
88
- # Where the last name is like a reference mode but the preceeding name isn't the identified concept,
89
- # strip it down (so turn Driver.PartyID into Driver.ID for example):
101
+ # If the reference is to the single identifying role of the concept making the reference,
102
+ # strip the concept name from the start of the reference role
90
103
  if a.size > 0 and
91
- (et = ref.from).is_a?(EntityType) and
104
+ (et = ref.from).is_a?(ActiveFacts::Metamodel::EntityType) and
105
+ # This instead of the next 2 would apply to all identifying roles, but breaks some examples:
106
+ # (role_ref = et.preferred_identifier.role_sequence.all_role_ref.detect{|rr| rr.role == ref.to_role}) and
92
107
  (role_ref = et.preferred_identifier.role_sequence.all_role_ref.single) and
93
108
  role_ref.role == ref.to_role and
94
109
  names[0][0...et.name.size].downcase == et.name.downcase
110
+
111
+ debug :columns, "truncating transitive identifying role #{names.inspect}"
95
112
  names[0] = names[0][et.name.size..-1]
96
113
  names.shift if names[0] == ""
97
114
  end
@@ -100,7 +117,13 @@ module ActiveFacts
100
117
 
101
118
  a += names
102
119
  a
103
- end
120
+ end.elide_repeated_subsequences { |a, b|
121
+ if a.is_a?(Array)
122
+ a.map{|e| e.downcase} == b.map{|e| e.downcase}
123
+ else
124
+ a.downcase == b.downcase
125
+ end
126
+ }
104
127
 
105
128
  name_array = names.map{|n| n.sub(/^[a-z]/){|s| s.upcase}}
106
129
  joiner ? name_array * joiner : name_array
@@ -158,7 +181,7 @@ module ActiveFacts
158
181
  def columns(excluded_supertypes) #:nodoc:
159
182
  kind = ""
160
183
  cols =
161
- if is_unary
184
+ if is_unary && !(@to && @to.fact_type)
162
185
  kind = "unary "
163
186
  [Column.new()]
164
187
  elsif is_self_value
@@ -275,10 +298,14 @@ module ActiveFacts
275
298
  debug :columns, "Reference Columns for #{name}" do
276
299
 
277
300
  if absorbed_via and
278
- # If this is a subtype that has its own identification, use that.
301
+ # If this is not a subtype, or is a subtype that has its own identification, use the id.
279
302
  (all_type_inheritance_as_subtype.size == 0 ||
280
303
  all_type_inheritance_as_subtype.detect{|ti| ti.provides_identification })
281
- return absorbed_via.from.reference_columns(excluded_supertypes)
304
+ rc = absorbed_via.from.reference_columns(excluded_supertypes)
305
+ # The absorbed_via reference gets skipped here, ans also in concept.rb
306
+ debug :columns, "Skipping #{absorbed_via}"
307
+ #rc.each{|col| col.prepend(absorbed_via)}
308
+ return rc
282
309
  end
283
310
 
284
311
  # REVISIT: Should have built preferred_identifier_references
@@ -302,7 +329,7 @@ module ActiveFacts
302
329
  references_from.sort_by do |ref|
303
330
  # Put supertypes first, in order, then non-subtype references, then subtypes, otherwise retaining their order:
304
331
  sups.index(ref.to) ||
305
- (!ref.fact_type.is_a?(TypeInheritance) && references_from.size+references_from.index(ref)) ||
332
+ (!ref.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) && references_from.size+references_from.index(ref)) ||
306
333
  references_from.size*2+references_from.index(ref)
307
334
  end.each do |ref|
308
335
  debug :columns, "Columns absorbed via #{ref}" do
@@ -332,7 +359,7 @@ module ActiveFacts
332
359
  # Override this method to change the transformations
333
360
  def finish_schema
334
361
  all_feature.each do |feature|
335
- feature.self_value_reference if feature.is_a?(ValueType) && feature.is_table
362
+ feature.self_value_reference if feature.is_a?(ActiveFacts::Metamodel::ValueType) && feature.is_table
336
363
  end
337
364
  end
338
365
 
@@ -342,7 +369,7 @@ module ActiveFacts
342
369
 
343
370
  debug :columns, "Populating all columns" do
344
371
  all_feature.each do |feature|
345
- next if !feature.is_a?(Concept) || !feature.is_table
372
+ next if !feature.is_a?(ActiveFacts::Metamodel::Concept) || !feature.is_table
346
373
  debug :columns, "Populating columns for table #{feature.name}" do
347
374
  feature.populate_columns
348
375
  end
@@ -350,7 +377,7 @@ module ActiveFacts
350
377
  end
351
378
  debug :columns, "Finished columns" do
352
379
  all_feature.each do |feature|
353
- next if !feature.is_a?(Concept) || !feature.is_table
380
+ next if !feature.is_a?(ActiveFacts::Metamodel::Concept) || !feature.is_table
354
381
  debug :columns, "Finished columns for table #{feature.name}" do
355
382
  feature.columns.each do |column|
356
383
  debug :columns, "#{column}"
@@ -1,3 +1,5 @@
1
+ require 'activefacts/support'
2
+
1
3
  module ActiveFacts
2
4
  module API
3
5
  module Concept
@@ -10,57 +12,169 @@ module ActiveFacts
10
12
  end
11
13
 
12
14
  def columns
13
- #puts "Calculating columns for #{basename}"
14
15
  return @columns if @columns
15
- @columns = (
16
- roles.
17
- values.
18
- select{|role| role.unique}.
19
- inject([]) do |columns, role|
20
- rn = role.name.to_s.split(/_/)
21
- columns += role.counterpart_concept.__absorb(rn, role.counterpart)
22
- end +
23
- # REVISIT: Need to use subtypes_transitive here:
24
- subtypes.
25
- select{|subtype| !subtype.is_table}. # Don't absorb separate subtypes
26
- inject([]) { |columns, subtype|
27
- sn = [subtype.basename]
28
- columns += subtype.__absorb(sn)
29
- # puts "subtype #{subtype.name} contributed #{columns.inspect}"
30
- columns
31
- }
32
- ).map{|col_names| col_names.uniq.map{|name| name.sub(/^[a-z]/){|c| c.upcase}}*"."}
16
+ debug :persistence, "Calculating columns for #{basename}" do
17
+ @columns = (
18
+ if superclass.is_entity_type
19
+ # REVISIT: Need keys to secondary supertypes as well, but no duplicates.
20
+ debug :persistence, "Separate subtype has a foreign key to its supertype" do
21
+ superclass.__absorb([[superclass.basename]], self)
22
+ end
23
+ else
24
+ []
25
+ end +
26
+ # Then absorb all normal roles:
27
+ roles.values.select do |role|
28
+ role.unique && !role.counterpart_unary_has_precedence
29
+ end.inject([]) do |columns, role|
30
+ rn = role.name.to_s.split(/_/)
31
+ debug :persistence, "Role #{rn*'.'}" do
32
+ columns += role.counterpart_concept.__absorb([rn], role.counterpart)
33
+ end
34
+ end +
35
+ # And finally all absorbed subtypes:
36
+ subtypes.
37
+ select{|subtype| !subtype.is_table}. # Don't absorb separate subtypes
38
+ inject([]) do |columns, subtype|
39
+ # Pass self as 2nd param here, not a role, standing for the supertype role
40
+ subtype_name = subtype.basename
41
+ debug :persistence, "Absorbing subtype #{subtype_name}" do
42
+ columns += subtype.__absorb([[subtype_name]], self)
43
+ end
44
+ end
45
+ ).map do |col_names|
46
+ last = nil
47
+ col_names.flatten.map do |name|
48
+ name.downcase.sub(/^[a-z]/){|c| c.upcase}
49
+ end.
50
+ reject do |n|
51
+ # Remove sequential duplicates:
52
+ dup = last == n
53
+ last = n
54
+ dup
55
+ end*"."
56
+ end
57
+ end
33
58
  end
34
59
 
35
60
  # Return an array of the absorbed columns, using prefix for name truncation
36
61
  def __absorb(prefix, except_role = nil)
37
- is_entity = respond_to?(:identifying_role_names)
38
- absorbed_into = nil
39
- if absorbed_into
40
- # REVISIT: if this concept is fully absorbed through one of its roles into another table, we absorb that tables identifying_roles
41
- absorbed_into.__absorb(prefix)
42
- elsif !@is_table
43
- # Not a table -> all roles are absorbed
44
- if is_entity
45
- roles.
46
- values.
47
- select{|role| role.unique && role.counterpart_concept != except_role }.
48
- inject([]) do |columns, role|
49
- columns += role.counterpart_concept.__absorb(prefix + role.name.to_s.split(/_/), self)
62
+ # also considered a table if the superclass isn't excluded and is (transitively) a table
63
+ if !@is_table && (except_role == superclass || !is_table_subtype)
64
+ if is_entity_type
65
+ if (role = fully_absorbed) && role != except_role
66
+ # If this non-table is fully absorbed into another table (not our caller!)
67
+ # (another table plays its single identifying role), then absorb that role only.
68
+ # counterpart_concept = role.counterpart_concept
69
+ # This omission matches the one in columns.rb, see EntityType#reference_columns
70
+ # new_prefix = prefix + [role.name.to_s.split(/_/)]
71
+ debug :persistence, "Reference to #{role.name} (absorbed elsewhere)" do
72
+ role.counterpart_concept.__absorb(prefix, role.counterpart)
73
+ end
74
+ else
75
+ # Not a table -> all roles are absorbed
76
+ roles.
77
+ values.
78
+ select do |role|
79
+ role.unique && role != except_role && !role.counterpart_unary_has_precedence
80
+ end.
81
+ inject([]) do |columns, role|
82
+ columns += __absorb_role(prefix, role)
83
+ end +
84
+ subtypes. # Absorb subtype roles too!
85
+ select{|subtype| !subtype.is_table}. # Don't absorb separate subtypes
86
+ inject([]) do |columns, subtype|
87
+ # Pass self as 2nd param here, not a role, standing for the supertype role
88
+ new_prefix = prefix[0..-2] + [[subtype.basename]]
89
+ debug :persistence, "Absorbed subtype #{subtype.basename}" do
90
+ columns += subtype.__absorb(new_prefix, self)
91
+ end
92
+ end
50
93
  end
51
94
  else
52
95
  [prefix]
53
96
  end
54
97
  else
55
- #puts "#{@is_table ? "referencing" : "absorbing"} #{is_entity ? "entity" : "value"} #{basename} using #{prefix.inspect}"
56
- if is_entity
57
- identifying_role_names.map{|role_name| prefix+role_name.to_s.split(/_/)}
98
+ # Create a foreign key to the table
99
+ if is_entity_type
100
+ ir = identifying_role_names.map{|role_name| roles(role_name) }
101
+ debug :persistence, "Reference to #{basename} with #{prefix.inspect}" do
102
+ ic = identifying_role_names.map{|role_name| role_name.to_s.split(/_/)}
103
+ ir.inject([]) do |columns, role|
104
+ columns += __absorb_role(prefix, role)
105
+ end
106
+ end
58
107
  else
59
108
  # Reference to value type which is a table
60
- [prefix + ["Value"]]
109
+ col = prefix.clone
110
+ debug :persistence, "Self-value #{col[-1]}.Value"
111
+ col[-1] += ["Value"]
112
+ col
61
113
  end
62
114
  end
63
115
  end
116
+
117
+ def __absorb_role(prefix, role)
118
+ if prefix.size > 0 and
119
+ (c = role.owner).is_entity_type and
120
+ c.identifying_roles == [role] and
121
+ (irn = c.identifying_role_names).size == 1 and
122
+ (n = irn[0].to_s.split(/_/)).size > 1 and
123
+ (owner = role.owner.basename.snakecase.split(/_/)) and
124
+ n[0...owner.size] == owner
125
+ debug :persistence, "truncating transitive identifying role #{n.inspect}"
126
+ owner.size.times { n.shift }
127
+ new_prefix = prefix + [n]
128
+ elsif (c = role.counterpart_concept).is_entity_type and
129
+ (irn = c.identifying_role_names).size == 1 and
130
+ #irn[0].to_s.split(/_/)[0] == role.owner.basename.downcase
131
+ irn[0] == role.counterpart.name
132
+ #debug :persistence, "=== #{irn[0].to_s.split(/_/)[0]} elided ==="
133
+ new_prefix = prefix
134
+ elsif (fa_role = fully_absorbed) && fa_role == role
135
+ new_prefix = prefix
136
+ else
137
+ new_prefix = prefix + [role.name.to_s.split(/_/)]
138
+ end
139
+ #debug :persistence, "new_prefix is #{new_prefix*"."}"
140
+
141
+ debug :persistence, "Absorbing role #{role.name} as #{new_prefix[prefix.size..-1]*"."}" do
142
+ role.counterpart_concept.__absorb(new_prefix, role.counterpart)
143
+ end
144
+ end
145
+
146
+ def is_table_subtype
147
+ return true if is_table
148
+ klass = superclass
149
+ while klass.is_entity_type
150
+ return true if klass.is_table
151
+ klass = klass.superclass
152
+ end
153
+ return false
154
+ end
155
+ end
156
+
157
+ module Entity
158
+ module ClassMethods
159
+ def fully_absorbed
160
+ return false unless (ir = identifying_role_names) && ir.size == 1
161
+ role = roles(ir[0])
162
+ return role if ((cp = role.counterpart_concept).is_table ||
163
+ (cp.is_entity_type && cp.fully_absorbed))
164
+ return superclass if superclass.is_entity_type # Absorbed subtype
165
+ nil
166
+ end
167
+ end
168
+ end
169
+
170
+ # A one-to-one can be absorbed into either table. We decide which by comparing
171
+ # the names, just as happens in Concept.populate_reference (see reference.rb)
172
+ class Role
173
+ def counterpart_unary_has_precedence
174
+ counterpart_concept.is_table_subtype and
175
+ counterpart.unique and
176
+ owner.name.downcase < counterpart.owner.name.downcase
177
+ end
64
178
  end
65
179
 
66
180
  end
@@ -70,4 +184,12 @@ class TrueClass
70
184
  def self.__absorb(prefix, except_role = nil)
71
185
  [prefix]
72
186
  end
187
+
188
+ def self.is_table
189
+ false
190
+ end
191
+
192
+ def self.is_table_subtype
193
+ false
194
+ end
73
195
  end
@@ -79,7 +79,7 @@ module ActiveFacts
79
79
 
80
80
  # Is this Reference from a unary Role?
81
81
  def is_unary
82
- !@to && @to_role && @to_role.fact_type.all_role.size == 1
82
+ @to_role && @to_role.fact_type.all_role.size == 1
83
83
  end
84
84
 
85
85
  # If this Reference is to an objectified FactType, there is no *to_role*
@@ -114,16 +114,20 @@ module ActiveFacts
114
114
  def to_names
115
115
  case
116
116
  when is_unary
117
- @to_role.fact_type.preferred_reading.reading_text.gsub(/\{[0-9]\}/,'').strip.split(/[_\s]/)
117
+ if @to && @to.fact_type
118
+ @to.name.camelwords
119
+ else
120
+ @to_role.fact_type.preferred_reading.reading_text.gsub(/\{[0-9]\}/,'').strip.camelwords
121
+ end
118
122
  when @to && !@to_role # @to is an objectified fact type so @to_role is a phantom
119
- @to.name.split(/[_\s]/)
123
+ @to.name.camelwords
120
124
  when !@to_role # Self-value role of an independent ValueType
121
- ["#{@from.name}", "Value"]
125
+ @from.name.camelwords + ["Value"]
122
126
  when @to_role.role_name # Named role
123
- @to_role.role_name.split(/[_\s]/)
127
+ @to_role.role_name.camelwords
124
128
  else # Use the name from the preferred reading
125
129
  role_ref = @to_role.preferred_reference
126
- [role_ref.leading_adjective, @to_role.concept.name, role_ref.trailing_adjective].compact.map{|w| w.split(/[_\s]/)}.flatten.reject{|s| s == ''}
130
+ [role_ref.leading_adjective, @to_role.concept.name, role_ref.trailing_adjective].compact.map{|w| w.camelwords}.flatten.reject{|s| s == ''}
127
131
  end
128
132
  end
129
133
 
@@ -304,8 +308,9 @@ module ActiveFacts
304
308
  end
305
309
 
306
310
  # Either both EntityTypes, or both ValueTypes.
307
- # Make an arbitrary (but stable) decision which way to go. We might flip it later.
308
- unless r.from.name < r.to.name or
311
+ # Make an arbitrary (but stable) decision which way to go. We might flip it later,
312
+ # but not frivolously; the Ruby API column name generation duplicates this logic.
313
+ unless r.from.name.downcase < r.to.name.downcase or
309
314
  (r.from == r.to && references_to.detect{|ref| ref.to_role == role}) # one-to-one self reference, done already
310
315
  r.tabulate
311
316
  end
@@ -9,6 +9,7 @@
9
9
  $debug_indent = nil
10
10
  $debug_nested = false
11
11
  $debug_keys = nil
12
+ $debug_available = {}
12
13
  def debug(*args, &block)
13
14
  unless $debug_indent
14
15
  # First time, initialise the tracing environment
@@ -16,13 +17,19 @@
16
17
  $debug_keys = {}
17
18
  if (e = ENV["DEBUG"])
18
19
  e.split(/[^a-zA-Z0-9]/).each{|k| $debug_keys[k.to_sym] = true }
20
+ if $debug_keys[:help]
21
+ at_exit {
22
+ $stderr.puts "---\nDebugging keys available: #{$debug_available.keys.map{|s| s.to_s}.sort*", "}"
23
+ }
24
+ end
19
25
  end
20
26
  end
21
27
 
22
28
  # Figure out whether this trace is enabled and nests:
23
29
  control = (!args.empty? && Symbol === args[0]) ? args.shift : :all
24
- key = control.to_s.sub(/_\Z/, '')
25
- enabled = $debug_nested || $debug_keys[key.to_sym]
30
+ key = control.to_s.sub(/_\Z/, '').to_sym
31
+ $debug_available[key] ||= key
32
+ enabled = $debug_nested || $debug_keys[key]
26
33
  nesting = control.to_s =~ /_\Z/
27
34
  old_nested = $debug_nested
28
35
  $debug_nested = nesting
@@ -56,4 +63,35 @@ class Array
56
63
  v == 1
57
64
  end.keys
58
65
  end
66
+
67
+ # Allow indexing using a custom comparator:
68
+ def index value, &compare_block
69
+ compare_block ||= lambda{|a,b| a == b}
70
+ (0...size).detect{|i| compare_block[value, self[i]] }
71
+ end
72
+
73
+ # If any element, or sequence of elements, repeats immediately, delete the repetition.
74
+ # Note that this doesn't remove all re-occurrences of a subsequence, only consecutive ones.
75
+ # The compare_block allows a custom equality comparison.
76
+ def elide_repeated_subsequences &compare_block
77
+ compare_block ||= lambda{|a,b| a == b}
78
+ i = 0
79
+ while i < size # Need to re-evaluate size on each loop - the array shrinks.
80
+ j = i
81
+ #puts "Looking for repetitions of #{self[i]}@[#{i}]"
82
+ while tail = self[j+1..-1] and k = tail.index(self[i], &compare_block)
83
+ length = j+1+k-i
84
+ #puts "Found at #{j+1+k} (subsequence of length #{j+1+k-i}), will need to repeat to #{j+k+length}"
85
+ if j+k+1+length <= size && compare_block[self[i, length], self[j+k+1, length]]
86
+ #puts "Subsequence from #{i}..#{j+k} repeats immediately at #{j+k+1}..#{j+k+length}"
87
+ slice!(j+k+1, length)
88
+ j = i
89
+ else
90
+ j += k+1
91
+ end
92
+ end
93
+ i += 1
94
+ end
95
+ self
96
+ end
59
97
  end