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.