Skip to content

Commit f16c1cd

Browse files
authored
Add support for duplicate named query parameters to the mysqli driver (#301)
1 parent 56aa950 commit f16c1cd

8 files changed

Lines changed: 702 additions & 6 deletions
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
/**
3+
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
4+
* @license GNU General Public License version 2 or later; see LICENSE
5+
*/
6+
7+
namespace Joomla\Database\Tests\Mysql;
8+
9+
use Joomla\Database\DatabaseDriver;
10+
use Joomla\Database\Exception\ExecutionFailureException;
11+
use Joomla\Database\Mysqli\MysqliStatement;
12+
use Joomla\Test\DatabaseTestCase;
13+
14+
class MysqlPreparedStatementTest extends DatabaseTestCase
15+
{
16+
/**
17+
* This method is called before the first test of this test class is run.
18+
*
19+
* @return void
20+
*/
21+
public static function setUpBeforeClass(): void
22+
{
23+
parent::setUpBeforeClass();
24+
25+
if (!static::$connection || static::$connection->getName() !== 'mysql') {
26+
self::markTestSkipped('MySQL database not configured.');
27+
}
28+
}
29+
30+
/**
31+
* Sets up the fixture.
32+
*
33+
* This method is called before a test is executed.
34+
*
35+
* @return void
36+
*/
37+
protected function setUp(): void
38+
{
39+
parent::setUp();
40+
41+
try {
42+
foreach (DatabaseDriver::splitSql(file_get_contents(dirname(__DIR__) . '/Stubs/Schema/mysql.sql')) as $query) {
43+
static::$connection->setQuery($query)
44+
->execute();
45+
}
46+
} catch (ExecutionFailureException $exception) {
47+
$this->markTestSkipped(
48+
\sprintf(
49+
'Could not load MySQL database: %s',
50+
$exception->getMessage()
51+
)
52+
);
53+
}
54+
}
55+
56+
/**
57+
* Tears down the fixture.
58+
*
59+
* This method is called after a test is executed.
60+
*/
61+
protected function tearDown(): void
62+
{
63+
foreach (static::$connection->getTableList() as $table) {
64+
static::$connection->dropTable($table);
65+
}
66+
}
67+
68+
/**
69+
* Make sure the mysqli driver correctly runs queries with named parameters appearing more than once.
70+
*
71+
* @doesNotPerformAssertions
72+
*/
73+
public function testPreparedStatementWithDuplicateKey()
74+
{
75+
$dummyValue = 'test';
76+
$query = static::$connection->getQuery(true);
77+
$query->select('*')
78+
->from($query->quoteName('dbtest'))
79+
->where([
80+
$query->quoteName('title') . ' LIKE :search',
81+
$query->quoteName('description') . ' LIKE :search',
82+
])
83+
->bind(':search', $dummyValue);
84+
85+
static::$connection->setQuery($query)->execute();
86+
}
87+
88+
/**
89+
* Regression test to ensure running queries with named parameters appearing once didn't break.
90+
*
91+
* @doesNotPerformAssertions
92+
*/
93+
public function testPreparedStatementWithSingleKey()
94+
{
95+
$dummyValue = 'test';
96+
$dummyValue2 = 'test';
97+
$query = static::$connection->getQuery(true);
98+
$query->select('*')
99+
->from($query->quoteName('dbtest'))
100+
->where([
101+
$query->quoteName('title') . ' LIKE :search',
102+
$query->quoteName('description') . ' LIKE :search2',
103+
])
104+
->bind(':search', $dummyValue)
105+
->bind(':search2', $dummyValue2);
106+
107+
static::$connection->setQuery($query)->execute();
108+
}
109+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
/**
3+
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
4+
* @license GNU General Public License version 2 or later; see LICENSE
5+
*/
6+
7+
namespace Joomla\Database\Tests\Mysqli;
8+
9+
use Joomla\Database\DatabaseDriver;
10+
use Joomla\Database\Exception\ExecutionFailureException;
11+
use Joomla\Database\Mysqli\MysqliStatement;
12+
use Joomla\Test\DatabaseTestCase;
13+
14+
class MysqliPreparedStatementTest extends DatabaseTestCase
15+
{
16+
/**
17+
* This method is called before the first test of this test class is run.
18+
*
19+
* @return void
20+
*/
21+
public static function setUpBeforeClass(): void
22+
{
23+
parent::setUpBeforeClass();
24+
25+
if (!static::$connection || static::$connection->getName() !== 'mysqli') {
26+
self::markTestSkipped('MySQL database not configured.');
27+
}
28+
}
29+
30+
/**
31+
* Sets up the fixture.
32+
*
33+
* This method is called before a test is executed.
34+
*
35+
* @return void
36+
*/
37+
protected function setUp(): void
38+
{
39+
parent::setUp();
40+
41+
try {
42+
foreach (DatabaseDriver::splitSql(file_get_contents(dirname(__DIR__) . '/Stubs/Schema/mysql.sql')) as $query) {
43+
static::$connection->setQuery($query)
44+
->execute();
45+
}
46+
} catch (ExecutionFailureException $exception) {
47+
$this->markTestSkipped(
48+
\sprintf(
49+
'Could not load MySQL database: %s',
50+
$exception->getMessage()
51+
)
52+
);
53+
}
54+
}
55+
56+
/**
57+
* Tears down the fixture.
58+
*
59+
* This method is called after a test is executed.
60+
*/
61+
protected function tearDown(): void
62+
{
63+
foreach (static::$connection->getTableList() as $table) {
64+
static::$connection->dropTable($table);
65+
}
66+
}
67+
68+
69+
/**
70+
* Make sure the mysqli driver correctly maps named query parameters appearing more than once.
71+
*/
72+
public function testPrepareParameterKeyMappingWithDuplicateKey()
73+
{
74+
$statement = 'SELECT * FROM dbtest WHERE `title` LIKE :search OR `description` LIKE :search';
75+
$mysqliStatementObject = new MysqliStatement(static::$connection->getConnection(), $statement);
76+
$rawQuery = $mysqliStatementObject->prepareParameterKeyMapping($statement);
77+
78+
$this->assertEquals(
79+
"SELECT * FROM dbtest WHERE `title` LIKE ? OR `description` LIKE ?",
80+
$rawQuery
81+
);
82+
83+
$refObject = new \ReflectionObject($mysqliStatementObject);
84+
$refMapping = $refObject->getProperty('parameterKeyMapping');
85+
/** @noinspection PhpExpressionResultUnusedInspection */
86+
$refMapping->setAccessible(true);
87+
$parameterKeyMapping = $refMapping->getValue($mysqliStatementObject);
88+
89+
$this->assertEquals(
90+
[
91+
':search' => [0, 1],
92+
],
93+
$parameterKeyMapping
94+
);
95+
}
96+
97+
/**
98+
* Regression test to ensure mapping query parameters appearing once didn't break.
99+
*/
100+
public function testPrepareParameterKeyMappingWithSingleKey()
101+
{
102+
$statement = 'SELECT * FROM dbtest WHERE `title` LIKE :search OR `description` LIKE :search2';
103+
$mysqliStatementObject = new MysqliStatement(static::$connection->getConnection(), $statement);
104+
$rawQuery = $mysqliStatementObject->prepareParameterKeyMapping($statement);
105+
106+
$this->assertEquals(
107+
"SELECT * FROM dbtest WHERE `title` LIKE ? OR `description` LIKE ?",
108+
$rawQuery
109+
);
110+
111+
$refObject = new \ReflectionObject($mysqliStatementObject);
112+
$refMapping = $refObject->getProperty('parameterKeyMapping');
113+
/** @noinspection PhpExpressionResultUnusedInspection */
114+
$refMapping->setAccessible(true);
115+
$parameterKeyMapping = $refMapping->getValue($mysqliStatementObject);
116+
117+
$this->assertEquals(
118+
[
119+
':search' => 0,
120+
':search2' => 1,
121+
],
122+
$parameterKeyMapping
123+
);
124+
}
125+
126+
/**
127+
* Make sure the mysqli driver correctly runs queries with named parameters appearing more than once.
128+
*
129+
* @doesNotPerformAssertions
130+
*/
131+
public function testPreparedStatementWithDuplicateKey()
132+
{
133+
$statement = 'SELECT * FROM dbtest WHERE `title` LIKE :search OR `description` LIKE :search';
134+
$mysqliStatementObject = new MysqliStatement(static::$connection->getConnection(), $statement);
135+
$dummyValue = 'test';
136+
$mysqliStatementObject->bindParam(':search', $dummyValue);
137+
138+
$mysqliStatementObject->execute();
139+
}
140+
141+
/**
142+
* Regression test to ensure running queries with named parameters appearing once didn't break.
143+
*
144+
* @doesNotPerformAssertions
145+
*/
146+
public function testPreparedStatementWithSingleKey()
147+
{
148+
$statement = 'SELECT * FROM dbtest WHERE `title` LIKE :search OR `description` LIKE :search2';
149+
$mysqliStatementObject = new MysqliStatement(static::$connection->getConnection(), $statement);
150+
$dummyValue = 'test';
151+
$dummyValue2 = 'test';
152+
$mysqliStatementObject->bindParam(':search', $dummyValue);
153+
$mysqliStatementObject->bindParam(':search2', $dummyValue);
154+
155+
$mysqliStatementObject->execute();
156+
}
157+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
/**
3+
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
4+
* @license GNU General Public License version 2 or later; see LICENSE
5+
*/
6+
7+
namespace Joomla\Database\Tests\Pgsql;
8+
9+
use Joomla\Database\DatabaseDriver;
10+
use Joomla\Database\Exception\ExecutionFailureException;
11+
use Joomla\Test\DatabaseTestCase;
12+
13+
class PgsqlPreparedStatementTest extends DatabaseTestCase
14+
{
15+
/**
16+
* This method is called before the first test of this test class is run.
17+
*
18+
* @return void
19+
*/
20+
public static function setUpBeforeClass(): void
21+
{
22+
$manager = static::getDatabaseManager();
23+
24+
$connection = $manager->getConnection();
25+
$manager->dropDatabase();
26+
$manager->createDatabase();
27+
$connection->select($manager->getDbName());
28+
29+
static::$connection = $connection;
30+
}
31+
32+
/**
33+
* Sets up the fixture.
34+
*
35+
* This method is called before a test is executed.
36+
*
37+
* @return void
38+
*/
39+
protected function setUp(): void
40+
{
41+
parent::setUp();
42+
43+
try {
44+
foreach (DatabaseDriver::splitSql(file_get_contents(dirname(__DIR__) . '/Stubs/Schema/pgsql.sql')) as $query) {
45+
static::$connection->setQuery($query)
46+
->execute();
47+
}
48+
} catch (ExecutionFailureException $exception) {
49+
$this->markTestSkipped(
50+
\sprintf(
51+
'Could not load PostgreSQL database: %s',
52+
$exception->getMessage()
53+
)
54+
);
55+
}
56+
}
57+
58+
/**
59+
* Tears down the fixture.
60+
*
61+
* This method is called after a test is executed.
62+
*/
63+
protected function tearDown(): void
64+
{
65+
foreach (static::$connection->getTableList() as $table) {
66+
static::$connection->dropTable($table);
67+
}
68+
}
69+
70+
/**
71+
* Make sure the mysqli driver correctly runs queries with named parameters appearing more than once.
72+
*
73+
* @doesNotPerformAssertions
74+
*/
75+
public function testPreparedStatementWithDuplicateKey()
76+
{
77+
$dummyValue = 'test';
78+
$query = static::$connection->getQuery(true);
79+
$query->select('*')
80+
->from($query->quoteName('dbtest'))
81+
->where([
82+
$query->quoteName('title') . ' LIKE :search',
83+
$query->quoteName('description') . ' LIKE :search',
84+
], 'OR')
85+
->bind(':search', $dummyValue);
86+
87+
static::$connection->setQuery($query)->execute();
88+
}
89+
90+
/**
91+
* Regression test to ensure running queries with named parameters appearing once didn't break.
92+
*
93+
* @doesNotPerformAssertions
94+
*/
95+
public function testPreparedStatementWithSingleKey()
96+
{
97+
$dummyValue = 'test';
98+
$dummyValue2 = 'test';
99+
$query = static::$connection->getQuery(true);
100+
$query->select('*')
101+
->from($query->quoteName('dbtest'))
102+
->where([
103+
$query->quoteName('title') . ' LIKE :search',
104+
$query->quoteName('description') . ' LIKE :search2',
105+
])
106+
->bind(':search', $dummyValue)
107+
->bind(':search2', $dummyValue2);
108+
109+
static::$connection->setQuery($query)->execute();
110+
}
111+
}

0 commit comments

Comments
 (0)