activefacts-compositions 1.9.6 → 1.9.8

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