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.
@@ -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
- trace :relational_mapping, "Generating relational composition" do
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
- trace :relational_, "Full relational composition" do
45
- @composition.all_composite.sort_by{|composite| composite.mapping.name}.each do |composite|
46
- composite.mapping.show_trace
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 :relational_mapping, "Preparing relational composition by setting default assumptions" do
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 :relational_mapping, "Optimise Relational Composition" do
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 :relational_mapping, "Starting optimisation pass #{pass}" do
83
+ trace :relational_optimiser, "Starting optimisation pass #{pass}" do
75
84
  finalised = optimise_absorption_pass(undecided)
76
85
  end
77
- trace :relational_mapping, "Finalised #{finalised.size} on this pass: #{finalised.map{|f| f.name}*', '}"
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 :relational_mapping, "Considering possible status of #{object_type.name}" do
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
- trace :relational_mapping, "Absorb objectified unary #{object_type.name} into #{f.all_role.single.object_type.name}"
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
- (pi_roles = object_type.preferred_identifier_roles).size == 1 and # Our PI has one role
101
- pi_roles[0].object_type.is_a?(MM::EntityType) and # played by another Entity Type
102
- candidate.references_from.detect do |absorption|
103
- next unless absorption.is_a?(MM::Absorption)
104
- next unless absorption.child_role == pi_roles[0] # Not the identifying absorption
105
-
106
- # Look at the other end; make sure it's a forward absorption:
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
- trace :relational_mapping, "#{object_type.name} is fully absorbed along its sole reference path #{absorbing_ref.inspect}"
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 |absorption|
120
- absorption.is_a?(MM::Absorption) && pi_roles.include?(absorption.child_role.base_role)
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 :relational_mapping, "#{object_type.name} has #{non_identifying_refs_from.size} non-identifying functional roles" do
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 :relational_mapping, a.inspect
145
+ trace :relational_optimiser, a.inspect
125
146
  end
126
147
  end
127
148
 
128
- trace :relational_mapping, "#{object_type.name} has #{candidate.references_to.size} references to it" do
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 :relational_mapping, a.inspect
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 :relational_mapping, "#{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"
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 that would prevent its absorption
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
- ).reject do |a|
147
- next true unless a.is_a?(MM::Absorption)
148
- cc = @candidates[a.child_role.object_type]
149
- next true if !cc.is_table
150
- next true if !(a.child_role.is_unique && a.parent_role.is_unique)
151
-
152
- # Allow the sole identifying role for this object
153
- next false if pi_roles.size == 1 && pi_roles.include?(a.parent_role)
154
- next true unless a.parent_role.is_mandatory
155
- next true if cc.is_absorbed # REVISIT: We can be absorbed into something that's also absorbed, but not into us!
156
- false
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 :relational_mapping, "#{object_type.name} has #{absorption_paths.size} absorption paths"
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 :relational_mapping, "#{object_type.name} is fully absorbed in #{absorption_paths.size} places" do
191
+ trace :relational_optimiser, "#{object_type.name} is fully absorbed in #{absorption_paths.size} places" do
164
192
  absorption_paths.each do |a|
165
- flip = a.reverse_absorption
166
- a.flip! if flip
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 fully absorbed (must be along an identifying role?)
177
- if non_identifying_refs_from.size == 0
178
- trace :relational_mapping, "#{object_type.name} is fully absorbed in #{candidate.references_to.size} places: #{candidate.references_to.map{|ref| ref.inspect}*", "}"
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.reverse_absorption # This is the reverse of some absorption
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
- @constellation.Composite.each do |key, composite|
250
+ @composition.all_composite.each do |composite|
204
251
  mapping = composite.mapping
205
- if mapping.object_type.is_a?(MM::ValueType) and !mapping.all_member.detect{|m| m.is_a?(MM::ValueField)}
206
- trace :relational_mapping, "Adding value field for #{mapping.object_type.name}"
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
- # After all table/non-table decisions are made, convert Mappings for tables into Composites and retract the rest:
219
- def make_composites
220
- @candidates.keys.to_a.each do |object_type|
221
- candidate = @candidates[object_type]
222
- mapping = candidate.mapping
223
- if candidate.is_table
224
- composite = @constellation.Composite(mapping, composition: @composition)
225
- else
226
- @candidates.delete(object_type)
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 :relational_mapping, "Absorbing full contents of all tables" do
383
+ trace :relational_columns!, "Computing contents of all tables" do
244
384
  @composition.all_composite_by_name.each do |composite|
245
- trace :relational_mapping, "Absorbing contents of #{composite.mapping.name}" do
246
- absorb_all composite.mapping, nil
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
- def absorb_all mapping, from
253
- (from||mapping).all_member.each do |member|
254
- member = fork_component_to_new_parent mapping, member if from # Top-level members are already instantiated
255
- if member.is_a?(MM::Absorption)
256
- # Should we absorb a foreign key or the whole contents?
257
- table = @candidates[member.child_role.object_type]
258
- trace :relational_mapping, "Absorbing #{table ? 'key' : 'contents'} of #{member.child_role.name} in #{member.inspect_reading}" do
259
- target = @binary_mappings[member.child_role.object_type]
260
- if table
261
- absorb_key member, target
262
- else
263
- absorb_all member, target
264
- end
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
- # mapping.re_rank
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.all_member.each do |member|
275
- next unless member.rank_key[0] == MM::Component::RANK_IDENT
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
- if member.is_a?(MM::Absorption)
278
- absorb_key member, @binary_mappings[member.child_role.object_type]
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 :is_absorbed
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 absorb and have a forward absorption for
687
+ # References from us are things we can own (non-Mappings) or have a unique forward absorption for
309
688
  def references_from
310
- # Anything that's not a Mapping must be an Absorption
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 things that can absorb us and they have a forward absorption for
693
+ # References to us are reverse absorptions where the forward absorption can absorb us
316
694
  def references_to
317
- @mapping.all_member.map{|m| m.is_a?(MM::Mapping) ? m.reverse_absorption : nil}.compact.select{|r| r.parent_role.is_unique }
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 :relational_mapping, "#{o.name} is a table because it's declared independent or separate"
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 :relational_mapping, "#{o.name} is not a table because it is auto assigned"
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 :relational_mapping, "#{o.name} is a table because it has references to absorb"
735
+ trace :relational_defaults, "#{o.name} is a table because it has references to absorb"
358
736
  definitely_table
359
737
  else
360
- trace :relational_mapping, "#{o.name} is not a table because it will be absorbed wherever needed"
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 :relational_mapping, "#{o.name} is a table because it has nothing to absorb it"
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.preferred_identifier.role_sequence.all_role_ref.to_a[0].role.fact_type
376
- if identifying_fact_type.is_a?(MM::TypeInheritance)
377
- trace :relational_mapping, "#{o.name} is absorbed into supertype #{identifying_fact_type.supertype_role.name}"
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
- trace :relational_mapping, "Subtype #{o.name} is initially presumed to be a table"
381
- probably_not_table
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
- v = nil
387
- if references_to.size > 1 and # Can be absorbed in more than one place
388
- o.preferred_identifier.role_sequence.all_role_ref.detect do |rr|
389
- (v = rr.role.object_type).is_a?(MM::ValueType) and v.is_auto_assigned
390
- end
391
- trace :relational_mapping, "#{o.name} must be a table to support its auto-assigned identifier #{v.name}"
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 :relational_mapping, "#{o.name} is initially presumed to be a table"
790
+ trace :relational_defaults, "#{o.name} is initially presumed to be a table"
397
791
  probably_table
398
792
 
399
793
  end # case