lib/activefacts/persistence/foreignkey.rb in activefacts-0.8.16 vs lib/activefacts/persistence/foreignkey.rb in activefacts-0.8.18

- old
+ new

@@ -12,22 +12,83 @@ # What table (ObjectType) is the FK to? def to; @to; end # What reference created the FK? - def reference; @fk_ref; end + def references; @references; end # What columns in the *from* table form the FK def from_columns; @from_columns; end # What columns in the *to* table form the identifier def to_columns; @to_columns; end - def initialize(from, to, fk_ref, from_columns, to_columns) #:nodoc: - @from, @to, @fk_ref, @from_columns, @to_columns = - from, to, fk_ref, from_columns, to_columns + def initialize(from, to, references, from_columns, to_columns) #:nodoc: + @from, @to, @references, @from_columns, @to_columns = + from, to, references, from_columns, to_columns end + + def describe + "foreign key from #{from.name}(#{from_columns.map{|c| c.name}*', '}) to #{to.name}(#{to_columns.map{|c| c.name}*', '})" + end + + def verbalised_path + # REVISIT: This should be a proper join path verbalisation: + references.map do |r| + (r.fact_type.entity_type ? r.fact_type.entity_type.name + ' (in which ' : '') + + r.fact_type.default_reading + + (r.fact_type.entity_type ? ')' : '') + end * ' and ' + end + + # Which references are absorbed into the "from" table? + def precursor_references + fk_jump = @references.detect(&:fk_jump) + jump_index = @references.index(fk_jump) + @references[0, jump_index] + end + + # Which references are absorbed into the "to" table? + def following_references + fk_jump = @references.detect(&:fk_jump) + jump_index = @references.index(fk_jump) + fk_jump != @references.last ? @references[jump_index+1..-1] : [] + end + + def jump_reference + @references.detect(&:fk_jump) + end + + def to_name + p = precursor_references + f = following_references + j = jump_reference + + @references.last.to_names + + (p.empty? && f.empty? ? [] : ['via'] + p.map{|r| r.to_names}.flatten + f.map{|r| r.from_names}.flatten) + end + + # The from_name is the role name of the table with the FK, viewed from the other end + # When there are no precursor_references or following_references, it's the jump_reference.from_names + # REVISIT: I'm still working out what to do with precursor_references and following_references + def from_name + p = precursor_references + f = following_references + j = jump_reference + + # pluralise unless j.is_one_to_one + + # REVISIT: references[0].from_names is where the FK lives; but the object of interest may be an absorbed subclass which we should use here instead: + # REVISIT: Should crunch superclasses in subtype traversals + # REVISIT: Need to add "_as_rolename" where rolename is not to.name + + [ + @references[0].from_names, + (p.empty? && f.empty? ? [] : ['via'] + p.map{|r| r.to_names}.flatten + f.map{|r| r.from_names}.flatten) + ] + end + end end module Metamodel #:nodoc: class ObjectType @@ -41,90 +102,87 @@ next array if absorbed_via && TypeInheritance === absorbed_via.fact_type # Ignore the case where a subtype is absorbed elsewhere: # REVISIT: Disabled, as this should never happen. # next array if ref.to.absorbed_via != ref.fact_type end + ref.fk_jump = true array << [ref] - elsif ref.is_absorbing - ref.to.all_absorbed_foreign_key_reference_path.each{|aref| - array << aref.insert(0, ref) - } + elsif ref.is_absorbing or (ref.to && !ref.to.is_table) + debug :fk, "getting fks absorbed into #{name} via #{ref}" do + ref.to.all_absorbed_foreign_key_reference_path.each do |aref| + array << aref.insert(0, ref) + end + end end array end end + def foreign_keys_to + @foreign_keys_to ||= [] + end + # Return an array of all the foreign keys from this table def foreign_keys - fk_ref_paths = all_absorbed_foreign_key_reference_path # Get the ForeignKey object for each absorbed reference path - fk_ref_paths.map do |fk_ref_path| - debug :fk, "\nFK: " + fk_ref_path.map{|fk_ref| fk_ref.reading }*" and " do + @foreign_keys ||= + begin + fk_ref_paths = all_absorbed_foreign_key_reference_path + fk_ref_paths.map do |fk_ref_path| + debug :fk, "\nFK: " + fk_ref_path.map{|fk_ref| fk_ref.reading }*" and " do - from_columns = (columns||all_columns({})).select{|column| - column.references[0...fk_ref_path.size] == fk_ref_path - } - debug :fk, "from_columns = #{from_columns.map { |column| column.name }*", "}" + from_columns = (columns||all_columns({})).select{|column| + column.references[0...fk_ref_path.size] == fk_ref_path + } + debug :fk, "from_columns = #{from_columns.map { |column| column.name }*", "}" - absorption_path = [] - to = fk_ref_path.last.to - # REVISIT: There should be a better way to find where it's absorbed (especially since this fails for absorbed subtypes having their own identification!) - while (r = to.absorbed_via) - absorption_path << r - to = r.to == to ? r.from : r.to - end - raise "REVISIT: #{fk_ref_path.inspect} is bad" unless to and to.columns + # Figure out absorption on the target end: + to = fk_ref_path.last.to + if to.absorbed_via + debug :fk, "Reference target #{fk_ref_path.last.to.name} is absorbed via:" do + while (r = to.absorbed_via) + m = r.reversed + debug :fk, "#{m.reading}" + fk_ref_path << m + to = m.from == to ? m.to : m.from + end + debug :fk, "Absorption ends at #{to.name}" + end + end - unless absorption_path.empty? - debug :fk, "Reference target #{fk_ref_path.last.to.name} is absorbed into #{to.name} via:" do - debug :fk, "#{absorption_path.map(&:reading)*" and "}" - end - end + # REVISIT: This test may no longer be necessary + raise "REVISIT: #{fk_ref_path.inspect} is bad" unless to and to.columns - debug :fk, "Looking at absorption depth of #{absorption_path.size} in #{to.name} for to_columns for #{from_columns.map(&:name)*", "}:" - to_supertypes = to.supertypes_transitive - to_columns = from_columns.map do |from_column| - debug :fk, "\tLooking for counterpart of #{from_column.name}: #{from_column.comment}" do - target_path = absorption_path + from_column.references[fk_ref_path.size..-1] - debug :fk, "\tcounterpart MUST MATCH #{target_path.map(&:reading)*" and "}" - c = to.columns.detect do |column| - debug :fk, "Considering #{column.references.map(&:reading) * " and "}" - debug :fk, "exact match: #{column.name}: #{column.comment}" if column.references == target_path - # Column may be inherited into "to", in which case target_path is too long. - cr = column.references - allowed_type = fk_ref_path.last.to - #debug :fk, "Check for absorption, need #{allowed_type.name}" if cr != target_path - cr == target_path or - cr == target_path[-cr.size..-1] && - !target_path[0...-cr.size].detect do |ref| - ft = ref.fact_type - next true if allowed_type.absorbed_via != ref # Problems if it doesn't match - allowed_type = ref.from - false - end - end - raise "REVISIT: Failed to find conterpart column for #{from_column.name}" unless c - c - end - end - debug :fk, "to_columns in #{to.name}: #{to_columns.map { |column| column ? column.name : "OOPS!" }*", "}" + # REVISIT: This fails for absorbed subtypes having their own identification. + # Check the CompanyDirectorEmployee model for example, EmployeeManagerNr -> Person (should reference EmployeeNr) + # Need to use the absorbed identifier_columns of the subtype, + # not the columns of the supertype that absorbs it. + # But in general, that isn't going to work because in most DBMS + # there's no suitable uniquen index on the subtype's identifier_columns - # Put the column pairs in a defined order, sorting key pairs by to-name: - froms, tos = from_columns.zip(to_columns).sort_by { |pair| - pair[1].name(nil) - }.transpose + to_columns = fk_ref_path[-1].to.identifier_columns - ActiveFacts::Persistence::ForeignKey.new(self, to, fk_ref_path[-1], froms, tos) - end - end. - sort_by do |fk| - # Put the foreign keys in a defined order: - [ fk.to.name, - fk.to_columns.map{|col| col.name(nil).sort}, - fk.from_columns.map{|col| col.name(nil).sort} - ] - end + # Put the column pairs in the correct order. They MUST be in the order they appear in the primary key + froms, tos = from_columns.zip(to_columns).sort_by { |pair| + to_columns.index(pair[1]) + }.transpose + + fk = ActiveFacts::Persistence::ForeignKey.new(self, to, fk_ref_path, froms, tos) + to.foreign_keys_to << fk + fk + end + end. + sort_by do |fk| + # Put the foreign keys in a defined order: +# debugger if !fk.to_columns || fk.to_columns.include?(nil) || !fk.from_columns || fk.from_columns.include?(nil) + [ fk.to.name, + fk.to_columns.map{|col| col.name(nil).sort}, + fk.from_columns.map{|col| col.name(nil).sort} + ] + end + end + end end end end