Advanced

Relation operators

We can define new relation operations in addition to the built-in ones. A relational operator takes zero or more relations and returns a relation.

We use op to declare a relation operator followed by the parameters in parentheses, and then the output type, and then a block of code between a begin and end. Inside the block of code, there should be a return statement to pass the result back to the caller.

The following example takes a relation with a heading of type {x:str, y:str} and returns the transitive closure with the same heading type. We give the input parameter a name of r.

tclose := op(r:{x:str, y:str}) {x:str, y:str} begin
	ttt:=r | ((r(r{z:=y, y:=x})){*, y:=z})
	if ttt = r begin
		return ttt
	end else begin
		return tclose(ttt)
	end
end

We call the operator using parentheses, e.g.

mm:={MAJOR_P:str, MINOR_P:str}{
     ["P1",  "P2"]
     ["P1",  "P3"]
     ["P2",  "P3"]
     ["P2",  "P4"]
     ["P3",  "P5"]
     ["P4",  "P6"]
}

print(tclose(mm{x:=MAJOR_P, y:=MINOR_P}))
┌────┬────┐
│ x  │ y  │
├────┼────┤
│ P1 │ P2 │
│ P1 │ P3 │
│ P1 │ P4 │
│ P1 │ P5 │
│ P1 │ P6 │
│ P2 │ P3 │
│ P2 │ P4 │
│ P2 │ P5 │
│ P2 │ P6 │
│ P3 │ P5 │
│ P4 │ P6 │
└────┴────┘

Wildcard parameters, *

Instead of having to specify the complete heading for each parameter, * can be used to stand for 'any other attributes, of any type'.

For example, s:{*} would mean a relation parameter, named s within the operator code block, with any number of attributes.

r:{*, x:int} would mean a relation parameter, named r within the operator code block, with an attribute named x of type int and zero or more other attributes.

As well, * can stand for 'any type'. So r:{x:*, y:*} would mean a relation parameter, named r within the operator code block, with two attributes named x and y of any type.

attrs()

Given a variable number of possibly unnamed attributes, possibly with types not known until call-time, the output type may need to dynamically adjust based on the headings of the arguments at call-time. To support this, we can use the attrs function to build a set of attribute declarations and return them in a format suitable for use in other operations, such as the output type of a relation operator or a relation projection. The attrs function is special here in that it is evaluated each time the operator is called, not when it is declared.

As an example, suppose we want to add a compose operator (in addition to the built-in method of nesting relations using parentheses). It would need to handle relations with any number of attributes and return a relation based on the types of inputs given. We can use the heading function to get a set of attributes for a relation parameter and so build up a suitable set of attributes to pass to the attrs function.

compose := op(r:{*}, s:{*}) {attrs((heading(r)|heading(s)) - (heading(r)&heading(s)))} begin
	return (r&s){attrs((heading(r)|heading(s)) - (heading(r)&heading(s)))}
end

The operator above declares 2 relation parameters, r and s, with any headings at all. It declares the output to be a relation with attributes from the union of all the attributes from r and s, whatever they may be at time of calling, with the intersection of the attributes of r and s removed (i.e. remove the ones they have in common). The return statement uses the same construction to project the required attributes to form the result.

Calling this with two sample relations, in this case with one common attribute:

print(compose(
    {c:float, b:str}{[3.0,"b"],[5.0,"b"]}, 
    {c:float, d:str}{[2.0,"d"],[5.0,"d"],[3.0,"e"]}
))
┌───┬───┐
│ b │ d │
├───┼───┤
│ b │ e │
│ b │ d │
└───┴───┘