lib/activefacts/input/orm.rb in activefacts-0.8.8 vs lib/activefacts/input/orm.rb in activefacts-0.8.9

- old
+ new

@@ -415,11 +415,11 @@ throw "Role is played by #{concept.class} not Concept" if !(@constellation.vocabulary.concept(:Concept) === concept) debug :orm, "Creating role #{name} nr#{fact_type.all_role.size} of #{fact_type.fact_type_id} played by #{concept.name}" role = @by_id[id] = @constellation.Role(fact_type, fact_type.all_role.size, :concept => concept) - role.role_name = name if name + role.role_name = name if name && name != concept.name debug :orm, "Fact #{fact_name} (id #{fact_type.fact_type_id.object_id}) role #{x['Name']} is played by #{concept.name}, role is #{role.object_id}" x_vr = x.xpath("orm:ValueRestriction/orm:RoleValueConstraint") x_vr.each{|vr| x_ranges = vr.xpath("orm:ValueRanges/orm:ValueRange") @@ -698,16 +698,23 @@ ib['IsImplicitBooleanValue']) unary_identifier = true end mc_id = nil + if (mc = @mandatory_constraints_by_rs[role_sequence]) # Remove absorbed mandatory constraints, leaving residual ones. debug :orm, "Absorbing MC #{mc['Name']} over #{role_sequence.describe}" @mandatory_constraints_by_rs.delete(role_sequence) mc_id = mc['id'] @mandatory_constraint_rs_by_id.delete(mc['id']) + elsif (fts = role_sequence.all_role_ref.map{|rr| rr.role.fact_type}.uniq).size == 1 and + fts[0].entity_type + # this uniqueness constraint is an internal UC on an objectified fact type, + # so the covered roles are always mandatory (wrt the OFT) + # That is, the phantom roles are mandatory, even if the visible roles are not. + mc = true else debug :orm, "No MC to absorb over #{role_sequence.describe}" end # A TypeInheritance fact type has a uniqueness constraint on each role. @@ -737,10 +744,43 @@ @by_id[mc_id] = pc if mc_id } end end + def subtype_join_step(join, ti) + subtype_node = join.all_join_node.detect{|jn| jn.concept == ti.subtype } || + @constellation.JoinNode(join, join.all_join_node.size, :concept => ti.subtype) + supertype_node = join.all_join_node.detect{|jn| jn.concept == ti.supertype } || + @constellation.JoinNode(join, join.all_join_node.size, :concept => ti.supertype) + rs = @constellation.RoleSequence(:new) + @constellation.RoleRef(rs, 0, :role => ti.subtype_role) + sub_jr = @constellation.JoinRole(subtype_node, ti.subtype_role) + @constellation.RoleRef(rs, 1, :role => ti.supertype_role) + sup_jr = @constellation.JoinRole(supertype_node, ti.supertype_role) + js = @constellation.JoinStep(sub_jr, sup_jr, :fact_type => ti) + debug :join, "New subtyping join step #{js.describe}" + js + end + + # Make as many join steps as it takes to get from subtype to supertype + def subtype_join_steps(join, subtype, supertype) + primary_ti = nil + other_ti = nil + subtype.all_type_inheritance_as_subtype.each do |ti| + next unless ti.supertype.supertypes_transitive.include? supertype + if ti.provides_identification + primary_ti ||= ti + else + other_ti ||= ti + end + end + ti = primary_ti || other_ti + # Make supertype join steps first: + (ti.supertype == supertype ? [] : subtype_join_steps(join, ti.supertype, supertype)) + + [subtype_join_step(join, ti)] + end + # Equality and subset joins involve two or more role sequences, # and the respective roles from each sequence must be compatible, # Compatibility might involve subtyping joins but not objectification joins # to the respective end-point (constrained object type). # Also, all roles in each sequence constitute a join over a single @@ -772,13 +812,16 @@ end # For each role_sequence, find the object type over which the join is implied (nil if no join) sequence_join_over = [] if role_sequences[0].all_role_ref.size > 1 # There are joins within each sequence. - sequence_join_over = role_sequences.map do |rs| + sequence_join_over = [] + sequence_joined_roles = [] + role_sequences.map do |rs| join_over, joined_roles = *ActiveFacts::Metamodel.join_roles_over(rs.all_role_ref.map{|rr| rr.role}) - join_over + sequence_join_over << join_over + sequence_joined_roles << joined_roles end end # If there are no joins, we can drop out here. if sequence_join_over.compact.empty? && !end_joins.detect{|e| true} @@ -790,100 +833,136 @@ join = nil debug :join, "#{constraint_type} join constraint #{name} constrains #{ end_points.zip(end_joins).map{|(p,j)| p.name+(j ? ' & subtypes':'')}*', ' }#{ if role_sequences[0].all_role_ref.size > 1 - ", joined over #{sequence_join_over.map{|o| o ? o.name : '(none)'}*', '}" + ", joined over #{ + sequence_join_over.zip(sequence_joined_roles).map{|o, roles| + (o ? o.name : '(none)') + + (roles ? " to (#{roles.map{|role| role ? role.fact_type.default_reading : 'null'}*','})" : '') + }*', '}" else '' end }" do - end - return # REVISIT: Disabled until we get the join verbaliser working. - while false - # There may be one join per role sequence: - role_sequences.zip(sequence_join_over).map do |role_sequence, join_over| + role_sequences.zip(sequence_join_over||[], sequence_joined_roles||[]).map do |role_sequence, join_over, joined_roles| # Skip if there's no join here (sequence join nor end-point subset join) - role_refs = role_sequence.all_role_ref.sort_by{|rr| rr.ordinal} - next unless join_over or role_refs.detect{|rr| rr.role.concept != end_points[rr.ordinal]} + role_refs = role_sequence.all_role_ref_in_order + if !join_over and !role_refs.detect{|rr| rr.role.concept != end_points[rr.ordinal]} + # No sequence join nor end_point join here + next + end + # A RoleSequence for the actual join end-points replacement_rs = @constellation.RoleSequence(:new) + join = @constellation.Join(:new) join_node = nil - role_sequence.all_role_ref.sort_by{|rr| rr.ordinal}.each_with_index do |role_ref, i| + join_role = nil + role_refs.zip(joined_roles||[]).each_with_index do |(role_ref, joined_role), i| + + # Each role_ref is to an object joined via joined_role to join_node (or which will be the join_node) + + # Create a join node for the actual end-point (supertype of the constrained roles) end_point = end_points[i] debug :join, "Join Node #{join.all_join_node.size} is for #{end_point.name}" end_node = @constellation.JoinNode(join, join.all_join_node.size, :concept => end_point) + + # We're going to rewrite the constraint to constrain the supertype roles, but assume they're the same: role_node = end_node + end_role = role_ref.role - if role_ref.role.concept != end_point - subtype = role_ref.role.concept - debug :join, "Making subtyping join step #{join.all_join_node.size} from #{subtype.name} to #{end_point.name}" do - role_node = @constellation.JoinNode(join, join.all_join_node.size, :concept => role_ref.role.concept) - ti = role_ref.role.concept.all_type_inheritance_as_subtype.detect{|ti| ti.supertype == end_point} - raise "REVISIT: Can't yet handle multiple subtyping levels in subtyping join" unless ti + # Create subtyping join steps at the end-point, if needed: + projecting_jr = nil + constrained_join_role = nil + if (subtype = role_ref.role.concept) != end_point + debug :join, "Making subtyping join steps from #{subtype.name} to #{end_point.name}" do + # There may be more than one supertyping level. Make the steps: + subtyping_steps = subtype_join_steps(join, subtype, end_point) + js = subtyping_steps[0] + constrained_join_role = subtyping_steps[-1].input_join_role - # Make a new role sequence over the subtyping fact type: - rs = @constellation.RoleSequence(:new) - @constellation.RoleRef(rs, 0, :role => ti.all_role.detect{|r| r.concept == role_ref.role.concept}, :join_node => role_node) - @constellation.RoleRef(rs, 1, :role => ti.all_role.detect{|r| r.concept == end_point}, :join_node => end_node) - - js = @constellation.JoinStep(role_node, end_node, :fact_type => ti) - debug :join, "New subtyping join step #{js.describe}" + # Replace the constrained role and node with the supertype ones: + end_node = join.all_join_node.detect{|jn| jn.concept == end_point } + projecting_jr = js.output_join_role + end_role = js.fact_type.all_role.detect{|r| r.concept == end_point } + role_node = join.all_join_node.detect{|jn| jn.concept == role_ref.role.concept } end end - @constellation.RoleRef(replacement_rs, 0, :role => role_ref.role, :join_node => end_node) + raise "Internal error: making illegal reference to join node" if end_role.concept != end_node.concept + rr = @constellation.RoleRef(replacement_rs, replacement_rs.all_role_ref.size, :role => end_role) + projecting_jr ||= (constrained_join_role = @constellation.JoinRole(end_node, end_role)) + projecting_jr.role_ref = rr # Project this RoleRef - # REVISIT: Something here needs to handle unaries, which have a join step over two copies of the same JoinNode - if join_over - if !join_node + if !join_node # Create the JoinNode when processing the first role debug :join, "Join Node #{join.all_join_node.size} is over #{join_over.name}" join_node = @constellation.JoinNode(join, join.all_join_node.size, :concept => join_over) end debug :join, "Making join step from #{end_point.name} to #{join_over.name}" do - role = role_ref.role - # Detect an objectification step. Here, the role is in an objectified type, but the end_point isn't another role player of that fact type... or is there a better test? - if join_over == role.fact_type.entity_type - p join_over.name - p role.fact_type.default_reading - p end_point.name - raise "REVISIT: Incomplete" - elsif end_point == role.fact_type.entity_type - fact_type = role.implicit_fact_type - # REVISIT: Might these be reversed... sometimes? - end_role = fact_type.all_role.single - join_role = role - else - fact_type = role.fact_type - end_role = role - # This is unambiguous, since it's come from NORMA which only supports (unambiguous) implicit joins: - join_role = role.fact_type.all_role.detect{|r| r.concept == join_over} - # debugger unless join_role && end_role - end - rs = @constellation.RoleSequence(:new) # Detect the fact type over which we're joining (may involve objectification) - @constellation.RoleRef(rs, 0, :role => end_role, :join_node => end_node) - @constellation.RoleRef(rs, 1, :role => join_role, :join_node => join_node) - js = @constellation.JoinStep(end_node, join_node, :fact_type => fact_type) + raise "Internal error: making illegal reference to join node" if role_ref.role.concept != role_node.concept + @constellation.RoleRef(rs, 0, :role => role_ref.role) + role_jr = @constellation.JoinRole(role_node, role_ref.role) + raise "Internal error: making illegal reference to join node" if joined_role.concept != join_node.concept + @constellation.RoleRef(rs, 1, :role => joined_role) + join_jr = @constellation.JoinRole(join_node, joined_role) + + js = @constellation.JoinStep(role_jr, join_jr, :fact_type => joined_role.fact_type) debug :join, "New join step #{js.describe}" end else debug :join, "Need join step for non-join_over role #{end_point.name} #{role_ref.describe} in #{role_ref.role.fact_type.default_reading}" - if role_ref.role.fact_type.all_role.size > 1 - raise unimplemented + if (roles = role_ref.role.fact_type.all_role.to_a).size > 1 + # Here we have an end join (step already created) but no sequence join + if join_node + raise "Internal error: making illegal join step" if role_ref.role.concept != role_node.concept + join_jr = @constellation.JoinRole(join_node, join_role) + role_jr = @constellation.JoinRole(role_node, role_ref.role) + js = @constellation.JoinStep(join_jr, role_jr, :fact_type => role_ref.role.fact_type) + roles -= [join_role, role_ref.role] + roles.each do |incidental_role| + jn = @constellation.JoinNode(join, join.all_join_node.size, :concept => incidental_role.concept) + jr = @constellation.JoinRole(jn, incidental_role, :join_step => js) + end + else + if role_sequence.all_role_ref.size > 1 + join_node = role_node + join_role = role_ref.role + else + # There's no join in this role sequence, so we'd drop of fthe bottom without doing the right things. Why? + # Without this case, Supervision.orm omits "that runs Company" from the exclusion constraint, and I'm not sure why. + # I think the "then" code causes it to drop out the bottom without making the step (which is otherwise made in every case, see CompanyDirectorEmployee for example) + role_jr = @constellation.JoinRole(role_node, role_ref.role) + js = nil + role_ref.role.fact_type.all_role.each do |role| + next if role == role_jr.role + next if role_sequence.all_role_ref.detect{|rr| rr.role == role} + jn = @constellation.JoinNode(join, join.all_join_node.size, :concept => role.concept) + jr = @constellation.JoinRole(jn, role) + if js + jr.join_step = js # Incidental role + else + js = @constellation.JoinStep(role_jr, jr, :fact_type => role_ref.role.fact_type) + end + end + end + end else - js = @constellation.JoinStep(role_node, role_node, :fact_type => role_ref.role.fact_type) + # Unary fact type, make a Join Step from and to the constrained_join_role + jr = @constellation.JoinRole(constrained_join_role.join_node, role_ref.role) + js = @constellation.JoinStep(jr, jr, :fact_type => role_ref.role.fact_type) end end end # Thoroughly check that this is a valid join join.validate + debug :join, "Join has projected nodes #{replacement_rs.describe}" # Constrain the replacement role sequence, which has the attached join: role_sequences[role_sequences.index(role_sequence)] = replacement_rs end end