33 Path Operations

Path operations combine whole path expressions. They are useful when the question is easier to write as two paths that are then combined.

For example, this expression reaches courses that are both advanced and offered by the mathematics department:


u.course.[advanced]
  ⊓ u.department.[name = "Mathematics"].course

Both sides output courses. The ⊓ operation keeps the course values that satisfy both sides.

Meet

Use ⊓ when the thought is "both paths must hold".


u.course.[advanced]
  ⊓ u.course.[assessment.exam.Σexists]

This reaches courses that are advanced and have at least one exam assessment. The two sides are ordinary path expressions; ⊓ combines their frame conditions with logical conjunction.

In relational-model terms, ⊓ is relational-join-like. When both sides expose the same output, it looks like intersection, as in the examples above. When the sides expose different outputs while sharing some surrounding condition, it looks more like a relational join.


u.course.(enrolment.student ⊓ session.room)

This combines students and rooms for each course. If a course has several enrolled students and several rooms, the expression can form combinations of those reached values, just as a join can.

This is the path-expression counterpart of relational natural join ⨝, but at a different level. The relational operator joins relations. The path operator ⊓ combines path denotations by aligning common result attributes and conjoining their frame conditions. When the path results have relation-shaped tuples, that frame meet behaves like natural join.


(E₁ ⊓ E₂).Σ = E₁.Σ ⨝ E₂.Σ

This law holds when E₁ and E₂ are evaluated in the same context and their result attributes are join-compatible. The left side performs the meet at the path/frame level and then collects the resulting tuples; the right side collects each path result first and then joins those tuple sets.

Union

Use ⊔ when the thought is "either path may hold".


u.course.[advanced]
  ⊔ u.department.[name = "Mathematics"].course

This reaches courses that are advanced, courses offered by the mathematics department, or both. When the two sides output the same attribute, the result is the ordinary union-like case.


(E₁ ⊔ E₂).Σ = E₁.Σ ∪ E₂.Σ

This ordinary set-union law holds when E₁ and E₂ are evaluated in the same context and have the same result shape. More generally, path union is frame-level disjunction: it aligns compatible result attributes and combines the branch conditions with ∨.

The operation can also combine paths that reach the same kind of output by different routes:


u.(student.name ⊔ department.name)

This exposes names reached through students or departments. That is a genuine path-level disjunction. It is not tuple construction; tuple construction would try to combine outputs side by side.

Difference

Use -- when the thought is "the left path, except where the right path also holds".


u.course -- u.student.completed

This reaches courses that are not among the completed courses of any student. The left side supplies the result shape. The right side removes assignments that also satisfy its path.


(E₁ -- E₂).Σ = E₁.Σ ∖ E₂.Σ

This ordinary set-difference law holds when E₁ and E₂ are evaluated in the same context and have the same result shape. In the general path case, -- is logical difference over frames: it keeps the left condition and conjoins the negation of the right condition.

Path difference is written --. Set difference is written with the set-minus operator, as in expressions over sets of attributes or values. Keeping those two spellings separate helps distinguish path difference from set difference.

Pointwise negation

The postfix operator ! negates a path while preserving the path's output. It is most intuitive on Boolean-valued paths:


u.course.(code, advanced! as notAdvanced)

Here advanced reaches the Boolean values recorded for the course. advanced! reaches the Boolean values not reached by advanced. Since advanced is a single-valued Boolean attribute in the example schema, this reads as the opposite Boolean value.

This is different from filtering courses that are not advanced:


u.course.[¬advanced]

The filter keeps courses. The expression advanced! still outputs Boolean values.

Path complement

The postfix operator ~ negates the existence of a path and returns to the current receiver. For example, this reaches courses with no prerequisites:


u.course.prerequisite~

The prerequisite path is tested from each course. If no prerequisite can be reached, the course itself is retained.

The same idea can be written with a fold:


u.course.[prerequisite.Σempty]

The fold version is often easier to read when the thought is simply "there are no values". The ~ form is useful when the expression is already being written as a path and the desired result is the current receiver.

The current receiver is kept when the following path has no match. In relational-model terms this resembles semidifference or anti-join, but in Predica it is defined as path complement relative to the current receiver.

Composability

Path operations compose with the earlier path tails. Each side of an operation can use navigation, filters, tuple construction, renaming, folds and universal navigation.


(
    u.course.[assessment.exam.Σexists]
    ⊔ u.course.[assessment.project.Σexists]
)
.[prerequisite.Σempty]
.(code, title)

This reaches courses that have an exam or a project, have no prerequisites, and then outputs the course code and title. The important habit is to read each operation as combining path expressions, not as changing what dots mean.