diff --git a/src/ORM/DataList.php b/src/ORM/DataList.php index d8c53b4bc..420e40ab8 100644 --- a/src/ORM/DataList.php +++ b/src/ORM/DataList.php @@ -963,7 +963,7 @@ class DataList extends ViewableData implements SS_List, Filterable, Sortable, Li */ public function exists() { - return $this->count() > 0; + return $this->dataQuery->exists(); } /** diff --git a/src/ORM/DataQuery.php b/src/ORM/DataQuery.php index 005b215d2..93d02b5c6 100644 --- a/src/ORM/DataQuery.php +++ b/src/ORM/DataQuery.php @@ -453,6 +453,41 @@ class DataQuery return $this->getFinalisedQuery()->count("DISTINCT {$quotedColumn}"); } + /** + * Return whether this dataquery will have records. This will use `EXISTS` statements in SQL which are more + * performant - especially when used in combination with indexed columns (that you're filtering on) + * + * @return bool + */ + public function exists(): bool + { + // Grab a statement selecting "everything" - the engine shouldn't care what's being selected in an "EXISTS" + // statement anyway + $statement = $this->getFinalisedQuery(); + + // Clear limit, distinct, and order as it's not relevant for an exists query + $statement->setDistinct(false); + $statement->setOrderBy(null); + $statement->setLimit(null); + + // We can remove grouping if there's no "having" that might be relying on an aggregate + // Additionally, the columns being selected no longer matter + $having = $statement->getHaving(); + if (empty($having)) { + $statement->setSelect('*'); + $statement->setGroupBy(null); + } + + // Wrap the whole thing in an "EXISTS" + $sql = 'SELECT EXISTS(' . $statement->sql($params) . ')'; + $result = DB::prepared_query($sql, $params); + $row = $result->first(); + $result = reset($row); + + // Checking for 't' supports PostgreSQL before silverstripe/postgresql@2.2 + return $result === true || $result === 1 || $result === 't'; + } + /** * Return the maximum value of the given field in this DataList * diff --git a/tests/php/ORM/DataQueryTest.php b/tests/php/ORM/DataQueryTest.php index ffadd116a..1b9a3a1a4 100644 --- a/tests/php/ORM/DataQueryTest.php +++ b/tests/php/ORM/DataQueryTest.php @@ -6,6 +6,7 @@ use SilverStripe\ORM\DataQuery; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DB; use SilverStripe\Dev\SapphireTest; +use SilverStripe\ORM\Tests\DataQueryTest\ObjectE; use SilverStripe\Security\Member; /** @@ -386,11 +387,7 @@ class DataQueryTest extends SapphireTest $query = new DataQuery(DataQueryTest\ObjectC::class); $query->sort('"SortOrder"'); $query->where( - [ - '"DataQueryTest_C"."Title" = ? OR "DataQueryTest_E"."SortOrder" > ?' => [ - 'First', 2 - ] - ] + ['"DataQueryTest_C"."Title" = ? OR "DataQueryTest_E"."SortOrder" > ?' => ['First', 2]] ); $result = $query->getFinalisedQuery(['Title']); $from = $result->getFrom(); @@ -466,4 +463,11 @@ class DataQueryTest extends SapphireTest $this->assertEquals('Second', $titles[1]); $this->assertEquals('Last', $titles[2]); } + + public function testExistsCreatesFunctionalQueries() + { + $this->assertTrue(ObjectE::get()->exists()); + $this->assertFalse(ObjectE::get()->where(['"Title" = ?' => 'Foo'])->exists()); + $this->assertTrue(ObjectE::get()->dataQuery()->groupby('"SortOrder"')->exists()); + } }