activefacts-compositions 1.9.6 → 1.9.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # ActiveFacts Compositions, Metamodel aspect to build compacted column names for (leaf) Components
3
3
  #
4
- # Compresses the names arising from absorption paths into usable column names
4
+ # Compresses the names arising from absorption paths into usable column names
5
5
  #
6
6
  # Copyright (c) 2016 Clifford Heath. Read the LICENSE file.
7
7
  #
@@ -11,69 +11,69 @@ module ActiveFacts
11
11
  module Metamodel
12
12
  class Component
13
13
  def column_name
14
- column_path = path[1..-1]
15
- prev_words = []
16
- String::Words.new(
17
- column_path.
18
- inject([]) do |na, member|
19
- is_absorption = member.is_a?(Absorption)
20
- is_type_inheritance = is_absorption && member.parent_role.fact_type.is_a?(TypeInheritance)
21
- fact_type = is_absorption && member.parent_role.fact_type
14
+ column_path = path[1..-1]
15
+ prev_words = []
16
+ String::Words.new(
17
+ column_path.
18
+ inject([]) do |na, member|
19
+ is_absorption = member.is_a?(Absorption)
20
+ is_type_inheritance = is_absorption && member.parent_role.fact_type.is_a?(TypeInheritance)
21
+ fact_type = is_absorption && member.parent_role.fact_type
22
22
 
23
- # If the parent object identifies the child via this absorption, skip it.
24
- if member != column_path.first and
25
- is_absorption and
26
- !is_type_inheritance and
27
- member.parent_role.base_role.is_identifying
28
- trace :names, "Skipping #{member}, identifies non-initial object"
29
- next na
30
- end
23
+ # If the parent object identifies the child via this absorption, skip it.
24
+ if member != column_path.first and
25
+ is_absorption and
26
+ !is_type_inheritance and
27
+ member.parent_role.base_role.is_identifying
28
+ trace :names, "Skipping #{member}, identifies non-initial object"
29
+ next na
30
+ end
31
31
 
32
- words = member.name.words
32
+ words = member.name.words
33
33
 
34
- if na.size > 0 && is_type_inheritance
35
- # When traversing type inheritances, keep the subtype name, not the supertype names as well:
36
- if member.child_role != fact_type.subtype_role
37
- trace :names, "Skipping supertype #{member}"
38
- next na
39
- end
40
- trace :names, "Eliding supertype in #{member}"
41
- prev_words.size.times{na.pop}
34
+ if na.size > 0 && is_type_inheritance
35
+ # When traversing type inheritances, keep the subtype name, not the supertype names as well:
36
+ if member.child_role != fact_type.subtype_role
37
+ trace :names, "Skipping supertype #{member}"
38
+ next na
39
+ end
40
+ trace :names, "Eliding supertype in #{member}"
41
+ prev_words.size.times{na.pop}
42
42
 
43
- elsif member.parent && member != column_path.first && is_absorption && member.child_role.base_role.is_identifying
44
- # When Xyz is followed by identifying XyzID (even if we skipped the Xyz), truncate that to just ID
45
- pnames = member.parent.name.words
46
- if pnames == words[0, pnames.size]
47
- pnames.size.times do
48
- pnames.shift
49
- words.shift
50
- end
51
- end
52
- end
43
+ elsif member.parent && member != column_path.first && is_absorption && member.child_role.base_role.is_identifying
44
+ # When Xyz is followed by identifying XyzID (even if we skipped the Xyz), truncate that to just ID
45
+ pnames = member.parent.name.words
46
+ if pnames == words[0, pnames.size]
47
+ pnames.size.times do
48
+ pnames.shift
49
+ words.shift
50
+ end
51
+ end
52
+ end
53
53
 
54
- # If the reference is to the single identifying role of the object_type making the reference,
55
- # strip the object_type name from the start of the reference role
56
- if na.size > 0 and
57
- is_absorption and
58
- member.child_role.base_role.is_identifying and
59
- (et = member.object_type).is_a?(EntityType) and
60
- et.preferred_identifier.role_sequence.all_role_ref.size == 0 and
61
- et.name.downcase == words[0][0...et.name.size].downcase
62
- trace :columns, "truncating transitive identifying role #{words.inspect}"
63
- words[0] = words[0][et.name.size..-1]
64
- words.shift if words[0] == ''
65
- end
54
+ # If the reference is to the single identifying role of the object_type making the reference,
55
+ # strip the object_type name from the start of the reference role
56
+ if na.size > 0 and
57
+ is_absorption and
58
+ member.child_role.base_role.is_identifying and
59
+ (et = member.object_type).is_a?(EntityType) and
60
+ et.preferred_identifier.role_sequence.all_role_ref.size == 0 and
61
+ et.name.downcase == words[0][0...et.name.size].downcase
62
+ trace :columns, "truncating transitive identifying role #{words.inspect}"
63
+ words[0] = words[0][et.name.size..-1]
64
+ words.shift if words[0] == ''
65
+ end
66
66
 
67
- prev_words = words
68
- na += words.to_a
69
- end.elide_repeated_subsequences do |a, b|
70
- if a.is_a?(Array)
71
- a.map{|e| e.downcase} == b.map{|e| e.downcase}
72
- else
73
- a.downcase == b.downcase
74
- end
75
- end
76
- )
67
+ prev_words = words
68
+ na += words.to_a
69
+ end.elide_repeated_subsequences do |a, b|
70
+ if a.is_a?(Array)
71
+ a.map{|e| e.downcase} == b.map{|e| e.downcase}
72
+ else
73
+ a.downcase == b.downcase
74
+ end
75
+ end
76
+ )
77
77
  end
78
78
  end
79
79
  end
@@ -1,10 +1,7 @@
1
1
  #
2
2
  # ActiveFacts Compositions, Relational Compositor.
3
3
  #
4
- # Computes an Optimal Normal Form (close to 5NF) relational schema.
5
- #
6
- # Options to the constructor:
7
- # single_sequence: The database technology can only increment one sequence per table (MS-SQL)
4
+ # Computes an Optimal Normal Form (close to 5NF) relational schema.
8
5
  #
9
6
  # Copyright (c) 2015 Clifford Heath. Read the LICENSE file.
10
7
  #
@@ -13,789 +10,901 @@ require "activefacts/compositions"
13
10
  module ActiveFacts
14
11
  module Compositions
15
12
  class Relational < Compositor
16
- public
13
+ MM = ActiveFacts::Metamodel unless const_defined?(:MM)
14
+
15
+ def self.options
16
+ {
17
+ surrogates: ['Boolean', "Inject a surrogate key into each table whose primary key is not already suitable as a foreign key"]
18
+ }
19
+ end
20
+
17
21
  def initialize constellation, name, options = {}
18
- # Extract recognised options so our superclass doesn't complain:
19
- @option_surrogates = options.delete('surrogates')
20
- super constellation, name, options
22
+ # Extract recognised options:
23
+ @option_surrogates = options.delete('surrogates')
24
+ super constellation, name, options
21
25
  end
22
26
 
23
27
  def generate
24
- super
28
+ super
29
+
30
+ trace :relational_details!, "Generating relational composition" do
31
+ # Make a data structure to help in computing the tables
32
+ make_candidates
25
33
 
26
- trace :relational_details!, "Generating relational composition" do
27
- # Make a data structure to help in computing the tables
28
- make_candidates
34
+ # Apply any obvious table/non-table factors
35
+ assign_default_tabulation
29
36
 
30
- # Apply any obvious table/non-table factors
31
- assign_default_tabulation
37
+ # Figure out how best to absorb things to reduce the number of tables
38
+ optimise_absorption
32
39
 
33
- # Figure out how best to absorb things to reduce the number of tables
34
- optimise_absorption
40
+ # Actually make a Composite object for each table:
41
+ make_composites
35
42
 
36
- # Actually make a Composite object for each table:
37
- make_composites
43
+ # If a value type has been mapped to a table, add a column to hold its value
44
+ inject_value_fields
38
45
 
39
- # If a value type has been mapped to a table, add a column to hold its value
40
- inject_value_fields
46
+ # Inject surrogate keys if the options ask for that
47
+ inject_surrogates if @option_surrogates
41
48
 
42
- # Inject surrogate keys if the options ask for that
43
- inject_surrogates if @option_surrogates
49
+ # Remove the un-used absorption paths
50
+ delete_reverse_absorptions
44
51
 
45
- # Remove the un-used absorption paths
46
- delete_reverse_absorptions
52
+ # Traverse the absorbed objects to build the path to each required column, including foreign keys:
53
+ absorb_all_columns
47
54
 
48
- # Traverse the absorbed objects to build the path to each required column, including foreign keys:
49
- absorb_all_columns
55
+ devolve_all
50
56
 
51
- # Populate the target fields of foreign keys
52
- complete_foreign_keys
57
+ # Populate the target fields of foreign keys
58
+ complete_foreign_keys
53
59
 
54
- # Remove mappings for objects we have absorbed
55
- clean_unused_mappings
56
- end
60
+ # Remove mappings for objects we have absorbed
61
+ clean_unused_mappings
62
+ end
57
63
 
58
- trace :relational!, "Full relational composition" do
59
- @composition.all_composite.sort_by{|composite| composite.mapping.name}.each do |composite|
60
- composite.show_trace
61
- end
62
- end
64
+ trace :relational!, "Full #{self.class.basename} composition" do
65
+ @composition.all_composite.sort_by{|composite| composite.mapping.name}.each do |composite|
66
+ composite.show_trace
67
+ end
68
+ end
63
69
  end
64
70
 
65
71
  def make_candidates
66
- @candidates = @binary_mappings.inject({}) do |hash, (absorption, mapping)|
67
- hash[mapping.object_type] = Candidate.new(self, mapping)
68
- hash
69
- end
72
+ @candidates = @binary_mappings.inject({}) do |hash, (absorption, mapping)|
73
+ hash[mapping.object_type] = Candidate.new(self, mapping)
74
+ hash
75
+ end
70
76
  end
71
77
 
72
78
  def assign_default_tabulation
73
- trace :relational_defaults!, "Preparing relational composition by setting default assumptions" do
74
- @candidates.each do |object_type, candidate|
75
- candidate.assign_default(@composition)
76
- end
77
- end
79
+ trace :relational_defaults!, "Preparing relational composition by setting default assumptions" do
80
+ @candidates.each do |object_type, candidate|
81
+ candidate.assign_default(@composition)
82
+ end
83
+ end
78
84
  end
79
85
 
80
86
  def optimise_absorption
81
- trace :relational_optimiser!, "Optimise Relational Composition" do
82
- undecided = @candidates.keys.select{|object_type| @candidates[object_type].is_tentative}
83
- pass = 0
84
- finalised = []
85
- begin
86
- pass += 1
87
- trace :relational_optimiser, "Starting optimisation pass #{pass}" do
88
- finalised = optimise_absorption_pass(undecided)
89
- end
90
- trace :relational_optimiser, "Finalised #{finalised.size} on this pass: #{finalised.map{|f| f.name}*', '}"
91
- undecided -= finalised
92
- end while !finalised.empty?
93
- end
87
+ trace :relational_optimiser!, "Optimise Relational Composition" do
88
+ undecided = @candidates.keys.select{|object_type| @candidates[object_type].is_tentative}
89
+ pass = 0
90
+ finalised = []
91
+ begin
92
+ pass += 1
93
+ trace :relational_optimiser, "Starting optimisation pass #{pass}" do
94
+ finalised = optimise_absorption_pass(undecided)
95
+ end
96
+ trace :relational_optimiser, "Finalised #{finalised.size} on this pass: #{finalised.map{|f| f.name}*', '}"
97
+ undecided -= finalised
98
+ end while !finalised.empty?
99
+ end
94
100
  end
95
101
 
96
102
  def optimise_absorption_pass undecided
97
- undecided.select do |object_type|
98
- candidate = @candidates[object_type]
99
- trace :relational_optimiser, "Considering possible status of #{object_type.name}" do
100
-
101
- # Rule 1: Always absorb an objectified unary into its role player (unless its forced to be separate)
102
- if !object_type.is_separate && (f = object_type.fact_type) && f.all_role.size == 1
103
- absorbing_ref = candidate.mapping.all_member.detect{|a| a.is_a?(MM::Absorption) and a.child_role.base_role == f.all_role.single}
104
- raise "REVISIT: Internal error" unless absorbing_ref.parent_role.object_type == object_type
105
- absorbing_ref = absorbing_ref.flip!
106
- candidate.full_absorption =
107
- @constellation.FullAbsorption(composition: @composition, absorption: absorbing_ref, object_type: object_type)
108
- trace :relational_optimiser, "Absorb objectified unary #{object_type.name} into #{f.all_role.single.object_type.name}"
109
- candidate.definitely_not_table
110
- next object_type
111
- end
112
-
113
- # Rule 2: If the preferred_identifier contains one role only, played by an entity type that can absorb us, do that:
114
- # (Leave pi_roles intact for further use below)
115
- absorbing_ref = nil
116
- pi_roles = []
117
- if object_type.is_a?(MM::EntityType) and # We're an entity type
118
- pi_roles = object_type.preferred_identifier_roles and # Our PI
119
- pi_roles.size == 1 and # has one role
120
- single_pi_role = pi_roles[0] and # that role is
121
- single_pi_role.object_type.is_a?(MM::EntityType) and # played by another Entity Type
122
- absorbing_ref =
123
- candidate.mapping.all_member.detect do |absorption|
124
- absorption.is_a?(MM::Absorption) && absorption.child_role.base_role == single_pi_role
125
- end
126
-
127
- absorbing_ref = absorbing_ref.forward_absorption || absorbing_ref.flip!
128
- candidate.full_absorption =
129
- @constellation.FullAbsorption(composition: @composition, absorption: absorbing_ref, object_type: object_type)
130
- trace :relational_optimiser, "EntityType #{single_pi_role.object_type.name} identifies EntityType #{object_type.name}, so absorbs it"
131
- candidate.definitely_not_table
132
- next object_type
133
- end
134
-
135
- # Rule 3: If there's more than one absorption path and any functional dependencies that can't absorb us, it's a table
136
- non_identifying_refs_from =
137
- candidate.references_from.reject do |member|
138
- case member
139
- when MM::Absorption
140
- pi_roles.include?(member.child_role.base_role)
141
- when MM::Indicator
142
- pi_roles.include?(member.role)
143
- else
144
- false
145
- end
146
- end
147
- trace :relational_optimiser, "#{object_type.name} has #{non_identifying_refs_from.size} non-identifying functional roles" do
148
- non_identifying_refs_from.each do |a|
149
- trace :relational_optimiser, a.inspect
150
- end
151
- end
152
-
153
- trace :relational_optimiser, "#{object_type.name} has #{candidate.references_to.size} references to it" do
154
- candidate.references_to.each do |a|
155
- trace :relational_optimiser, a.inspect
156
- end
157
- end
158
- if candidate.references_to.size > 1 and # More than one place wants us
159
- non_identifying_refs_from.size > 0 # And we carry dependent values so cannot be absorbed
160
- trace :relational_optimiser, "#{object_type.name} has #{non_identifying_refs_from.size} non-identifying functional dependencies and #{candidate.references_to.size} absorption paths so 3NF requires it be a table"
161
- candidate.definitely_table
162
- next object_type
163
- end
164
-
165
- # At this point, this object either has no functional dependencies or only one place it would be absorbed
166
- next false if !candidate.is_table # We can't reduce the number of tables by absorbing this one
167
-
168
- absorption_paths =
169
- ( non_identifying_refs_from + # But we should exclude any that are already involved in an absorption; pre-decided ET=>ET or supertype absorption!
170
- candidate.references_to # These are our reverse absorptions that could absorb us
171
- ).select do |a|
172
- next false unless a.is_a?(MM::Absorption) # Skip Indicators, we can't be absorbed there
173
- child_candidate = @candidates[a.child_role.object_type]
174
-
175
- # It's ok if we absorbed them already
176
- next true if a.full_absorption && child_candidate.full_absorption.absorption != a
177
-
178
- # If our counterpart is a full absorption, don't try to reverse that!
179
- next false if (a.forward_absorption || a.reverse_absorption).full_absorption
180
-
181
- # Otherwise the other end must already be a table or fully absorbed into one
182
- next false unless child_candidate.is_table || child_candidate.full_absorption
183
-
184
- next false unless a.child_role.is_unique && a.parent_role.is_unique # Must be one-to-one
185
-
186
- # next true if pi_roles.size == 1 && pi_roles.include?(a.parent_role) # Allow the sole identifying role for this object
187
- next false unless a.parent_role.is_mandatory # Don't absorb an object along a non-mandatory role
188
- true
189
- end
190
-
191
- trace :relational_optimiser, "#{object_type.name} has #{absorption_paths.size} absorption paths"
192
-
193
- # Rule 4: If this object can be fully absorbed along non-identifying roles, do that (maybe flip some absorptions)
194
- if absorption_paths.size > 0
195
- trace :relational_optimiser, "#{object_type.name} is fully absorbed in #{absorption_paths.size} places" do
196
- absorption_paths.each do |a|
197
- a = a.flip! if a.forward_absorption
198
- trace :relational_optimiser, "#{object_type.name} is fully absorbed via #{a.inspect}"
199
- end
200
- end
201
-
202
- candidate.definitely_not_table
203
- next object_type
204
- end
205
-
206
- # Rule 5: If this object has no functional dependencies (only its identifier), it can be absorbed in multiple places
207
- # We don't create FullAbsorptions, because they're only used to resolve references to this object; and there are none here
208
- refs_to = candidate.references_to.reject{|a|a.parent_role.base_role.is_identifying}
209
- if !refs_to.empty? and non_identifying_refs_from.size == 0
210
- refs_to.map! do |a|
211
- a = a.flip! if a.reverse_absorption # We were forward, but the other end must be
212
- a.forward_absorption
213
- end
214
- trace :relational_optimiser, "#{object_type.name} is fully absorbed in #{refs_to.size} places: #{refs_to.map{|ref| ref.inspect}*", "}"
215
- candidate.definitely_not_table
216
- next object_type
217
- end
218
-
219
- false # Otherwise we failed to make a decision about this object type
220
- end
221
- end
103
+ undecided.select do |object_type|
104
+ candidate = @candidates[object_type]
105
+ trace :relational_optimiser, "Considering possible status of #{object_type.name}" do
106
+
107
+ # Rule 1: Always absorb an objectified unary into its role player (unless its forced to be separate)
108
+ if !object_type.is_separate && (f = object_type.fact_type) && f.all_role.size == 1
109
+ absorbing_ref = candidate.mapping.all_member.detect{|a| a.is_a?(MM::Absorption) and a.child_role.base_role == f.all_role.single}
110
+ raise "REVISIT: Internal error" unless absorbing_ref.parent_role.object_type == object_type
111
+ absorbing_ref = absorbing_ref.flip!
112
+ candidate.full_absorption =
113
+ @constellation.FullAbsorption(composition: @composition, absorption: absorbing_ref, object_type: object_type)
114
+ trace :relational_optimiser, "Fully absorb objectified unary #{object_type.name} into #{f.all_role.single.object_type.name}"
115
+ candidate.definitely_not_table
116
+ next object_type
117
+ end
118
+
119
+ # Rule 2: If the preferred_identifier contains one role only, played by an entity type that can absorb us, do that:
120
+ # (Leave pi_roles intact for further use below)
121
+ absorbing_ref = nil
122
+ pi_roles = []
123
+ if object_type.is_a?(MM::EntityType) and # We're an entity type
124
+ pi_roles = object_type.preferred_identifier_roles and # Our PI
125
+ pi_roles.size == 1 and # has one role
126
+ single_pi_role = pi_roles[0] and # that role is
127
+ single_pi_role.object_type.is_a?(MM::EntityType) and # played by another Entity Type
128
+ absorbing_ref =
129
+ candidate.mapping.all_member.detect do |absorption|
130
+ absorption.is_a?(MM::Absorption) && absorption.child_role.base_role == single_pi_role
131
+ end
132
+
133
+ absorbing_ref = absorbing_ref.forward_absorption || absorbing_ref.flip!
134
+ candidate.full_absorption =
135
+ @constellation.FullAbsorption(composition: @composition, absorption: absorbing_ref, object_type: object_type)
136
+ trace :relational_optimiser, "EntityType #{single_pi_role.object_type.name} identifies EntityType #{object_type.name}, so fully absorbs it via #{absorbing_ref.inspect}"
137
+ candidate.definitely_not_table
138
+ next object_type
139
+ end
140
+
141
+ # Rule 3: If there's more than one absorption path and any functional dependencies that can't absorb us, it's a table
142
+ non_identifying_refs_from =
143
+ candidate.references_from.reject do |member|
144
+ case member
145
+ when MM::Absorption
146
+ pi_roles.include?(member.child_role.base_role)
147
+ when MM::Indicator
148
+ pi_roles.include?(member.role)
149
+ else
150
+ false
151
+ end
152
+ end
153
+ trace :relational_optimiser, "#{object_type.name} has #{non_identifying_refs_from.size} non-identifying functional roles" do
154
+ non_identifying_refs_from.each do |a|
155
+ trace :relational_optimiser, a.inspect
156
+ end
157
+ end
158
+
159
+ trace :relational_optimiser, "#{object_type.name} has #{candidate.references_to.size} references to it" do
160
+ candidate.references_to.each do |a|
161
+ trace :relational_optimiser, a.inspect
162
+ end
163
+ end
164
+ if candidate.references_to.size > 1 and # More than one place wants us
165
+ non_identifying_refs_from.size > 0 # And we carry dependent values so cannot be absorbed
166
+ trace :relational_optimiser, "#{object_type.name} has #{non_identifying_refs_from.size} non-identifying functional dependencies and #{candidate.references_to.size} absorption paths so 3NF requires it be a table"
167
+ candidate.definitely_table
168
+ next object_type
169
+ end
170
+
171
+ # At this point, this object either has no functional dependencies or only one place it would be absorbed
172
+ next false if !candidate.is_table # We can't reduce the number of tables by absorbing this one
173
+
174
+ absorption_paths =
175
+ ( non_identifying_refs_from + # But we should exclude any that are already involved in an absorption; pre-decided ET=>ET or supertype absorption!
176
+ candidate.references_to # These are our reverse absorptions that could absorb us
177
+ ).select do |a|
178
+ next false unless a.is_a?(MM::Absorption) # Skip Indicators, we can't be absorbed there
179
+ child_candidate = @candidates[a.child_role.object_type]
180
+
181
+ # It's ok if we absorbed them already
182
+ next true if a.full_absorption && child_candidate.full_absorption.absorption != a
183
+
184
+ # If our counterpart is a full absorption, don't try to reverse that!
185
+ next false if (a.forward_absorption || a.reverse_absorption).full_absorption
186
+
187
+ # Otherwise the other end must already be a table or fully absorbed into one
188
+ next false unless child_candidate.is_table || child_candidate.full_absorption
189
+
190
+ next false unless a.child_role.is_unique && a.parent_role.is_unique # Must be one-to-one
191
+
192
+ # next true if pi_roles.size == 1 && pi_roles.include?(a.parent_role) # Allow the sole identifying role for this object
193
+ next false unless a.parent_role.is_mandatory # Don't absorb an object along a non-mandatory role
194
+ true
195
+ end
196
+
197
+ trace :relational_optimiser, "#{object_type.name} has #{absorption_paths.size} absorption paths"
198
+
199
+ # Rule 4: If this object can be fully absorbed along non-identifying roles, do that (maybe flip some absorptions)
200
+ if absorption_paths.size > 0
201
+ trace :relational_optimiser, "#{object_type.name} is fully absorbed in #{absorption_paths.size} places" do
202
+ absorption_paths.each do |a|
203
+ a = a.flip! if a.forward_absorption
204
+ trace :relational_optimiser, "#{object_type.name} is fully absorbed via #{a.inspect}"
205
+ end
206
+ end
207
+
208
+ candidate.definitely_not_table
209
+ next object_type
210
+ end
211
+
212
+ # Rule 5: If this object has no functional dependencies (only its identifier), it can be absorbed in multiple places
213
+ # We don't create FullAbsorptions, because they're only used to resolve references to this object; and there are none here
214
+ refs_to = candidate.references_to.reject{|a|a.parent_role.base_role.is_identifying}
215
+ if !refs_to.empty? and non_identifying_refs_from.size == 0
216
+ refs_to.map! do |a|
217
+ a = a.flip! if a.reverse_absorption # We were forward, but the other end must be
218
+ a.forward_absorption
219
+ end
220
+ trace :relational_optimiser, "#{object_type.name} is fully absorbed in #{refs_to.size} places: #{refs_to.map{|ref| ref.inspect}*", "}"
221
+ candidate.definitely_not_table
222
+ next object_type
223
+ end
224
+
225
+ false # Otherwise we failed to make a decision about this object type
226
+ end
227
+ end
222
228
  end
223
229
 
224
230
  # Remove the unused reverse absorptions:
225
231
  def delete_reverse_absorptions
226
- @binary_mappings.each do |object_type, mapping|
227
- mapping.all_member.to_a. # Avoid problems with deletion from all_member
228
- each do |member|
229
- next unless member.is_a?(MM::Absorption)
230
- member.retract if member.forward_absorption # This is the reverse of some absorption
231
- end
232
- mapping.re_rank
233
- end
232
+ @binary_mappings.each do |object_type, mapping|
233
+ mapping.all_member.to_a. # Avoid problems with deletion from all_member
234
+ each do |member|
235
+ next unless member.is_a?(MM::Absorption)
236
+ member.retract if member.forward_absorption # This is the reverse of some absorption
237
+ end
238
+ mapping.re_rank
239
+ end
234
240
  end
235
241
 
236
242
  # After all table/non-table decisions are made, convert Mappings for tables into Composites and retract the rest:
237
243
  def make_composites
238
- @composites = {}
239
- @candidates.keys.to_a.each do |object_type|
240
- candidate = @candidates[object_type]
241
- mapping = candidate.mapping
244
+ @composites = {}
245
+ @candidates.keys.to_a.each do |object_type|
246
+ candidate = @candidates[object_type]
247
+
248
+ if candidate.is_table
249
+ make_composite candidate
250
+ else
251
+ @candidates.delete(object_type)
252
+ end
253
+ end
254
+ end
242
255
 
243
- if candidate.is_table
244
- composite = @constellation.Composite(mapping, composition: @composition)
245
- @composites[object_type] = composite
246
- else
247
- @candidates.delete(object_type)
248
- end
249
- end
256
+ def make_composite candidate
257
+ mapping = candidate.mapping
258
+ @composites[mapping.object_type] =
259
+ @constellation.Composite(mapping, composition: @composition)
250
260
  end
251
261
 
252
262
  # Inject a ValueField for each value type that's a table:
253
263
  def inject_value_fields
254
- @composition.all_composite.each do |composite|
255
- mapping = composite.mapping
256
- if mapping.object_type.is_a?(MM::ValueType) and # Composite needs a ValueField
257
- !mapping.all_member.detect{|m| m.is_a?(MM::ValueField)} # And don't already have one
258
- trace :relational_columns, "Adding value field for #{mapping.object_type.name}"
259
- @constellation.ValueField(
260
- :new,
261
- parent: mapping,
262
- name: mapping.object_type.name+" Value",
263
- object_type: mapping.object_type
264
- )
265
- mapping.re_rank
266
- end
267
- end
264
+ @composition.all_composite.each do |composite|
265
+ mapping = composite.mapping
266
+ if mapping.object_type.is_a?(MM::ValueType) and # Composite needs a ValueField
267
+ !mapping.all_member.detect{|m| m.is_a?(MM::ValueField)} # And don't already have one
268
+ trace :relational_columns, "Adding value field for #{mapping.object_type.name}"
269
+ @constellation.ValueField(
270
+ :new,
271
+ parent: mapping,
272
+ name: mapping.object_type.name+" Value",
273
+ object_type: mapping.object_type
274
+ )
275
+ mapping.re_rank
276
+ end
277
+ end
268
278
  end
269
279
 
270
280
  def inject_surrogates
271
- surrogate_type_name = [true, '', 'true', 'yes'].include?(t = @option_surrogates) ? 'Auto Counter' : t
272
- composites = @composition.all_composite.to_a
273
- return if composites.empty?
274
- vocabulary = composites[0].mapping.object_type.vocabulary # REVISIT: Crappy: choose the first (currently always single)
275
- surrogate_type =
276
- @constellation.ValueType(
277
- vocabulary: vocabulary,
278
- name: surrogate_type_name,
279
- concept: [:new, :implication_rule => "surrogate injection"]
280
- )
281
- @composition.all_composite.each do |composite|
282
- next unless needs_surrogate(composite)
283
- surrogate_component =
284
- @constellation.SurrogateKey(
285
- :new,
286
- parent: composite.mapping,
287
- name: composite.mapping.object_type.name+" ID",
288
- object_type: surrogate_type
289
- )
290
- index =
291
- @constellation.Index(
292
- :new,
293
- composite: composite.mapping.root,
294
- is_unique: true,
295
- presence_constraint: nil, # No PC exists
296
- composite_as_primary_index: composite.mapping.root # Usurp the primary key
297
- )
298
- @constellation.IndexField(access_path: index, ordinal: 0, component: surrogate_component)
299
- composite.mapping.re_rank
300
- end
281
+ composites = @composition.all_composite.to_a
282
+ return if composites.empty?
283
+
284
+ trace :surrogates, "Injecting any required surrogates" do
285
+ @composition.all_composite.each do |composite|
286
+ next unless needs_surrogate(composite)
287
+ inject_surrogate composite
288
+ end
289
+ end
290
+ end
291
+
292
+ def surrogate_type
293
+ @surrogate_type ||= begin
294
+ surrogate_type_name = [true, '', 'true', 'yes', nil].include?(t = @option_surrogates) ? 'Auto Counter' : t
295
+ # REVISIT: Crappy: choose the first (currently always single)
296
+ vocabulary = @composition.all_composite.to_a[0].mapping.object_type.vocabulary
297
+ @constellation.ValueType(
298
+ vocabulary: vocabulary,
299
+ name: surrogate_type_name,
300
+ concept: [:new, :implication_rule => "surrogate injection"]
301
+ )
302
+ end
303
+ end
304
+
305
+ def inject_surrogate composite, extension = ' ID'
306
+ trace :surrogates, "Injecting surrogate for #{composite.inspect}" do
307
+ surrogate_component =
308
+ @constellation.SurrogateKey(
309
+ :new,
310
+ parent: composite.mapping,
311
+ name: composite.mapping.name+extension,
312
+ object_type: surrogate_type
313
+ )
314
+ index =
315
+ @constellation.Index(:new, composite: composite, is_unique: true,
316
+ presence_constraint: nil, composite_as_primary_index: composite)
317
+ @constellation.IndexField(access_path: index, ordinal: 0, component: surrogate_component)
318
+ composite.mapping.re_rank
319
+ surrogate_component
320
+ end
301
321
  end
302
322
 
303
323
  def needs_surrogate(composite)
304
- object_type = composite.mapping.object_type
305
- if MM::ValueType === object_type
306
- trace :surrogates, "#{composite.inspect} is a ValueType that #{object_type.is_auto_assigned ? "is auto-assigned already" : "requires a surrogate" }"
307
- return !object_type.is_auto_assigned
308
- end
309
-
310
- non_key_members, key_members = composite.mapping.all_member.reject do |member|
311
- member.is_a?(MM::Absorption) and member.forward_absorption
312
- end.partition do |member|
313
- member.rank_key[0] > MM::Component::RANK_IDENT
314
- end
315
-
316
- non_fk_surrogate =
317
- key_members.detect do |member|
318
- next true unless member.is_a?(MM::Absorption)
319
- next false if @composites[member.object_type] or @composition.all_full_absorption[member.object_type] # It's a table or absorbed into one
320
- true
321
- end
322
-
323
- if key_members.size > 1
324
- # Multi-part identifiers are only allowed if:
325
- # * each part is a foreign key (i.e. it's a join table),
326
- # * there are no other columns (that might require updating) and
327
- # * the object is not the target of a foreign key:
328
- if non_fk_surrogate
329
- trace :surrogates, "#{composite.inspect} has non-FK identifiers so requires a surrogate"
330
- return true
331
- end
332
-
333
- if non_key_members.size > 0
334
- trace :surrogates, "#{composite.inspect} has non-identifying fields so requires a surrogate"
335
- return true
336
- end
337
-
338
- if @candidates[object_type].references_to.size > 0
339
- trace :surrogates, "#{composite.inspect} is the target of at least one foreign key so requires a surrogate"
340
- return true
341
- end
342
-
343
- trace :surrogates, "#{composite.inspect} is a join table that does NOT require a surrogate"
344
- return false
345
- end
346
-
347
- # A single-part PK is replaced by a surrogate unless the single part is a surrogate, an FK to a surrogate, or is an Absorbed auto-assigned VT
348
-
349
- key_member = key_members[0]
350
- if !non_fk_surrogate
351
- trace :surrogates, "#{composite.inspect} has an identifier that's an FK so does NOT require a surrogate"
352
- return false
353
- end
354
-
355
- if key_member.is_a?(MM::SurrogateKey)
356
- trace :surrogates, "#{composite.inspect} already has an injected SurrogateKey so does NOT require a surrogate"
357
- return false
358
- end
359
- unless key_member.is_a?(MM::Absorption)
360
- trace :surrogates, "#{composite.inspect} is identified by a non-Absorption so requires a surrogate"
361
- return true
362
- end
363
- if key_member.object_type.is_a?(MM::EntityType)
364
- trace :surrogates, "#{composite.inspect} is identified by another entity type so requires a surrogate"
365
- return true
366
- end
367
- if key_member.object_type.is_auto_assigned
368
- trace :surrogates, "#{composite.inspect} already has an auto-assigned key so does NOT require a surrogate"
369
- return false
370
- end
371
- trace :surrogates, "#{composite.inspect} requires a surrogate"
372
- return true
324
+ object_type = composite.mapping.object_type
325
+ if MM::ValueType === object_type
326
+ trace :surrogates, "#{composite.inspect} is a ValueType that #{object_type.is_auto_assigned ? "is auto-assigned already" : "requires a surrogate" }"
327
+ return !object_type.is_auto_assigned
328
+ end
329
+
330
+ non_key_members, key_members = composite.mapping.all_member.reject do |member|
331
+ member.is_a?(MM::Absorption) and member.forward_absorption
332
+ end.partition do |member|
333
+ member.rank_key[0] > MM::Component::RANK_IDENT
334
+ end
335
+
336
+ non_fk_surrogate =
337
+ key_members.detect do |member|
338
+ next true unless member.is_a?(MM::Absorption)
339
+ next false if @composites[member.object_type] or @composition.all_full_absorption[member.object_type] # It's a table or absorbed into one
340
+ true
341
+ end
342
+
343
+ if key_members.size > 1
344
+ # Multi-part identifiers are only allowed if:
345
+ # * each part is a foreign key (i.e. it's a join table),
346
+ # * there are no other columns (that might require updating) and
347
+ # * the object is not the target of a foreign key:
348
+ if non_fk_surrogate
349
+ trace :surrogates, "#{composite.inspect} has non-FK identifiers so requires a surrogate"
350
+ return true
351
+ end
352
+
353
+ if non_key_members.size > 0
354
+ trace :surrogates, "#{composite.inspect} has non-identifying fields so requires a surrogate"
355
+ return true
356
+ end
357
+
358
+ if @candidates[object_type].references_to.size > 0
359
+ trace :surrogates, "#{composite.inspect} is the target of at least one foreign key so requires a surrogate"
360
+ return true
361
+ end
362
+
363
+ trace :surrogates, "#{composite.inspect} is a join table that does NOT require a surrogate"
364
+ return false
365
+ end
366
+
367
+ # A single-part PK is replaced by a surrogate unless the single part is a surrogate, an FK to a surrogate, or is an Absorbed auto-assigned VT
368
+
369
+ key_member = key_members[0]
370
+ if !non_fk_surrogate
371
+ trace :surrogates, "#{composite.inspect} has an identifier that's an FK so does NOT require a surrogate"
372
+ return false
373
+ end
374
+
375
+ if key_member.is_a?(MM::SurrogateKey)
376
+ trace :surrogates, "#{composite.inspect} already has an injected SurrogateKey so does NOT require a surrogate"
377
+ return false
378
+ end
379
+ unless key_member.is_a?(MM::Absorption)
380
+ trace :surrogates, "#{composite.inspect} is identified by a non-Absorption so requires a surrogate"
381
+ return true
382
+ end
383
+ if key_member.object_type.is_a?(MM::EntityType)
384
+ trace :surrogates, "#{composite.inspect} is identified by another entity type so requires a surrogate"
385
+ return true
386
+ end
387
+ if key_member.object_type.is_auto_assigned
388
+ trace :surrogates, "#{composite.inspect} already has an auto-assigned key so does NOT require a surrogate"
389
+ return false
390
+ end
391
+ trace :surrogates, "#{composite.inspect} requires a surrogate"
392
+ return true
373
393
  end
374
394
 
375
395
  def clean_unused_mappings
376
- @candidates.keys.to_a.each do |object_type|
377
- candidate = @candidates[object_type]
378
- next if candidate.is_table
379
- mapping = candidate.mapping
380
- mapping.retract
381
- @binary_mappings.delete(object_type)
382
- end
396
+ @candidates.keys.to_a.each do |object_type|
397
+ candidate = @candidates[object_type]
398
+ next if candidate.is_table
399
+ mapping = candidate.mapping
400
+ mapping.retract
401
+ @binary_mappings.delete(object_type)
402
+ end
403
+ end
404
+
405
+ def is_empty_inheritance mapping
406
+ # Cannot be an empty inheritance unless it's an TypeInheritance absorption
407
+ return false if !mapping.is_a?(MM::Absorption) || !mapping.parent_role.fact_type.is_a?(MM::TypeInheritance)
408
+
409
+ # It's empty if it's a TypeInheritance which has no non-empty members
410
+ !mapping.all_member.to_a.any? do |member|
411
+ !is_empty_inheritance(member)
412
+ end
413
+ end
414
+
415
+ def elide_empty_inheritance mapping
416
+ mapping.all_member.to_a.each do |member|
417
+ if member.is_a?(MM::Absorption) && member.parent_role.fact_type.is_a?(MM::TypeInheritance)
418
+ elide_empty_inheritance member
419
+ if member.all_member.size == 0
420
+ trace :relational, "Retracting empty inheritance #{member.inspect}"
421
+ member.retract
422
+ end
423
+ end
424
+ end
383
425
  end
384
426
 
385
427
  # Absorb all items which aren't tables (and keys to those which are) recursively
386
428
  def absorb_all_columns
387
- trace :relational_columns!, "Computing contents of all tables" do
388
- @composition.all_composite_by_name.each do |composite|
389
- trace :relational_columns, "Computing contents of #{composite.mapping.name}" do
390
- absorb_all composite.mapping, composite.mapping
391
- end
392
- end
393
- end
429
+ trace :relational_columns!, "Computing contents of all tables" do
430
+ @composition.all_composite_by_name.each do |composite|
431
+ trace :relational_columns, "Computing contents of #{composite.mapping.name}" do
432
+ absorb_all composite.mapping, composite.mapping
433
+ end
434
+ end
435
+ end
436
+ end
437
+
438
+ # This method duplicates part of the absorb_all process,
439
+ # looking for foreign keys from this composite.
440
+ # This must be done in a Data Vault mapping before we decide
441
+ # what will be a hub and what will be a link, and that controls
442
+ # whether we will absorb a foreign key or a copy of the natural
443
+ # key while expanding a table's identifiers.
444
+ # Hub tables never absorb a surrogate FK, only natural keys.
445
+ def enumerate_foreign_keys mapping, from = nil, accumulator = [], path = []
446
+ return if path.include?(mapping)
447
+ path << mapping
448
+ from ||= mapping
449
+
450
+ # REVISIT: This corrects some instability (should not actually be order-dependent) but doesn't fix the underlying problem
451
+ mapping.re_rank
452
+ ordered = from.all_member.sort_by(&:ordinal)
453
+
454
+ ordered.each do |member|
455
+ # Only consider forward Absorptions:
456
+ next if !member.is_a?(MM::Absorption)
457
+ next if member.forward_absorption
458
+
459
+ child_object_type = member.child_role.object_type
460
+ child_mapping = @binary_mappings[child_object_type]
461
+ if child_mapping.composite
462
+ trace :fks, "FK to #{member.child_role.name} in #{member.inspect_reading}"
463
+ accumulator << child_mapping.composite
464
+ next
465
+ end
466
+
467
+ full_absorption = child_object_type.all_full_absorption[@composition]
468
+ if full_absorption && full_absorption.absorption.parent_role.fact_type != member.parent_role.fact_type
469
+ begin # Follow transitive target absorption
470
+ child_object_type = full_absorption.absorption.parent_role.object_type
471
+ end while full_absorption = child_object_type.all_full_absorption[@composition]
472
+ child_mapping = @binary_mappings[child_object_type]
473
+ trace :fks, "FK to #{child_mapping.name} in #{member.inspect_reading} (for fully-absorbed #{member.child_role.name})"
474
+ accumulator << child_mapping.composite
475
+ next
476
+ end
477
+
478
+ trace :fks, "Descending all of #{member.child_role.name} in #{member.inspect_reading}" do
479
+ enumerate_foreign_keys member, child_mapping, accumulator, path
480
+ end
481
+ end
482
+ accumulator
483
+ end
484
+
485
+ def devolve_all
486
+ # Data Vaults have satellites, not normal relational schemas.
394
487
  end
395
488
 
396
489
  # This member is an Absorption. Process it recursively, absorbing all its members or just a key
397
490
  # depending on whether the absorbed object is a Composite (or absorbed into one) or not.
398
491
  def absorb_nested mapping, member, paths
399
- # Should we absorb a foreign key or the whole contents?
400
-
401
- child_object_type = member.child_role.object_type
402
- table = @candidates[child_object_type]
403
- child_mapping = @binary_mappings[child_object_type]
404
- if table
405
- trace :relational_columns?, "Absorbing FK to #{member.child_role.name} in #{member.inspect_reading}" do
406
- paths[member] = @constellation.ForeignKey(:new, source_composite: mapping.root, composite: child_mapping.composite, absorption: member)
407
- absorb_key member, child_mapping, paths
408
- return
409
- end
410
- end
411
-
412
- # Is our target object_type fully absorbed (and not through this absorption)?
413
- full_absorption = child_object_type.all_full_absorption[@composition]
414
- # We can't use member.full_absorption here, as it's not populated on forked copies
415
- # if full_absorption && full_absorption != member.full_absorption
416
- if full_absorption && full_absorption.absorption.parent_role.fact_type != member.parent_role.fact_type
417
-
418
- # REVISIT: This should be done by recursing to absorb_key, not using a loop
419
- absorption = member # Retain this for the ForeignKey
420
- begin # Follow transitive target absorption
421
- member = mirror(full_absorption.absorption, member)
422
- child_object_type = full_absorption.absorption.parent_role.object_type
423
- end while full_absorption = child_object_type.all_full_absorption[@composition]
424
- child_mapping = @binary_mappings[child_object_type]
425
-
426
- trace :relational_columns?, "Absorbing FK to #{absorption.child_role.name} (fully absorbed into #{child_object_type.name}) in #{member.inspect_reading}" do
427
- paths[absorption] = @constellation.ForeignKey(:new, source_composite: mapping.root, composite: child_mapping.composite, absorption: absorption)
428
- absorb_key member, child_mapping, paths
429
- end
430
- return
431
- end
432
-
433
- trace :relational_columns?, "Absorbing all of #{member.child_role.name} in #{member.inspect_reading}" do
434
- absorb_all member, child_mapping, paths
435
- end
492
+ # Should we absorb a foreign key or the whole contents?
493
+
494
+ child_object_type = member.child_role.object_type
495
+ child_mapping = @binary_mappings[child_object_type]
496
+ if child_mapping.composite
497
+ trace :relational_columns?, "Absorbing FK to #{member.child_role.name} in #{member.inspect_reading}" do
498
+ paths[member] = @constellation.ForeignKey(:new, source_composite: mapping.root, composite: child_mapping.composite, absorption: member)
499
+ absorb_key member, child_mapping, paths
500
+ return
501
+ end
502
+ end
503
+
504
+ # Is our target object_type fully absorbed (and not through this absorption)?
505
+ full_absorption = child_object_type.all_full_absorption[@composition]
506
+ # We can't use member.full_absorption here, as it's not populated on forked copies
507
+ # if full_absorption && full_absorption != member.full_absorption
508
+ if full_absorption && full_absorption.absorption.parent_role.fact_type != member.parent_role.fact_type
509
+
510
+ # REVISIT: This should be done by recursing to absorb_key, not using a loop
511
+ absorption = member # Retain this for the ForeignKey
512
+ begin # Follow transitive target absorption
513
+ member = mirror(full_absorption.absorption, member)
514
+ child_object_type = full_absorption.absorption.parent_role.object_type
515
+ end while full_absorption = child_object_type.all_full_absorption[@composition]
516
+ child_mapping = @binary_mappings[child_object_type]
517
+
518
+ trace :relational_columns?, "Absorbing FK to #{absorption.child_role.name} (fully absorbed into #{child_object_type.name}) in #{member.inspect_reading}" do
519
+ paths[absorption] = @constellation.ForeignKey(:new, source_composite: mapping.root, composite: child_mapping.composite, absorption: absorption)
520
+ absorb_key member, child_mapping, paths
521
+ end
522
+ return
523
+ end
524
+
525
+ trace :relational_columns?, "Absorbing all of #{member.child_role.name} in #{member.inspect_reading}" do
526
+ absorb_all member, child_mapping, paths
527
+ end
528
+ end
529
+
530
+ # May be overridden in subclasses
531
+ def prefer_natural_key building_natural_key, source_composite, target_composite
532
+ false
436
533
  end
437
534
 
438
535
  # Recursively add members to this component for the existential roles of
439
536
  # the composite mapping for the absorbed (child_role) object:
440
537
  def absorb_key mapping, target, paths
441
- target.re_rank
442
- target.all_member.sort_by(&:ordinal).each do |member|
443
- rank = member.rank_key[0]
444
- next unless rank <= MM::Component::RANK_IDENT
445
- member = fork_component_to_new_parent mapping, member
446
- augment_paths paths, member
447
- if member.is_a?(MM::SurrogateKey)
448
- break # Will always be first (higher rank), and usurps others
449
- elsif member.is_a?(MM::Absorption)
450
- object_type = member.child_role.object_type
451
- fa = @composition.all_full_absorption[member.child_role.object_type]
452
- if fa
453
- # The target object is fully absorbed. Absorb a key to where it was absorbed
454
- # We can't recurse here, because we must descend supertype absorptions
455
- while fa
456
- member = mirror fa.absorption, member
457
- augment_paths paths, member
458
- # This doesn't "feel" right, but it works right. Perhaps I'll understand why one day.
459
- absorb_key member, fa.absorption.parent, paths
460
- fa = @composition.all_full_absorption[member.child_role.object_type]
461
- end
462
- else
463
- absorb_key member, @binary_mappings[member.child_role.object_type], paths
464
- end
465
- end
466
- end
467
- # mapping.re_rank
538
+ building_natural_key = paths.detect{|k,i| i.is_a?(MM::Index) && i.composite_as_natural_index}
539
+ prefer_natural = prefer_natural_key(building_natural_key, mapping.root, target.composite)
540
+ prefer_natural = false unless !target.composite || target.composite.primary_index != target.composite.natural_index
541
+ target.re_rank
542
+ target.all_member.sort_by(&:ordinal).each do |member|
543
+ rank = member.rank_key[0]
544
+ next unless rank <= MM::Component::RANK_IDENT
545
+ if rank == MM::Component::RANK_SURROGATE && prefer_natural
546
+ next
547
+ end
548
+ member = fork_component_to_new_parent mapping, member
549
+ augment_paths paths, member
550
+ if rank == MM::Component::RANK_SURROGATE && !prefer_natural
551
+ break # Will always be first (higher rank), and usurps others
552
+ elsif member.is_a?(MM::Absorption)
553
+ object_type = member.child_role.object_type
554
+ full_absorption = @composition.all_full_absorption[member.child_role.object_type]
555
+ if full_absorption
556
+ # The target object is fully absorbed. Absorb a key to where it was absorbed
557
+ # We can't recurse here, because we must descend supertype absorptions
558
+ while full_absorption
559
+ trace :relational_columns?, "Absorbing key of fully absorbed #{member.child_role.name}" do
560
+ member = mirror full_absorption.absorption, member
561
+ augment_paths paths, member
562
+ # Descend so the key fields get fully populated
563
+ absorb_key member, full_absorption.absorption.parent, paths
564
+ full_absorption = @composition.all_full_absorption[member.child_role.object_type]
565
+ end
566
+ end
567
+ else
568
+ absorb_key member, @binary_mappings[member.child_role.object_type], paths
569
+ end
570
+ end
571
+ end
572
+ # mapping.re_rank
468
573
  end
469
574
 
470
575
  # Augment the mapping with copies of the children of the "from" mapping.
471
576
  # At the top level, no "from" is given and the children already exist
472
577
  def absorb_all mapping, from, paths = {}
473
- top_level = mapping == from
474
-
475
- pcs = []
476
- newpaths = {}
477
- if mapping.composite || mapping.full_absorption
478
- pcs = find_uniqueness_constraints(mapping)
479
-
480
- # Don't build an index from the same PresenceConstraint twice on the same composite (e.g. for a subtype)
481
- existing_pcs = mapping.root.all_access_path.select{|ap| MM::Index === ap}.map(&:presence_constraint)
482
- newpaths = make_new_paths mapping, paths.keys+existing_pcs, pcs
483
- end
484
-
485
- from.re_rank
486
- ordered = from.all_member.sort_by(&:ordinal)
487
- ordered.each do |member|
488
- trace :relational_columns, proc {"#{top_level ? 'Existing' : 'Absorbing'} #{member.inspect}"} do
489
- unless top_level # Top-level members are already instantiated
490
- member = fork_component_to_new_parent(mapping, member)
491
- end
492
- rel = paths.merge(relevant_paths(newpaths, member))
493
- augment_paths rel, member
494
-
495
- if member.is_a?(MM::Absorption)
496
- absorb_nested mapping, member, rel
497
- end
498
- end
499
- end
500
- newpaths.each do |pc, path|
501
- path.retract if path.all_index_field.size == 0
502
- end
503
-
504
- # mapping.re_rank
578
+ top_level = mapping == from
579
+
580
+ pcs = []
581
+ newpaths = {}
582
+ if mapping.composite || mapping.full_absorption
583
+ pcs = find_uniqueness_constraints(mapping)
584
+
585
+ # Don't build an index from the same PresenceConstraint twice on the same composite (e.g. for a subtype)
586
+ existing_pcs = mapping.root.all_access_path.select{|ap| MM::Index === ap}.map(&:presence_constraint)
587
+ newpaths = make_new_paths mapping, paths.keys+existing_pcs, pcs
588
+ end
589
+
590
+ from.re_rank
591
+ ordered = from.all_member.sort_by(&:ordinal)
592
+ ordered.each do |member|
593
+ trace :relational_columns, proc {"#{top_level ? 'Existing' : 'Absorbing'} #{member.inspect}"} do
594
+ unless top_level # Top-level members are already instantiated
595
+ member = fork_component_to_new_parent(mapping, member)
596
+ end
597
+ rel = paths.merge(relevant_paths(newpaths, member))
598
+ augment_paths rel, member
599
+
600
+ if member.is_a?(MM::Absorption) && !member.forward_absorption
601
+ # Only forward absorptions here please...
602
+ absorb_nested mapping, member, rel
603
+ end
604
+ end
605
+ end
606
+
607
+ newpaths.values.select{|ix| ix.all_index_field.size == 0}.each(&:retract)
505
608
  end
506
609
 
507
610
  # Find all PresenceConstraints to index the object in this Mapping
508
611
  def find_uniqueness_constraints mapping
509
- return [] unless mapping.object_type.is_a?(MM::EntityType)
510
-
511
- start_roles =
512
- mapping.
513
- object_type.
514
- all_role_transitive. # Includes objectification roles for objectified fact types
515
- select do |role|
516
- (role.is_unique || # Must be unique on near role
517
- role.fact_type.is_unary) && # Or be a unary role
518
- !(role.fact_type.is_a?(MM::TypeInheritance) && role == role.fact_type.supertype_role) # allow roles as subtype
519
- end.
520
- map(&:counterpart). # (Same role if it's a unary)
521
- compact. # Ignore nil counterpart of a role in an n-ary
522
- map(&:base_role). # In case it's a link fact type
523
- uniq
524
-
525
- pcs =
526
- start_roles.
527
- flat_map(&:all_role_ref). # All role_refs
528
- map(&:role_sequence). # The role_sequence
529
- uniq.
530
- flat_map(&:all_presence_constraint).
531
- uniq.
532
- reject do |pc|
533
- pc.max_frequency != 1 || # Must be unique
534
- pc.enforcement || # and alethic
535
- pc.role_sequence.all_role_ref.detect do |rr|
536
- !start_roles.include?(rr.role) # and span only valid roles
537
- end || # and not be the full absorption path
538
- ( # Reject a constraint that caused full absorption
539
- pc.role_sequence.all_role_ref.size == 1 and
540
- mapping.is_a?(MM::Absorption) and
541
- fa = mapping.full_absorption and
542
- pc.role_sequence.all_role_ref.single.role.base_role == fa.absorption.parent_role.base_role
543
- )
544
- end # Alethic uniqueness constraint on far end
545
-
546
- non_absorption_pcs = pcs.reject do |pc|
547
- # An absorption PC is a PC that covers some role that is involved in a FullAbsorption
548
- full_absorptions =
549
- pc.
550
- role_sequence.
551
- all_role_ref.
552
- map(&:role).
553
- flat_map do |role|
554
- (role.all_absorption_as_parent_role.to_a + role.all_absorption_as_child_role.to_a).
555
- select do |abs|
556
- abs.full_absorption && abs.full_absorption.composition == @composition
557
- end
558
- end
559
- full_absorptions.size > 0
560
- end
561
- pcs = non_absorption_pcs
562
-
563
- trace :relational_paths, "Uniqueness Constraints for #{mapping.object_type.name}" do
564
- pcs.each do |pc|
565
- trace :relational_paths, "#{pc.describe.inspect}#{pc.is_preferred_identifier ? ' (PI)' : ''}"
566
- end
567
- end
568
-
569
- pcs
612
+ return [] unless mapping.object_type.is_a?(MM::EntityType)
613
+
614
+ start_roles =
615
+ mapping.
616
+ object_type.
617
+ all_role_transitive. # Includes objectification roles for objectified fact types
618
+ select do |role|
619
+ (role.is_unique || # Must be unique on near role
620
+ role.fact_type.is_unary) && # Or be a unary role
621
+ !(role.fact_type.is_a?(MM::TypeInheritance) && role == role.fact_type.supertype_role) # allow roles as subtype
622
+ end.
623
+ map(&:counterpart). # (Same role if it's a unary)
624
+ compact. # Ignore nil counterpart of a role in an n-ary
625
+ map(&:base_role). # In case it's a link fact type
626
+ uniq
627
+
628
+ pcs =
629
+ start_roles.
630
+ flat_map(&:all_role_ref). # All role_refs
631
+ map(&:role_sequence). # The role_sequence
632
+ uniq.
633
+ flat_map(&:all_presence_constraint).
634
+ uniq.
635
+ reject do |pc|
636
+ pc.max_frequency != 1 || # Must be unique
637
+ pc.enforcement || # and alethic
638
+ pc.role_sequence.all_role_ref.detect do |rr|
639
+ !start_roles.include?(rr.role) # and span only valid roles
640
+ end || # and not be the full absorption path
641
+ ( # Reject a constraint that caused full absorption
642
+ pc.role_sequence.all_role_ref.size == 1 and
643
+ mapping.is_a?(MM::Absorption) and
644
+ fa = mapping.full_absorption and
645
+ pc.role_sequence.all_role_ref.single.role.base_role == fa.absorption.parent_role.base_role
646
+ )
647
+ end # Alethic uniqueness constraint on far end
648
+
649
+ non_absorption_pcs = pcs.reject do |pc|
650
+ # An absorption PC is a PC that covers some role that is involved in a FullAbsorption
651
+ full_absorptions =
652
+ pc.
653
+ role_sequence.
654
+ all_role_ref.
655
+ map(&:role).
656
+ flat_map do |role|
657
+ (role.all_absorption_as_parent_role.to_a + role.all_absorption_as_child_role.to_a).
658
+ select do |abs|
659
+ abs.full_absorption && abs.full_absorption.composition == @composition
660
+ end
661
+ end
662
+ full_absorptions.size > 0
663
+ end
664
+ pcs = non_absorption_pcs
665
+
666
+ trace :relational_paths, "Uniqueness Constraints for #{mapping.object_type.name}" do
667
+ pcs.each do |pc|
668
+ trace :relational_paths, "#{pc.describe.inspect}#{pc.is_preferred_identifier ? ' (PI)' : ''}"
669
+ end
670
+ end
671
+
672
+ pcs
570
673
  end
571
674
 
572
675
  def make_new_paths mapping, existing_pcs, pcs
573
- newpaths = {}
574
- new_pcs = pcs-existing_pcs
575
- trace :relational_paths?, "Adding #{new_pcs.size} new indices for presence constraints on #{mapping.inspect}" do
576
- new_pcs.each do |pc|
577
- newpaths[pc] = index = @constellation.Index(:new, composite: mapping.root, is_unique: true, presence_constraint: pc)
578
- if mapping.object_type.preferred_identifier == pc and
579
- !@composition.all_full_absorption[mapping.object_type] and
580
- !mapping.root.primary_index
581
- index.composite_as_primary_index = mapping.root
582
- end
583
- trace :relational_paths, "Added new index #{index.inspect} for #{pc.describe} on #{pc.role_sequence.all_role_ref.map(&:role).map(&:fact_type).map(&:default_reading).inspect}"
584
- end
585
- end
586
- newpaths
676
+ newpaths = {}
677
+ new_pcs = pcs-existing_pcs
678
+ trace :relational_paths?, "Adding #{new_pcs.size} new indices for presence constraints on #{mapping.inspect}" do
679
+ new_pcs.each do |pc|
680
+ newpaths[pc] = index = @constellation.Index(:new, composite: mapping.root, is_unique: true, presence_constraint: pc)
681
+ if mapping.object_type.preferred_identifier == pc and
682
+ !@composition.all_full_absorption[mapping.object_type] and
683
+ !mapping.root.natural_index
684
+ mapping.root.natural_index = index
685
+ mapping.root.primary_index ||= index # Not if we have a surrogate already
686
+ end
687
+ trace :relational_paths, "Added new index #{index.inspect} for #{pc.describe} on #{pc.role_sequence.all_role_ref.map(&:role).map(&:fact_type).map(&:default_reading).inspect}"
688
+ end
689
+ end
690
+ newpaths
587
691
  end
588
692
 
589
693
  def relevant_paths path_hash, component
590
- rel = {} # REVISIT: return a hash subset of path_hash containing paths relevant to this component
591
- case component
592
- when MM::Absorption
593
- role = component.child_role.base_role
594
- when MM::Indicator
595
- role = component.role
596
- else
597
- return rel # Can't participate in an AccessPath
598
- end
599
-
600
- path_hash.each do |pc, path|
601
- next unless pc.role_sequence.all_role_ref.detect{|rr| rr.role == role}
602
- rel[pc] = path
603
- end
604
- rel
694
+ rel = {} # REVISIT: return a hash subset of path_hash containing paths relevant to this component
695
+ case component
696
+ when MM::Absorption
697
+ role = component.child_role.base_role
698
+ when MM::Indicator
699
+ role = component.role
700
+ else
701
+ return rel # Can't participate in an AccessPath
702
+ end
703
+
704
+ path_hash.each do |pc, path|
705
+ next unless pc.role_sequence.all_role_ref.detect{|rr| rr.role == role}
706
+ rel[pc] = path
707
+ end
708
+ rel
605
709
  end
606
710
 
607
711
  def augment_paths paths, mapping
608
- return unless MM::Indicator === mapping || MM::ValueType === mapping.object_type
609
-
610
- if MM::ValueField === mapping && mapping.parent.composite # ValueType that's a composite (table) by itself
611
- # This AccessPath has exactly one field and no presence constraint, so just make the index.
612
- existing_pk = mapping.parent.composite.primary_index
613
- paths[nil] = @constellation.Index(:new, composite: mapping.root, is_unique: true, presence_constraint: nil, composite_as_primary_index: existing_pk ? nil : mapping.root)
614
- end
615
-
616
- paths.each do |pc, path|
617
- trace :relational_paths, "Adding #{mapping.inspect} to #{path.inspect}" do
618
- case path
619
- when MM::Index
620
- @constellation.IndexField(access_path: path, ordinal: path.all_index_field.size, component: mapping)
621
- when MM::ForeignKey
622
- @constellation.ForeignKeyField(foreign_key: path, ordinal: path.all_foreign_key_field.size, component: mapping)
623
- end
624
- end
625
- end
712
+ return unless MM::Indicator === mapping || MM::ValueType === mapping.object_type
713
+
714
+ if MM::ValueField === mapping && mapping.parent.composite # ValueType that's a composite (table) by itself
715
+ # This AccessPath has exactly one field and no presence constraint, so just make the index.
716
+ composite = mapping.parent.composite
717
+ paths[nil] =
718
+ index = @constellation.Index(:new, composite: mapping.root, is_unique: true, presence_constraint: nil, composite_as_natural_index: composite)
719
+ composite.primary_index ||= index
720
+ end
721
+
722
+ paths.each do |pc, path|
723
+ trace :relational_paths, "Adding #{mapping.inspect} to #{path.inspect}" do
724
+ case path
725
+ when MM::Index
726
+ @constellation.IndexField(access_path: path, ordinal: path.all_index_field.size, component: mapping)
727
+ when MM::ForeignKey
728
+ @constellation.ForeignKeyField(foreign_key: path, ordinal: path.all_foreign_key_field.size, component: mapping)
729
+ end
730
+ end
731
+ end
626
732
  end
627
733
 
628
734
  def complete_foreign_keys
629
- trace :relational_paths, "Completing foreign keys" do
630
- @composition.all_composite.each do |composite|
631
- composite.all_access_path.each do |path|
632
- next if MM::Index === path
633
-
634
- target_object_type = path.absorption.child_role.object_type
635
- while fa = target_object_type.all_full_absorption[@composition]
636
- target_object_type = fa.absorption.parent_role.object_type
637
- end
638
- target = @composites[target_object_type]
639
- trace :relational_paths, "Completing #{path.inspect} to #{target.mapping.inspect}"
640
- if target.primary_index
641
- target.primary_index.all_index_field.each do |index_field|
642
- @constellation.IndexField access_path: path, ordinal: index_field.ordinal, component: index_field.component
643
- end
644
- else
645
- raise "Foreign key from #{path.source_composite.mapping.name} references target table #{target.mapping.name} which has no primary index"
646
- end
647
- end
648
- end
649
- end
735
+ trace :relational_paths, "Completing foreign keys" do
736
+ @composition.all_composite.each do |composite|
737
+ composite.all_access_path.each do |path|
738
+ next if MM::Index === path
739
+
740
+ next if path.all_foreign_key_field.size == path.all_index_field.size
741
+ target_object_type = path.absorption.child_role.object_type
742
+ while fa = target_object_type.all_full_absorption[@composition]
743
+ target_object_type = fa.absorption.parent_role.object_type
744
+ end
745
+ target = @composites[target_object_type]
746
+ prefer_natural = prefer_natural_key(false, composite, target)
747
+ trace :relational_paths, "Completing #{path.inspect} to #{target.mapping.inspect}"
748
+ index = (prefer_natural && target.natural_index) || target.primary_index
749
+ if index
750
+ index.all_index_field.each do |index_field|
751
+ @constellation.IndexField access_path: path, ordinal: index_field.ordinal, component: index_field.component
752
+ end
753
+ else
754
+ raise "Foreign key from #{path.source_composite.mapping.name} references target table #{target.mapping.name} which has no primary index"
755
+ end
756
+ end
757
+ end
758
+ end
650
759
  end
651
760
 
652
761
  def fork_component_to_new_parent parent, component
653
- case component
654
- # A place to put more special cases.
655
- when MM::ValueField
656
- # When we fork from a ValueField, we want to use the name of the ValueType, not the ValueField name
657
- @constellation.fork component, guid: :new, parent: parent, name: component.object_type.name
658
- else
659
- @constellation.fork component, guid: :new, parent: parent
660
- end
762
+ case component
763
+ # A place to put more special cases.
764
+ when MM::ValueField
765
+ # When we fork from a ValueField, we want to use the name of the ValueType, not the ValueField name
766
+ @constellation.fork component, guid: :new, parent: parent, name: component.object_type.name
767
+ else
768
+ @constellation.fork component, guid: :new, parent: parent
769
+ end
661
770
  end
662
771
 
663
772
  # Make a new Absorption in the reverse direction from the one given
664
773
  def mirror absorption, parent
665
- @constellation.fork(
666
- absorption,
667
- guid: :new,
668
- object_type: absorption.parent_role.object_type,
669
- parent: parent,
670
- parent_role: absorption.child_role,
671
- child_role: absorption.parent_role,
672
- ordinal: 0,
673
- name: role_name(absorption.parent_role)
674
- )
774
+ @constellation.fork(
775
+ absorption,
776
+ guid: :new,
777
+ object_type: absorption.parent_role.object_type,
778
+ parent: parent,
779
+ parent_role: absorption.child_role,
780
+ child_role: absorption.parent_role,
781
+ ordinal: 0,
782
+ name: role_name(absorption.parent_role)
783
+ )
675
784
  end
676
785
 
677
786
  # A candidate is a Mapping of an object type which may become a Composition (a table, in relational-speak)
678
787
  class Candidate
679
- attr_reader :mapping, :is_table, :is_tentative
680
- attr_accessor :full_absorption
681
-
682
- def initialize compositor, mapping
683
- @compositor = compositor
684
- @mapping = mapping
685
- end
686
-
687
- def object_type
688
- @mapping.object_type
689
- end
690
-
691
- # References from us are things we can own (non-Mappings) or have a unique forward absorption for
692
- def references_from
693
- @mapping.all_member.select{|m| !m.is_a?(MM::Absorption) or !m.forward_absorption && m.parent_role.is_unique }
694
- end
695
- alias_method :rf, :references_from
696
-
697
- # References to us are reverse absorptions where the forward absorption can absorb us
698
- def references_to
699
- @mapping.all_member.select{|m| m.is_a?(MM::Absorption) and f = m.forward_absorption and f.parent_role.is_unique}
700
- end
701
- alias_method :rt, :references_to
702
-
703
- def has_references
704
- @mapping.all_member.select{|m| m.is_a?(MM::Absorption) }
705
- end
706
-
707
- def definitely_not_table
708
- @is_tentative = @is_table = false
709
- end
710
-
711
- def definitely_table
712
- @is_tentative = false
713
- @is_table = true
714
- end
715
-
716
- def probably_not_table
717
- @is_tentative = true
718
- @is_table = false
719
- end
720
-
721
- def probably_table
722
- @is_tentative = @is_table = true
723
- end
724
-
725
- def assign_default composition
726
- o = object_type
727
- if o.is_separate
728
- trace :relational_defaults, "#{o.name} is a table because it's declared independent or separate"
729
- definitely_table
730
- return
731
- end
732
-
733
- case o
734
- when MM::ValueType
735
- if o.is_auto_assigned
736
- trace :relational_defaults, "#{o.name} is not a table because it is auto assigned"
737
- definitely_not_table
738
- elsif references_from.size > 0
739
- trace :relational_defaults, "#{o.name} is a table because it has references to absorb"
740
- definitely_table
741
- else
742
- trace :relational_defaults, "#{o.name} is not a table because it will be absorbed wherever needed"
743
- definitely_not_table
744
- end
745
-
746
- when MM::EntityType
747
- if references_to.empty? and
748
- !references_from.detect do |absorption| # detect whether anything can absorb this entity type
749
- absorption.is_a?(MM::Mapping) && absorption.parent_role.is_unique && absorption.child_role.is_unique
750
- end
751
- trace :relational_defaults, "#{o.name} is a table because it has nothing to absorb it"
752
- definitely_table
753
- return
754
- end
755
- if !o.supertypes.empty?
756
- # We know that this entity type is not a separate or partitioned subtype, so a supertype that can absorb us does
757
- identifying_fact_type = o.all_type_inheritance_as_subtype.detect{|ti| ti.provides_identification}
758
- if identifying_fact_type
759
- fact_type = identifying_fact_type
760
- else
761
- if o.all_type_inheritance_as_subtype.size > 1
762
- trace :relational_defaults, "REVISIT: #{o.name} cannot be absorbed into a supertype that doesn't also absorb all our other supertypes (or is absorbed into one of its supertypes that does)"
763
- end
764
- fact_type = o.all_type_inheritance_as_subtype.to_a[0]
765
- end
766
-
767
- absorbing_ref = mapping.all_member.detect{|m| m.is_a?(MM::Absorption) && m.child_role.fact_type == fact_type}
768
-
769
- absorbing_ref = absorbing_ref.flip! if absorbing_ref.reverse_absorption # We were forward, but the other end must be
770
- absorbing_ref = absorbing_ref.forward_absorption
771
- self.full_absorption =
772
- o.constellation.FullAbsorption(composition: composition, absorption: absorbing_ref, object_type: o)
773
- trace :relational_defaults, "Supertype #{fact_type.supertype_role.name} absorbs subtype #{o.name}"
774
- definitely_not_table
775
- return
776
- end # subtype
777
-
778
- # If the preferred_identifier consists of a ValueType that's auto-assigned ON COMMIT (like an SQL sequence),
779
- # that can only happen in one table, which controls the sequence.
780
- auto_assigned_identifying_role_player = nil
781
- pi_role_refs = o.preferred_identifier.role_sequence.all_role_ref
782
- if pi_role_refs.size == 1 and
783
- rr = pi_role_refs.single and
784
- (v = rr.role.object_type).is_a?(MM::ValueType) and
785
- v.is_auto_assigned == 'commit'
786
- auto_assigned_identifying_role_player = v
787
- end
788
- if (@compositor.options['single_sequence'] || references_to.size > 1) and auto_assigned_identifying_role_player # Can be absorbed in more than one place
789
- trace :relational_defaults, "#{o.name} must be a table to support its auto-assigned identifier #{auto_assigned_identifying_role_player.name}"
790
- definitely_table
791
- return
792
- end
793
-
794
- trace :relational_defaults, "#{o.name} is initially presumed to be a table"
795
- probably_table
796
-
797
- end # case
798
- end
788
+ attr_reader :mapping, :is_table, :is_tentative
789
+ attr_accessor :full_absorption
790
+
791
+ def initialize compositor, mapping
792
+ @compositor = compositor
793
+ @mapping = mapping
794
+ end
795
+
796
+ def object_type
797
+ @mapping.object_type
798
+ end
799
+
800
+ # References from us are things we can own (non-Mappings) or have a unique forward absorption for
801
+ def references_from
802
+ @mapping.all_member.select{|m| !m.is_a?(MM::Absorption) or !m.forward_absorption && m.parent_role.is_unique }
803
+ end
804
+ alias_method :rf, :references_from
805
+
806
+ # References to us are reverse absorptions where the forward absorption can absorb us
807
+ def references_to
808
+ @mapping.all_member.select{|m| m.is_a?(MM::Absorption) and f = m.forward_absorption and f.parent_role.is_unique}
809
+ end
810
+ alias_method :rt, :references_to
811
+
812
+ def has_references
813
+ @mapping.all_member.select{|m| m.is_a?(MM::Absorption) }
814
+ end
815
+
816
+ def definitely_not_table
817
+ @is_tentative = @is_table = false
818
+ end
819
+
820
+ def definitely_table
821
+ @is_tentative = false
822
+ @is_table = true
823
+ end
824
+
825
+ def probably_not_table
826
+ @is_tentative = true
827
+ @is_table = false
828
+ end
829
+
830
+ def probably_table
831
+ @is_tentative = @is_table = true
832
+ end
833
+
834
+ def assign_default composition
835
+ o = object_type
836
+ if o.is_separate
837
+ trace :relational_defaults, "#{o.name} is a table because it's declared independent or separate"
838
+ definitely_table
839
+ return
840
+ end
841
+
842
+ case o
843
+ when MM::ValueType
844
+ if o.is_auto_assigned
845
+ trace :relational_defaults, "#{o.name} is not a table because it is auto assigned"
846
+ definitely_not_table
847
+ elsif references_from.size > 0
848
+ trace :relational_defaults, "#{o.name} is a table because it has references to absorb"
849
+ definitely_table
850
+ else
851
+ trace :relational_defaults, "#{o.name} is not a table because it will be absorbed wherever needed"
852
+ definitely_not_table
853
+ end
854
+
855
+ when MM::EntityType
856
+ if references_to.empty? and
857
+ !references_from.detect do |absorption| # detect whether anything can absorb this entity type
858
+ absorption.is_a?(MM::Mapping) && absorption.parent_role.is_unique && absorption.child_role.is_unique
859
+ end
860
+ trace :relational_defaults, "#{o.name} is a table because it has nothing to absorb it"
861
+ definitely_table
862
+ return
863
+ end
864
+ if !o.supertypes.empty?
865
+ # We know that this entity type is not a separate or partitioned subtype, so a supertype that can absorb us does
866
+ identifying_fact_type = o.all_type_inheritance_as_subtype.detect{|ti| ti.provides_identification}
867
+ if identifying_fact_type
868
+ fact_type = identifying_fact_type
869
+ else
870
+ if o.all_type_inheritance_as_subtype.size > 1
871
+ trace :relational_defaults, "REVISIT: #{o.name} cannot be absorbed into a supertype that doesn't also absorb all our other supertypes (or is absorbed into one of its supertypes that does)"
872
+ end
873
+ fact_type = o.all_type_inheritance_as_subtype.to_a[0]
874
+ end
875
+
876
+ absorbing_ref = mapping.all_member.detect{|m| m.is_a?(MM::Absorption) && m.child_role.fact_type == fact_type}
877
+
878
+ absorbing_ref = absorbing_ref.flip! if absorbing_ref.reverse_absorption # We were forward, but the other end must be
879
+ absorbing_ref = absorbing_ref.forward_absorption
880
+ self.full_absorption =
881
+ o.constellation.FullAbsorption(composition: composition, absorption: absorbing_ref, object_type: o)
882
+ trace :relational_defaults, "Supertype #{fact_type.supertype_role.name} fully absorbs subtype #{o.name} via #{absorbing_ref.inspect}"
883
+ definitely_not_table
884
+ return
885
+ end # subtype
886
+
887
+ # If the preferred_identifier consists of a ValueType that's auto-assigned ON COMMIT (like an SQL sequence),
888
+ # that can only happen in one table, which controls the sequence.
889
+ auto_assigned_identifying_role_player = nil
890
+ pi_role_refs = o.preferred_identifier.role_sequence.all_role_ref
891
+ if pi_role_refs.size == 1 and
892
+ rr = pi_role_refs.single and
893
+ (v = rr.role.object_type).is_a?(MM::ValueType) and
894
+ v.is_auto_assigned == 'commit'
895
+ auto_assigned_identifying_role_player = v
896
+ end
897
+ if (@compositor.options['single_sequence'] || references_to.size > 1) and auto_assigned_identifying_role_player # Can be absorbed in more than one place
898
+ trace :relational_defaults, "#{o.name} must be a table to support its auto-assigned identifier #{auto_assigned_identifying_role_player.name}"
899
+ definitely_table
900
+ return
901
+ end
902
+
903
+ trace :relational_defaults, "#{o.name} is initially presumed to be a table"
904
+ probably_table
905
+
906
+ end # case
907
+ end
799
908
 
800
909
  end
801
910