<?php

namespace Doctrine\Tests\DBAL;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\SQLParserUtils;

/**
 * @group DBAL-78
 * @group DDC-1372
 */
class SQLParserUtilsTest extends \Doctrine\Tests\DbalTestCase
{
    public function dataGetPlaceholderPositions()
    {
        return array(
            // none
            array('SELECT * FROM Foo', true, array()),
            array('SELECT * FROM Foo', false, array()),

            // Positionals
            array('SELECT ?', true, array(7)),
            array('SELECT * FROM Foo WHERE bar IN (?, ?, ?)', true, array(32, 35, 38)),
            array('SELECT ? FROM ?', true, array(7, 14)),
            array('SELECT "?" FROM foo', true, array()),
            array("SELECT '?' FROM foo", true, array()),
            array("SELECT `?` FROM foo", true, array()), // Ticket DBAL-552
            array("SELECT [?] FROM foo", true, array()),
            array("SELECT 'Doctrine\DBAL?' FROM foo", true, array()), // Ticket DBAL-558
            array('SELECT "Doctrine\DBAL?" FROM foo', true, array()), // Ticket DBAL-558
            array('SELECT `Doctrine\DBAL?` FROM foo', true, array()), // Ticket DBAL-558
            array('SELECT [Doctrine\DBAL?] FROM foo', true, array()), // Ticket DBAL-558
            array('SELECT "?" FROM foo WHERE bar = ?', true, array(32)),
            array("SELECT '?' FROM foo WHERE bar = ?", true, array(32)),
            array("SELECT `?` FROM foo WHERE bar = ?", true, array(32)), // Ticket DBAL-552
            array("SELECT [?] FROM foo WHERE bar = ?", true, array(32)),
            array('SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[?])', true, array(56)), // Ticket GH-2295
            array("SELECT 'Doctrine\DBAL?' FROM foo WHERE bar = ?", true, array(45)), // Ticket DBAL-558
            array('SELECT "Doctrine\DBAL?" FROM foo WHERE bar = ?', true, array(45)), // Ticket DBAL-558
            array('SELECT `Doctrine\DBAL?` FROM foo WHERE bar = ?', true, array(45)), // Ticket DBAL-558
            array('SELECT [Doctrine\DBAL?] FROM foo WHERE bar = ?', true, array(45)), // Ticket DBAL-558
            array("SELECT * FROM FOO WHERE bar = 'it\\'s a trap? \\\\' OR bar = ?\nAND baz = \"\\\"quote\\\" me on it? \\\\\" OR baz = ?", true, array(58, 104)),
            array('SELECT * FROM foo WHERE foo = ? AND bar = ?', true, array(1 => 42, 0 => 30)), // explicit keys

            // named
            array('SELECT :foo FROM :bar', false, array(7 => 'foo', 17 => 'bar')),
            array('SELECT * FROM Foo WHERE bar IN (:name1, :name2)', false, array(32 => 'name1', 40 => 'name2')),
            array('SELECT ":foo" FROM Foo WHERE bar IN (:name1, :name2)', false, array(37 => 'name1', 45 => 'name2')),
            array("SELECT ':foo' FROM Foo WHERE bar IN (:name1, :name2)", false, array(37 => 'name1', 45 => 'name2')),
            array('SELECT :foo_id', false, array(7 => 'foo_id')), // Ticket DBAL-231
            array('SELECT @rank := 1', false, array()), // Ticket DBAL-398
            array('SELECT @rank := 1 AS rank, :foo AS foo FROM :bar', false, array(27 => 'foo', 44 => 'bar')), // Ticket DBAL-398
            array('SELECT * FROM Foo WHERE bar > :start_date AND baz > :start_date', false, array(30 => 'start_date', 52 =>  'start_date')), // Ticket GH-113
            array('SELECT foo::date as date FROM Foo WHERE bar > :start_date AND baz > :start_date', false, array(46 => 'start_date', 68 =>  'start_date')), // Ticket GH-259
            array('SELECT `d.ns:col_name` FROM my_table d WHERE `d.date` >= :param1', false, array(57 => 'param1')), // Ticket DBAL-552
            array('SELECT [d.ns:col_name] FROM my_table d WHERE [d.date] >= :param1', false, array(57 => 'param1')), // Ticket DBAL-552
            array('SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[:foo])', false, array(56 => 'foo')), // Ticket GH-2295
            array(
<<<'SQLDATA'
SELECT * FROM foo WHERE 
bar = ':not_a_param1 ''":not_a_param2"'''
OR bar=:a_param1
OR bar=:a_param2||':not_a_param3'
OR bar=':not_a_param4 '':not_a_param5'' :not_a_param6'
OR bar=''
OR bar=:a_param3
SQLDATA
                , false, array(74 => 'a_param1', 91 => 'a_param2', 190 => 'a_param3')
            ),
            array("SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data WHERE (data.description LIKE :condition_0 ESCAPE '\\\\') AND (data.description LIKE :condition_1 ESCAPE '\\\\') ORDER BY id ASC", false, array(121 => 'condition_0', 174 => 'condition_1')),
            array('SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data WHERE (data.description LIKE :condition_0 ESCAPE "\\\\") AND (data.description LIKE :condition_1 ESCAPE "\\\\") ORDER BY id ASC', false, array(121 => 'condition_0', 174 => 'condition_1')),
            array('SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data WHERE (data.description LIKE :condition_0 ESCAPE "\\\\") AND (data.description LIKE :condition_1 ESCAPE \'\\\\\') ORDER BY id ASC', false, array(121 => 'condition_0', 174 => 'condition_1')),
            array('SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data WHERE (data.description LIKE :condition_0 ESCAPE `\\\\`) AND (data.description LIKE :condition_1 ESCAPE `\\\\`) ORDER BY id ASC', false, array(121 => 'condition_0', 174 => 'condition_1')),
            array('SELECT data.age AS age, data.id AS id, data.name AS name, data.id AS id FROM test_data data WHERE (data.description LIKE :condition_0 ESCAPE \'\\\\\') AND (data.description LIKE :condition_1 ESCAPE `\\\\`) ORDER BY id ASC', false, array(121 => 'condition_0', 174 => 'condition_1')),

        );
    }

    /**
     * @dataProvider dataGetPlaceholderPositions
     */
    public function testGetPlaceholderPositions($query, $isPositional, $expectedParamPos)
    {
        $actualParamPos = SQLParserUtils::getPlaceholderPositions($query, $isPositional);
        self::assertEquals($expectedParamPos, $actualParamPos);
    }

    public function dataExpandListParameters()
    {
        return array(
            // Positional: Very simple with one needle
            array(
                "SELECT * FROM Foo WHERE foo IN (?)",
                array(array(1, 2, 3)),
                array(Connection::PARAM_INT_ARRAY),
                'SELECT * FROM Foo WHERE foo IN (?, ?, ?)',
                array(1, 2, 3),
                array(ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::INTEGER)
            ),
            // Positional: One non-list before d one after list-needle
            array(
                "SELECT * FROM Foo WHERE foo = ? AND bar IN (?)",
                array("string", array(1, 2, 3)),
                array(ParameterType::STRING, Connection::PARAM_INT_ARRAY),
                'SELECT * FROM Foo WHERE foo = ? AND bar IN (?, ?, ?)',
                array("string", 1, 2, 3),
                array(ParameterType::STRING, ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::INTEGER)
            ),
            // Positional: One non-list after list-needle
            array(
                "SELECT * FROM Foo WHERE bar IN (?) AND baz = ?",
                array(array(1, 2, 3), "foo"),
                array(Connection::PARAM_INT_ARRAY, ParameterType::STRING),
                'SELECT * FROM Foo WHERE bar IN (?, ?, ?) AND baz = ?',
                array(1, 2, 3, "foo"),
                array(ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::STRING)
            ),
            // Positional: One non-list before and one after list-needle
            array(
                "SELECT * FROM Foo WHERE foo = ? AND bar IN (?) AND baz = ?",
                array(1, array(1, 2, 3), 4),
                array(ParameterType::INTEGER, Connection::PARAM_INT_ARRAY, ParameterType::INTEGER),
                'SELECT * FROM Foo WHERE foo = ? AND bar IN (?, ?, ?) AND baz = ?',
                array(1, 1, 2, 3, 4),
                array(
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                )
            ),
            // Positional: Two lists
            array(
                "SELECT * FROM Foo WHERE foo IN (?, ?)",
                array(array(1, 2, 3), array(4, 5)),
                array(Connection::PARAM_INT_ARRAY, Connection::PARAM_INT_ARRAY),
                'SELECT * FROM Foo WHERE foo IN (?, ?, ?, ?, ?)',
                array(1, 2, 3, 4, 5),
                array(
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                )
            ),
            // Positional: Empty "integer" array DDC-1978
            array(
                "SELECT * FROM Foo WHERE foo IN (?)",
                array(array()),
                array(Connection::PARAM_INT_ARRAY),
                'SELECT * FROM Foo WHERE foo IN (NULL)',
                array(),
                array()
            ),
            // Positional: Empty "str" array DDC-1978
            array(
                "SELECT * FROM Foo WHERE foo IN (?)",
                array(array()),
                array(Connection::PARAM_STR_ARRAY),
                'SELECT * FROM Foo WHERE foo IN (NULL)',
                array(),
                array()
            ),
            // Positional: explicit keys for params and types
            array(
                "SELECT * FROM Foo WHERE foo = ? AND bar = ? AND baz = ?",
                array(1 => 'bar', 2 => 'baz', 0 => 1),
                array(2 => ParameterType::STRING, 1 => ParameterType::STRING),
                'SELECT * FROM Foo WHERE foo = ? AND bar = ? AND baz = ?',
                array(1 => 'bar', 0 => 1, 2 => 'baz'),
                array(1 => ParameterType::STRING, 2 => ParameterType::STRING),
            ),
            // Positional: explicit keys for array params and array types
            array(
                "SELECT * FROM Foo WHERE foo IN (?) AND bar IN (?) AND baz = ?",
                array(1 => array('bar1', 'bar2'), 2 => true, 0 => array(1, 2, 3)),
                array(2 => ParameterType::BOOLEAN, 1 => Connection::PARAM_STR_ARRAY, 0 => Connection::PARAM_INT_ARRAY),
                'SELECT * FROM Foo WHERE foo IN (?, ?, ?) AND bar IN (?, ?) AND baz = ?',
                array(1, 2, 3, 'bar1', 'bar2', true),
                array(
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                    ParameterType::STRING,
                    ParameterType::STRING,
                    ParameterType::BOOLEAN,
                )
            ),
            // Positional starts from 1: One non-list before and one after list-needle
            array(
                "SELECT * FROM Foo WHERE foo = ? AND bar IN (?) AND baz = ? AND foo IN (?)",
                array(1 => 1, 2 => array(1, 2, 3), 3 => 4, 4 => array(5, 6)),
                array(
                    1 => ParameterType::INTEGER,
                    2 => Connection::PARAM_INT_ARRAY,
                    3 => ParameterType::INTEGER,
                    4 => Connection::PARAM_INT_ARRAY,
                ),
                'SELECT * FROM Foo WHERE foo = ? AND bar IN (?, ?, ?) AND baz = ? AND foo IN (?, ?)',
                array(1, 1, 2, 3, 4, 5, 6),
                array(
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                    ParameterType::INTEGER,
                )
            ),
            //  Named parameters : Very simple with param int
            array(
                "SELECT * FROM Foo WHERE foo = :foo",
                array('foo'=>1),
                array('foo' => ParameterType::INTEGER),
                'SELECT * FROM Foo WHERE foo = ?',
                array(1),
                array(ParameterType::INTEGER),
            ),

             //  Named parameters : Very simple with param int and string
            array(
                "SELECT * FROM Foo WHERE foo = :foo AND bar = :bar",
                array('bar'=>'Some String','foo'=>1),
                array('foo' => ParameterType::INTEGER, 'bar' => ParameterType::STRING),
                'SELECT * FROM Foo WHERE foo = ? AND bar = ?',
                array(1,'Some String'),
                array(ParameterType::INTEGER, ParameterType::STRING)
            ),
            //  Named parameters : Very simple with one needle
            array(
                "SELECT * FROM Foo WHERE foo IN (:foo)",
                array('foo'=>array(1, 2, 3)),
                array('foo'=>Connection::PARAM_INT_ARRAY),
                'SELECT * FROM Foo WHERE foo IN (?, ?, ?)',
                array(1, 2, 3),
                array(ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::INTEGER),
            ),
            // Named parameters: One non-list before d one after list-needle
            array(
                "SELECT * FROM Foo WHERE foo = :foo AND bar IN (:bar)",
                array('foo'=>"string", 'bar'=>array(1, 2, 3)),
                array('foo' => ParameterType::STRING, 'bar' => Connection::PARAM_INT_ARRAY),
                'SELECT * FROM Foo WHERE foo = ? AND bar IN (?, ?, ?)',
                array("string", 1, 2, 3),
                array(ParameterType::STRING, ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::INTEGER)
            ),
            // Named parameters: One non-list after list-needle
            array(
                "SELECT * FROM Foo WHERE bar IN (:bar) AND baz = :baz",
                array('bar'=>array(1, 2, 3), 'baz'=>"foo"),
                array('bar'=>Connection::PARAM_INT_ARRAY, 'baz'=>ParameterType::STRING),
                'SELECT * FROM Foo WHERE bar IN (?, ?, ?) AND baz = ?',
                array(1, 2, 3, "foo"),
                array(ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::STRING)
            ),
            // Named parameters: One non-list before and one after list-needle
            array(
                "SELECT * FROM Foo WHERE foo = :foo AND bar IN (:bar) AND baz = :baz",
                array('bar'=>array(1, 2, 3),'foo'=>1, 'baz'=>4),
                array('bar'=>Connection::PARAM_INT_ARRAY, 'foo'=>ParameterType::INTEGER, 'baz'=>ParameterType::INTEGER),
                'SELECT * FROM Foo WHERE foo = ? AND bar IN (?, ?, ?) AND baz = ?',
                array(1, 1, 2, 3, 4),
                array(ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::INTEGER)
            ),
            // Named parameters: Two lists
            array(
                "SELECT * FROM Foo WHERE foo IN (:a, :b)",
                array('b'=>array(4, 5),'a'=>array(1, 2, 3)),
                array('a'=>Connection::PARAM_INT_ARRAY, 'b'=>Connection::PARAM_INT_ARRAY),
                'SELECT * FROM Foo WHERE foo IN (?, ?, ?, ?, ?)',
                array(1, 2, 3, 4, 5),
                array(ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::INTEGER)
            ),
            //  Named parameters : With the same name arg type string
            array(
                "SELECT * FROM Foo WHERE foo <> :arg AND bar = :arg",
                array('arg'=>"Some String"),
                array('arg'=>ParameterType::STRING),
                'SELECT * FROM Foo WHERE foo <> ? AND bar = ?',
                array("Some String","Some String"),
                array(ParameterType::STRING,ParameterType::STRING,)
            ),
             //  Named parameters : With the same name arg
            array(
                "SELECT * FROM Foo WHERE foo IN (:arg) AND NOT bar IN (:arg)",
                array('arg'=>array(1, 2, 3)),
                array('arg'=>Connection::PARAM_INT_ARRAY),
                'SELECT * FROM Foo WHERE foo IN (?, ?, ?) AND NOT bar IN (?, ?, ?)',
                array(1, 2, 3, 1, 2, 3),
                array(ParameterType::INTEGER,ParameterType::INTEGER, ParameterType::INTEGER,ParameterType::INTEGER,ParameterType::INTEGER, ParameterType::INTEGER)
            ),

             //  Named parameters : Same name, other name in between DBAL-299
            array(
                "SELECT * FROM Foo WHERE (:foo = 2) AND (:bar = 3) AND (:foo = 2)",
                array('foo'=>2,'bar'=>3),
                array('foo'=>ParameterType::INTEGER,'bar'=>ParameterType::INTEGER),
                'SELECT * FROM Foo WHERE (? = 2) AND (? = 3) AND (? = 2)',
                array(2, 3, 2),
                array(ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::INTEGER)
            ),
             //  Named parameters : Empty "integer" array DDC-1978
            array(
                "SELECT * FROM Foo WHERE foo IN (:foo)",
                array('foo'=>array()),
                array('foo'=>Connection::PARAM_INT_ARRAY),
                'SELECT * FROM Foo WHERE foo IN (NULL)',
                array(),
                array()
            ),
             //  Named parameters : Two empty "str" array DDC-1978
            array(
                "SELECT * FROM Foo WHERE foo IN (:foo) OR bar IN (:bar)",
                array('foo'=>array(), 'bar'=>array()),
                array('foo'=>Connection::PARAM_STR_ARRAY, 'bar'=>Connection::PARAM_STR_ARRAY),
                'SELECT * FROM Foo WHERE foo IN (NULL) OR bar IN (NULL)',
                array(),
                array()
            ),
            array(
                "SELECT * FROM Foo WHERE foo IN (:foo) OR bar = :bar OR baz = :baz",
                array('foo' => array(1, 2), 'bar' => 'bar', 'baz' => 'baz'),
                array('foo' => Connection::PARAM_INT_ARRAY, 'baz' => 'string'),
                'SELECT * FROM Foo WHERE foo IN (?, ?) OR bar = ? OR baz = ?',
                array(1, 2, 'bar', 'baz'),
                array(ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::STRING, 'string')
            ),
            array(
                "SELECT * FROM Foo WHERE foo IN (:foo) OR bar = :bar",
                array('foo' => array(1, 2), 'bar' => 'bar'),
                array('foo' => Connection::PARAM_INT_ARRAY),
                'SELECT * FROM Foo WHERE foo IN (?, ?) OR bar = ?',
                array(1, 2, 'bar'),
                array(ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::STRING)
            ),
            // Params/types with colons
            array(
                "SELECT * FROM Foo WHERE foo = :foo OR bar = :bar",
                array(':foo' => 'foo', ':bar' => 'bar'),
                array(':foo' => ParameterType::INTEGER),
                'SELECT * FROM Foo WHERE foo = ? OR bar = ?',
                array('foo', 'bar'),
                array(ParameterType::INTEGER, ParameterType::STRING)
            ),
            array(
                "SELECT * FROM Foo WHERE foo = :foo OR bar = :bar",
                array(':foo' => 'foo', ':bar' => 'bar'),
                array(':foo' => ParameterType::INTEGER, 'bar' => ParameterType::INTEGER),
                'SELECT * FROM Foo WHERE foo = ? OR bar = ?',
                array('foo', 'bar'),
                array(ParameterType::INTEGER, ParameterType::INTEGER)
            ),
            array(
                "SELECT * FROM Foo WHERE foo IN (:foo) OR bar = :bar",
                array(':foo' => array(1, 2), ':bar' => 'bar'),
                array('foo' => Connection::PARAM_INT_ARRAY),
                'SELECT * FROM Foo WHERE foo IN (?, ?) OR bar = ?',
                array(1, 2, 'bar'),
                array(ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::STRING)
            ),
            array(
                "SELECT * FROM Foo WHERE foo IN (:foo) OR bar = :bar",
                array('foo' => array(1, 2), 'bar' => 'bar'),
                array(':foo' => Connection::PARAM_INT_ARRAY),
                'SELECT * FROM Foo WHERE foo IN (?, ?) OR bar = ?',
                array(1, 2, 'bar'),
                array(ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::STRING)
            ),
            // DBAL-522 - null valued parameters are not considered
            array(
                'INSERT INTO Foo (foo, bar) values (:foo, :bar)',
                array('foo' => 1, 'bar' => null),
                array(':foo' => ParameterType::INTEGER, ':bar' => ParameterType::NULL),
                'INSERT INTO Foo (foo, bar) values (?, ?)',
                array(1, null),
                array(ParameterType::INTEGER, ParameterType::NULL)
            ),
            array(
                'INSERT INTO Foo (foo, bar) values (?, ?)',
                array(1, null),
                array(ParameterType::INTEGER, ParameterType::NULL),
                'INSERT INTO Foo (foo, bar) values (?, ?)',
                array(1, null),
                array(ParameterType::INTEGER, ParameterType::NULL)
            ),
            // DBAL-1205 - Escaped single quotes SQL- and C-Style
            array(
                "SELECT * FROM Foo WHERE foo = :foo||''':not_a_param''\\'' OR bar = ''':not_a_param''\\'':bar",
                array(':foo' => 1, ':bar' => 2),
                array(':foo' => ParameterType::INTEGER, 'bar' => ParameterType::INTEGER),
                'SELECT * FROM Foo WHERE foo = ?||\'\'\':not_a_param\'\'\\\'\' OR bar = \'\'\':not_a_param\'\'\\\'\'?',
                array(1, 2),
                array(ParameterType::INTEGER, ParameterType::INTEGER)
            ),
        );
    }

    /**
     * @dataProvider dataExpandListParameters
     */
    public function testExpandListParameters($q, $p, $t, $expectedQuery, $expectedParams, $expectedTypes)
    {
        list($query, $params, $types) = SQLParserUtils::expandListParameters($q, $p, $t);

        self::assertEquals($expectedQuery, $query, "Query was not rewritten correctly.");
        self::assertEquals($expectedParams, $params, "Params dont match");
        self::assertEquals($expectedTypes, $types, "Types dont match");
    }

    public function dataQueryWithMissingParameters()
    {
        return array(
            array(
                "SELECT * FROM foo WHERE bar = :param",
                array('other' => 'val'),
                array(),
            ),
            array(
                "SELECT * FROM foo WHERE bar = :param",
                array(),
                array(),
            ),
            array(
                "SELECT * FROM foo WHERE bar = :param",
                array(),
                array('param' => Connection::PARAM_INT_ARRAY),
            ),
            array(
                "SELECT * FROM foo WHERE bar = :param",
                array(),
                array(':param' => Connection::PARAM_INT_ARRAY),
            ),
            array(
                "SELECT * FROM foo WHERE bar = :param",
                array(),
                array('bar' => Connection::PARAM_INT_ARRAY),
            ),
             array(
                "SELECT * FROM foo WHERE bar = :param",
                array('bar' => 'value'),
                array('bar' => Connection::PARAM_INT_ARRAY),
            ),
        );
    }

    /**
     * @dataProvider dataQueryWithMissingParameters
     */
    public function testExceptionIsThrownForMissingParam($query, $params, $types = array())
    {
        $this->expectException(
            'Doctrine\DBAL\SQLParserUtilsException',
            'Value for :param not found in params array. Params array key should be "param"'
        );

        SQLParserUtils::expandListParameters($query, $params, $types);
    }
}