mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge branch 'orm-join-bug' of git://github.com/stojg/sapphire into stojg-orm-join-bug
This commit is contained in:
commit
2e9b5e9221
@ -190,7 +190,7 @@ class DataQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($joinTable) {
|
if ($joinTable) {
|
||||||
$query->addLeftJoin($tableClass, "\"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"") ;
|
$query->addLeftJoin($tableClass, "\"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"", $tableClass, 10) ;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,18 +276,28 @@ class SQLQuery {
|
|||||||
* Add a LEFT JOIN criteria to the FROM clause.
|
* Add a LEFT JOIN criteria to the FROM clause.
|
||||||
*
|
*
|
||||||
* @param string $table Unquoted table name
|
* @param string $table Unquoted table name
|
||||||
* @param string $onPredicate The "ON" SQL fragment in a "LEFT JOIN ... AS ... ON ..." statement,
|
* @param string $onPredicate The "ON" SQL fragment in a "LEFT JOIN ... AS ... ON ..." statement, Needs to be valid
|
||||||
* Needs to be valid (quoted) SQL.
|
* (quoted) SQL.
|
||||||
* @param string $tableAlias Optional alias which makes it easier to identify and replace joins later on
|
* @param string $tableAlias Optional alias which makes it easier to identify and replace joins later on
|
||||||
|
* @param int $order A numerical index to control the order that joins are added to the query; lower order values
|
||||||
|
* will cause the query to appear first. The default is 20, and joins created automatically by the
|
||||||
|
* ORM have a value of 10.
|
||||||
* @return SQLQuery
|
* @return SQLQuery
|
||||||
*/
|
*/
|
||||||
public function addLeftJoin($table, $onPredicate, $tableAlias = null) {
|
public function addLeftJoin($table, $onPredicate, $tableAlias = '', $order = 20) {
|
||||||
if(!$tableAlias) $tableAlias = $table;
|
if(!$tableAlias) {
|
||||||
$this->from[$tableAlias] = array('type' => 'LEFT', 'table' => $table, 'filter' => array($onPredicate));
|
$tableAlias = $table;
|
||||||
|
}
|
||||||
|
$this->from[$tableAlias] = array(
|
||||||
|
'type' => 'LEFT',
|
||||||
|
'table' => $table,
|
||||||
|
'filter' => array($onPredicate),
|
||||||
|
'order' => $order
|
||||||
|
);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function leftjoin($table, $onPredicate, $tableAlias = null) {
|
public function leftjoin($table, $onPredicate, $tableAlias = null, $order = 20) {
|
||||||
Deprecation::notice('3.0', 'Please use addLeftJoin() instead!');
|
Deprecation::notice('3.0', 'Please use addLeftJoin() instead!');
|
||||||
$this->addLeftJoin($table, $onPredicate, $tableAlias);
|
$this->addLeftJoin($table, $onPredicate, $tableAlias);
|
||||||
}
|
}
|
||||||
@ -296,20 +306,28 @@ class SQLQuery {
|
|||||||
* Add an INNER JOIN criteria to the FROM clause.
|
* Add an INNER JOIN criteria to the FROM clause.
|
||||||
*
|
*
|
||||||
* @param string $table Unquoted table name
|
* @param string $table Unquoted table name
|
||||||
* @param string $onPredicate The "ON" SQL fragment in an "INNER JOIN ... AS ... ON ..." statement.
|
* @param string $onPredicate The "ON" SQL fragment in an "INNER JOIN ... AS ... ON ..." statement. Needs to be
|
||||||
* Needs to be valid (quoted) SQL.
|
* valid (quoted) SQL.
|
||||||
* @param string $tableAlias Optional alias which makes it easier to identify and replace joins later on
|
* @param string $tableAlias Optional alias which makes it easier to identify and replace joins later on
|
||||||
|
* @param int $order A numerical index to control the order that joins are added to the query; lower order values
|
||||||
|
* will cause the query to appear first. The default is 20, and joins created automatically by the
|
||||||
|
* ORM have a value of 10.
|
||||||
* @return SQLQuery
|
* @return SQLQuery
|
||||||
*/
|
*/
|
||||||
public function addInnerJoin($table, $onPredicate, $tableAlias = null) {
|
public function addInnerJoin($table, $onPredicate, $tableAlias = null, $order = 20) {
|
||||||
if(!$tableAlias) $tableAlias = $table;
|
if(!$tableAlias) $tableAlias = $table;
|
||||||
$this->from[$tableAlias] = array('type' => 'INNER', 'table' => $table, 'filter' => array($onPredicate));
|
$this->from[$tableAlias] = array(
|
||||||
|
'type' => 'INNER',
|
||||||
|
'table' => $table,
|
||||||
|
'filter' => array($onPredicate),
|
||||||
|
'order' => $order
|
||||||
|
);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function innerjoin($table, $onPredicate, $tableAlias = null) {
|
public function innerjoin($table, $onPredicate, $tableAlias = null, $order = 20) {
|
||||||
Deprecation::notice('3.0', 'Please use addInnerJoin() instead!');
|
Deprecation::notice('3.0', 'Please use addInnerJoin() instead!');
|
||||||
return $this->addInnerJoin($table, $onPredicate, $tableAlias);
|
return $this->addInnerJoin($table, $onPredicate, $tableAlias, $order);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -876,12 +894,13 @@ class SQLQuery {
|
|||||||
// TODO: Don't require this internal-state manipulate-and-preserve - let sqlQueryToString() handle the new
|
// TODO: Don't require this internal-state manipulate-and-preserve - let sqlQueryToString() handle the new
|
||||||
// syntax
|
// syntax
|
||||||
$origFrom = $this->from;
|
$origFrom = $this->from;
|
||||||
|
// Sort the joins
|
||||||
|
$this->from = $this->getOrderedJoins($this->from);
|
||||||
// Build from clauses
|
// Build from clauses
|
||||||
foreach($this->from as $alias => $join) {
|
foreach($this->from as $alias => $join) {
|
||||||
// $join can be something like this array structure
|
// $join can be something like this array structure
|
||||||
// array('type' => 'inner', 'table' => 'SiteTree', 'filter' => array("SiteTree.ID = 1",
|
// array('type' => 'inner', 'table' => 'SiteTree', 'filter' => array("SiteTree.ID = 1",
|
||||||
// "Status = 'approved'"))
|
// "Status = 'approved'", 'order' => 20))
|
||||||
if(is_array($join)) {
|
if(is_array($join)) {
|
||||||
if(is_string($join['filter'])) $filter = $join['filter'];
|
if(is_string($join['filter'])) $filter = $join['filter'];
|
||||||
else if(sizeof($join['filter']) == 1) $filter = $join['filter'][0];
|
else if(sizeof($join['filter']) == 1) $filter = $join['filter'][0];
|
||||||
@ -902,10 +921,9 @@ class SQLQuery {
|
|||||||
$this->from = $origFrom;
|
$this->from = $origFrom;
|
||||||
|
|
||||||
// The query was most likely just created and then exectued.
|
// The query was most likely just created and then exectued.
|
||||||
if($sql === 'SELECT *') {
|
if(trim($sql) === 'SELECT * FROM') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $sql;
|
return $sql;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1087,5 +1105,80 @@ class SQLQuery {
|
|||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that framework "auto-generated" table JOINs are first in the finalised SQL query.
|
||||||
|
* This prevents issues where developer-initiated JOINs attempt to JOIN using relations that haven't actually
|
||||||
|
* yet been scaffolded by the framework. Demonstrated by PostGres in errors like:
|
||||||
|
*"...ERROR: missing FROM-clause..."
|
||||||
|
*
|
||||||
|
* @param $from array - in the format of $this->select
|
||||||
|
* @return array - and reorderded list of selects
|
||||||
|
*/
|
||||||
|
protected function getOrderedJoins($from) {
|
||||||
|
// shift the first FROM table out from so we only deal with the JOINs
|
||||||
|
$baseFrom = array_shift($from);
|
||||||
|
$this->mergesort($from, function($firstJoin, $secondJoin) {
|
||||||
|
if($firstJoin['order'] == $secondJoin['order']) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return ($firstJoin['order'] < $secondJoin['order']) ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Put the first FROM table back into the results
|
||||||
|
array_unshift($from, $baseFrom);
|
||||||
|
return $from;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since uasort don't preserve the order of an array if the comparison is equal
|
||||||
|
* we have to resort to a merge sort. It's quick and stable: O(n*log(n)).
|
||||||
|
*
|
||||||
|
* @see http://stackoverflow.com/q/4353739/139301
|
||||||
|
*
|
||||||
|
* @param array &$array - the array to sort
|
||||||
|
* @param callable $cmpFunction - the function to use for comparison
|
||||||
|
*/
|
||||||
|
protected function mergesort(&$array, $cmpFunction = 'strcmp') {
|
||||||
|
// Arrays of size < 2 require no action.
|
||||||
|
if (count($array) < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Split the array in half
|
||||||
|
$halfway = count($array) / 2;
|
||||||
|
$array1 = array_slice($array, 0, $halfway);
|
||||||
|
$array2 = array_slice($array, $halfway);
|
||||||
|
// Recurse to sort the two halves
|
||||||
|
$this->mergesort($array1, $cmpFunction);
|
||||||
|
$this->mergesort($array2, $cmpFunction);
|
||||||
|
// If all of $array1 is <= all of $array2, just append them.
|
||||||
|
if(call_user_func($cmpFunction, end($array1), reset($array2)) < 1) {
|
||||||
|
$array = array_merge($array1, $array2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Merge the two sorted arrays into a single sorted array
|
||||||
|
$array = array();
|
||||||
|
$val1 = reset($array1);
|
||||||
|
$val2 = reset($array2);
|
||||||
|
do {
|
||||||
|
if (call_user_func($cmpFunction, $val1, $val2) < 1) {
|
||||||
|
$array[key($array1)] = $val1;
|
||||||
|
$val1 = next($array1);
|
||||||
|
} else {
|
||||||
|
$array[key($array2)] = $val2;
|
||||||
|
$val2 = next($array2);
|
||||||
|
}
|
||||||
|
} while($val1 && $val2);
|
||||||
|
|
||||||
|
// Merge the remainder
|
||||||
|
while($val1) {
|
||||||
|
$array[key($array1)] = $val1;
|
||||||
|
$val1 = next($array1);
|
||||||
|
}
|
||||||
|
while($val2) {
|
||||||
|
$array[key($array2)] = $val2;
|
||||||
|
$val2 = next($array2);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class DataQueryTest extends SapphireTest {
|
class DataQueryTest extends SapphireTest {
|
||||||
|
|
||||||
|
protected $extraDataObjects = array(
|
||||||
|
'DataQueryTest_A',
|
||||||
|
'DataQueryTest_B',
|
||||||
|
'DataQueryTest_D',
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the leftJoin() and innerJoin method of the DataQuery object
|
* Test the leftJoin() and innerJoin method of the DataQuery object
|
||||||
*/
|
*/
|
||||||
@ -32,6 +39,12 @@ class DataQueryTest extends SapphireTest {
|
|||||||
$this->assertEquals('DataQueryTest_B', $dq->applyRelation('ManyTestBs'),
|
$this->assertEquals('DataQueryTest_B', $dq->applyRelation('ManyTestBs'),
|
||||||
'DataQuery::applyRelation should return the name of the related object.');
|
'DataQuery::applyRelation should return the name of the related object.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testRelationOrderWithCustomJoin() {
|
||||||
|
$dataQuery = new DataQuery('DataQueryTest_B');
|
||||||
|
$dataQuery->innerJoin('DataQueryTest_D', '"DataQueryTest_D"."RelationID" = "DataQueryTest_B"."ID"');
|
||||||
|
$dataQuery->execute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -56,6 +69,7 @@ class DataQueryTest_B extends DataQueryTest_A {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DataQueryTest_C extends DataObject implements TestOnly {
|
class DataQueryTest_C extends DataObject implements TestOnly {
|
||||||
|
|
||||||
public static $has_one = array(
|
public static $has_one = array(
|
||||||
'TestA' => 'DataQueryTest_A',
|
'TestA' => 'DataQueryTest_A',
|
||||||
'TestB' => 'DataQueryTest_B',
|
'TestB' => 'DataQueryTest_B',
|
||||||
@ -71,3 +85,10 @@ class DataQueryTest_C extends DataObject implements TestOnly {
|
|||||||
'ManyTestBs' => 'DataQueryTest_B',
|
'ManyTestBs' => 'DataQueryTest_B',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DataQueryTest_D extends DataObject implements TestOnly {
|
||||||
|
|
||||||
|
public static $has_one = array(
|
||||||
|
'Relation' => 'DataQueryTest_B',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -293,6 +293,7 @@ class SQLQueryTest extends SapphireTest {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function testSetWhereAny() {
|
public function testSetWhereAny() {
|
||||||
$query = new SQLQuery();
|
$query = new SQLQuery();
|
||||||
$query->setFrom('MyTable');
|
$query->setFrom('MyTable');
|
||||||
@ -376,4 +377,17 @@ class SQLQueryTest_DO extends DataObject implements TestOnly {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SQLQueryTestBase extends DataObject implements TestOnly {
|
||||||
|
static $db = array(
|
||||||
|
"Title" => "Varchar",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SQLQueryTestChild extends SQLQueryTestBase {
|
||||||
|
static $db = array(
|
||||||
|
"Name" => "Varchar",
|
||||||
|
);
|
||||||
|
|
||||||
|
static $has_one = array(
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user