From 1647f345b658ac8745e172c14d4f8b6312d791d2 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 31 Jan 2025 13:18:50 +0100 Subject: [PATCH] FEAT add sortByField and remove mysql specific code from DataList --- src/ORM/Connect/Database.php | 22 ++++++++++++ src/ORM/Connect/MySQLDatabase.php | 18 ++++++++++ src/ORM/DataList.php | 13 ++++--- tests/php/ORM/DatabaseTest.php | 59 +++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 4 deletions(-) diff --git a/src/ORM/Connect/Database.php b/src/ORM/Connect/Database.php index 6b7dc9c0129..0654918cefa 100644 --- a/src/ORM/Connect/Database.php +++ b/src/ORM/Connect/Database.php @@ -954,4 +954,26 @@ abstract public function now(); * @return string Expression for a random value */ abstract public function random(); + + /** + * Generate SQL for sorting by a specific field using CASE logic. + * + * Subclasses can override this method to provide optimized implementations + * (e.g., using MySQL's FIELD method). + * + * @param string $field The name of the field to sort by. + * @param array $values The values to order by. + * @return string SQL snippet for ordering. + */ + public function sortByField(string $field, array $values): string + { + $caseStatements = []; + foreach ($values as $index => $value) { + $escaped = is_int($value) ? $value : "'" . addslashes($value) . "'"; + $caseStatements[] = "CASE {$field} = {$escaped} THEN {$index}"; + } + $count = count($caseStatements); + $sqlCase = implode(' ', $caseStatements); + return "CASE {$sqlCase} ELSE {$count} END"; + } } diff --git a/src/ORM/Connect/MySQLDatabase.php b/src/ORM/Connect/MySQLDatabase.php index dd9440096bb..554928cbb9a 100644 --- a/src/ORM/Connect/MySQLDatabase.php +++ b/src/ORM/Connect/MySQLDatabase.php @@ -586,4 +586,22 @@ public function clearTable($table) $this->query("ALTER TABLE \"$table\" AUTO_INCREMENT = 1"); } } + + /** + * Generate SQL for sorting by a specific field using MySQL's FIELD function. + * + * @param string $field The name of the field to sort by. + * @param array $values The values to order by. + * @return string SQL snippet for ordering. + */ + public function sortByField(string $field, array $values): string + { + $escapedValues = []; + foreach ($values as $value) { + $escaped = is_int($value) ? $value : "'" . addslashes($value) . "'"; + $escapedValues[] = $escaped; + } + $sqlIds = implode(',', $escapedValues); + return "FIELD({$field}, {$sqlIds})"; + } } diff --git a/src/ORM/DataList.php b/src/ORM/DataList.php index 43824002391..488e435e833 100644 --- a/src/ORM/DataList.php +++ b/src/ORM/DataList.php @@ -1374,16 +1374,21 @@ private function fetchEagerLoadManyMany( // Note that $joinRows also holds extra fields data $joinRows = []; if (!empty($parentIDs) && !empty($fetchedIDs)) { - $fetchedIDsAsString = implode(',', $fetchedIDs); - $joinRows = DB::query( + // Use sortByField to generate the ORDER BY clause + $orderByClause = DB::get_conn()->sortByField($childIDField, $fetchedIDs); + + $joinQuery = DB::query( 'SELECT * FROM "' . $joinTable // Only get joins relevant for the parent list . '" WHERE "' . $parentIDField . '" IN (' . implode(',', $parentIDs) . ')' // Exclude any children that got filtered out - . ' AND ' . $childIDField . ' IN (' . $fetchedIDsAsString . ')' + . ' AND ' . $childIDField . ' IN (' . implode(',', $fetchedIDs) . ')' // Respect sort order of fetched items - . ' ORDER BY FIELD(' . $childIDField . ', ' . $fetchedIDsAsString . ')' + . ' ORDER BY ' . $orderByClause ); + + // Execute the query + $joinRows = DB::query($joinQuery); } // Store the children in an EagerLoadedList against the correct parent diff --git a/tests/php/ORM/DatabaseTest.php b/tests/php/ORM/DatabaseTest.php index a216343a560..be46f1ca9c7 100644 --- a/tests/php/ORM/DatabaseTest.php +++ b/tests/php/ORM/DatabaseTest.php @@ -494,4 +494,63 @@ public function testRewindWithPredicates() $i++; } } + + /** + * Test that the sortByField method correctly generates an ORDER BY clause + * and the query returns results in the expected order. + */ + public function testSortByFieldIntegration() + { + // Define test data + $parentIDs = [1, 2]; + $fetchedIDs = [3, 2, 1]; // Expected order + $expectedOrder = [3, 2, 1]; + + $joinTable = 'TestJoinTable'; + $parentIDField = 'ParentID'; + $childIDField = 'ChildID'; + + // Create a temporary test table + DB::query(" + CREATE TEMPORARY TABLE {$joinTable} ( + {$childIDField} INT NOT NULL, + {$parentIDField} INT NOT NULL + ) + "); + + // Insert mock data + DB::query(" + INSERT INTO {$joinTable} ({$childIDField}, {$parentIDField}) + VALUES (1, 1), (2, 2), (3, 1) + "); + + // Use sortByField to generate the ORDER BY clause + $orderByClause = DB::get_conn()->sortByField($childIDField, $fetchedIDs); + + // Build the query with sortByField + $query = " + SELECT * + FROM {$joinTable} + WHERE {$parentIDField} IN (" . implode(',', $parentIDs) . ") + AND {$childIDField} IN (" . implode(',', $fetchedIDs) . ") + ORDER BY {$orderByClause} + "; + + // Execute the query and convert results to an array + $result = DB::query($query); + $rows = []; + foreach ($result as $row) { + $rows[] = $row; + } + + // Extract the actual order of fetched ChildIDs + $actualOrder = array_column($rows, $childIDField); + + // Assert that the results are ordered as expected + $this->assertEquals( + $expectedOrder, + $actualOrder, + 'The results should maintain the custom order defined by fetchedIDs' + ); + } }