Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
(JoinAssociationDeclaration | RangeVariableDeclaration)
["WITH" ConditionalExpression]

@return AST\Join
775346
public function Join()
    {
        // Check Join type
        $joinType = AST\Join::JOIN_TYPE_INNER;

        switch (true) {
            case $this->lexer->isNextToken(Lexer::T_LEFT):
                $this->match(Lexer::T_LEFT);

                $joinType = AST\Join::JOIN_TYPE_LEFT;

                // Possible LEFT OUTER join
                if ($this->lexer->isNextToken(Lexer::T_OUTER)) {
                    $this->match(Lexer::T_OUTER);

                    $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
                }
                break;

            case $this->lexer->isNextToken(Lexer::T_INNER):
                $this->match(Lexer::T_INNER);
                break;

            default:
                // Do nothing
        }

        $this->match(Lexer::T_JOIN);

        $next            = $this->lexer->glimpse();
        $joinDeclaration = $next['type'] === Lexer::T_DOT ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
        $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH);
        $join            = new AST\Join($joinType, $joinDeclaration);

        // Describe non-root join declaration
        if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
            $joinDeclaration->isRoot = false;
        }

        // Check for ad-hoc Join conditions
        if ($adhocConditions) {
            $this->match(Lexer::T_WITH);

            $join->conditionalExpression = $this->ConditionalExpression();
        }

        return $join;
    }
344484
private function buildAddJoin() {
        $aliases = array();
        foreach($this->getRelationships() as $relationship) {
            $aliases[] = $relationship->getAlias();
        }
        $content = 'switch($alias) {
';
        foreach($this->getRelationships() as $relationship) {
            $method = new Object\Method();
            $method->setAccess('private');
            $method->setName('addJoin' . $relationship->getAlias());
            $method->setDescription('This adds a join for the specific relationship');
            $method->setReturnType('string - The name of the model we just joined on');
            $method->setContent(
                '//Check if join already added
				$joins = $criteriaHelper->getJoins();
				foreach($joins as $join) {
					if($join[2]==\'' . $relationship->getAlias() . '\') {
						return \'' . $relationship->getRemoteModel() . '\';
					}
				}
				$criteriaHelper->getCriteria()->leftJoin(
					\'' . $relationship->getRemoteModel() . '\',
					$currentModelAlias.\'.' . $this->getField($relationship->getLocalColumn())->getShortName()
                . '=' . $relationship->getAlias()
                . '.' . $relationship->getRemoteShortColumn() . '\',
					\'' . $relationship->getAlias() . '\'
				);
				return \'' . $relationship->getRemoteModel() . '\';'
            );
            $parameter = new Object\Parameter();
            $parameter->setName('criteriaHelper');
            $parameter->setDescription('The criteria we are adding the join on to');
            $parameter->setType('CriteriaHelper');
            $parameter->setStrictType(true);
            $method->addParameter($parameter);
            $parameter = new Object\Parameter();
            $parameter->setName('currentModelAlias');
            $parameter->setDescription('The current model\'s alias');
            $parameter->setType('string');
            $method->addParameter($parameter);
            $this->getAbstractClass()->addMethod($method);

            $content .= '			case \'' . $relationship->getAlias() . '\':
				return $this->addJoin' . $relationship->getAlias() . '($criteriaHelper, $currentModelAlias);
				break;
';
        }
        $content .= '		}
		return null;';

        $method = new Object\Method();
        $method->setAccess('private');
        $method->setName('addJoinFromAlias');
        $method->setDescription('This adds a join based off of the aliases to an existing criteria, you can do nested joins, using a ., such as User.BranchSharing');
        $method->setReturnType('string - The name of the model we just joined on');
        $method->setContent($content);
        $parameter = new Object\Parameter();
        $parameter->setName('criteriaHelper');
        $parameter->setDescription('The criteria we are adding the join on to');
        $parameter->setType('CriteriaHelper');
        $parameter->setStrictType(true);
        $method->addParameter($parameter);
        $parameter = new Object\Parameter();
        $parameter->setName('alias');
        $parameter->setDescription('The alias of the relationship');
        $parameter->setType('string');
        $method->addParameter($parameter);
        $parameter = new Object\Parameter();
        $parameter->setName('currentModelAlias');
        $parameter->setDescription('The current model\'s alias');
        $parameter->setType('string');
        $method->addParameter($parameter);

        $this->getAbstractClass()->addMethod($method);

        $this->getAbstractClass()->addUse('Bullhorn\FastRest\Api\Services\Database\CriteriaHelper');
        $method = new Object\Method();
        $method->setAccess('public');
        $method->setName('addJoin');
        $method->setDescription('This adds a join based off of the aliases to an existing criteria, you can do nested joins, using a ., such as User.BranchSharing');
        $method->setReturnType('string - The name of the model we just joined on');
        $method->setContent(
            'if(is_null($currentModelAlias)) {
			$currentModelAlias = \'' . str_replace('\Generated', '', $this->getAbstractClass()->getNamespace()) . '\\' . $this->getAbstractClass()->getName() . '\';
		}
		if(strpos($alias, \'.\')!==false) {
			$parts = explode(\'.\', $alias);
			$part = array_shift($parts);
			$modelName = $this->addJoin($criteriaHelper, $part, $currentModelAlias);
			/** @type GeneratedInterface $model */
			$model = new $modelName();
			return $model->addJoin($criteriaHelper, implode(\'.\', $parts), $part);
		}
		$allowedAliases = [
			\'' . implode("',\n\t\t\t'", $aliases) . '\'
		];
		if(!in_array($alias, $allowedAliases)) {
			throw new \InvalidArgumentException(\'The alias "\'.$alias.\'" is not in the allowed list of: \'.implode(\', \', $allowedAliases));
		}
		return $this->addJoinFromAlias($criteriaHelper, $alias, $currentModelAlias);
'
        );
        $parameter = new Object\Parameter();
        $parameter->setName('criteriaHelper');
        $parameter->setDescription('The criteria we are adding the join on to');
        $parameter->setType('CriteriaHelper');
        $parameter->setStrictType(true);
        $method->addParameter($parameter);

        $parameter = new Object\Parameter();
        $parameter->setName('alias');
        $parameter->setDescription('The alias of the relationship');
        $parameter->setType('string');
        $method->addParameter($parameter);
        $parameter = new Object\Parameter();
        $parameter->setName('currentModelAlias');
        $parameter->setDescription('The current model\'s alias');
        $parameter->setType('string');
        $parameter->setDefaultValue('null');
        $method->addParameter($parameter);

        $this->getAbstractClass()->addMethod($method);
    }
56973
public function join($join, $condition = null, $type = 'INNER')
    {
        if (empty($condition)) {
            // 如果为组数，则循环调用join
            foreach ($join as $key => $value) {
                if (is_array($value) && 2 <= count($value)) {
                    $this->join($value[0], $value[1], isset($value[2]) ? $value[2] : $type);
                }
            }
        } else {
            $table = $this->getJoinTable($join);

            $this->options['join'][] = [$table, strtoupper($type), $condition];
        }

        return $this;
    }
1290358
public function join(Clause\Join $clause)
    {
        if (is_array($clause)) {
            $this->join = array_merge($this->join[], array_values($clause));
        } else {
            $this->join[] = $clause;
        }

        return $this;
    }
1228426
public function CoalesceExpression()
    {
        $this->match(Lexer::T_COALESCE);
        $this->match(Lexer::T_OPEN_PARENTHESIS);

        // Process ScalarExpressions (1..N)
        $scalarExpressions = array();
        $scalarExpressions[] = $this->ScalarExpression();

        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
            $this->match(Lexer::T_COMMA);

            $scalarExpressions[] = $this->ScalarExpression();
        }

        $this->match(Lexer::T_CLOSE_PARENTHESIS);

        return new AST\CoalesceExpression($scalarExpressions);
    }
621910
public function join()
	{
		// We do a left outer join here because we're not trying to limit the primary table's results
		// This function is primarily used when needing to sort by a field in the joined table
		$this->model->select($this->model->getQualifiedFieldName('*'))
		            ->select($this->related->getQualifiedFieldName('*'))
		            ->join($this->associativeTable,
		                   $this->model->getQualifiedFieldName($this->localKey),
		                   $this->associativeLocal,
		                   'LEFT OUTER')
		            ->join($this->related->getTableName(),
		                   $this->associativeRelated,
		                   $this->related->getQualifiedFieldName($this->relatedKey),
		                   'LEFT OUTER');

		return $this;
	}
630862
public function RangeVariableDeclaration()
    {
        $abstractSchemaName = $this->AbstractSchemaName();

        if ($this->lexer->isNextToken(Lexer::T_AS)) {
            $this->match(Lexer::T_AS);
        }

        $token = $this->lexer->lookahead;
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
        $classMetadata = $this->em->getClassMetadata($abstractSchemaName);

        // Building queryComponent
        $queryComponent = array(
            'metadata'     => $classMetadata,
            'parent'       => null,
            'relation'     => null,
            'map'          => null,
            'nestingLevel' => $this->nestingLevel,
            'token'        => $token
        );

        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;

        return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);
    }
90674
function join($relation, $expr = null, $mode = Join::INNER)
    {
        if (null !== $expr) {
            $expr = new On(new AndX(array($expr)));
        }

        $join = new Join($relation, $expr);
        $join->mode = $mode;

        $this->nodes->source->right[] = $join;
        return $this;
    }
742637
public function join(string $table, string $leftField, string $comparison, $rightField): self
    {
        $this->join[] = ['inner', $table, $leftField, $comparison, $rightField];

        return $this;
    }
543138
function Join(Table $table, JoinType $join, Condition $condition, $prepend = false)
    {
        $result = $this->Duplicate();
        if ($prepend)
        {
            array_unshift($result->tables, $table);
            array_unshift($result->joins, $join);
            array_unshift($result->conditions, $condition);
        }
        else
        {    
            $result->tables[] = $table;
            $result->joins[] = $join;
            $result->conditions[] = $condition;
        }
        return $result;
    }
139332
public function leftJoinRelations($relations)
    {
        $relations = is_array($relations) ? $relations : func_get_args();

        return $this->joinRelations($relations, 'left');
    }
898870
public function compileJoin(string $condition): string
	{
		$parts = $this->parseJoin($condition);
		$count = count($parts['identifiers']);

		// Go through and quote the identifiers
		for($i=0; $i <= $count; $i++)
		{
			if (in_array($parts['combined'][$i], $parts['identifiers']) && ! is_numeric($parts['combined'][$i]))
			{
				$parts['combined'][$i] = $this->db->quoteIdent($parts['combined'][$i]);
			}
		}

		return implode('', $parts['combined']);
	}
1102325
public function join(Join $join)
    {
        $takenAliases = $this->getTakenAliases();

        if (in_array($join->getAlias(), $takenAliases, true)) {
            throw InvalidArgumentException::format(
                'Invalid join supplied to %s::%s: alias \'%s\' is already taken, taken aliases are (%s)',
                get_class($this), __FUNCTION__, $join->getAlias(), Debug::formatValues($takenAliases)
            );
        }


        $this->joins[] = $join;

        return $this;
    }
151948
public function getJoins(&$parameters = array())
    {
        if (func_num_args() == 0) {
            Deprecation::notice(
                '4.0',
                'SQLConditionalExpression::getJoins() now may produce parameters which are necessary to
				execute this query'
            );
        }

        // Sort the joins
        $parameters = array();
        $joins = $this->getOrderedJoins($this->from);

        // Build from clauses
        foreach ($joins as $alias => $join) {
            // $join can be something like this array structure
            // array('type' => 'inner', 'table' => 'SiteTree', 'filter' => array("SiteTree.ID = 1",
            // "Status = 'approved'", 'order' => 20))
            if (!is_array($join)) {
                if (empty($alias) || is_numeric($alias)) {
                    continue;
                }

                if (preg_match('/AS\s+(?:"[^"]+"|[A-Z0-9_]+)\s*$/i', $join)) {
                    // custom aliases override the ones defined through array keys
                    // this is only meant to keep backward compatibility with SS <= 4.3,
                    // to be removed in SS5
                    continue;
                }

                $trimmedAlias = trim($alias, '"');

                if ($trimmedAlias !== trim($join, '"')) {
                    $joins[$alias] = "{$join} AS \"{$trimmedAlias}\"";
                }

                continue;
            }

            if (is_string($join['filter'])) {
                $filter = $join['filter'];
            } elseif (sizeof($join['filter']) == 1) {
                $filter = $join['filter'][0];
            } else {
                $filter = "(" . implode(") AND (", $join['filter']) . ")";
            }

            // Ensure tables are quoted, unless the table is actually a sub-select
            $table = preg_match('/\bSELECT\b/i', $join['table'])
                ? $join['table']
                : "\"{$join['table']}\"";
            $aliasClause = ($alias != $join['table'])
                ? " AS \"{$alias}\""
                : "";
            $joins[$alias] = strtoupper($join['type']) . " JOIN " . $table . "$aliasClause ON $filter";
            if (!empty($join['parameters'])) {
                $parameters = array_merge($parameters, $join['parameters']);
            }
        }

        return $joins;
    }
1019729
public function innerJoin(
        $joinTableName,
        $key,
        $comparator,
        $value = null,
        $shouldCompareFields = true,
        $tableName = null
    ) {
        return $this->join(
            self::JOIN_INNER,
            $joinTableName,
            $key,
            $comparator,
            $value,
            $shouldCompareFields,
            $tableName
        );
    }
894869
public function PartialObjectExpression()
    {
        $this->match(Lexer::T_PARTIAL);

        $partialFieldSet = array();

        $identificationVariable = $this->IdentificationVariable();

        $this->match(Lexer::T_DOT);
        $this->match(Lexer::T_OPEN_CURLY_BRACE);
        $this->match(Lexer::T_IDENTIFIER);

        $partialFieldSet[] = $this->lexer->token['value'];

        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
            $this->match(Lexer::T_COMMA);
            $this->match(Lexer::T_IDENTIFIER);
    
            $field = $this->lexer->token['value'];
    
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
                $this->match(Lexer::T_DOT);
                $this->match(Lexer::T_IDENTIFIER);
                $field .= '.'.$this->lexer->token['value'];
            }
    
            $partialFieldSet[] = $field;
        }

        $this->match(Lexer::T_CLOSE_CURLY_BRACE);

        $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);

        // Defer PartialObjectExpression validation
        $this->deferredPartialObjectExpressions[] = array(
            'expression'   => $partialObjectExpression,
            'nestingLevel' => $this->nestingLevel,
            'token'        => $this->lexer->token,
        );

        return $partialObjectExpression;
    }
636713
protected function compileJoins(array $joinTokens): string
    {
        $statement = '';
        foreach ($joinTokens as $join) {
            $statement .= "\n{$join['type']} JOIN {$this->quote($join['outer'], true)}";

            if (!empty($join['alias'])) {
                $this->quoter->registerAlias($join['alias'], (string)$join['outer']);
                $statement .= " AS " . $this->quote($join['alias']);
            }

            $statement .= $this->optional("\n    ON", $this->compileWhere($join['on']));
        }

        return $statement;
    }