Add support for having/lock options

This commit is contained in:
chinpei215 2017-02-03 14:35:15 +09:00
parent 4f98f01427
commit 923b73a7ba
7 changed files with 323 additions and 13 deletions

View file

@ -591,4 +591,14 @@ class Sqlite extends DboSource {
return $this->useNestedTransactions && version_compare($this->getVersion(), '3.6.8', '>='); return $this->useNestedTransactions && version_compare($this->getVersion(), '3.6.8', '>=');
} }
/**
* Returns a locking hint for the given mode.
* Sqlite Datasource doesn't support row-level locking.
*
* @param mixed $mode Lock mode
* @return string|null
*/
public function getLockingHint($mode) {
return null;
}
} }

View file

@ -526,6 +526,9 @@ class Sqlserver extends DboSource {
extract($data); extract($data);
$fields = trim($fields); $fields = trim($fields);
$having = !empty($having) ? " $having" : '';
$lock = !empty($lock) ? " $lock" : '';
if (strpos($limit, 'TOP') !== false && strpos($fields, 'DISTINCT ') === 0) { if (strpos($limit, 'TOP') !== false && strpos($fields, 'DISTINCT ') === 0) {
$limit = 'DISTINCT ' . trim($limit); $limit = 'DISTINCT ' . trim($limit);
$fields = substr($fields, 9); $fields = substr($fields, 9);
@ -547,7 +550,7 @@ class Sqlserver extends DboSource {
$rowCounter = static::ROW_COUNTER; $rowCounter = static::ROW_COUNTER;
$sql = "SELECT {$limit} * FROM ( $sql = "SELECT {$limit} * FROM (
SELECT {$fields}, ROW_NUMBER() OVER ({$order}) AS {$rowCounter} SELECT {$fields}, ROW_NUMBER() OVER ({$order}) AS {$rowCounter}
FROM {$table} {$alias} {$joins} {$conditions} {$group} FROM {$table} {$alias}{$lock} {$joins} {$conditions} {$group}{$having}
) AS _cake_paging_ ) AS _cake_paging_
WHERE _cake_paging_.{$rowCounter} > {$offset} WHERE _cake_paging_.{$rowCounter} > {$offset}
ORDER BY _cake_paging_.{$rowCounter} ORDER BY _cake_paging_.{$rowCounter}
@ -555,9 +558,9 @@ class Sqlserver extends DboSource {
return trim($sql); return trim($sql);
} }
if (strpos($limit, 'FETCH') !== false) { if (strpos($limit, 'FETCH') !== false) {
return trim("SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order} {$limit}"); return trim("SELECT {$fields} FROM {$table} {$alias}{$lock} {$joins} {$conditions} {$group}{$having} {$order} {$limit}");
} }
return trim("SELECT {$limit} {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order}"); return trim("SELECT {$limit} {$fields} FROM {$table} {$alias}{$lock} {$joins} {$conditions} {$group}{$having} {$order}");
case "schema": case "schema":
extract($data); extract($data);
@ -814,4 +817,17 @@ class Sqlserver extends DboSource {
return $this->config['schema']; return $this->config['schema'];
} }
/**
* Returns a locking hint for the given mode.
* Currently, this method only returns WITH (UPDLOCK) when the mode is true.
*
* @param mixed $mode Lock mode
* @return string|null
*/
public function getLockingHint($mode) {
if ($mode !== true) {
return null;
}
return ' WITH (UPDLOCK)';
}
} }

View file

@ -213,7 +213,9 @@ class DboSource extends DataSource {
'limit' => null, 'limit' => null,
'joins' => array(), 'joins' => array(),
'group' => null, 'group' => null,
'offset' => null 'offset' => null,
'having' => null,
'lock' => null,
); );
/** /**
@ -1732,7 +1734,9 @@ class DboSource extends DataSource {
'joins' => $queryData['joins'], 'joins' => $queryData['joins'],
'conditions' => $queryData['conditions'], 'conditions' => $queryData['conditions'],
'order' => $queryData['order'], 'order' => $queryData['order'],
'group' => $queryData['group'] 'group' => $queryData['group'],
'having' => $queryData['having'],
'lock' => $queryData['lock'],
), ),
$Model $Model
); );
@ -2011,7 +2015,9 @@ class DboSource extends DataSource {
'order' => $this->order($query['order'], 'ASC', $Model), 'order' => $this->order($query['order'], 'ASC', $Model),
'limit' => $this->limit($query['limit'], $query['offset']), 'limit' => $this->limit($query['limit'], $query['offset']),
'joins' => implode(' ', $query['joins']), 'joins' => implode(' ', $query['joins']),
'group' => $this->group($query['group'], $Model) 'group' => $this->group($query['group'], $Model),
'having' => $this->having($query['having'], true, $Model),
'lock' => $this->getLockingHint($query['lock']),
)); ));
} }
@ -2041,7 +2047,9 @@ class DboSource extends DataSource {
switch (strtolower($type)) { switch (strtolower($type)) {
case 'select': case 'select':
return trim("SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group} {$order} {$limit}"); $having = !empty($having) ? " $having" : '';
$lock = !empty($lock) ? " $lock" : '';
return trim("SELECT {$fields} FROM {$table} {$alias} {$joins} {$conditions} {$group}{$having} {$order} {$limit}{$lock}");
case 'create': case 'create':
return "INSERT INTO {$table} ({$fields}) VALUES ({$values})"; return "INSERT INTO {$table} ({$fields}) VALUES ({$values})";
case 'update': case 'update':
@ -2549,6 +2557,8 @@ class DboSource extends DataSource {
static $base = null; static $base = null;
if ($base === null) { if ($base === null) {
$base = array_fill_keys(array('conditions', 'fields', 'joins', 'order', 'limit', 'offset', 'group'), array()); $base = array_fill_keys(array('conditions', 'fields', 'joins', 'order', 'limit', 'offset', 'group'), array());
$base['having'] = null;
$base['lock'] = null;
$base['callbacks'] = null; $base['callbacks'] = null;
} }
return (array)$data + $base; return (array)$data + $base;
@ -3125,6 +3135,35 @@ class DboSource extends DataSource {
return ' GROUP BY ' . $this->_quoteFields($fields); return ' GROUP BY ' . $this->_quoteFields($fields);
} }
/**
* Create a HAVING SQL clause.
*
* @param mixed $fields Array or string of conditions
* @param bool $quoteValues If true, values should be quoted
* @param Model $Model A reference to the Model instance making the query
* @return string|null
*/
public function having($fields, $quoteValues = true, Model $Model = null) {
if (!$fields) {
return null;
}
return ' HAVING ' . $this->conditions($fields, $quoteValues, false, $Model);
}
/**
* Returns a locking hint for the given mode.
* Currently, this method only returns FOR UPDATE when the mode is true.
*
* @param mixed $mode Lock mode
* @return string|null
*/
public function getLockingHint($mode) {
if ($mode !== true) {
return null;
}
return ' FOR UPDATE';
}
/** /**
* Disconnects database, kills the connection and says the connection is closed. * Disconnects database, kills the connection and says the connection is closed.
* *

View file

@ -626,4 +626,25 @@ SQL;
$this->assertEquals($expected, $result); $this->assertEquals($expected, $result);
} }
/**
* Test Sqlite Datasource doesn't support locking hint
*
* @return void
*/
public function testBuildStatementWithoutLockingHint() {
$model = new TestModel();
$sql = $this->Dbo->buildStatement(
array(
'fields' => array('id'),
'table' => 'users',
'alias' => 'User',
'order' => array('id'),
'limit' => 1,
'lock' => true,
),
$model
);
$expected = 'SELECT id FROM users AS "User" WHERE 1 = 1 ORDER BY "id" ASC LIMIT 1';
$this->assertEquals($expected, $sql);
}
} }

View file

@ -707,4 +707,117 @@ SQL;
$this->assertEquals(2, $result['value']); $this->assertEquals(2, $result['value']);
} }
/**
* Test build statement
*
* @return void
*/
public function testBuildStatement() {
$db = $this->getMock('SqlserverTestDb', array('getVersion'), array($this->Dbo->config));
$db->expects($this->any())
->method('getVersion')
->will($this->returnValue('11.00.0000'));
// HAVING
$query = array(
'fields' => array('user_id', 'COUNT(*) AS count'),
'table' => 'articles',
'alias' => 'Article',
'group' => 'user_id',
'order' => array('COUNT(*)' => 'DESC'),
'limit' => 5,
'having' => array('COUNT(*) >' => 10),
);
$sql = $db->buildStatement($query, $this->model);
$expected = 'SELECT TOP 5 user_id, COUNT(*) AS count FROM articles AS [Article] WHERE 1 = 1 GROUP BY user_id HAVING COUNT(*) > 10 ORDER BY COUNT(*) DESC';
$this->assertEquals($expected, $sql);
$sql = $db->buildStatement(array('offset' => 15) + $query, $this->model);
$expected = 'SELECT user_id, COUNT(*) AS count FROM articles AS [Article] WHERE 1 = 1 GROUP BY user_id HAVING COUNT(*) > 10 ORDER BY COUNT(*) DESC OFFSET 15 ROWS FETCH FIRST 5 ROWS ONLY';
$this->assertEquals($expected, $sql);
// WITH (UPDLOCK)
$query = array(
'fields' => array('id'),
'table' => 'users',
'alias' => 'User',
'order' => array('id'),
'limit' => 1,
'lock' => true,
);
$sql = $db->buildStatement($query, $this->model);
$expected = 'SELECT TOP 1 id FROM users AS [User] WITH (UPDLOCK) WHERE 1 = 1 ORDER BY [id] ASC';
$this->assertEquals($expected, $sql);
$sql = $db->buildStatement(array('offset' => 15) + $query, $this->model);
$expected = 'SELECT id FROM users AS [User] WITH (UPDLOCK) WHERE 1 = 1 ORDER BY [id] ASC OFFSET 15 ROWS FETCH FIRST 1 ROWS ONLY';
$this->assertEquals($expected, $sql);
}
/**
* Test build statement with legacy version
*
* @return void
*/
public function testBuildStatementWithLegacyVersion() {
$db = $this->getMock('SqlserverTestDb', array('getVersion'), array($this->Dbo->config));
$db->expects($this->any())
->method('getVersion')
->will($this->returnValue('10.00.0000'));
// HAVING
$query = array(
'fields' => array('user_id', 'COUNT(*) AS count'),
'table' => 'articles',
'alias' => 'Article',
'group' => 'user_id',
'order' => array('COUNT(*)' => 'DESC'),
'limit' => 5,
'having' => array('COUNT(*) >' => 10),
);
$sql = $db->buildStatement($query, $this->model);
$expected = 'SELECT TOP 5 user_id, COUNT(*) AS count FROM articles AS [Article] WHERE 1 = 1 GROUP BY user_id HAVING COUNT(*) > 10 ORDER BY COUNT(*) DESC';
$this->assertEquals($expected, $sql);
$sql = $db->buildStatement(array('offset' => 15) + $query, $this->model);
$expected = <<<SQL
SELECT TOP 5 * FROM (
SELECT user_id, COUNT(*) AS count, ROW_NUMBER() OVER ( ORDER BY COUNT(*) DESC) AS _cake_page_rownum_
FROM articles AS [Article] WHERE 1 = 1 GROUP BY user_id HAVING COUNT(*) > 10
) AS _cake_paging_
WHERE _cake_paging_._cake_page_rownum_ > 15
ORDER BY _cake_paging_._cake_page_rownum_
SQL;
$this->assertEquals($expected, preg_replace('/^\s+|\s+$/m', '', $sql));
// WITH (UPDLOCK)
$query = array(
'fields' => array('id'),
'table' => 'users',
'alias' => 'User',
'order' => array('id'),
'limit' => 1,
'lock' => true,
);
$sql = $db->buildStatement($query, $this->model);
$expected = 'SELECT TOP 1 id FROM users AS [User] WITH (UPDLOCK) WHERE 1 = 1 ORDER BY [id] ASC';
$this->assertEquals($expected, $sql);
$sql = $db->buildStatement(array('offset' => 15) + $query, $this->model);
$expected = <<<SQL
SELECT TOP 1 * FROM (
SELECT id, ROW_NUMBER() OVER ( ORDER BY [id] ASC) AS _cake_page_rownum_
FROM users AS [User] WITH (UPDLOCK) WHERE 1 = 1
) AS _cake_paging_
WHERE _cake_paging_._cake_page_rownum_ > 15
ORDER BY _cake_paging_._cake_page_rownum_
SQL;
$this->assertEquals($expected, preg_replace('/^\s+|\s+$/m', '', $sql));
}
} }

View file

@ -1294,6 +1294,33 @@ class DboSourceTest extends CakeTestCase {
$this->assertEquals(' GROUP BY created', $result); $this->assertEquals(' GROUP BY created', $result);
} }
/**
* Test having method
*
* @return void
*/
public function testHaving() {
$this->loadFixtures('User');
$result = $this->testDb->having(array('COUNT(*) >' => 0));
$this->assertEquals(' HAVING COUNT(*) > 0', $result);
$User = ClassRegistry::init('User');
$result = $this->testDb->having('COUNT(User.id) > 0', true, $User);
$this->assertEquals(' HAVING COUNT(`User`.`id`) > 0', $result);
}
/**
* Test getLockingHint method
*
* @return void
*/
public function testGetLockingHint() {
$this->assertEquals(' FOR UPDATE', $this->testDb->getLockingHint(true));
$this->assertNull($this->testDb->getLockingHint(false));
$this->assertNull($this->testDb->getLockingHint(null));
}
/** /**
* Test getting the last error. * Test getting the last error.
* *
@ -1427,24 +1454,56 @@ class DboSourceTest extends CakeTestCase {
*/ */
public function testBuildStatementDefaults() { public function testBuildStatementDefaults() {
$conn = $this->getMock('MockPDO', array('quote')); $conn = $this->getMock('MockPDO', array('quote'));
$conn->expects($this->at(0)) $conn->expects($this->any())
->method('quote') ->method('quote')
->will($this->returnValue('foo bar')); ->will($this->returnArgument(0));
$db = new DboTestSource(); $db = new DboTestSource();
$db->setConnection($conn); $db->setConnection($conn);
$subQuery = $db->buildStatement( $subQuery = $db->buildStatement(
array( array(
'fields' => array('DISTINCT(AssetsTag.asset_id)'), 'fields' => array('DISTINCT(AssetsTag.asset_id)'),
'table' => "assets_tags", 'table' => 'assets_tags',
'alias' => "AssetsTag", 'alias' => 'AssetsTag',
'conditions' => array("Tag.name" => 'foo bar'), 'conditions' => array('Tag.name' => 'foo bar'),
'limit' => null, 'limit' => null,
'group' => "AssetsTag.asset_id" 'group' => 'AssetsTag.asset_id'
), ),
$this->Model $this->Model
); );
$expected = 'SELECT DISTINCT(AssetsTag.asset_id) FROM assets_tags AS AssetsTag WHERE Tag.name = foo bar GROUP BY AssetsTag.asset_id'; $expected = 'SELECT DISTINCT(AssetsTag.asset_id) FROM assets_tags AS AssetsTag WHERE Tag.name = foo bar GROUP BY AssetsTag.asset_id';
$this->assertEquals($expected, $subQuery); $this->assertEquals($expected, $subQuery);
// HAVING
$sql = $db->buildStatement(
array(
'fields' => array('user_id', 'COUNT(*) AS count'),
'table' => 'articles',
'alias' => 'Article',
'group' => 'user_id',
'order' => array('COUNT(*)' => 'DESC'),
'limit' => 5,
'having' => array('COUNT(*) >' => 10),
),
$this->Model
);
$expected = 'SELECT user_id, COUNT(*) AS count FROM articles AS Article WHERE 1 = 1 GROUP BY user_id HAVING COUNT(*) > 10 ORDER BY COUNT(*) DESC LIMIT 5';
$this->assertEquals($expected, $sql);
// FOR UPDATE
$sql = $db->buildStatement(
array(
'fields' => array('id'),
'table' => 'users',
'alias' => 'User',
'order' => array('id'),
'limit' => 1,
'lock' => true,
),
$this->Model
);
$expected = 'SELECT id FROM users AS User WHERE 1 = 1 ORDER BY id ASC LIMIT 1 FOR UPDATE';
$this->assertEquals($expected, $sql);
} }
/** /**
@ -2024,4 +2083,29 @@ class DboSourceTest extends CakeTestCase {
$result = $this->db->length("enum('One Value','ANOTHER ... VALUE ...')"); $result = $this->db->length("enum('One Value','ANOTHER ... VALUE ...')");
$this->assertEquals(21, $result); $this->assertEquals(21, $result);
} }
/**
* Test find with locking hint
*/
public function testFindWithLockingHint() {
$db = $this->getMock('DboTestSource', array('connect', '_execute', 'execute', 'describ'));
$Test = $this->getMock('Test', array('getDataSource'));
$Test->expects($this->any())
->method('getDataSource')
->will($this->returnValue($db));
$expected = 'SELECT Test.id FROM tests AS Test WHERE id = 1 ORDER BY Test.id ASC LIMIT 1 FOR UPDATE';
$db->expects($this->once())
->method('execute')
->with($expected);
$Test->find('first', array(
'recursive' => -1,
'fields' => array('id'),
'conditions' => array('id' => 1),
'lock' => true,
));
}
} }

View file

@ -352,6 +352,33 @@ class ModelReadTest extends BaseModelTest {
$this->assertEquals($expected, $result); $this->assertEquals($expected, $result);
} }
/**
* Test find method with having clause
*
* @return void
*/
public function testHaving() {
$this->loadFixtures('Comment');
$Comment = ClassRegistry::init('Comment');
$comments = $Comment->find('all', array(
'fields' => array('user_id', 'COUNT(*) AS count'),
'group' => array('user_id'),
'having' => array('COUNT(*) >' => 1),
'order' => array('COUNT(*)' => 'DESC'),
'recursive' => -1,
));
$results = Hash::combine($comments, '{n}.Comment.user_id', '{n}.0.count');
$expected = array(
1 => 3,
2 => 2,
);
$this->assertEquals($expected, $results);
}
/** /**
* testOldQuery method * testOldQuery method
* *