activefacts-compositions 1.9.6 → 1.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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