*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockConnection;
/**
* @internal
*/
final class BaseQueryTest extends CIUnitTestCase
{
protected $db;
protected function setUp(): void
{
parent::setUp();
$this->db = new MockConnection([]);
}
public function testQueryStoresSQL()
{
$query = new Query($this->db);
$sql = 'SELECT * FROM users';
$query->setQuery($sql);
$this->assertSame($sql, $query->getQuery());
}
public function testStoresDuration()
{
$query = new Query($this->db);
$start = microtime(true);
$query->setDuration($start, $start + 5);
$this->assertSame(5, (int) $query->getDuration());
}
public function testGetStartTime()
{
$query = new Query($this->db);
$start = round(microtime(true));
$query->setDuration($start, $start + 5);
$this->assertSame($start, $query->getStartTime(true));
}
public function testGetStartTimeNumberFormat()
{
$query = new Query($this->db);
$start = microtime(true);
$query->setDuration($start, $start + 5);
$this->assertSame(number_format($start, 6), $query->getStartTime());
}
public function testsStoresErrorInformation()
{
$query = new Query($this->db);
$code = 13;
$msg = 'Oops, yo!';
$this->assertFalse($query->hasError());
$query->setError($code, $msg);
$this->assertTrue($query->hasError());
$this->assertSame($code, $query->getErrorCode());
$this->assertSame($msg, $query->getErrorMessage());
}
public function testSwapPrefix()
{
$query = new Query($this->db);
$origPrefix = 'db_';
$newPrefix = 'ci_';
$origSQL = 'SELECT * FROM db_users WHERE db_users.id = 1';
$newSQL = 'SELECT * FROM ci_users WHERE ci_users.id = 1';
$query->setQuery($origSQL);
$query->swapPrefix($origPrefix, $newPrefix);
$this->assertSame($newSQL, $query->getQuery());
}
public function queryTypes()
{
return [
'select' => [
false,
'SELECT * FROM users',
],
'set' => [
true,
'SET ...',
],
'insert' => [
true,
'INSERT INTO ...',
],
'update' => [
true,
'UPDATE ...',
],
'delete' => [
true,
'DELETE ...',
],
'replace' => [
true,
'REPLACE ...',
],
'create' => [
true,
'CREATE ...',
],
'drop' => [
true,
'DROP ...',
],
'truncate' => [
true,
'TRUNCATE ...',
],
'load' => [
true,
'LOAD ...',
],
'copy' => [
true,
'COPY ...',
],
'alter' => [
true,
'ALTER ...',
],
'rename' => [
true,
'RENAME ...',
],
'grant' => [
true,
'GRANT ...',
],
'revoke' => [
true,
'REVOKE ...',
],
'lock' => [
true,
'LOCK ...',
],
'unlock' => [
true,
'UNLOCK ...',
],
'reindex' => [
true,
'REINDEX ...',
],
];
}
/**
* @dataProvider queryTypes
*
* @param mixed $expected
* @param mixed $sql
*/
public function testIsWriteType($expected, $sql)
{
$query = new Query($this->db);
$query->setQuery($sql);
$this->assertSame($expected, $query->isWriteType());
}
public function testSingleBindingOutsideOfArray()
{
$query = new Query($this->db);
$query->setQuery('SELECT * FROM users WHERE id = ?', 13);
$expected = 'SELECT * FROM users WHERE id = 13';
$this->assertSame($expected, $query->getQuery());
}
public function testBindingSingleElementInArray()
{
$query = new Query($this->db);
$query->setQuery('SELECT * FROM users WHERE id = ?', [13]);
$expected = 'SELECT * FROM users WHERE id = 13';
$this->assertSame($expected, $query->getQuery());
}
public function testBindingMultipleItems()
{
$query = new Query($this->db);
$query->setQuery('SELECT * FROM users WHERE id = ? OR name = ?', [13, 'Vader']);
$expected = "SELECT * FROM users WHERE id = 13 OR name = 'Vader'";
$this->assertSame($expected, $query->getQuery());
}
public function testBindingAutoEscapesParameters()
{
$query = new Query($this->db);
$query->setQuery('SELECT * FROM users WHERE name = ?', ["O'Reilly"]);
$expected = "SELECT * FROM users WHERE name = 'O''Reilly'";
$this->assertSame($expected, $query->getQuery());
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/5114
*/
public function testBindingWithTwoColons()
{
$query = new Query($this->db);
$query->setQuery(
"SELECT mytable.id, DATE_FORMAT(mytable.created_at,'%d/%m/%Y %H:%i:%s') AS created_at_uk FROM mytable WHERE mytable.id = ?",
[1]
);
$expected = "SELECT mytable.id, DATE_FORMAT(mytable.created_at,'%d/%m/%Y %H:%i:%s') AS created_at_uk FROM mytable WHERE mytable.id = 1";
$this->assertSame($expected, $query->getQuery());
}
public function testNamedBinds()
{
$query = new Query($this->db);
$query->setQuery('SELECT * FROM users WHERE id = :id: OR name = :name:', ['id' => 13, 'name' => 'Geoffrey']);
$expected = "SELECT * FROM users WHERE id = 13 OR name = 'Geoffrey'";
$this->assertSame($expected, $query->getQuery());
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/3566
*/
public function testNamedBindsWithColonElsewhere()
{
$query = new Query($this->db);
$query->setQuery('SELECT `email`, @total:=(total+1) FROM `users` WHERE `id` = :id:', ['id' => 10]);
$sql = 'SELECT `email`, @total:=(total+1) FROM `users` WHERE `id` = 10';
$this->assertSame($sql, $query->getQuery());
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/pull/5138
*/
public function testNamedBindsWithBindMarkerElsewhere()
{
$query = new Query($this->db);
$query->setQuery('SELECT * FROM posts WHERE id = :id: AND title = \'The default bind marker is "?"\'', ['id' => 10]);
$sql = 'SELECT * FROM posts WHERE id = 10 AND title = \'The default bind marker is "?"\'';
$this->assertSame($sql, $query->getQuery());
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/pull/5138
*/
public function testSimpleBindsWithNamedBindPlaceholderElsewhere()
{
$query = new Query($this->db);
$query->setQuery('SELECT * FROM posts WHERE id = ? AND title = \'A named bind placeholder looks like ":foobar:"\'', 10);
$sql = 'SELECT * FROM posts WHERE id = 10 AND title = \'A named bind placeholder looks like ":foobar:"\'';
$this->assertSame($sql, $query->getQuery());
}
/**
* @group single
*
* @see https://github.com/codeigniter4/CodeIgniter4/issues/201
*/
public function testSimilarNamedBinds()
{
$query = new Query($this->db);
$query->setQuery('SELECT * FROM users WHERE sitemap = :sitemap: OR site = :site:', ['sitemap' => 'sitemap', 'site' => 'site']);
$expected = "SELECT * FROM users WHERE sitemap = 'sitemap' OR site = 'site'";
$this->assertSame($expected, $query->getQuery());
}
public function testNamedBindsDontGetReplacedAgain()
{
$query = new Query($this->db);
$query->setQuery(
'SELECT * FROM posts WHERE content = :content: OR foobar = :foobar:',
['content' => 'a placeholder looks like :foobar:', 'foobar' => 'bazqux']
);
$expected = "SELECT * FROM posts WHERE content = 'a placeholder looks like :foobar:' OR foobar = 'bazqux'";
$this->assertSame($expected, $query->getQuery());
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/1705
*/
public function testSetQueryBindsWithSetEscapeTrue()
{
$query = new Query($this->db);
$query->setQuery('UPDATE user_table SET `x` = NOW() WHERE `id` = :id:', ['id' => 22], true);
$expected = 'UPDATE user_table SET `x` = NOW() WHERE `id` = 22';
$this->assertSame($expected, $query->getQuery());
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/1705
*/
public function testSetQueryBindsWithSetEscapeFalse()
{
$query = new Query($this->db);
// The only time setQuery is called with setEscape = false
// is when the query builder has already stored the escaping info...
$binds = [
'id' => [
22,
1,
],
];
$query->setQuery('UPDATE user_table SET `x` = NOW() WHERE `id` = :id:', $binds, false);
$expected = 'UPDATE user_table SET `x` = NOW() WHERE `id` = 22';
$this->assertSame($expected, $query->getQuery());
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/4973
*/
public function testSetQueryBindsWithSetEscapeNegativeIntegers()
{
$query = new Query($this->db);
$query->setQuery(
'SELECT * FROM product WHERE date_pickup < DateAdd(month, ?, Convert(date, GetDate())',
[-6],
true
);
$expected = 'SELECT * FROM product WHERE date_pickup < DateAdd(month, -6, Convert(date, GetDate())';
$this->assertSame($expected, $query->getQuery());
}
public function testSetQueryNamedBindsWithNegativeIntegers()
{
$query = new Query($this->db);
$query->setQuery(
'SELECT * FROM product WHERE date_pickup < DateAdd(month, :num:, Convert(date, GetDate())',
['num' => -6]
);
$expected = 'SELECT * FROM product WHERE date_pickup < DateAdd(month, -6, Convert(date, GetDate())';
$this->assertSame($expected, $query->getQuery());
}
/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/2762
*/
public function testSetQueryBinds()
{
$query = new Query($this->db);
$binds = [
1,
2,
];
$query->setQuery('SELECT @factorA := ?, @factorB := ?', $binds);
$expected = 'SELECT @factorA := 1, @factorB := 2';
$this->assertSame($expected, $query->getQuery());
}
public function testGetQueryMultipleTimes()
{
$query = new Query($this->db);
$query->setQuery('SELECT * FROM users WHERE name = :name:', ['name' => 'placeholder :name: in value']);
$expected = "SELECT * FROM users WHERE name = 'placeholder :name: in value'";
// Triggers compileBinds()
$query->getQuery();
$this->assertSame($expected, $query->getQuery());
}
public function testSetBindsMultipleTimes()
{
$query = new Query($this->db);
$query->setQuery('SELECT * FROM users WHERE name = :name:', ['name' => 'John']);
// Triggers compileBinds()
$query->getQuery();
$query->setBinds(['name' => 'Jane']);
$expected = "SELECT * FROM users WHERE name = 'Jane'";
$this->assertSame($expected, $query->getQuery());
}
public function testSetQueryMultipleTimesKeepingBinds()
{
$query = new Query($this->db);
$query->setQuery('SELECT * FROM users WHERE name = :name:', ['name' => 'John']);
// Triggers compileBinds()
$query->getQuery();
$query->setQuery('SELECT * FROM users WHERE name = :name: OR id = 1');
$expected = "SELECT * FROM users WHERE name = 'John' OR id = 1";
$this->assertSame($expected, $query->getQuery());
}
public function testSetQueryMultipleTimesReplacingBinds()
{
$query = new Query($this->db);
$query->setQuery('SELECT * FROM users WHERE name = :name:', ['name' => 'John']);
// Triggers compileBinds()
$query->getQuery();
$query->setQuery('SELECT * FROM users WHERE name = :name: OR id = 1', ['name' => 'Jane']);
$expected = "SELECT * FROM users WHERE name = 'Jane' OR id = 1";
$this->assertSame($expected, $query->getQuery());
}
public function testSetQueryMultipleTimesRemovingBinds()
{
$query = new Query($this->db);
$query->setQuery('SELECT * FROM users WHERE name = :name:', ['name' => 'John']);
// Triggers compileBinds()
$query->getQuery();
$query->setQuery('SELECT * FROM users WHERE name = :name: OR id = 1', []);
$expected = 'SELECT * FROM users WHERE name = :name: OR id = 1';
$this->assertSame($expected, $query->getQuery());
}
public function testSwapPrefixMultipleTimes()
{
$query = new Query($this->db);
$origPrefix = 'db_';
$newPrefix1 = 'ci_';
$newPrefix2 = 'zz_';
$origSQL = 'SELECT * FROM db_users WHERE db_users.id = 1';
$newSQL = 'SELECT * FROM zz_users WHERE zz_users.id = 1';
$query->setQuery($origSQL);
$query->swapPrefix($origPrefix, $newPrefix1);
$query->swapPrefix($newPrefix1, $newPrefix2);
$this->assertSame($newSQL, $query->getQuery());
}
public function testSwapPrefixBeforeSetBinds()
{
$query = new Query($this->db);
$origSQL = 'SELECT * FROM db_users WHERE db_users.name = ?';
$newSQL = "SELECT * FROM ci_users WHERE ci_users.name = 'John'";
$query->setQuery($origSQL);
$query->swapPrefix('db_', 'ci_');
$query->setBinds(['John']);
$this->assertSame($newSQL, $query->getQuery());
}
public function testSwapPrefixAfterSetBinds()
{
$query = new Query($this->db);
$origSQL = 'SELECT * FROM db_users WHERE db_users.name = ?';
$newSQL = "SELECT * FROM ci_users WHERE ci_users.name = 'John'";
$query->setQuery($origSQL);
$query->setBinds(['John']);
$query->swapPrefix('db_', 'ci_');
$this->assertSame($newSQL, $query->getQuery());
}
public function testSwapPrefixIsAppliedBeforeBinds()
{
$query = new Query($this->db);
$origSQL = 'SELECT * FROM db_users WHERE db_users.name = ?';
$newSQL = "SELECT * FROM ci_users WHERE ci_users.name = 'John db_foobar John'";
$query->setQuery($origSQL);
$query->setBinds(['John db_foobar John']);
$query->swapPrefix('db_', 'ci_');
$this->assertSame($newSQL, $query->getQuery());
}
public function testSwapPrefixAfterGetQuery()
{
$query = new Query($this->db);
$query->setQuery('SELECT * FROM db_users WHERE db_users.id = 1');
// Triggers compileBinds()
$query->getQuery();
$query->swapPrefix('db_', 'ci_');
$expected = 'SELECT * FROM ci_users WHERE ci_users.id = 1';
$this->assertSame($expected, $query->getQuery());
}
public function queryKeywords()
{
return [
'highlightKeyWords' => [
'SELECT `a`.*, `b`.`id` AS `b_id` FROM `a` LEFT JOIN `b` ON `b`.`a_id` = `a`.`id` WHERE `b`.`id` IN ('1') AND `a`.`deleted_at` IS NOT NULL LIMIT 1',
'SELECT `a`.*, `b`.`id` AS `b_id` FROM `a` LEFT JOIN `b` ON `b`.`a_id` = `a`.`id` WHERE `b`.`id` IN (\'1\') AND `a`.`deleted_at` IS NOT NULL LIMIT 1',
],
'ignoreKeyWordsInValues' => [
'SELECT * FROM `a` WHERE `a`.`col` = 'SELECT escaped keyword in value' LIMIT 1',
'SELECT * FROM `a` WHERE `a`.`col` = \'SELECT escaped keyword in value\' LIMIT 1',
],
'escapeHtmlValues' => [
'SELECT '<s>' FROM dual',
'SELECT \'\' FROM dual',
],
];
}
/**
* @dataProvider queryKeywords
*
* @param mixed $expected
* @param mixed $sql
*/
public function testHighlightQueryKeywords($expected, $sql)
{
$query = new Query($this->db);
$query->setQuery($sql);
$this->assertSame($expected, $query->debugToolbarDisplay());
}
}