activefacts 0.7.2 → 0.7.3

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