activefacts-rmap 1.7.1

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,187 @@
1
+ #
2
+ # ActiveFacts Relational mapping
3
+ # A ForeignKey exists for every Reference from a ObjectType to another ObjectType that's a table.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ module ActiveFacts
8
+ module RMap
9
+ class ForeignKey
10
+ # What table (ObjectType) is the FK from?
11
+ def from; @from; end
12
+
13
+ # What table (ObjectType) is the FK to?
14
+ def to; @to; end
15
+
16
+ # What reference created the FK?
17
+ def references; @references; end
18
+
19
+ # What columns in the *from* table form the FK
20
+ def from_columns; @from_columns; end
21
+
22
+ # What columns in the *to* table form the identifier
23
+ def to_columns; @to_columns; end
24
+
25
+ def initialize(from, to, references, from_columns, to_columns) #:nodoc:
26
+ @from, @to, @references, @from_columns, @to_columns =
27
+ from, to, references, from_columns, to_columns
28
+ end
29
+
30
+ def describe
31
+ "foreign key from #{from.name}(#{from_columns.map{|c| c.name}*', '}) to #{to.name}(#{to_columns.map{|c| c.name}*', '})"
32
+ end
33
+
34
+ def verbalised_path reverse = false
35
+ # REVISIT: This should be a proper join path verbalisation:
36
+ refs = reverse ? references.reverse : references
37
+ refs.map do |r|
38
+ r.verbalised_path reverse
39
+ end * ' and '
40
+ end
41
+
42
+ # Which references are absorbed into the "from" table?
43
+ def precursor_references
44
+ fk_jump = @references.detect(&:fk_jump)
45
+ jump_index = @references.index(fk_jump)
46
+ @references[0, jump_index]
47
+ end
48
+
49
+ # Which references are absorbed into the "to" table?
50
+ def following_references
51
+ fk_jump = @references.detect(&:fk_jump)
52
+ jump_index = @references.index(fk_jump)
53
+ fk_jump != @references.last ? @references[jump_index+1..-1] : []
54
+ end
55
+
56
+ def jump_reference
57
+ @references.detect(&:fk_jump)
58
+ end
59
+
60
+ def to_name
61
+ p = precursor_references
62
+ f = following_references
63
+ j = jump_reference
64
+
65
+ @references.last.to_names +
66
+ (p.empty? && f.empty? ? [] : ['via'] + p.map{|r| r.to_names}.flatten + f.map{|r| r.from_names}.flatten)
67
+ end
68
+
69
+ # The from_name is the role name of the table with the FK, viewed from the other end
70
+ # When there are no precursor_references or following_references, it's the jump_reference.from_names
71
+ # REVISIT: I'm still working out what to do with precursor_references and following_references
72
+ def from_name
73
+ p = precursor_references
74
+ f = following_references
75
+ j = jump_reference
76
+
77
+ # pluralise unless j.is_one_to_one
78
+
79
+ # REVISIT: references[0].from_names is where the FK lives; but the object of interest may be an absorbed subclass which we should use here instead:
80
+ # REVISIT: Should crunch superclasses in subtype traversals
81
+ # REVISIT: Need to add "_as_rolename" where rolename is not to.name
82
+
83
+ [
84
+ @references[0].from_names,
85
+ (p.empty? && f.empty? ? [] : ['via'] + p.map{|r| r.to_names}.flatten + f.map{|r| r.from_names}.flatten)
86
+ ]
87
+ end
88
+
89
+ end
90
+ end
91
+
92
+ module Metamodel #:nodoc:
93
+ class ObjectType
94
+ # When an EntityType is fully absorbed, its foreign keys are too.
95
+ # Return an Array of Reference paths for such absorbed FKs
96
+ def all_absorbed_foreign_key_reference_path
97
+ references_from.inject([]) do |array, ref|
98
+ if ref.is_simple_reference
99
+ if TypeInheritance === ref.fact_type
100
+ # Ignore references to secondary supertypes, when absorption is through primary.
101
+ next array if absorbed_via && TypeInheritance === absorbed_via.fact_type
102
+ # Ignore the case where a subtype is absorbed elsewhere:
103
+ # REVISIT: Disabled, as this should never happen.
104
+ # next array if ref.to.absorbed_via != ref.fact_type
105
+ end
106
+ ref.fk_jump = true
107
+ array << [ref]
108
+ elsif ref.is_absorbing or (ref.to && !ref.to.is_table)
109
+ trace :fk, "getting fks absorbed into #{name} via #{ref}" do
110
+ ref.to.all_absorbed_foreign_key_reference_path.each do |aref|
111
+ array << aref.insert(0, ref)
112
+ end
113
+ end
114
+ end
115
+ array
116
+ end
117
+ end
118
+
119
+ def foreign_keys_to
120
+ @foreign_keys_to ||= []
121
+ end
122
+
123
+ # Return an array of all the foreign keys from this table
124
+ def foreign_keys
125
+
126
+ # Get the ForeignKey object for each absorbed reference path
127
+ @foreign_keys ||=
128
+ begin
129
+ fk_ref_paths = all_absorbed_foreign_key_reference_path
130
+ fk_ref_paths.map do |fk_ref_path|
131
+ trace :fk, "\nFK: " + fk_ref_path.map{|fk_ref| fk_ref.reading }*" and " do
132
+
133
+ from_columns = (columns||all_columns({})).select{|column|
134
+ column.references[0...fk_ref_path.size] == fk_ref_path
135
+ }
136
+ trace :fk, "from_columns = #{from_columns.map { |column| column.name }*", "}"
137
+
138
+ # Figure out absorption on the target end:
139
+ to = fk_ref_path.last.to
140
+ if to.absorbed_via
141
+ trace :fk, "Reference target #{fk_ref_path.last.to.name} is absorbed via:" do
142
+ while (r = to.absorbed_via)
143
+ m = r.reversed
144
+ trace :fk, "#{m.reading}"
145
+ fk_ref_path << m
146
+ to = m.from == to ? m.to : m.from
147
+ end
148
+ trace :fk, "Absorption ends at #{to.name}"
149
+ end
150
+ end
151
+
152
+ # REVISIT: This test may no longer be necessary
153
+ raise "REVISIT: #{fk_ref_path.inspect} is bad" unless to and to.columns
154
+
155
+ # REVISIT: This fails for absorbed subtypes having their own identification.
156
+ # Check the CompanyDirectorEmployee model for example, EmployeeManagerNr -> Person (should reference EmployeeNr)
157
+ # Need to use the absorbed identifier_columns of the subtype,
158
+ # not the columns of the supertype that absorbs it.
159
+ # But in general, that isn't going to work because in most DBMS
160
+ # there's no suitable uniquen index on the subtype's identifier_columns
161
+
162
+ to_columns = fk_ref_path[-1].to.identifier_columns
163
+
164
+ # Put the column pairs in the correct order. They MUST be in the order they appear in the primary key
165
+ froms, tos = from_columns.zip(to_columns).sort_by { |pair|
166
+ to_columns.index(pair[1])
167
+ }.transpose
168
+
169
+ fk = ActiveFacts::RMap::ForeignKey.new(self, to, fk_ref_path, froms, tos)
170
+ to.foreign_keys_to << fk
171
+ fk
172
+ end
173
+ end.
174
+ sort_by do |fk|
175
+ # Put the foreign keys in a defined order:
176
+ # debugger if !fk.to_columns || fk.to_columns.include?(nil) || !fk.from_columns || fk.from_columns.include?(nil)
177
+ [ fk.to.name,
178
+ fk.to_columns.map{|col| col.name(nil).sort},
179
+ fk.from_columns.map{|col| col.name(nil).sort}
180
+ ]
181
+ end
182
+ end
183
+
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,237 @@
1
+ #
2
+ # ActiveFacts Relational mapping
3
+ # An Index on a ObjectType is used to represent a unique constraint across roles absorbed
4
+ # into that object_type's table.
5
+ #
6
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
7
+ #
8
+
9
+ module ActiveFacts
10
+ module RMap
11
+ class Index
12
+ # The UniquenessConstraint that created this index
13
+ def uniqueness_constraint; @uniqueness_constraint; end
14
+
15
+ # The table that the index is on
16
+ def on; @on; end
17
+
18
+ # If a non-mandatory reference was absorbed, only the non-nil instances are unique.
19
+ # Return the ObjectType that was absorbed, which might differ from this Index's table.
20
+ def over; @over; end
21
+
22
+ # Return the array of columns in this index
23
+ def columns; @columns; end
24
+
25
+ # Is this index the primary key for this table?
26
+ def is_primary; @is_primary; end
27
+
28
+ # Is this index unique?
29
+ def is_unique; @is_unique; end
30
+
31
+ # An Index arises from a uniqueness constraint and applies to a table,
32
+ # but because the UC may actually be over an object absorbed into the table,
33
+ # we must record that object also.
34
+ # We record the columns it's over, whether it's primary (for 'over'),
35
+ # and whether it's unique (always, at present)
36
+ def initialize(uc, on, over, columns, is_primary, is_unique = true) #:nodoc:
37
+ @uniqueness_constraint, @on, @over, @columns, @is_primary, @is_unique =
38
+ uc, on, over, columns, is_primary, is_unique
39
+ end
40
+
41
+ # The name that was assigned (perhaps implicitly by NORMA)
42
+ def real_name
43
+ @uniqueness_constraint.name && @uniqueness_constraint.name != '' ? @uniqueness_constraint.name.gsub(' ','') : nil
44
+ end
45
+
46
+ # This name is either the name explicitly assigned (if any) or is constructed to form a unique index name.
47
+ def name
48
+ uc = @uniqueness_constraint
49
+ r = real_name
50
+ return r if r && r !~ /^(Ex|In)ternalUniquenessConstraint[0-9]+$/
51
+ (uc.is_preferred_identifier ? "PK_" : "IX_") +
52
+ view_name +
53
+ (uc.is_preferred_identifier ? "" : "By"+column_names*"")
54
+ end
55
+
56
+ # An array of the names of the columns this index covers
57
+ def column_names(separator = "")
58
+ columns.map{|column| column.name(separator)}
59
+ end
60
+
61
+ # An array of the names of the columns this index covers, with some lexical truncations.
62
+ def abbreviated_column_names(separator = "")
63
+ columns.map{|column| column.name(separator).sub(/^#{over.name}/,'')}
64
+ end
65
+
66
+ # The name of a view that can be created to enforce uniqueness over non-null key values
67
+ def view_name
68
+ "#{over.name.gsub(' ','')}#{on == over ? "" : "In"+on.name.gsub(' ','')}"
69
+ end
70
+
71
+ def to_s #:nodoc:
72
+ if @uniqueness_constraint
73
+ name = @uniqueness_constraint.name
74
+ preferred = @uniqueness_constraint.is_preferred_identifier ? " (preferred)" : ""
75
+ else
76
+ name = "#{@on.name}IsUnique"
77
+ preferred = !@on.injected_surrogate_role ? " (preferred)" : ""
78
+ end
79
+ colnames = @columns.map(&:name)*", "
80
+ "Index #{name} on #{@on.name} over #{@over.name}(#{colnames})#{preferred}"
81
+ end
82
+ end
83
+ end
84
+
85
+ module Metamodel #:nodoc:
86
+ class EntityType
87
+ def self_index
88
+ nil
89
+ end
90
+ end
91
+
92
+ class ValueType
93
+ def self_index
94
+ ActiveFacts::RMap::Index.new(
95
+ nil, # The implied uniqueness constraint is not created
96
+ self, # ValueType being indexed
97
+ self, # Absorbed object being indexed
98
+ columns.select{|c| c.references[0].is_self_value},
99
+ injected_surrogate_role ? false : true
100
+ )
101
+ end
102
+ end
103
+
104
+ class ObjectType
105
+ # An array of each Index for this table
106
+ def indices
107
+ @indices || populate_indices
108
+ end
109
+
110
+ def clear_indices #:nodoc:
111
+ # Clear any previous indices
112
+ @indices = nil
113
+ end
114
+
115
+ def populate_indices #:nodoc:
116
+ # The absorption path of a column indicates how it came to be in this table.
117
+ # It might be a direct many:one valuetype relationship, or it might be in such
118
+ # a relationship to an entity that was absorbed into this table (and so on).
119
+ # The reference path is the set of absorption references and one past it.
120
+ # Stopping here means we don't dig into the definitions of FK column counterparts.
121
+ # Note that many columns of an object may have the same ref_path.
122
+ #
123
+ # REVISIT:
124
+ # Note also that this produces columns ordered for each refpath the same as the
125
+ # order of the columns, not the same as the columns in the PK for which they might be an FK.
126
+ all_column_by_ref_path =
127
+ trace :index2, "Indexing columns by ref_path" do
128
+ columns.inject({}) do |hash, column|
129
+ trace :index2, "References in column #{name}.#{column.name}" do
130
+ ref_path = column.absorption_references
131
+ raise "No absorption_references for #{column.name} from #{column.references.map(&:to_s)*" and "}" if !ref_path || ref_path.empty?
132
+ (hash[ref_path] ||= []) << column
133
+ trace :index2, "#{column.name} involves #{ref_path.map(&:to_s)*" and "}"
134
+ end
135
+ hash
136
+ end
137
+ end
138
+
139
+ columns_by_unique_constraint = {}
140
+ all_column_by_role_ref =
141
+ all_column_by_ref_path.
142
+ keys. # Go through all refpaths and find uniqueness constraints
143
+ inject({}) do |hash, ref_path|
144
+ ref_path.each do |ref|
145
+ next unless ref.to_role
146
+ # trace :index2, "Considering #{ref_path.map(&:to_s)*" and "} yielding columns #{all_column_by_ref_path[ref_path].map{|c| c.name('.')}*", "}"
147
+ ref.to_role.all_role_ref.each do |role_ref|
148
+ all_pcs = role_ref.role_sequence.all_presence_constraint
149
+ # puts "pcs over #{ref_path.map{|r| r.to_names}.flatten*'.'}: #{role_ref.role_sequence.all_presence_constraint.map(&:describe)*"; "}" if all_pcs.size > 0
150
+ pcs = all_pcs.
151
+ reject do |pc|
152
+ !pc.max_frequency or # No maximum freq; cannot be a uniqueness constraint
153
+ pc.max_frequency != 1 or # maximum is not 1
154
+ # Constraint is not over a unary fact type role (NORMA does this)
155
+ pc.role_sequence.all_role_ref.size == 1 && ref_path[-1].to_role.fact_type.all_role.size == 1
156
+ end
157
+ next unless pcs.size > 0
158
+ # The columns for this ref_path support the UCs in "pcs".
159
+ pcs.each do |pc|
160
+ ref_columns = all_column_by_ref_path[ref_path]
161
+ ordinal = role_ref.ordinal # Position in priority order
162
+ ref_columns.each_with_index do |column, index|
163
+ #puts "Adding index column #{column.name} in rank[#{ordinal},#{index}]"
164
+ # REVISIT: the "index" here might be a duplicate in some cases: change sort_by below to just sort and run the SeparateSubtypes CQL model for example.
165
+ (columns_by_unique_constraint[pc] ||= []) << [ordinal, index, column]
166
+ end
167
+ end
168
+ hash[role_ref] = all_column_by_ref_path[ref_path]
169
+ end
170
+ end
171
+ hash
172
+ end
173
+
174
+ trace :index, "All Indices in #{name}:" do
175
+ @indices = columns_by_unique_constraint.map do |uc, columns_with_ordinal|
176
+ trace :index, "Index due to uc #{uc.concept.guid} on #{name} over (#{columns_with_ordinal.sort_by{|onc|onc[0]}.map{|ca| ca[2].name}.inspect})"
177
+ columns = columns_with_ordinal.sort_by{|ca| [ca[0,2], ca[2].name]}.map{|ca| ca[2]}
178
+ absorption_level = columns.map(&:absorption_level).min
179
+ over = columns[0].references[absorption_level].from
180
+
181
+ # Absorption through a one-to-one forms a UC that we don't need to enforce using an index:
182
+ if over != self and
183
+ over.absorbed_via == columns[0].references[absorption_level-1] and
184
+ (rr = uc.role_sequence.all_role_ref.single) and
185
+ over.absorbed_via.fact_type.all_role.include?(rr.role)
186
+ next nil
187
+ end
188
+
189
+ index = ActiveFacts::RMap::Index.new(
190
+ uc,
191
+ self,
192
+ over,
193
+ columns,
194
+ uc.is_preferred_identifier
195
+ )
196
+ trace :index, index
197
+ index
198
+ end.
199
+ compact.
200
+ sort_by do |index|
201
+ # Put the indices in a defined order:
202
+ index.columns.map(&:name)+['', index.over.name]
203
+ end
204
+ end
205
+ si = self_index
206
+ @indices.unshift(si) if si
207
+ @indices
208
+ end
209
+
210
+ end
211
+
212
+ class Vocabulary
213
+ def populate_all_indices #:nodoc:
214
+ trace :index, "Populating all object_type indices" do
215
+ all_object_type.each do |object_type|
216
+ object_type.clear_indices
217
+ end
218
+ tables.each do |object_type|
219
+ trace :index, "Populating indices for #{object_type.name}" do
220
+ object_type.populate_indices
221
+ end
222
+ end
223
+ end
224
+ trace :index, "Finished object_type indices" do
225
+ tables.each do |object_type|
226
+ trace :index?, "#{object_type.name}:" do
227
+ object_type.indices.each do |index|
228
+ trace :index, index
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
235
+
236
+ end
237
+ end
@@ -0,0 +1,198 @@
1
+ require 'activefacts/support'
2
+
3
+ module ActiveFacts
4
+ module API
5
+ module ObjectType
6
+ def table
7
+ @is_table = true
8
+ end
9
+
10
+ def is_table
11
+ @is_table
12
+ end
13
+
14
+ def columns
15
+ raise "This method is no longer in use"
16
+ =begin
17
+ return @columns if @columns
18
+ trace :rmap, "Calculating columns for #{basename}" do
19
+ @columns = (
20
+ if superclass.is_entity_type
21
+ # REVISIT: Need keys to secondary supertypes as well, but no duplicates.
22
+ trace :rmap, "Separate subtype has a foreign key to its supertype" do
23
+ superclass.__absorb([[superclass.basename]], self)
24
+ end
25
+ else
26
+ []
27
+ end +
28
+ # Then absorb all normal roles:
29
+ roles.values.select do |role|
30
+ role.unique && !role.counterpart_unary_has_precedence
31
+ end.inject([]) do |columns, role|
32
+ rn = role.name.to_s.split(/_/)
33
+ trace :rmap, "Role #{rn*'.'}" do
34
+ columns += role.counterpart_object_type.__absorb([rn], role.counterpart)
35
+ end
36
+ end +
37
+ # And finally all absorbed subtypes:
38
+ subtypes.
39
+ select{|subtype| !subtype.is_table}. # Don't absorb separate subtypes
40
+ inject([]) do |columns, subtype|
41
+ # Pass self as 2nd param here, not a role, standing for the supertype role
42
+ subtype_name = subtype.basename
43
+ trace :rmap, "Absorbing subtype #{subtype_name}" do
44
+ columns += subtype.__absorb([[subtype_name]], self)
45
+ end
46
+ end
47
+ ).map do |col_names|
48
+ last = nil
49
+ col_names.flatten.map do |name|
50
+ name.downcase.sub(/^[a-z]/){|c| c.upcase}
51
+ end.
52
+ reject do |n|
53
+ # Remove sequential duplicates:
54
+ dup = last == n
55
+ last = n
56
+ dup
57
+ end*"."
58
+ end
59
+ end
60
+ =end
61
+ end
62
+
63
+ # Return an array of the absorbed columns, using prefix for name truncation
64
+ def __absorb(prefix, except_role = nil)
65
+ # also considered a table if the superclass isn't excluded and is (transitively) a table
66
+ if !@is_table && (except_role == superclass || !is_table_subtype)
67
+ if is_entity_type
68
+ if (role = fully_absorbed) && role != except_role
69
+ # If this non-table is fully absorbed into another table (not our caller!)
70
+ # (another table plays its single identifying role), then absorb that role only.
71
+ # counterpart_object_type = role.counterpart_object_type
72
+ # This omission matches the one in columns.rb, see EntityType#reference_columns
73
+ # new_prefix = prefix + [role.name.to_s.split(/_/)]
74
+ trace :rmap, "Reference to #{role.name} (absorbed elsewhere)" do
75
+ role.counterpart_object_type.__absorb(prefix, role.counterpart)
76
+ end
77
+ else
78
+ # Not a table -> all roles are absorbed
79
+ roles.
80
+ values.
81
+ select do |role|
82
+ role.unique && role != except_role && !role.counterpart_unary_has_precedence
83
+ end.
84
+ inject([]) do |columns, role|
85
+ columns += __absorb_role(prefix, role)
86
+ end +
87
+ subtypes. # Absorb subtype roles too!
88
+ select{|subtype| !subtype.is_table}. # Don't absorb separate subtypes
89
+ inject([]) do |columns, subtype|
90
+ # Pass self as 2nd param here, not a role, standing for the supertype role
91
+ new_prefix = prefix[0..-2] + [[subtype.basename]]
92
+ trace :rmap, "Absorbed subtype #{subtype.basename}" do
93
+ columns += subtype.__absorb(new_prefix, self)
94
+ end
95
+ end
96
+ end
97
+ else
98
+ [prefix]
99
+ end
100
+ else
101
+ # Create a foreign key to the table
102
+ if is_entity_type
103
+ ir = identifying_role_names.map{|role_name| roles(role_name) }
104
+ trace :rmap, "Reference to #{basename} with #{prefix.inspect}" do
105
+ ic = identifying_role_names.map{|role_name| role_name.to_s.split(/_/)}
106
+ ir.inject([]) do |columns, role|
107
+ columns += __absorb_role(prefix, role)
108
+ end
109
+ end
110
+ else
111
+ # Reference to value type which is a table
112
+ col = prefix.clone
113
+ trace :rmap, "Self-value #{col[-1]}.Value"
114
+ col[-1] += ["Value"]
115
+ col
116
+ end
117
+ end
118
+ end
119
+
120
+ def __absorb_role(prefix, role)
121
+ if prefix.size > 0 and
122
+ (c = role.owner).is_entity_type and
123
+ c.identifying_roles == [role] and
124
+ (irn = c.identifying_role_names).size == 1 and
125
+ (n = irn[0].to_s.split(/_/)).size > 1 and
126
+ (owner = role.owner.basename.snakecase.split(/_/)) and
127
+ n[0...owner.size] == owner
128
+ trace :rmap, "truncating transitive identifying role #{n.inspect}"
129
+ owner.size.times { n.shift }
130
+ new_prefix = prefix + [n]
131
+ elsif (c = role.counterpart_object_type).is_entity_type and
132
+ (irn = c.identifying_role_names).size == 1 and
133
+ #irn[0].to_s.split(/_/)[0] == role.owner.basename.downcase
134
+ irn[0] == role.counterpart.name
135
+ #trace :rmap, "=== #{irn[0].to_s.split(/_/)[0]} elided ==="
136
+ new_prefix = prefix
137
+ elsif (fa_role = fully_absorbed) && fa_role == role
138
+ new_prefix = prefix
139
+ else
140
+ new_prefix = prefix + [role.name.to_s.split(/_/)]
141
+ end
142
+ #trace :rmap, "new_prefix is #{new_prefix*"."}"
143
+
144
+ trace :rmap, "Absorbing role #{role.name} as #{new_prefix[prefix.size..-1]*"."}" do
145
+ role.counterpart_object_type.__absorb(new_prefix, role.counterpart)
146
+ end
147
+ end
148
+
149
+ def is_table_subtype
150
+ return true if is_table
151
+ klass = superclass
152
+ while klass.is_entity_type
153
+ return true if klass.is_table
154
+ klass = klass.superclass
155
+ end
156
+ return false
157
+ end
158
+ end
159
+
160
+ module Entity
161
+ module ClassMethods
162
+ def fully_absorbed
163
+ return false unless (ir = identifying_role_names) && ir.size == 1
164
+ role = roles(ir[0])
165
+ return role if ((cp = role.counterpart_object_type).is_table ||
166
+ (cp.is_entity_type && cp.fully_absorbed))
167
+ return superclass if superclass.is_entity_type # Absorbed subtype
168
+ nil
169
+ end
170
+ end
171
+ end
172
+
173
+ # A one-to-one can be absorbed into either table. We decide which by comparing
174
+ # the names, just as happens in ObjectType.populate_reference (see reference.rb)
175
+ class Role
176
+ def counterpart_unary_has_precedence
177
+ counterpart_object_type.is_table_subtype and
178
+ counterpart.unique and
179
+ owner.name.downcase < counterpart.owner.name.downcase
180
+ end
181
+ end
182
+
183
+ end
184
+ end
185
+
186
+ class TrueClass
187
+ def self.__absorb(prefix, except_role = nil)
188
+ [prefix]
189
+ end
190
+
191
+ def self.is_table
192
+ false
193
+ end
194
+
195
+ def self.is_table_subtype
196
+ false
197
+ end
198
+ end