Skip to content

Commit

Permalink
Add support for having/lock options
Browse files Browse the repository at this point in the history
  • Loading branch information
chinpei215 committed Feb 3, 2017
1 parent 4f98f01 commit 923b73a
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 13 deletions.
10 changes: 10 additions & 0 deletions lib/Cake/Model/Datasource/Database/Sqlite.php
Expand Up @@ -591,4 +591,14 @@ public function nestedTransactionSupported() {
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;
}
}
22 changes: 19 additions & 3 deletions lib/Cake/Model/Datasource/Database/Sqlserver.php
Expand Up @@ -526,6 +526,9 @@ public function renderStatement($type, $data) {
extract($data);
$fields = trim($fields);

$having = !empty($having) ? " $having" : '';
$lock = !empty($lock) ? " $lock" : '';

if (strpos($limit, 'TOP') !== false && strpos($fields, 'DISTINCT ') === 0) {
$limit = 'DISTINCT ' . trim($limit);
$fields = substr($fields, 9);
Expand All @@ -547,17 +550,17 @@ public function renderStatement($type, $data) {
$rowCounter = static::ROW_COUNTER;
$sql = "SELECT {$limit} * FROM (
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_
WHERE _cake_paging_.{$rowCounter} > {$offset}
ORDER BY _cake_paging_.{$rowCounter}
";
return trim($sql);
}
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":
extract($data);

Expand Down Expand Up @@ -814,4 +817,17 @@ public function getSchemaName() {
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)';
}
}
47 changes: 43 additions & 4 deletions lib/Cake/Model/Datasource/DboSource.php
Expand Up @@ -213,7 +213,9 @@ class DboSource extends DataSource {
'limit' => null,
'joins' => array(),
'group' => null,
'offset' => null
'offset' => null,
'having' => null,
'lock' => null,
);

/**
Expand Down Expand Up @@ -1732,7 +1734,9 @@ public function buildAssociationQuery(Model $Model, $queryData) {
'joins' => $queryData['joins'],
'conditions' => $queryData['conditions'],
'order' => $queryData['order'],
'group' => $queryData['group']
'group' => $queryData['group'],
'having' => $queryData['having'],
'lock' => $queryData['lock'],
),
$Model
);
Expand Down Expand Up @@ -2011,7 +2015,9 @@ public function buildStatement($query, Model $Model) {
'order' => $this->order($query['order'], 'ASC', $Model),
'limit' => $this->limit($query['limit'], $query['offset']),
'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']),
));
}

Expand Down Expand Up @@ -2041,7 +2047,9 @@ public function renderStatement($type, $data) {

switch (strtolower($type)) {
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':
return "INSERT INTO {$table} ({$fields}) VALUES ({$values})";
case 'update':
Expand Down Expand Up @@ -2549,6 +2557,8 @@ protected function _scrubQueryData($data) {
static $base = null;
if ($base === null) {
$base = array_fill_keys(array('conditions', 'fields', 'joins', 'order', 'limit', 'offset', 'group'), array());
$base['having'] = null;
$base['lock'] = null;
$base['callbacks'] = null;
}
return (array)$data + $base;
Expand Down Expand Up @@ -3125,6 +3135,35 @@ public function group($fields, Model $Model = null) {
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.
*
Expand Down
21 changes: 21 additions & 0 deletions lib/Cake/Test/Case/Model/Datasource/Database/SqliteTest.php
Expand Up @@ -626,4 +626,25 @@ public function testFetchColumnRowParsingMoreComplex() {
$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);
}
}
113 changes: 113 additions & 0 deletions lib/Cake/Test/Case/Model/Datasource/Database/SqlserverTest.php
Expand Up @@ -707,4 +707,117 @@ public function testStoredProcedureReturn() {
$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));
}
}

0 comments on commit 923b73a

Please sign in to comment.