class ActiveFacts::Compositions::DataVault

Constants

BDV_ANNOTATIONS
BDV_BRIDGE_ANNOTATIONS
BDV_PIT_ANNOTATIONS
BDV_SAT_ANNOTATIONS

Public Class Methods

compatibility() click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 43
def self.compatibility
  %i{datavault relational}
end
new(constellation, name, options = {}) click to toggle source
Calls superclass method
# File lib/activefacts/compositions/datavault.rb, line 47
def initialize constellation, name, options = {}
  # Extract recognised options:
  datavault_initialize options

  @option_reference = options.delete('reference')
  @option_id = '+ ' + (options.delete('id') || 'HID')
  @option_hub_name = options.delete('hubname') || 'HUB'
  @option_hub_name.sub!(/^/,'+ ') unless @option_hub_name =~ /\+/
  @option_link_name = options.delete('linkname') || 'LINK'
  @option_link_name.sub!(/^/,'+ ') unless @option_link_name =~ /\+/
  @option_sat_name = options.delete('satname') || 'SAT'
  @option_sat_name.sub!(/^/,'+ ') unless @option_sat_name =~ /\+/
  @option_pit_name = options.delete('refname') || 'PIT'
  @option_pit_name.sub!(/^/,'+ ') unless @option_ref_name =~ /\+/
  @option_bridge_name = options.delete('refname') || 'BRIDGE'
  @option_bridge_name.sub!(/^/,'+ ') unless @option_ref_name =~ /\+/
  @option_ref_name = options.delete('refname') || 'REF'
  @option_ref_name.sub!(/^/,'+ ') unless @option_ref_name =~ /\+/

  super constellation, name, options, 'DataVault'

  @option_surrogates = true   # Always inject surrogates regardless of superclass
end
options() click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 24
def self.options
  datavault_options.
  merge({
    reference: ['Boolean', "Emit the reference (static) tables as well. Default is to omit them"],
    id: ['String', "Append this to data vault surrogate key names (default HID)"],
    hubname: ['String', "Suffix or pattern for naming hub tables. Include a + to insert the name. Default 'HUB'"],
    linkname: ['String', "Suffix or pattern for naming link tables. Include a + to insert the name. Default 'LINK'"],
    satname: ['String', "Suffix or pattern for naming satellite tables. Include a + to insert the name. Default 'SAT'"],
    pitname: ['String', "Suffix or pattern for naming point in time tables. Include a + to insert the name. Default 'PIT'"],
    bridgename: ['String', "Suffix or pattern for naming bridge tables. Include a + to insert the name. Default 'BRIDGE'"],
    refname: ['String', "Suffix or pattern for naming reference tables. Include a + to insert the name. Default '+'"],
  }).
  merge(Relational.options).
  reject{|k,v| [:fk, :surrogates].include?(k) }.  # Datavault surrogates are not optional
  merge({
    fk: [%w{hash}, "Enforce foreign keys using a hash of the natural keys (ala Data Vault 2)"],
  })
end

Public Instance Methods

apply_composite_name_pattern() click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 697
def apply_composite_name_pattern
  @reference_composites.each do |composite|
    composite.mapping.name = patterned_name(@option_ref_name, composite.mapping.name)
  end
  @hub_composites.each do |composite|
    composite.mapping.name = patterned_name(@option_hub_name, composite.mapping.name)
  end
  @link_composites.each do |composite|
    composite.mapping.name = patterned_name(@option_link_name, composite.mapping.name)
  end
  @bdv_link_composites.each do |composite|
    composite.mapping.name = patterned_name(@option_link_name, composite.mapping.name)
  end
  @pit_composites.each do |composite|
    composite.mapping.name = patterned_name(@option_pit_name, composite.mapping.name)
  end
  @bridge_composites.each do |composite|
    composite.mapping.name = patterned_name(@option_bridge_name, composite.mapping.name)
  end
end
apply_schema_transformations() click to toggle source
Calls superclass method
# File lib/activefacts/compositions/datavault.rb, line 308
def apply_schema_transformations
  delete_reference_table_foreign_keys

  # For each hub and link, move each non-identifying member
  # to a new satellite or promote it to a new link.
  (@hub_composites + @link_composites).each do |composite|
    split_satellites_from composite
  end

  # Rename parents for rdv and bdv
  apply_composite_name_pattern

  # Inject datetime and record source into the LoadBatch table
  if @option_audit == 'batch'
    inject_audit_fields(loadbatch_composite)
  end

  unless @option_reference
    if trace :reference_retraction
      # Add a logger so we can trace the resultant retractions:
      @constellation.loggers << proc do |*args|
        trace :reference_retraction, args.inspect
      end
    end

    @reference_composites.each do |rc|
      trace :reference_retraction, "Retracting #{rc.inspect}" do
        rc.retract
      end
    end
    @reference_composites = []

    @constellation.loggers.pop if trace :reference_retraction
  end

  # Populate fields of any point in time tables
  @pit_composites.each do |composite|
    populate_pit(composite)
  end

  super
end
bdv_classify_composites() click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 152
def bdv_classify_composites
  # Classify the business links and bridges
  @bdv_composites.sort_by{|c| c.mapping.name}.each do |composite|
    trace :datavault, "Decide whether #{composite.mapping.name} is a link or bridge"
    object_type = composite.mapping.object_type
    composite.composite_group = 'bdv'
    mapped_to = object_type.fact_type.all_role.to_a
    trace :datavault, "#{composite.mapping.name} encloses foreign keys to #{mapped_to.inspect}" unless mapped_to.compact.empty?

    all_ca = composite.mapping.object_type.concept.all_concept_annotation
    if all_ca.detect{ |ca| ca.mapping_annotation =~ BDV_LINK_ANNOTATIONS}
      @bdv_link_composites << composite
    elsif all_ca.detect{ |ca| ca.mapping_annotation =~ BDV_BRIDGE_ANNOTATIONS}
      @bridge_composites << composite
    end
  end

  # Classify the point in time tables
  @rdv_composites.sort_by{|c| c.mapping.name}.each do |composite|
    composite.composite_group = 'rdv'
    trace :datavault, "Decide whether #{composite.mapping.name} has point in time table"

    pit_members = composite.mapping.all_member.select do |member|
      if member.is_a?(MM::Absorption)
        if found_pit = check_pit(member)
          trace :datavault, "Found PIT member #{member.child_role.object_type.name}"
        end
        found_pit
      else
        false
      end
    end

    if pit_members.size > 0
      pit_composite = create_pit(composite.mapping.name, composite)
      @pit_composites << pit_composite
      @pit_hub[pit_composite] = composite
      @pit_members[pit_composite] = pit_members
    end
  end
end
change_all_fk_source(component, source_composite) click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 579
def change_all_fk_source component, source_composite
  if component.is_a?(MM::Absorption) && component.foreign_key
    trace :datavault, "Setting new source composite for #{component.foreign_key.inspect}"
    component.foreign_key.source_composite = source_composite
  end

  component.all_member.each do |member|
    change_all_fk_source member, source_composite
  end
end
check_pit(component) click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 194
def check_pit component
  all_pc = component.parent_role.all_role_ref.map(&:role_sequence).uniq.flat_map(&:all_presence_constraint).uniq
  all_pc.detect do |pc|
    pc.concept.all_concept_annotation.detect{ |ca| ca.mapping_annotation =~ BDV_PIT_ANNOTATIONS}
  end
end
classify_composites() click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 114
def classify_composites
  detect_reference_tables

  @bdv_composites, @rdv_composites =
    @non_reference_composites.partition { |composite| composite_is_bdv(composite) }

  trace :datavault, "Classify non-reference composites into hubs, links, pits and bridges" do
    # Make an initial determination, then adjust for foreign keys to links afterwards
    @hub_composites = []
    @link_composites = []
    @sat_composites = []
    @bdv_link_composites = []
    @bdv_sat_composites = []
    @pit_composites = []
    @bridge_composites = []
    @key_structure = {}
    @links_as_hubs = {}
    @pit_hub = {}
    @pit_members = {}
    @pit_satellite = {}

    rdv_classify_composites
    bdv_classify_composites

    trace :datavault_classification!, "Data Vault classification of composites:" do
      trace :datavault, "Reference: #{@reference_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
      trace :datavault, "Raw: #{@rdv_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
      trace :datavault, "Business: #{@bdv_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
      trace :datavault, "Hub: #{@hub_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
      trace :datavault, "Link: #{@link_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
      trace :datavault, "BDV Link: #{@bdv_link_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
      trace :datavault, "BDV Sat: #{@bdv_sat_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
      trace :datavault, "PIT: #{@pit_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
      trace :datavault, "Bridge: #{@bridge_composites.map(&:mapping).map(&:object_type).map(&:name)*', '}"
    end
  end
end
composite_is_bdv(composite) click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 84
def composite_is_bdv composite
  object_type = composite.mapping.object_type
  all_ca = object_type.concept.all_concept_annotation

  trace :datavault, "composite #{composite.mapping.name} annotations #{all_ca.map{|ca| ca.mapping_annotation} *' '}"
  all_ca.detect{|ca| ca.mapping_annotation =~ BDV_ANNOTATIONS}
end
composite_is_reference(composite) click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 76
def composite_is_reference composite
  object_type = composite.mapping.object_type
  all_ca = object_type.concept.all_concept_annotation

  all_ca.detect{|ca| ca.mapping_annotation == 'static'} or
    !object_type.is_a?(ActiveFacts::Metamodel::EntityType)
end
composite_key_structure(composite) click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 270
def composite_key_structure composite
  # We know that composite.mapping.object_type is an EntityType because all ValueType composites are reference tables
  object_type = composite.mapping.object_type

  mapped_to =
    object_type.preferred_identifier.role_sequence.all_role_ref_in_order.map do |role_ref|
      player = role_ref.role.object_type
      next nil if player == object_type && role_ref.role.fact_type.all_role.size == 1 # Unaries.
      candidate = @candidates[player]
      next nil unless candidate
      # Take care of full absorption
      while candidate.full_absorption
        candidate = candidate.full_absorption.composition
      end
      @non_reference_composites.include?(c = candidate.mapping.composite) ? c : nil
    end

  trace :datavault, "Preferred identifier for #{composite.mapping.name} encloses foreign keys to #{mapped_to.inspect}" unless mapped_to.compact.empty?

  number_of_keys = mapped_to.compact.size
  number_of_values = mapped_to.size-number_of_keys
  trace :datavault_classify,
    if number_of_keys > 1
      # Links have more than one FK to a hub in their key
      "Link #{composite.mapping.name} links #{mapped_to.compact.inspect} with #{number_of_values} values"
    elsif number_of_keys == 1 && number_of_values > 0
      # This is a new hub with a composite key - but we will have to eliminate the foreign key to the base hub
      "Augmented Hub #{composite.mapping.name} has a hub link to #{mapped_to.compact[0].inspect} and #{number_of_values} values"
    elsif number_of_keys == 1
      # This is a new hub with a single-part key that references another hub.
      "Dependent Hub #{composite.mapping.name} is identified by another hub: #{mapped_to.compact[0].inspect}"
    else
      "Hub #{composite.mapping.name} has #{mapped_to.size} parts in its key"
    end

  mapped_to
end
create_pit(name, composite) click to toggle source

Create a new PIT for the same object_type as this composite

# File lib/activefacts/compositions/datavault.rb, line 202
def create_pit name, composite
  mapping = @constellation.Mapping(:new, name: name, object_type: composite.mapping.object_type)
  @constellation.Composite(mapping, composition: @composition, composite_group: 'bdv')
end
create_satellite(name, composite) click to toggle source

Create a new satellite for the same object_type as this composite

# File lib/activefacts/compositions/datavault.rb, line 566
def create_satellite name, composite
  mapping = @constellation.Mapping(:new, name: name, object_type: composite.mapping.object_type)
  @constellation.Composite(mapping, composition: @composition)
end
delete_reference_table_foreign_keys() click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 419
def delete_reference_table_foreign_keys
  trace :datavault, "Delete foreign keys to reference tables" do
    # Delete all foreign keys to reference tables
    @reference_composites.each do |composite|
      composite.all_foreign_key_as_target_composite.each(&:retract)
    end
  end
end
detect_reference_tables() click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 264
def detect_reference_tables
  initial_composites = @composition.all_composite.select{|c| c != loadbatch_composite }
  @reference_composites, @non_reference_composites =
    initial_composites.partition { |composite| composite_is_reference(composite) }
end
generate() click to toggle source
Calls superclass method
# File lib/activefacts/compositions/datavault.rb, line 71
def generate
  create_loadbatch if @option_audit == 'batch'
  super
end
identify_satellite(composite, satellite) click to toggle source

Add the audit and foreign key fields to a satellite for this composite

# File lib/activefacts/compositions/datavault.rb, line 503
def identify_satellite composite, satellite
  trace :datavault, "Adding parent key and load time to satellite #{satellite.mapping.name.inspect}" do
    # Add a primary (which is also natural) key:
    natural_index =
      @constellation.Index(:new, composite: satellite, is_unique: true,
        # composite_as_natural_index: satellite,  # Must not be natural for the absorption to work correctly
        composite_as_primary_index: satellite)

    # Absorb and index a foreign key to the hub or link. We'll add the version_field to the index.
    # REVISIT: The name here should be the renamed version of the parent's name AFTER it's been adjusted
    object_type = composite.mapping.object_type
    member = @constellation.Mapping(guid: :new, parent: satellite.mapping, name: composite.mapping.name, object_type: object_type)
    paths = {object_type.preferred_identifier => natural_index}
    absorb_nested satellite.mapping, member, paths
    # Satellites don't really have a natural key, but never mind...
    satellite.natural_index = satellite.primary_index

    # Add the audit and time-versioning fields
    version_field =
      if @option_audit == 'batch'
        inject_audit_fields satellite, composite
      else
        inject_audit_fields satellite
      end
    @constellation.IndexField(access_path: natural_index, ordinal: natural_index.all_index_field.size, component: version_field)
    satellite.mapping.re_rank

    if satellite.composite_group == 'bdv'
      @bdv_sat_composites << satellite
    else
      @sat_composites << satellite
    end
  end
end
inject_loadbatch_relationships() click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 718
def inject_loadbatch_relationships
  # We can't do it the way our trait does, we treat hub+links, satellites, and fact links all differently
end
inject_surrogate(composite, name_pattern = @option_id) click to toggle source

Change the default extension from our superclass':

Calls superclass method
# File lib/activefacts/compositions/datavault.rb, line 110
def inject_surrogate composite, name_pattern = @option_id
  super
end
inject_surrogates() click to toggle source
Calls superclass method
# File lib/activefacts/compositions/datavault.rb, line 103
def inject_surrogates
  # We need to find links that need surrogate keys before we inject the surrogates
  classify_composites
  super
end
name_satellite(component) click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 560
def name_satellite component
  name, is_computed = *satellite_base_name_and_type(component)
  [patterned_name(@option_sat_name, name), is_computed != nil]
end
needs_surrogate(composite) click to toggle source

Data Vaults need a surrogate key on every Hub and Link. Don't add a surrogate on a Reference table!

Calls superclass method
# File lib/activefacts/compositions/datavault.rb, line 94
def needs_surrogate(composite)
  return false if composite_is_reference(composite)

  # REVISIT: The following is debatable. If the natural primary key is an ok surrogate, should we inject another?
  return true if @non_reference_composites.include?(composite)

  super
end
populate_pit(pit_composite) click to toggle source

A Point-In-Time table links a hub to its satellites applicable at a particular time

# File lib/activefacts/compositions/datavault.rb, line 352
def populate_pit(pit_composite)
  # inject standard PIT components: surrogate key, hub hash key and snapshot date time
  inject_surrogate(pit_composite)

  hub_composite = @pit_hub[pit_composite]
  hub_hash_field = hub_composite.primary_index.all_index_field.single.component
  pit_hub_field = hub_hash_field.fork_to_new_parent(pit_composite.mapping)
  date_field = @constellation.ValidFrom(:new,
    parent: pit_composite.mapping,
    name: "Snapshot "+datestamp_type_name,
    object_type: datestamp_type,
    injection_annotation: "datavault"
  )

  natural_index =
    @constellation.Index(
      :new, composite: pit_composite, is_unique: true,
      composite_as_natural_index: pit_composite #, composite_as_primary_index: pit_composite
    )
  @constellation.IndexField(access_path: natural_index, ordinal: 0, component: pit_hub_field)
  @constellation.IndexField(access_path: natural_index, ordinal: 1, component: date_field)

  # Add a foreign key to the hub
  fk = @constellation.ForeignKey(
      :new,
      source_composite: pit_composite,
      composite: hub_composite
    )
  @constellation.ForeignKeyField(foreign_key: fk, ordinal: 0, component: pit_hub_field)
  # This should be filled in by complete_foreign_keys, but there is no Absorption
  @constellation.IndexField(access_path: fk, ordinal: 0, component: hub_hash_field)

  # inject hash and load date time for sats associated with all pit members
  pit_members = @pit_members[pit_composite]
  sat_composites = pit_members.map{|pm| @pit_satellite[pm]}.compact.uniq

  sat_composites.each do |sat_composite|
    sat_name = sat_composite.mapping.name
    sat_hash_name = patterned_name(@option_id, sat_name)

    src_hash_field = hub_hash_field.fork_to_new_parent(pit_composite.mapping)
    src_hash_field.name = sat_hash_name
    src_load_field = @constellation.ValidFrom(:new,
      parent: pit_composite.mapping,
      name: "#{sat_name} Load "+datestamp_type_name,
      object_type: datestamp_type,
      injection_annotation: "datavault"
    )

    sat_index_fields = sat_composite.primary_index.all_index_field.to_a
    sat_hash_field = sat_index_fields[0].component
    sat_load_field = sat_index_fields[1].component

    # Add a foreign key to the satellite's primary key and load date time
    fk = @constellation.ForeignKey(
        :new,
        source_composite: pit_composite,
        composite: sat_composite
      )
    @constellation.ForeignKeyField(foreign_key: fk, ordinal: 0, component: src_hash_field)
    @constellation.IndexField(access_path: fk, ordinal: 0, component: sat_hash_field)
    @constellation.ForeignKeyField(foreign_key: fk, ordinal: 1, component: src_load_field)
    @constellation.IndexField(access_path: fk, ordinal: 1, component: sat_load_field)
  end
  pit_composite.mapping.re_rank
end
preferred_fk_type(building_natural_key, source_composite, target_composite) click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 428
def preferred_fk_type building_natural_key, source_composite, target_composite
  return :hash if @option_fk == :hash
  return :primary if building_natural_key && (@link_composites+@bdv_link_composites).include?(source_composite)
  building_natural_key && @hub_composites.include?(target_composite) ? :natural : :primary
end
rdv_classify_composites() click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 207
def rdv_classify_composites
  @link_composites, @hub_composites =
    @rdv_composites.
    sort_by{|c| c.mapping.name}.
    partition do |composite|
      trace :datavault, "Decide whether #{composite.mapping.name} is a link or a hub" do
        @key_structure[composite] =
          mapped_to =
          composite_key_structure composite

        # It's a Link if the preferred identifier includes more than one non_reference_composite.
        mapped_to.compact.size > 1
      end
    end

  trace :datavault, "Checking for foreign keys that reference links" do
    # Links may never be the target of a foreign key.
    # Any such links must be defined as hubs instead.

    fk_dependencies_by_target = {}
    fk_dependencies_by_source = {}
    (@hub_composites+@link_composites).each do |composite|
      target_composites = enumerate_foreign_keys composite.mapping
      target_composites.each do |target_composite|
        next if @reference_composites.include?(target_composite)
        (fk_dependencies_by_target[target_composite] ||= []) << composite
        (fk_dependencies_by_source[composite] ||= []) << target_composite
      end
    end

    fk_dependencies_by_target.keys.each do |target_composite|
      if @link_composites.delete(target_composite)
        trace :datavault, "Link #{target_composite.inspect} must be a hub because foreign keys reference it"
        @hub_composites << target_composite
        @links_as_hubs[target_composite] = true
      end
    end

    begin
      converted =
        @link_composites.select do |composite|
          targets = fk_dependencies_by_source[composite]
          id_targets = composite_key_structure(composite).compact
          next if targets.size == id_targets.size
          trace :datavault, "Link #{composite.mapping.name} must be a hub because it has non-identifying FK references"
          @link_composites.delete(composite)
          @hub_composites << composite
          @links_as_hubs[composite] = true
        end
    end while converted.size > 0

    # Note: We may still have hubs whose identifiers contain foreign keys to one or more other hubs.
    # REVISIT: These foreign keys will be deleted so these hubs stand alone,
    # but have been re-instated as new links to the referenced hubs.
  end
end
remove_indices(component) click to toggle source

This component is being moved to a new composite, so any indexes that it or its children contribute to, cannot now be used to search for the specified composite. A component being moved to a satellite or a hub cannot keep its indices.

# File lib/activefacts/compositions/datavault.rb, line 574
def remove_indices component
  component.all_index_field.map(&:access_path).uniq.each(&:retract)
  component.all_member.each{|member| remove_indices member}
end
remove_satellite_validations(satellite) click to toggle source
# File lib/activefacts/compositions/datavault.rb, line 538
def remove_satellite_validations satellite
  satellite.classify_constraints
  satellite.all_local_constraint.map(&:local_constraint).each(&:retract)
  leaf_constraints = satellite.mapping.all_leaf.flat_map(&:all_leaf_constraint).map(&:leaf_constraint).each(&:retract)
end
satellite_base_name_and_type(component) click to toggle source

Decide what to call a new satellite that will adopt this component

# File lib/activefacts/compositions/datavault.rb, line 545
def satellite_base_name_and_type component
  computed_name = nil
  satellite_name =
    if component.is_a?(MM::Absorption)
      pc = component.parent_role.base_role.uniqueness_constraint and
      pc.concept.all_concept_annotation.map do |ca|
        computed_name = ca.mapping_annotation =~ /^computed satellite *(.*)/ && "#{$1} Computed"
        ca.mapping_annotation =~ /^satellite *(.*)/ && $1 or computed_name
      end.compact.uniq[0]
    # REVISIT: How do we name the satellite for an Indicator? Add a Concept Annotation on the fact type?
    end
  satellite_name = satellite_name.words.capcase if satellite_name
  [ satellite_name || component.root.mapping.name, computed_name ]
end
split_off_member_to_satellite(satellite, member) click to toggle source

Move this member from its current parent to the satellite

# File lib/activefacts/compositions/datavault.rb, line 591
def split_off_member_to_satellite satellite, member
  remove_indices member

  member.parent = satellite.mapping
  change_all_fk_source member, satellite
  trace :datavault, "Satellite #{satellite.mapping.name.inspect} field #{member.inspect}"
end
split_satellites_from(composite, split_off_links = true) click to toggle source

For each member of this composite, decide whether to split it to a satellite or to a new link. If it goes to a link that's still part of this natural key, we need to leave that key intact, but remove the foreign key it entails.

New links and satellites get new fields for the load date-time and a references to the surrogate(s) on the hub or link, and add an index over those two fields.

# File lib/activefacts/compositions/datavault.rb, line 441
def split_satellites_from composite, split_off_links = true
  trace :datavault?, "Devolving non-identifying fields for #{composite.inspect}" do
    # Find the members of this mapping that contain identifying leaves:
    pi = composite.primary_index
    ni = composite.natural_index
    identifiers =
      (Array(pi ? pi.all_index_field : nil) +
       Array(ni ? ni.all_index_field : nil)).
      map{|ixf| ixf.component.path[1]}.
      uniq

    satellites = {}
    is_link = @link_composites.include?(composite) || @links_as_hubs.include?(composite)
    member_snapshot = composite.mapping.all_member.to_a
    version_field = inject_audit_fields(composite)
    absorb_nested composite.mapping, version_field, {} if MM::Absorption === version_field
    member_snapshot.each do |member|

      # Any member that is the absorption of a foreign key to a hub or link
      # (which is all, since we removed FKs to reference tables)
      # must be converted to a Mapping for a new Entity Type that notionally
      # objectifies the absorbed fact type. This Mapping is a new link composite.
      if split_off_links && member.is_a?(MM::Absorption) && member.foreign_key
        next if is_link
        split_off_absorption_to_link member, identifiers.include?(member)
        next
      end

      # If this member is in the natural or surrogate key, leave it there
      # REVISIT: But if it is an FK to another hub, split it to a link as well.
      next if identifiers.include?(member)

      # We may absorb a subtype that has no contents. There's no point moving these to a satellite.
      next if is_empty_inheritance member

      satellite_name, is_computed = *name_satellite(member)
      if !(satellite = satellites[satellite_name])
        satellite = satellites[satellite_name] = create_satellite(satellite_name, composite)
        identify_satellite composite, satellite
        satellite.composite_group = (is_computed ? 'bdv' : 'rdv')
        if member.is_a?(MM::Absorption) && check_pit(member)
          @pit_satellite[member] = satellite
        end
      end

      split_off_member_to_satellite satellite, member
    end
    composite.mapping.re_rank

    if @hub_composites.include?(composite)
      # Links-as-hubs have foreign keys over natural indexes; these must be deleted.
      composite.all_foreign_key_as_source_composite.to_a.each(&:retract)
    end

    # Add the audit and identity fields for the satellites:
    satellites.values.each do |satellite|
      remove_satellite_validations satellite
    end
  end
end