activefacts-compositions 1.9.1 → 1.9.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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