private function tokensMatch(SqlToken $patternToken, SqlToken $token, $offset = 0, &$firstMatchIndex = null, &$lastMatchIndex = null)
    {
        if (
            $patternToken->getIsCollection() !== $token->getIsCollection()
            || (!$patternToken->getIsCollection() && $patternToken->content !== $token->content)
        ) {
            return false;
        }

        if ($patternToken->children === $token->children) {
            $firstMatchIndex = $lastMatchIndex = $offset;
            return true;
        }

        $firstMatchIndex = $lastMatchIndex = null;
        $wildcard = false;
        for ($index = 0, $count = count($patternToken->children); $index < $count; $index++) {
            // Here we iterate token by token with an exception of "any" that toggles
            // an iteration until we matched with a next pattern token or EOF.
            if ($patternToken[$index]->content === 'any') {
                $wildcard = true;
                continue;
            }

            for ($limit = $wildcard ? count($token->children) : $offset + 1; $offset < $limit; $offset++) {
                if (!$wildcard && !isset($token[$offset])) {
                    break;
                }

                if (!$this->tokensMatch($patternToken[$index], $token[$offset])) {
                    continue;
                }

                if ($firstMatchIndex === null) {
                    $firstMatchIndex = $offset;
                    $lastMatchIndex = $offset;
                } else {
                    $lastMatchIndex = $offset;
                }
                $wildcard = false;
                $offset++;
                continue 2;
            }

            return false;
        }

        return true;
    }