activefacts-compositions 1.9.6 → 1.9.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,75 +9,95 @@ module ActiveFacts
9
9
  module Metamodel
10
10
  class Composition
11
11
  def retract_constraint_classifications
12
- all_composite.each(&:retract_constraint_classifications)
12
+ all_composite.each(&:retract_constraint_classifications)
13
13
  end
14
14
 
15
15
  def classify_constraints
16
- retract_constraint_classifications
17
- all_composite.each(&:classify_constraints)
16
+ retract_constraint_classifications
17
+ all_composite.each(&:classify_constraints)
18
+ end
19
+ end
20
+
21
+ class Component
22
+ def gather_constraints all_composite_roles = [], all_composite_constraints = [], constraints_by_leaf = {}
23
+ all_role.each do |role|
24
+ all_composite_roles << role
25
+ role.all_constraint.each do |constraint|
26
+ # Exclude single-role mandatory constraints and all uniqueness constraints:
27
+ next if constraint.is_a?(PresenceConstraint) and
28
+ constraint.max_frequency == 1 ||
29
+ (constraint.role_sequence.all_role_ref.size == 1 && constraint.min_frequency == 1 && constraint.is_mandatory)
30
+ all_composite_constraints << constraint
31
+ (constraints_by_leaf[self] ||= []) << constraint
32
+ end
33
+ end
34
+
35
+ gather_nested_constraints all_composite_roles, all_composite_constraints, constraints_by_leaf
36
+ end
37
+
38
+ def gather_nested_constraints all_composite_roles, all_composite_constraints, constraints_by_leaf
39
+ all_member.each do |member|
40
+ member.gather_constraints all_composite_roles, all_composite_constraints, constraints_by_leaf
41
+ end
42
+ end
43
+ end
44
+
45
+ class Absorption
46
+ def gather_nested_constraints all_composite_roles, all_composite_constraints, constraints_by_leaf
47
+ return if foreign_key # This has gone far enough!
48
+ super
18
49
  end
19
50
  end
20
51
 
21
52
  class Composite
22
53
  def retract_constraint_classifications
23
- all_spanning_constraint.to_a.each(&:retract)
24
- all_local_constraint.to_a.each(&:retract)
25
- mapping.leaves.each do |component|
26
- component.all_leaf_constraint.to_a.each(&:retract)
27
- end
54
+ all_spanning_constraint.to_a.each(&:retract)
55
+ all_local_constraint.to_a.each(&:retract)
56
+ mapping.all_leaf.each do |component|
57
+ component.all_leaf_constraint.to_a.each(&:retract)
58
+ end
28
59
  end
29
60
 
30
61
  def classify_constraints
31
- leaves = mapping.leaves
32
-
33
- # Categorise and index all constraints not already baked-in to the composition
34
- all_composite_roles = []
35
- all_composite_constraints = []
36
- constraints_by_leaf = {}
37
- leaves.each do |leaf|
38
- all_composite_roles += leaf.path.flat_map(&:all_role) # May be non-unique, fix later
39
- leaf.all_role.each do |role|
40
- role.all_constraint.each do |constraint|
41
- if constraint.is_a?(PresenceConstraint)
42
- # Exclude single-role mandatory constraints and all uniqueness constraints:
43
- if constraint.role_sequence.all_role_ref.size == 1 && constraint.min_frequency == 1 && constraint.is_mandatory or
44
- constraint.max_frequency == 1
45
- next
46
- end
47
- end
48
- all_composite_constraints << constraint
49
- (constraints_by_leaf[leaf] ||= []) << constraint
50
- end
51
- end
52
- end
62
+ leaves = mapping.all_leaf
53
63
 
54
- all_composite_roles.uniq!
55
- all_composite_constraints.uniq!
56
- spanning_constraints =
57
- all_composite_constraints.reject do |constraint|
58
- (constraint.all_constrained_role-all_composite_roles).size == 0
59
- end
60
- local_constraints = all_composite_constraints - spanning_constraints
64
+ # Categorise and index all constraints not already baked-in to the composition
65
+ # We recurse down the hierarchy, stopping at any foreign keys
66
+ all_composite_roles = []
67
+ all_composite_constraints = []
68
+ constraints_by_leaf = {}
69
+ mapping.gather_constraints all_composite_roles, all_composite_constraints, constraints_by_leaf
70
+ all_composite_roles.uniq!
71
+ all_composite_constraints.uniq!
61
72
 
62
- spanning_constraints.each do |spanning_constraint|
63
- constellation.SpanningConstraint(composite: self, spanning_constraint: spanning_constraint)
64
- end
73
+ # Spanning constraints constrain some role outside this composite:
74
+ spanning_constraints =
75
+ all_composite_constraints.select do |constraint|
76
+ (constraint.all_constrained_role-all_composite_roles).size > 0
77
+ end
78
+ spanning_constraints.each do |spanning_constraint|
79
+ constellation.SpanningConstraint(composite: self, spanning_constraint: spanning_constraint)
80
+ end
65
81
 
66
- leaves.each do |leaf|
67
- # Find any constraints that affect just this leaf:
68
- leaf_constraints = (constraints_by_leaf[leaf]||[]).
69
- reject do |constraint|
70
- (constraint.all_constrained_role - leaf.all_role).size > 0
71
- end
72
- local_constraints -= leaf_constraints
73
- leaf_constraints.each do |leaf_constraint|
74
- constellation.LeafConstraint(component: leaf, leaf_constraint: leaf_constraint)
75
- end
76
- end
82
+ # Local and leaf constraints are what remains. Extract the leaf constraints:
83
+ local_constraints = all_composite_constraints - spanning_constraints
84
+ leaves.each do |leaf|
85
+ # Find any constraints that affect just this leaf:
86
+ leaf_constraints =
87
+ leaf.path.flat_map{|component| Array(constraints_by_leaf[component]) }.
88
+ select do |constraint|
89
+ # Does this constraint constrain only this leaf?
90
+ (constraint.all_constrained_role - leaf.path.flat_map(&:all_role)).size == 0
91
+ end
92
+ leaf_constraints.each do |leaf_constraint|
93
+ constellation.LeafConstraint(component: leaf, leaf_constraint: leaf_constraint)
94
+ end
95
+ local_constraints -= leaf_constraints
96
+ end
77
97
 
78
- local_constraints.each do |local_constraint|
79
- constellation.LocalConstraint(composite: self, local_constraint: local_constraint)
80
- end
98
+ local_constraints.each do |local_constraint|
99
+ constellation.LocalConstraint(composite: self, local_constraint: local_constraint)
100
+ end
81
101
 
82
102
  end
83
103
  end
@@ -0,0 +1,545 @@
1
+ #
2
+ # ActiveFacts Compositions, DataVault Compositor.
3
+ #
4
+ # Computes a Data Vault schema.
5
+ #
6
+ # Copyright (c) 2015 Clifford Heath. Read the LICENSE file.
7
+ #
8
+ require "activefacts/compositions/relational"
9
+
10
+ module ActiveFacts
11
+ module Compositions
12
+ class DataVault < Relational
13
+ public
14
+ def self.options
15
+ {
16
+ reference: ['Boolean', "Emit the reference (static) tables as well. Default is to omit them"],
17
+ datestamp: ['String', "Use this data type for date stamps"],
18
+ id: ['String', "Append this to data vault surrogate keys (default VID)"],
19
+ hubname: ['String', "Suffix or pattern for naming hub tables. Include a + to insert the name. Default 'HUB'"],
20
+ linkname: ['String', "Suffix or pattern for naming link tables. Include a + to insert the name. Default 'LINK'"],
21
+ satname: ['String', "Suffix or pattern for naming satellite tables. Include a + to insert the name. Default 'SAT'"],
22
+ refname: ['String', "Suffix or pattern for naming reference tables. Include a + to insert the name. Default '+'"],
23
+ }
24
+ end
25
+
26
+ def initialize constellation, name, options = {}
27
+ # Extract recognised options:
28
+ @option_reference = options.delete('reference')
29
+ @option_datestamp = options.delete('datestamp')
30
+ @option_id = ' ' + (options.delete('id') || 'VID')
31
+ @option_hub_name = options.delete('hubname') || 'HUB'
32
+ @option_hub_name.sub!(/^/,'+ ') unless @option_hub_name =~ /\+/
33
+ @option_link_name = options.delete('linkname') || 'LINK'
34
+ @option_link_name.sub!(/^/,'+ ') unless @option_link_name =~ /\+/
35
+ @option_sat_name = options.delete('satname') || 'SAT'
36
+ @option_sat_name.sub!(/^/,'+ ') unless @option_sat_name =~ /\+/
37
+ @option_ref_name = options.delete('refname') || 'SAT'
38
+ @option_ref_name.sub!(/^/,'+ ') unless @option_ref_name =~ /\+/
39
+
40
+ super constellation, name, options
41
+
42
+ @option_surrogates = true # Always inject surrogates regardless of superclass
43
+ end
44
+
45
+ # We need to find links that need surrogate keys before we inject the surrogates
46
+ def inject_surrogates
47
+ classify_composites
48
+
49
+ super
50
+ end
51
+
52
+ def composite_is_reference composite
53
+ object_type = composite.mapping.object_type
54
+ object_type.concept.all_concept_annotation.detect{|ca| ca.mapping_annotation == 'static'} or
55
+ !object_type.is_a?(ActiveFacts::Metamodel::EntityType)
56
+ end
57
+
58
+ # Data Vaults need a surrogate key on every Hub and Link.
59
+ # Don't add a surrogate on a Reference table!
60
+ def needs_surrogate(composite)
61
+ return false if composite_is_reference(composite)
62
+
63
+ # REVISIT: The following is debatable. If the natural primary key is an ok surrogate, should we inject another?
64
+ return true if @non_reference_composites.include?(composite)
65
+
66
+ super
67
+ end
68
+
69
+ # Change the default extension from our superclass':
70
+ def inject_surrogate composite, extension = @option_id
71
+ super
72
+ end
73
+
74
+ def classify_composites
75
+ detect_reference_tables
76
+
77
+ trace :datavault, "Classify non-reference tables into hubs and links" do
78
+ # Make an initial determination, then adjust for foreign keys to links afterwards
79
+ @key_structure = {}
80
+ @link_composites, @hub_composites =
81
+ @non_reference_composites.
82
+ sort_by{|c| c.mapping.name}.
83
+ partition do |composite|
84
+ trace :datavault, "Decide whether #{composite.mapping.name} is a link or a hub" do
85
+ @key_structure[composite] =
86
+ mapped_to =
87
+ composite_key_structure composite
88
+
89
+ # It's a Link if the preferred identifier includes more than non_reference_composite.
90
+ mapped_to.compact.size > 1
91
+ end
92
+ end
93
+
94
+ trace :datavault, "Checking for foreign keys that reference links" do
95
+ # Links may never be the target of a foreign key.
96
+ # Any such links must be defined as hubs instead.
97
+ @links_as_hubs = {}
98
+ fk_dependencies_by_target = {}
99
+ fk_dependencies_by_source = {}
100
+ (@hub_composites+@link_composites).
101
+ each do |composite|
102
+ target_composites = enumerate_foreign_keys composite.mapping
103
+ target_composites.each do |target_composite|
104
+ next if @reference_composites.include?(target_composite)
105
+ (fk_dependencies_by_target[target_composite] ||= []) << composite
106
+ (fk_dependencies_by_source[composite] ||= []) << target_composite
107
+ end
108
+ end
109
+
110
+ fk_dependencies_by_target.keys.each do |target_composite|
111
+ if @link_composites.delete(target_composite)
112
+ trace :datavault, "Link #{target_composite.inspect} must be a hub because foreign keys reference it"
113
+ @hub_composites << target_composite
114
+ @links_as_hubs[target_composite] = true
115
+ end
116
+ end
117
+
118
+ begin
119
+ converted =
120
+ @link_composites.select do |composite|
121
+ targets = fk_dependencies_by_source[composite]
122
+ id_targets = composite_key_structure(composite).compact
123
+ next if targets.size == id_targets.size
124
+ trace :datavault, "Link #{composite.mapping.name} must be a hub because it has non-identifying FK references"
125
+ @link_composites.delete(composite)
126
+ @hub_composites << composite
127
+ @links_as_hubs[composite] = true
128
+ end
129
+ end while converted.size > 0
130
+
131
+ end
132
+
133
+ # Note: We may still have hubs whose identifiers contain foreign keys to one or more other hubs.
134
+ # REVISIT: These foreign keys will be deleted so these hubs stand alone,
135
+ # but have been re-instated as new links to the referenced hubs.
136
+ end
137
+
138
+ trace :datavault_classification!, "Data Vault classification of composites:" do
139
+ trace :datavault, "Reference: #{@reference_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
140
+ trace :datavault, "Hub: #{@hub_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
141
+ trace :datavault, "Link: #{@link_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
142
+ end
143
+ end
144
+
145
+ def detect_reference_tables
146
+ initial_composites = @composition.all_composite.to_a
147
+ @reference_composites, @non_reference_composites =
148
+ initial_composites.partition { |composite| composite_is_reference(composite) }
149
+ end
150
+
151
+ def devolve_all
152
+ delete_reference_table_foreign_keys
153
+
154
+ # For each hub and link, move each non-identifying member
155
+ # to a new satellite or promote it to a new link.
156
+
157
+ @non_reference_composites.
158
+ each do |composite|
159
+ devolve composite
160
+ end
161
+
162
+ rename_parents
163
+
164
+ unless @option_reference
165
+ if trace :reference_retraction
166
+ # Add a logger so we can trace the resultant retractions:
167
+ @constellation.loggers << proc do |*args|
168
+ trace :reference_retraction, args.inspect
169
+ end
170
+ end
171
+
172
+ @reference_composites.each do |rc|
173
+ trace :reference_retraction, "Retracting #{rc.inspect}" do
174
+ rc.retract
175
+ end
176
+ end
177
+ @reference_composites = []
178
+
179
+ @constellation.loggers.pop if trace :reference_retraction
180
+ end
181
+ end
182
+
183
+ def delete_reference_table_foreign_keys
184
+ trace :datavault, "Delete foreign keys to reference tables" do
185
+ # Delete all foreign keys to reference tables
186
+ @reference_composites.each do |composite|
187
+ composite.all_foreign_key_as_target_composite.each(&:retract)
188
+ end
189
+ end
190
+ end
191
+
192
+ def prefer_natural_key building_natural_key, source_composite, target_composite
193
+ return false if building_natural_key && @link_composites.include?(source_composite)
194
+ building_natural_key && @hub_composites.include?(target_composite)
195
+ end
196
+
197
+ def composite_key_structure composite
198
+ # We know that composite.mapping.object_type is an EntityType because all ValueType composites are reference tables
199
+
200
+ object_type = composite.mapping.object_type
201
+ mapped_to =
202
+ object_type.preferred_identifier.role_sequence.all_role_ref_in_order.map do |role_ref|
203
+ player = role_ref.role.object_type
204
+ next nil if player == object_type && role_ref.role.fact_type.all_role.size == 1 # Unaries.
205
+ candidate = @candidates[player]
206
+ next nil unless candidate
207
+ # Take care of full absorption
208
+ while candidate.full_absorption
209
+ candidate = candidate.full_absorption.composition
210
+ end
211
+ @non_reference_composites.include?(c = candidate.mapping.composite) ? c : nil
212
+ end
213
+
214
+ trace :datavault, "Preferred identifier for #{composite.mapping.name} encloses foreign keys to #{mapped_to.inspect}" unless mapped_to.compact.empty?
215
+
216
+ number_of_keys = mapped_to.compact.size
217
+ number_of_values = mapped_to.size-number_of_keys
218
+ trace :datavault_classify,
219
+ if number_of_keys > 1
220
+ # Links have more than one FK to a hub in their key
221
+ "Link #{composite.mapping.name} links #{mapped_to.compact.inspect} with #{number_of_values} values"
222
+ elsif number_of_keys == 1 && number_of_values > 0
223
+ # This is a new hub with a composite key - but we will have to eliminate the foreign key to the base hub
224
+ "Augmented Hub #{composite.mapping.name} has a hub link to #{mapped_to.compact[0].inspect} and #{number_of_values} values"
225
+ elsif number_of_keys == 1
226
+ # This is a new hub with a single-part key that references another hub.
227
+ "Dependent Hub #{composite.mapping.name} is identified by another hub: #{mapped_to.compact[0].inspect}"
228
+ else
229
+ "Hub #{composite.mapping.name} has #{mapped_to.size} parts in its key"
230
+ end
231
+
232
+ mapped_to
233
+ end
234
+
235
+ # For each member of this composite, decide whether to devolve it to a satellite
236
+ # or to a new link. If it goes to a link that's still part of this natural key,
237
+ # we need to leave that key intact, but remove the foreign key it entails.
238
+ #
239
+ # New links and satellites get new fields for the load date-time and a
240
+ # references to the surrogate(s) on the hub or link, and add an index over
241
+ # those two fields.
242
+ def devolve composite, devolve_links = true
243
+ trace :datavault?, "Devolving non-identifying fields for #{composite.inspect}" do
244
+ # Find the members of this mapping that contain identifying leaves:
245
+ pi = composite.primary_index
246
+ ni = composite.natural_index
247
+ identifiers =
248
+ (Array(pi ? pi.all_index_field : nil) +
249
+ Array(ni ? ni.all_index_field : nil)).
250
+ map{|ixf| ixf.component.path[1]}.
251
+ uniq
252
+
253
+ satellites = {}
254
+ is_link = @link_composites.include?(composite) || @links_as_hubs.include?(composite)
255
+ composite.mapping.all_member.to_a.each do |member|
256
+
257
+ # Any member that is the absorption of a foreign key to a hub or link
258
+ # (which is all, since we removed FKs to reference tables)
259
+ # must be converted to a Mapping for a new Entity Type that notionally
260
+ # objectifies the absorbed fact type. This Mapping is a new link composite.
261
+ if devolve_links && member.is_a?(MM::Absorption) && member.foreign_key
262
+ next if is_link
263
+ devolve_absorption_to_link member, identifiers.include?(member)
264
+ next
265
+ end
266
+
267
+ # If this member is in the natural or surrogate key, leave it there
268
+ # REVISIT: But if it is an FK to another hub, devolve it to a link as well.
269
+ next if identifiers.include?(member)
270
+
271
+ # We may absorb a subtype that has no contents. There's no point moving these to a satellite.
272
+ next if is_empty_inheritance member
273
+
274
+ satellite_name = name_satellite member
275
+ satellite = satellites[satellite_name]
276
+ unless satellite
277
+ satellite =
278
+ satellites[satellite_name] =
279
+ create_satellite satellite_name, composite
280
+ end
281
+
282
+ devolve_member_to_satellite satellite, member
283
+ end
284
+ composite.mapping.re_rank
285
+
286
+ if @hub_composites.include?(composite)
287
+ # Links-as-hubs have foreign keys over natural indexes; these must be deleted.
288
+ composite.all_foreign_key_as_source_composite.to_a.each(&:retract)
289
+ end
290
+
291
+ # Add the audit and identity fields for the satellite:
292
+ satellites.values.each do |satellite|
293
+ audit_satellite composite, satellite
294
+ end
295
+ end
296
+ end
297
+
298
+ # Add the audit and foreign key fields to a satellite for this composite
299
+ def audit_satellite composite, satellite
300
+ trace :datavault, "Adding parent key and load time to satellite #{satellite.mapping.name.inspect}" do
301
+
302
+ # Add a Surrogate foreign Key to the parent composite
303
+ fk_target = composite.primary_index.all_index_field.single
304
+ fk_field = fork_component_to_new_parent(satellite.mapping, fk_target.component)
305
+
306
+ # Add a load DateTime value
307
+ date_field = @constellation.ValidFrom(
308
+ :new,
309
+ parent: satellite.mapping,
310
+ name: "Load"+datestamp_type_name,
311
+ object_type: datestamp_type
312
+ )
313
+
314
+ # Add a natural key:
315
+ natural_index =
316
+ @constellation.Index(:new, composite: satellite, is_unique: true,
317
+ presence_constraint: nil, composite_as_natural_index: satellite, composite_as_primary_index: satellite)
318
+ @constellation.IndexField(access_path: natural_index, ordinal: 0, component: fk_field)
319
+ @constellation.IndexField(access_path: natural_index, ordinal: 1, component: date_field)
320
+
321
+ # REVISIT: re-ranking members without a preferred_identifier does not rank the PK fields in order.
322
+ satellite.mapping.re_rank
323
+
324
+ # Add a foreign key to the hub
325
+ fk = @constellation.ForeignKey(
326
+ :new,
327
+ source_composite: satellite,
328
+ composite: composite,
329
+ absorption: nil # REVISIT: This is a ForeignKey without its mandatory Absorption. That's gonna hurt
330
+ )
331
+ @constellation.ForeignKeyField(foreign_key: fk, ordinal: 0, component: fk_field)
332
+ # This should be filled in by complete_foreign_keys, but there is no Absorption
333
+ @constellation.IndexField(access_path: fk, ordinal: 0, component: fk_target.component)
334
+
335
+ satellite.classify_constraints
336
+ satellite.all_local_constraint.map(&:local_constraint).each(&:retract)
337
+ leaf_constraints = satellite.mapping.all_leaf.flat_map(&:all_leaf_constraint).map(&:leaf_constraint).each(&:retract)
338
+ end
339
+ end
340
+
341
+ def datestamp_type_name
342
+ @datestamp_type_name ||= begin
343
+ [true, '', 'true', 'yes', nil].include?(t = @option_datestamp) ? 'DateTime' : t
344
+ end
345
+ end
346
+
347
+ def datestamp_type
348
+ @datestamp_type ||= begin
349
+ vocabulary = @composition.all_composite.to_a[0].mapping.object_type.vocabulary
350
+ @constellation.ObjectType[[[vocabulary.name], datestamp_type_name]] or
351
+ @constellation.ValueType(
352
+ vocabulary: vocabulary,
353
+ name: datestamp_type_name,
354
+ concept: [:new, :implication_rule => "datestamp injection"]
355
+ )
356
+ end
357
+ end
358
+
359
+ # Decide what to call a new satellite that will adopt this component
360
+ def name_satellite component
361
+ satellite_name =
362
+ if component.is_a?(MM::Absorption)
363
+ pc = component.parent_role.base_role.uniqueness_constraint and
364
+ pc.concept.all_concept_annotation.map{|ca| ca.mapping_annotation =~ /^satellite *(.*)/ && $1}.compact.uniq[0]
365
+ # REVISIT: How do we name the satellite for an Indicator? Add a Concept Annotation on the fact type?
366
+ end
367
+ satellite_name = satellite_name.words.capcase if satellite_name
368
+ satellite_name ||= component.root.mapping.name
369
+ satellite_name = apply_name(@option_sat_name, satellite_name)
370
+ end
371
+
372
+ # Create a new satellite for the same object_type as this composite
373
+ def create_satellite name, composite
374
+ mapping = @constellation.Mapping(:new, name: name, object_type: composite.mapping.object_type)
375
+ @constellation.Composite(mapping, composition: @composition)
376
+ end
377
+
378
+ # This component is being moved to a new composite, so any indexes that it or its
379
+ # children contribute to, cannot now be used to search for the specified composite.
380
+ # A component being moved to a satellite or a hub cannot keep its indices.
381
+ def remove_indices component
382
+ component.all_index_field.map(&:access_path).uniq.each(&:retract)
383
+ component.all_member.each{|member| remove_indices member}
384
+ end
385
+
386
+ def change_all_fk_source component, source_composite
387
+ if component.is_a?(MM::Absorption) && component.foreign_key
388
+ trace :datavault, "Setting new source composite for #{component.foreign_key.inspect}"
389
+ component.foreign_key.source_composite = source_composite
390
+ end
391
+
392
+ component.all_member.each do |member|
393
+ change_all_fk_source member, source_composite
394
+ end
395
+ end
396
+
397
+ # Move this member from its current parent to the satellite
398
+ def devolve_member_to_satellite satellite, member
399
+ remove_indices member
400
+
401
+ member.parent = satellite.mapping
402
+ change_all_fk_source member, satellite
403
+ trace :datavault, "Satellite #{satellite.mapping.name.inspect} field #{member.inspect}"
404
+ end
405
+
406
+ # This absorption reflects a time-varying fact type that involves another Hub, which becomes a new link.
407
+ # REVISIT: "make_copy" says that the original field must remain, because it's in its parent's natural key.
408
+ def devolve_absorption_to_link absorption, make_copy
409
+ trace :datavault, "Promote #{absorption.inspect} to a new Link" do
410
+
411
+ # REVISIT: Here we need a new objectified fact type with the same two players and the same readings,
412
+ # complete with LinkFactTypes. Then we need two Absorptions, one for each LinkFactType, and with
413
+ # the same child role names as the role names in our original fact type.
414
+ #
415
+ # The current code tries to re-use the same fact type, but the absorptions cannot work for both as
416
+ # the parent object type can only be one of the two types. That's why this is currently failing its
417
+ # validation tests.
418
+
419
+ link_name =
420
+ absorption.
421
+ parent_role.
422
+ fact_type.
423
+ reading_preferably_starting_with_role(absorption.parent_role).
424
+ expand([], false).words.capwords*' '
425
+ # A simpler naming, not using the fact type reading
426
+ # link_name = absorption.root.mapping.name + ' ' + absorption.child_role.name
427
+
428
+ link_from = absorption.parent.composite
429
+ link_to = absorption.foreign_key.composite
430
+
431
+ # A new composition that maps the same object type as this absorption's parent:
432
+ mapping = @constellation.Mapping(:new, name: link_name, object_type: absorption.parent_role.object_type)
433
+ link = @constellation.Composite(mapping, composition: @composition)
434
+
435
+ unless make_copy
436
+ remove_indices absorption
437
+
438
+ # Move the absorption across to here
439
+ absorption.parent = mapping
440
+
441
+ if absorption.foreign_key
442
+ trace :datavault, "Setting new source composite for #{absorption.foreign_key.inspect}"
443
+ absorption.foreign_key.source_composite = link
444
+ debugger unless absorption.foreign_key.all_foreign_key_field.single
445
+ fk2_component = absorption.foreign_key.all_foreign_key_field.single.component
446
+ end
447
+ end
448
+
449
+ # Add a surrogate key:
450
+ inject_surrogate link
451
+
452
+ # Add a Surrogate foreign Key to the link_from composite
453
+ fk1_target = link_from.primary_index.all_index_field.single
454
+ raise "Internal error: #{link_from.inspect} should have a surrogate key" unless fk1_target
455
+ # Here, we're jumping directly to the foreign key field.
456
+ # Normally we'd have the Absorption of the object type, containing the FK field.
457
+ # We have no fact type for this absorption; it should be the LinkFactType of the notional objectification
458
+ # This affects the absorption path comment on the related SQL coliumn, for example.
459
+ # REVISIT: Add the LinkFactType for the notional objectification, and use that.
460
+ fk1_component = fork_component_to_new_parent(mapping, fk1_target.component)
461
+
462
+ fk2_target = link_to.primary_index.all_index_field.single
463
+ if make_copy
464
+ # See the above comment for fk1_component; it aplies here also
465
+ fk2_component = fork_component_to_new_parent(mapping, fk2_target.component)
466
+ else
467
+ # We're using the leaf component of the absorption we moved across
468
+ end
469
+
470
+ # Add a natural key:
471
+ natural_index =
472
+ @constellation.Index(:new, composite: link, is_unique: true,
473
+ presence_constraint: nil, composite_as_natural_index: link)
474
+ @constellation.IndexField(access_path: natural_index, ordinal: 0, component: fk1_component)
475
+ @constellation.IndexField(access_path: natural_index, ordinal: 1, component: fk2_component)
476
+
477
+ # Add ForeignKeys
478
+ fk1 = @constellation.ForeignKey(
479
+ :new,
480
+ source_composite: link,
481
+ composite: link_from,
482
+ absorption: nil # REVISIT: This is a ForeignKey without its mandatory Absorption. That's gonna hurt
483
+ )
484
+ @constellation.ForeignKeyField(foreign_key: fk1, ordinal: 0, component: fk1_component)
485
+ # REVISIT: This should be filled in by complete_foreign_keys, but it has no Absorption
486
+ @constellation.IndexField(access_path: fk1, ordinal: 0, component: fk1_target.component)
487
+
488
+ if make_copy
489
+ fk2 = @constellation.ForeignKey(
490
+ :new,
491
+ source_composite: link,
492
+ composite: link_to,
493
+ absorption: nil # REVISIT: This is a ForeignKey without its mandatory Absorption. That's gonna hurt
494
+ )
495
+ @constellation.ForeignKeyField(foreign_key: fk2, ordinal: 0, component: fk2_component)
496
+ # REVISIT: This should be filled in by complete_foreign_keys, but it has no Absorption
497
+ @constellation.IndexField(access_path: fk2, ordinal: 0, component: fk2_target.component)
498
+ absorption.foreign_key.retract
499
+ end
500
+
501
+ =begin
502
+ issues = 0
503
+ link.validate do |object, problem|
504
+ $stderr.puts "#{object.inspect}: #{problem}"
505
+ issues += 1
506
+ end
507
+ debugger if issues > 0
508
+ =end
509
+
510
+ # Add a load DateTime value
511
+ date_field = @constellation.ValidFrom(:new,
512
+ parent: mapping,
513
+ name: "FirstLoad"+datestamp_type_name,
514
+ object_type: datestamp_type
515
+ )
516
+ mapping.re_rank
517
+
518
+ #link.mapping.re_rank
519
+
520
+ # devolve link, false
521
+ @link_composites << link
522
+ end
523
+ end
524
+
525
+ def apply_name pattern, name
526
+ pattern.sub(/\+/, name)
527
+ end
528
+
529
+ def rename_parents
530
+ @link_composites.each do |composite|
531
+ composite.mapping.name = apply_name(@option_link_name, composite.mapping.name)
532
+ end
533
+ @hub_composites.each do |composite|
534
+ composite.mapping.name = apply_name(@option_hub_name, composite.mapping.name)
535
+ end
536
+ @reference_composites.each do |composite|
537
+ composite.mapping.name = apply_name(@option_ref_name, composite.mapping.name)
538
+ end
539
+ end
540
+
541
+ end
542
+
543
+ publish_compositor(DataVault)
544
+ end
545
+ end