Red.git | lib/Red/Attr/ | Relationship.rakumod


use Red::AST;
use Red::AST::Infixes;
use Red::AST::Value;
use Red::HiddenFromSQLCommenting;
use X::Red::Exceptions;
use Red::Model;

unit role Red::Attr::Relationship[
	&rel1,
	&rel2?,
	Str         :$model,
	Str         :$require = $model,
	Bool        :$optional,
	Bool        :$no-prefetch,
	Bool        :$has-one,
  Red::Model  :$model-type,
];

has Mu:U $!type;
has Bool $.has-lazy-relationship = ?$model;
has Mu:U $!relationship-model;
has Bool $!loaded-model          = False;
has Bool $!optional              = $optional;
has Bool $.has-one               = $has-one;
has Bool $.no-prefetch           = $!has-one // $no-prefetch // self.type ~~ Positional;
has Str $.rel-name is rw;
has $!model-type                 = $model-type;

submethod TWEAK(|) {
    if $!model-type !=== Red::Model {
        $!has-lazy-relationship = True;
        $!relationship-model   := $!model-type;
        $!loaded-model          = True
    }
}

method transfer(Mu:U $package) {
    my $attr = Attribute.new: :$package, :$.name, :$.type;
    $attr but Red::Attr::Relationship[&rel1, &rel2, :$model, :$require]
}

method !to-use-with-rel {
	do if self.has-one || self.type ~~ Positional {
		do if $!relationship-model<> =:= Mu {
			self.type.of
		} else {
			$!relationship-model<>
		}
	} else {
		self.package
	}
}

method rel {
    CATCH {
        default {
            note "ERROR running relationship { self.name }: ", .message;
            .throw
        }
    }
    my $*RED-INTERNAL = True;
    my \type = self!to-use-with-rel;
    rel1 type
}

method relationship-model(--> Mu:U) is hidden-from-sql-commenting {
    return self.type if !$model.DEFINITE && !$!loaded-model;
    unless $!loaded-model {
        my $t = ::($model);
        if !$t && $t ~~ Failure {
            require ::($require);
            $t = ::($model);
        }
        $!relationship-model := $t;
        $!loaded-model = True;
    }
    $!relationship-model
}

method set-data(\instance, Mu $value) is hidden-from-sql-commenting {
    do given $.rel {
        my $attr = .attr;
        my $ref  = .ref;
        $attr.set_value: instance, $ref.attr.get_value: $value;
        instance.^set-dirty: $attr;
    }
}

method build-relationship(\instance) is hidden-from-sql-commenting {
    my \type = self.type;
    my \attr = self;
    my \rel-model = $model ?? self.relationship-model !! type ~~ Positional ?? type.of !! type;
    use nqp;
    nqp::bindattr(nqp::decont(instance), $.package, $.name, Proxy.new:
        FETCH => method () {
            my $*RED-INTERNAL = True;
            my \ret = do if &rel1.count <= 1 {
                do if type ~~ Positional || attr.has-one {
                    my \relation = rel1(rel-model);
                    rel-model.^rs.where: relation.map(-> $rel {
                        X::Red::RelationshipNotColumn.new(
                            :relationship(attr),
                            :points-to($rel)
                        ).throw unless $rel ~~ Red::Column;

                        my $ref = $rel.ref;
                        X::Red::RelationshipNotRelated.new(
                            :relationship(attr),
                            :points-to($rel)
                        ).throw unless $ref.DEFINITE;

                        my $val = do given $ref.attr but role :: {
                            method package {
                                instance.WHAT
                            }
                        } {
                            instance.^get-attr: .name.substr: 2
                        }
                        my \value = ast-value $val;
                        Red::AST::Eq.new: $rel, value, :bind-right
                    }).reduce: -> $left, $right? {
                        $right.DEFINITE
                        ?? Red::AST::AND.new: $left, $right
                        !! $left
                    }
                } else {
                    my @models = rel1(instance.WHAT).map(-> $rel {
                        my $val = $rel.attr.get_value: instance;
                        do with $val {
                            my \value = ast-value $val;
                            Red::AST::Eq.new: $rel.ref, value, :bind-right
                        }
                    }).grep(*.defined);
                    return rel-model unless @models;
                    rel-model.^rs.where(@models.reduce(-> $left, $right? {
                        $right.DEFINITE
                        ?? Red::AST::AND.new: $left, $right
                        !! $left
                    }))
                }
            } else {
                my $filter = instance.^all.join-model(:name(attr.name.substr: 2), rel-model, &rel1).ast.filter;
                # No point of running a query that should return no records...
                # Maybe it should be on driver
                if $filter ~~ Red::AST::Value && $filter.value.not {
                    return Nil
                }
                instance.^all.join-model: :name(attr.name.substr: 2), rel-model, &rel1;
            }
	    return ret.head if type !~~ Positional || attr.has-one;
	    ret
        },
        STORE => method ($value where type) {
            my $*RED-INTERNAL = True;
            die X::Assignment::RO.new(value => attr.type) unless attr.rw;
            if type !~~ Positional {
                attr.set-data: instance, $value
            } else {
                die "NYI Couldnt set value"
            }
        }
    );
    return
}

method target-type {
    $model ?? self.relationship-model !! self.type ~~ Positional ?? self.type.of !! self.type
}

method source-type {
    self.package
}

method relationship-argument-type {
    do if self.type ~~ Positional || self.has-one {
        $.target-type
    } else {
        $.source-type
    }
}

method joined-model {
	Empty.&return unless self.type ~~ Positional;
	self.package.^join: self.relationship-argument-type, -> | { self.relationship-ast: self.package }, name => self.rel-name
}

method !relationship-ast($t1, $t2) {
    my $*RED-INTERNAL = True;
    return rel1 $t1, $t2 if &rel1.count > 1;
    my \col1 = |rel1 $t1;
    return col1 if col1 ~~ Red::AST && col1 !~~ Red::Column;

    col1.map({
        X::Red::RelationshipNotColumn.new(
            :relationship(self),
            :points-to($_)
        ).throw unless $_ ~~ Red::Column;

        Red::AST::Eq.new: $_, .ref: $t2
    }).reduce: -> $agg, $i? {
        return $agg without $i;
        Red::AST::AND.new: $agg, $i
    }
}

multi method relationship-ast($type, $oposite) is hidden-from-sql-commenting {
    self!relationship-ast($type, $oposite)
}
multi method relationship-ast($type = Nil) is hidden-from-sql-commenting {
    self!relationship-ast(self.relationship-argument-type, $type)
}

method join-type {
    my $*RED-INTERNAL = True;
    with $!optional {
        return $!optional ?? :left !! :inner
    }
    return :left if &rel1.count > 1;
    do given rel1 self.relationship-argument-type {
        when .?nullable { :left  }
        default         { :inner }
    }
}