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