From 0c31062733c9d364251ac45c9f723198de86b5e0 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 12 Mar 2024 16:41:06 +0100 Subject: [PATCH 1/8] tests: Add more cases to test filter combinations --- tests/RelationFilterTest.php | 1295 ++++++++++++++++++++++++++++++++++ 1 file changed, 1295 insertions(+) create mode 100644 tests/RelationFilterTest.php diff --git a/tests/RelationFilterTest.php b/tests/RelationFilterTest.php new file mode 100644 index 0000000..522bf81 --- /dev/null +++ b/tests/RelationFilterTest.php @@ -0,0 +1,1295 @@ +createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' WHERE e.name = ?' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('bar', $offices[1]['city'] ?? 'not found'); + $this->assertSame('oof', $offices[2]['city'] ?? 'not found'); + $this->assertSame(3, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::equal('employee.name', 'foo')); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('bar', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('oof', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame(3, count($results), $sql); + } + + /** + * @equivalenceClass a:single, f:negation + * @dataProvider databases + * + * @param Connection $db + */ + public function testSingleNegativeCondition(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id AND e.name = ?' + . ' WHERE e.id IS NULL' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo'] + )->fetchAll(); + + $this->assertSame('baz', $offices[0]['city'] ?? 'not found'); + $this->assertSame('qux', $offices[1]['city'] ?? 'not found'); + $this->assertSame('quux', $offices[2]['city'] ?? 'not found'); + $this->assertSame(3, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::unequal('employee.name', 'foo')); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('baz', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('qux', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('quux', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame(3, count($results), $sql); + } + + /** + * Test whether multiple equal filters combined with OR on the same column of + * the same to-many relation, include results that match any condition. + * + * @equivalenceClass a:multiple, b:OR, c:single, d:same, e:same, f:affirmation + * @dataProvider databases + * + * @param Connection $db + */ + public function testOrChainTargetingASingleRelationColumnWithTheSameAffirmativeOperator(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' + . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' + . ' WHERE e1.id IS NOT NULL OR e2.id IS NOT NULL' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('bar', $offices[1]['city'] ?? 'not found'); + $this->assertSame('oof', $offices[2]['city'] ?? 'not found'); + $this->assertSame('quux', $offices[3]['city'] ?? 'not found'); + $this->assertSame(4, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::any( + Filter::equal('employee.name', 'foo'), + Filter::equal('employee.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('bar', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('oof', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('quux', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame(4, count($results), $sql); + } + + /** + * Test whether multiple unequal filters combined with OR on the same column + * of the same to-many relation, filter out results that match all conditions. + * + * @equivalenceClass a:multiple, b:OR, c:single, d:same, e:same, f:negation + * @dataProvider databases + * + * @param Connection $db + */ + public function testOrChainTargetingASingleRelationColumnWithTheSameNegativeOperator(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' + . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' + . ' WHERE NOT (e1.id IS NOT NULL AND e2.id IS NOT NULL)' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('bar', $offices[0]['city'] ?? 'not found'); + $this->assertSame('baz', $offices[1]['city'] ?? 'not found'); + $this->assertSame('qux', $offices[2]['city'] ?? 'not found'); + $this->assertSame('quux', $offices[3]['city'] ?? 'not found'); + $this->assertSame(4, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::any( + Filter::unequal('employee.name', 'foo'), + Filter::unequal('employee.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('bar', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('baz', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('qux', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('quux', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame(4, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:OR, c:single, d:same, e:different + * @dataProvider databases + * + * @param Connection $db + */ + public function testOrChainTargetingASingleRelationColumnWithDifferentOperators(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' + . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' + . ' WHERE e1.id IS NOT NULl OR e2.id IS NULL' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('bar', $offices[1]['city'] ?? 'not found'); + $this->assertSame('baz', $offices[2]['city'] ?? 'not found'); + $this->assertSame('oof', $offices[3]['city'] ?? 'not found'); + $this->assertSame('qux', $offices[4]['city'] ?? 'not found'); + $this->assertSame(5, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::any( + Filter::equal('employee.name', 'foo'), + Filter::unequal('employee.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('bar', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('baz', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('oof', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('qux', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); + } + + /** + * Test whether multiple equal filters combined with AND on the same column of + * the same to-many relation, only include results that match all conditions. + * + * @equivalenceClass a:multiple, b:AND, c:single, d:same, e:same, f:affirmation + * @dataProvider databases + * + * @param Connection $db + */ + public function testAndChainTargetingASingleRelationColumnWithTheSameAffirmativeOperator(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' + . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' + . ' WHERE e1.id IS NOT NULL AND e2.id IS NOT NULL' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('oof', $offices[1]['city'] ?? 'not found'); + $this->assertSame(2, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::all( + Filter::equal('employee.name', 'foo'), + Filter::equal('employee.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('oof', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame(2, count($results), $sql); + } + + /** + * Test whether multiple unequal filters combined with AND on the same column + * of the same to-many relation, filter out results that match any condition. + * + * @equivalenceClass a:multiple, b:AND, c:single, d:same, e:same, f:negation + * @dataProvider databases + * + * @param Connection $db + */ + public function testAndChainTargetingASingleRelationColumnWithTheSameNegativeOperator(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' + . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' + . ' WHERE e1.id IS NULL AND e2.id IS NULL' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('baz', $offices[0]['city'] ?? 'not found'); + $this->assertSame('qux', $offices[1]['city'] ?? 'not found'); + $this->assertSame(2, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::all( + Filter::unequal('employee.name', 'foo'), + Filter::unequal('employee.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('baz', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('qux', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame(2, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:AND, c:single, d:same, e:different + * @dataProvider databases + * + * @param Connection $db + */ + public function testAndChainTargetingASingleRelationColumnWithDifferentOperators(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' + . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' + . ' WHERE e1.id IS NOT NULL AND e2.id IS NULL' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('bar', $offices[0]['city'] ?? 'not found'); + $this->assertSame(1, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::all( + Filter::equal('employee.name', 'foo'), + Filter::unequal('employee.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('bar', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame(1, count($results), $sql); + } + + /** + * Test whether multiple equal filters combined with NOT on the same column of + * the same to-many relation, include results that match none of the conditions. + * + * @equivalenceClass a:multiple, b:NOT, c:single, d:same, e:same, f:affirmation + * @dataProvider databases + * + * @param Connection $db + */ + public function testNotChainTargetingASingleRelationColumnWithTheSameAffirmativeOperator(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' + . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' + . ' WHERE NOT (e1.id IS NOT NULL OR e2.id IS NOT NULL)' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('baz', $offices[0]['city'] ?? 'not found'); + $this->assertSame('qux', $offices[1]['city'] ?? 'not found'); + $this->assertSame(2, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::none( + Filter::equal('employee.name', 'foo'), + Filter::equal('employee.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('baz', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('qux', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame(2, count($results), $sql); + } + + /** + * Test whether multiple unequal filters combined with NOT on the same column + * of the same to-many relation, only include results that match all conditions. + * + * @equivalenceClass a:multiple, b:NOT, c:single, d:same, e:same, f:negation + * @dataProvider databases + * + * @param Connection $db + */ + public function testNotChainTargetingASingleRelationColumnWithTheSameNegativeOperator(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' + . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' + . ' WHERE NOT (e1.id IS NULL OR e2.id IS NULL)' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('oof', $offices[1]['city'] ?? 'not found'); + $this->assertSame(2, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::none( + Filter::unequal('employee.name', 'foo'), + Filter::unequal('employee.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('oof', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame(2, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:NOT, c:single, d:same, e:different + * @dataProvider databases + * + * @param Connection $db + */ + public function testNotChainTargetingASingleRelationColumnWithDifferentOperators(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' + . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' + . ' WHERE NOT (e1.id IS NOT NULL OR e2.id IS NULL)' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('quux', $offices[0]['city'] ?? 'not found'); + $this->assertSame(1, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::none( + Filter::equal('employee.name', 'foo'), + Filter::unequal('employee.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('quux', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame(1, count($results), $sql); + } + + /** + * Test whether the ORM produces correct results if an unequal filter is combined + * with an equal filter on the same 1-n relation and both with different columns + * + * @equivalenceClass a:multiple, b:AND, c:single, d:different, e:different + * @dataProvider databases + * + * @param Connection $db + */ + public function testAndChainTargetingASingleRelationButDifferentColumnsWithDifferentOperators(Connection $db) + { + $db->insert('department', ['id' => 1, 'name' => 'foo']); + $db->insert('employee', ['id' => 1, 'department_id' => 1, 'name' => 'foo', 'role' => 'bar']); + $db->insert('employee', ['id' => 2, 'department_id' => 1, 'name' => 'bar', 'role' => 'foo']); + $db->insert('department', ['id' => 2, 'name' => 'bar']); + $db->insert('employee', ['id' => 5, 'department_id' => 2, 'name' => 'foo', 'role' => 'oof']); + $db->insert('department', ['id' => 3, 'name' => 'baz']); + $db->insert('employee', ['id' => 6, 'department_id' => 3, 'name' => 'foo', 'role' => null]); + $db->insert('employee', ['id' => 7, 'department_id' => 3, 'name' => 'bar', 'role' => null]); + $db->insert('department', ['id' => 4, 'name' => 'qux']); + $db->insert('employee', ['id' => 8, 'department_id' => 4, 'name' => 'foo', 'role' => 'bar']); + $db->insert('employee', ['id' => 9, 'department_id' => 4, 'name' => 'foo', 'role' => 'baz']); + + // First a proof of concept by using a manually crafted SQL query + $departments = $db->prepexec( + 'SELECT department.name FROM department' + . ' LEFT JOIN employee e on department.id = e.department_id' + . ' WHERE e.name = ? AND (e.role != ? OR e.role IS NULL)' + . ' GROUP BY department.id' + . ' ORDER BY department.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('bar', $departments[0]['name'] ?? 'not found'); + $this->assertSame('baz', $departments[1]['name'] ?? 'not found'); + $this->assertSame('qux', $departments[2]['name'] ?? 'not found'); + $this->assertSame(3, count($departments)); + + // Now let's do the same using the ORM + $departments = Department::on($db) + ->columns(['department.name']) + ->orderBy('department.id') + ->filter(Filter::all( + Filter::equal('employee.name', 'foo'), + Filter::unequal('employee.role', 'bar') + )); + $results = iterator_to_array($departments); + $sql = $this->getSql($departments); + + $this->assertSame('bar', $results[0]['name'] ?? 'not found', $sql); + $this->assertSame('baz', $results[1]['name'] ?? 'not found', $sql); + $this->assertSame('qux', $results[2]['name'] ?? 'not found', $sql); + $this->assertSame(3, count($results), $sql); + + // The ORM may perform fine till now, but let's see what happens if we include some false positives + $db->insert('department', ['id' => 5, 'name' => 'quux']); + $db->insert('employee', ['id' => 10, 'department_id' => 5, 'name' => 'bar', 'role' => 'oof']); + + // This employee's role doesn't match but the name does neither, resulting in the department not showing up + $db->insert('employee', ['id' => 11, 'department_id' => 5, 'name' => 'oof', 'role' => 'foo']); + + // This department has no employees and as such none with the desired name, although the role, being not + // set due to the left join, would match. It might also show up due to a NOT EXISTS/NOT IN. + $db->insert('department', ['id' => 6, 'name' => 'qa']); + + // Proof of concept first, again + $departments = $db->prepexec( + 'SELECT department.name FROM department' + . ' LEFT JOIN employee e on department.id = e.department_id' + . ' WHERE e.name = ? AND (e.role != ? OR e.role IS NULL)' + . ' GROUP BY department.id' + . ' ORDER BY department.id', + ['bar', 'foo'] + )->fetchAll(); + + $this->assertSame('baz', $departments[0]['name'] ?? 'not found'); + $this->assertSame('quux', $departments[1]['name'] ?? 'not found'); + $this->assertSame(2, count($departments)); + + // Now the ORM. Note that the result depends on how the subqueries are constructed to filter the results + $departments = Department::on($db) + ->columns(['department.name']) + ->orderBy('department.id') + ->filter(Filter::all( + Filter::equal('employee.name', 'bar'), + Filter::unequal('employee.role', 'foo') + )); + $results = iterator_to_array($departments); + $sql = $this->getSql($departments); + + $this->assertSame('baz', $results[0]['name'] ?? 'not found', $sql); + $this->assertSame('quux', $results[1]['name'] ?? 'not found', $sql); + $this->assertSame(2, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:AND, c:single, d:different, e:same + * @dataProvider databases + * + * @param Connection $db + */ + public function testAndChainTargetingASingleRelationButDifferentColumnsWithTheSameOperator(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e ON e.office_id = office.id' + . ' WHERE e.name = ? AND e.role = ?' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('oof', $offices[1]['city'] ?? 'not found'); + $this->assertSame(2, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::all( + Filter::equal('employee.name', 'foo'), + Filter::equal('employee.role', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('oof', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame(2, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:OR, c:single, d:different, e:different + * @dataProvider databases + * + * @param Connection $db + */ + public function testOrChainTargetingASingleRelationButDifferentColumnsWithDifferentOperators(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e ON e.office_id = office.id' + . ' WHERE e.name = ? OR (e.role != ? OR e.role IS NULL)' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('bar', $offices[1]['city'] ?? 'not found'); + $this->assertSame('baz', $offices[2]['city'] ?? 'not found'); + $this->assertSame('oof', $offices[3]['city'] ?? 'not found'); + $this->assertSame('qux', $offices[4]['city'] ?? 'not found'); + $this->assertSame('quux', $offices[5]['city'] ?? 'not found'); + $this->assertSame(6, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::any( + Filter::equal('employee.name', 'foo'), + Filter::unequal('employee.role', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('bar', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('baz', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('oof', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('qux', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame('quux', $results[5]['city'] ?? 'not found', $sql); + $this->assertSame(6, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:OR, c:single, d:different, e:same + * @dataProvider databases + * + * @param Connection $db + */ + public function testOrChainTargetingASingleRelationButDifferentColumnsWithTheSameOperator(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e ON e.office_id = office.id' + . ' WHERE e.name = ? OR e.role = ?' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('bar', $offices[1]['city'] ?? 'not found'); + $this->assertSame('oof', $offices[2]['city'] ?? 'not found'); + $this->assertSame(3, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::any( + Filter::equal('employee.name', 'foo'), + Filter::equal('employee.role', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('bar', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('oof', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame(3, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:NOT, c:single, d:different, e:different + * @dataProvider databases + * + * @param Connection $db + */ + public function testNotChainTargetingASingleRelationButDifferentColumnsWithDifferentOperators(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e ON e.office_id = office.id' + . ' WHERE NOT (e.name = ? OR (e.role != ? OR e.role IS NULL))' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'qux'] + )->fetchAll(); + + $this->assertSame('baz', $offices[0]['city'] ?? 'not found'); + $this->assertSame(1, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::none( + Filter::equal('employee.name', 'foo'), + Filter::unequal('employee.role', 'qux') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('baz', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame(1, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:NOT, c:single, d:different, e:same + * @dataProvider databases + * + * @param Connection $db + */ + public function testNotChainTargetingASingleRelationButDifferentColumnsWithTheSameOperator(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e ON e.office_id = office.id' + . ' WHERE NOT (e.name = ? OR e.role = ?) OR e.id IS NULL' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('baz', $offices[1]['city'] ?? 'not found'); + $this->assertSame('oof', $offices[2]['city'] ?? 'not found'); + $this->assertSame('qux', $offices[3]['city'] ?? 'not found'); + $this->assertSame('quux', $offices[4]['city'] ?? 'not found'); + $this->assertSame(5, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::none( + Filter::equal('employee.name', 'foo'), + Filter::equal('employee.role', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('baz', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('oof', $offices[2]['city'] ?? 'not found', $sql); + $this->assertSame('qux', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('quux', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:AND, c:multiple, e:same, f:affirmation + * @dataProvider databases + * + * @param Connection $db + */ + public function testAndChainTargetingMultipleRelationsWithTheSameAffirmativeOperator(Connection $db) + { + $this->createOfficesEmployeesAndDepartments($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE e.name = ? AND d.name = ?' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame(1, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::all( + Filter::equal('employee.name', 'foo'), + Filter::equal('employee.department.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame(1, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:AND, c:multiple, e:same, f:negation + * @dataProvider databases + * + * @param Connection $db + */ + public function testAndChainTargetingMultipleRelationsWithTheSameNegativeOperator(Connection $db) + { + $this->createOfficesEmployeesAndDepartments($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id AND e.name = ?' + . ' LEFT JOIN department d on e.department_id = d.id AND d.name = ?' + . ' WHERE e.id IS NULL AND d.id IS NULL' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('bar', $offices[0]['city'] ?? 'not found'); + $this->assertSame('baz', $offices[1]['city'] ?? 'not found'); + $this->assertSame(2, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::all( + Filter::unequal('employee.name', 'foo'), + Filter::unequal('employee.department.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('bar', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('baz', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame(2, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:AND, c:multiple, e:different + * @dataProvider databases + * + * @param Connection $db + */ + public function testAndChainTargetingMultipleRelationsWithDifferentOperators(Connection $db) + { + $this->createOfficesEmployeesAndDepartments($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE e.name = ? AND d.name != ?' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('oof', $offices[0]['city'] ?? 'not found'); + $this->assertSame(1, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::all( + Filter::equal('employee.name', 'foo'), + Filter::unequal('employee.department.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('oof', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame(1, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:OR, c:multiple, e:same, f:affirmation + * @dataProvider databases + * + * @param Connection $db + */ + public function testOrChainTargetingMultipleRelationsWithTheSameAffirmativeOperator(Connection $db) + { + $this->createOfficesEmployeesAndDepartments($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE e.name = ? OR d.name = ?' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('oof', $offices[1]['city'] ?? 'not found'); + $this->assertSame('baz', $offices[2]['city'] ?? 'not found'); + $this->assertSame(3, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::any( + Filter::equal('employee.name', 'foo'), + Filter::equal('employee.department.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('oof', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('baz', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame(3, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:OR, c:multiple, e:same, f:negation + * @dataProvider databases + * + * @param Connection $db + */ + public function testOrChainTargetingMultipleRelationsWithTheSameNegativeOperator(Connection $db) + { + $this->createOfficesEmployeesAndDepartments($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id AND e.name = ?' + . ' LEFT JOIN department d on e.department_id = d.id AND d.name = ?' + . ' WHERE e.id IS NULL OR d.id IS NULL' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('oof', $offices[0]['city'] ?? 'not found'); + $this->assertSame('bar', $offices[1]['city'] ?? 'not found'); + $this->assertSame('baz', $offices[2]['city'] ?? 'not found'); + $this->assertSame(3, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::any( + Filter::unequal('employee.name', 'foo'), + Filter::unequal('employee.department.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('oof', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('bar', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('baz', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame(3, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:OR, c:multiple, e:different + * @dataProvider databases + * + * @param Connection $db + */ + public function testOrChainTargetingMultipleRelationsWithDifferentOperators(Connection $db) + { + $this->createOfficesEmployeesAndDepartments($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE e.name = ? OR d.name != ?' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('oof', $offices[1]['city'] ?? 'not found'); + $this->assertSame('bar', $offices[2]['city'] ?? 'not found'); + $this->assertSame(3, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::any( + Filter::equal('employee.name', 'foo'), + Filter::unequal('employee.department.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('oof', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('bar', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame(3, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:NOT, c:multiple, e:same, f:affirmation + * @dataProvider databases + * + * @param Connection $db + */ + public function testNotChainTargetingMultipleRelationsWithTheSameAffirmativeOperator(Connection $db) + { + $this->createOfficesEmployeesAndDepartments($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE NOT (e.name = ? OR d.name = ?)' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('bar', $offices[1]['city'] ?? 'not found'); + $this->assertSame(2, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::none( + Filter::equal('employee.name', 'foo'), + Filter::equal('employee.department.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('bar', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame(2, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:NOT, c:multiple, e:same, f:negation + * @dataProvider databases + * + * @param Connection $db + */ + public function testNotChainTargetingMultipleRelationsWithTheSameNegativeOperator(Connection $db) + { + $this->createOfficesEmployeesAndDepartments($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id AND e.name = ?' + . ' LEFT JOIN department d on e.department_id = d.id AND d.name = ?' + . ' WHERE NOT (e.id IS NULL OR d.id IS NULL)' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame(1, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::none( + Filter::unequal('employee.name', 'foo'), + Filter::unequal('employee.department.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame(1, count($results), $sql); + } + + /** + * @equivalenceClass a:multiple, b:NOT, c:multiple, e:different + * @dataProvider databases + * + * @param Connection $db + */ + public function testNotChainTargetingMultipleRelationsWithDifferentOperators(Connection $db) + { + $this->createOfficesEmployeesAndDepartments($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE NOT (e.name = ? OR d.name != ?)' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['foo', 'bar'] + )->fetchAll(); + + $this->assertSame('oof', $offices[0]['city'] ?? 'not found'); + $this->assertSame('baz', $offices[1]['city'] ?? 'not found'); + $this->assertSame(2, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::none( + Filter::equal('employee.name', 'foo'), + Filter::unequal('employee.department.name', 'bar') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('oof', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('baz', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame(2, count($results), $sql); + } + + /** + * Test whether an unequal, that targets a to-many relation to which a link can only be established through an + * optional other relation, is built by the ORM in a way that coincidental matches are ignored + * + * This will fail if the ORM generates a NOT IN which uses a subquery that produces NULL values. + * + * @dataProvider databases + */ + public function testUnequalTargetingAnOptionalToManyRelationIgnoresFalsePositives(Connection $db) + { + $db->insert('office', ['id' => 1, 'city' => 'foo']); + $db->insert('department', ['id' => 1, 'name' => 'bar']); + $db->insert('department', ['id' => 2, 'name' => 'baz']); + $db->insert('employee', ['id' => 1, 'department_id' => 1, 'name' => 'qux', 'role' => 'quux']); // remote + $db->insert( + 'employee', + ['id' => 2, 'department_id' => 2, 'office_id' => 1, 'name' => 'corge', 'role' => 'grault'] + ); + + // This POC uses inner joins to achieve the desired result + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' INNER JOIN employee e on e.office_id = office.id' + . ' INNER JOIN department d on e.department_id = d.id' + . ' WHERE d.name != ?' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['bar'] + )->fetchAll(); + + $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + + // The ORM will use a NOT IN and needs to ignore false positives explicitly + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::unequal('employee.department.name', 'bar')); + $results = iterator_to_array($offices); + + $this->assertSame('foo', $results[0]['city'] ?? 'not found', $this->getSql($offices)); + } + + /** + * Create a definite set of permutations with employees and offices + * + * @param Connection $db + */ + protected function createOfficesAndEmployees(Connection $db) + { + $db->insert('office', ['id' => 1, 'city' => 'foo']); // Two + $db->insert('employee', ['id' => 1, 'office_id' => 1, 'name' => 'foo', 'role' => 'bar']); + $db->insert('employee', ['id' => 2, 'office_id' => 1, 'name' => 'bar', 'role' => 'foo']); + $db->insert('office', ['id' => 2, 'city' => 'bar']); // One of the two + $db->insert('employee', ['id' => 3, 'office_id' => 2, 'name' => 'foo', 'role' => 'oof']); + $db->insert('office', ['id' => 3, 'city' => 'baz']); // None of the two + $db->insert('employee', ['id' => 4, 'office_id' => 3, 'name' => 'oof', 'role' => 'qux']); + $db->insert('office', ['id' => 4, 'city' => 'oof']); // The two plus another one + $db->insert('employee', ['id' => 5, 'office_id' => 4, 'name' => 'foo', 'role' => 'bar']); + $db->insert('employee', ['id' => 6, 'office_id' => 4, 'name' => 'bar', 'role' => 'foo']); + $db->insert('employee', ['id' => 7, 'office_id' => 4, 'name' => 'quux', 'role' => 'corge']); + $db->insert('office', ['id' => 5, 'city' => 'qux']); // None + $db->insert('office', ['id' => 6, 'city' => 'quux']); // The other one of the two + $db->insert('employee', ['id' => 8, 'office_id' => 6, 'name' => 'bar', 'role' => 'baz']); + } + + /** + * Create a definite set of permutations with employees, offices and departments + * + * @param Connection $db + */ + protected function createOfficesEmployeesAndDepartments(Connection $db) + { + $db->insert('office', ['id' => 1, 'city' => 'foo']); + $db->insert('office', ['id' => 2, 'city' => 'oof']); + $db->insert('office', ['id' => 3, 'city' => 'bar']); + $db->insert('office', ['id' => 4, 'city' => 'baz']); + $db->insert('department', ['id' => 1, 'name' => 'bar']); + $db->insert('department', ['id' => 2, 'name' => 'baz']); + $db->insert( + 'employee', + ['id' => 1, 'office_id' => 1, 'department_id' => 1, 'name' => 'foo', 'role' => 'bar'] + ); + $db->insert( + 'employee', + ['id' => 2, 'office_id' => 1, 'department_id' => 2, 'name' => 'oof', 'role' => 'corge'] + ); + $db->insert( + 'employee', + ['id' => 3, 'office_id' => 2, 'department_id' => 2, 'name' => 'foo', 'role' => 'oof'] + ); + $db->insert( + 'employee', + ['id' => 4, 'office_id' => 2, 'department_id' => 1, 'name' => 'bar', 'role' => 'baz'] + ); + $db->insert( + 'employee', + ['id' => 5, 'office_id' => 3, 'department_id' => 2, 'name' => 'bar', 'role' => 'qux'] + ); + $db->insert( + 'employee', + ['id' => 6, 'office_id' => 4, 'department_id' => 1, 'name' => 'baz', 'role' => 'foo'] + ); + } + + protected function createSchema(Connection $db, string $driver): void + { + $db->exec('CREATE TABLE office (id INT PRIMARY KEY, city VARCHAR(255))'); + $db->exec('CREATE TABLE department (id INT PRIMARY KEY, name VARCHAR(255))'); + $db->exec( + 'CREATE TABLE employee (id INT PRIMARY KEY, department_id INT,' + . ' office_id INT, name VARCHAR(255), role VARCHAR(255))' + ); + } + + protected function dropSchema(Connection $db, string $driver): void + { + $db->exec('DROP TABLE IF EXISTS employee'); + $db->exec('DROP TABLE IF EXISTS department'); + $db->exec('DROP TABLE IF EXISTS office'); + } + + /** + * Format the given query to SQL + * + * @param Query $query + * + * @return string + */ + protected function getSql(Query $query): string + { + list($sql, $values) = $query->getDb()->getQueryBuilder()->assembleSelect($query->assembleSelect()); + foreach ($values as $value) { + $pos = strpos($sql, '?'); + if ($pos !== false) { + if (is_string($value)) { + $value = "'" . $value . "'"; + } + + $sql = substr_replace($sql, $value, $pos, 1); + } + } + + return $sql; + } +} From c4fba18dadb09b0a22953d5aa29a89c1e169efff Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Wed, 28 Feb 2024 16:20:59 +0100 Subject: [PATCH 2/8] RelationFilterTest: Enhance test data --- tests/RelationFilterTest.php | 555 ++++++++++++++++++----------------- 1 file changed, 288 insertions(+), 267 deletions(-) diff --git a/tests/RelationFilterTest.php b/tests/RelationFilterTest.php index 522bf81..415e8fd 100644 --- a/tests/RelationFilterTest.php +++ b/tests/RelationFilterTest.php @@ -61,24 +61,24 @@ public function testSingleAffirmativeCondition(Connection $db) . ' WHERE e.name = ?' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo'] + ['Donald'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); - $this->assertSame('bar', $offices[1]['city'] ?? 'not found'); - $this->assertSame('oof', $offices[2]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found'); $this->assertSame(3, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') - ->filter(Filter::equal('employee.name', 'foo')); + ->filter(Filter::equal('employee.name', 'Donald')); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('bar', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('oof', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[2]['city'] ?? 'not found', $sql); $this->assertSame(3, count($results), $sql); } @@ -98,24 +98,24 @@ public function testSingleNegativeCondition(Connection $db) . ' WHERE e.id IS NULL' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo'] + ['Donald'] )->fetchAll(); - $this->assertSame('baz', $offices[0]['city'] ?? 'not found'); - $this->assertSame('qux', $offices[1]['city'] ?? 'not found'); - $this->assertSame('quux', $offices[2]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[2]['city'] ?? 'not found'); $this->assertSame(3, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') - ->filter(Filter::unequal('employee.name', 'foo')); + ->filter(Filter::unequal('employee.name', 'Donald')); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('baz', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('qux', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('quux', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[2]['city'] ?? 'not found', $sql); $this->assertSame(3, count($results), $sql); } @@ -139,29 +139,29 @@ public function testOrChainTargetingASingleRelationColumnWithTheSameAffirmativeO . ' WHERE e1.id IS NOT NULL OR e2.id IS NOT NULL' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Huey'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); - $this->assertSame('bar', $offices[1]['city'] ?? 'not found'); - $this->assertSame('oof', $offices[2]['city'] ?? 'not found'); - $this->assertSame('quux', $offices[3]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[3]['city'] ?? 'not found'); $this->assertSame(4, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::any( - Filter::equal('employee.name', 'foo'), - Filter::equal('employee.name', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::equal('employee.name', 'Huey') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('bar', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('oof', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame('quux', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[3]['city'] ?? 'not found', $sql); $this->assertSame(4, count($results), $sql); } @@ -185,29 +185,29 @@ public function testOrChainTargetingASingleRelationColumnWithTheSameNegativeOper . ' WHERE NOT (e1.id IS NOT NULL AND e2.id IS NOT NULL)' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Huey'] )->fetchAll(); - $this->assertSame('bar', $offices[0]['city'] ?? 'not found'); - $this->assertSame('baz', $offices[1]['city'] ?? 'not found'); - $this->assertSame('qux', $offices[2]['city'] ?? 'not found'); - $this->assertSame('quux', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[3]['city'] ?? 'not found'); $this->assertSame(4, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::any( - Filter::unequal('employee.name', 'foo'), - Filter::unequal('employee.name', 'bar') + Filter::unequal('employee.name', 'Donald'), + Filter::unequal('employee.name', 'Huey') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('bar', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('baz', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('qux', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame('quux', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[3]['city'] ?? 'not found', $sql); $this->assertSame(4, count($results), $sql); } @@ -228,31 +228,31 @@ public function testOrChainTargetingASingleRelationColumnWithDifferentOperators( . ' WHERE e1.id IS NOT NULl OR e2.id IS NULL' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Huey'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); - $this->assertSame('bar', $offices[1]['city'] ?? 'not found'); - $this->assertSame('baz', $offices[2]['city'] ?? 'not found'); - $this->assertSame('oof', $offices[3]['city'] ?? 'not found'); - $this->assertSame('qux', $offices[4]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[4]['city'] ?? 'not found'); $this->assertSame(5, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::any( - Filter::equal('employee.name', 'foo'), - Filter::unequal('employee.name', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::unequal('employee.name', 'Huey') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('bar', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('baz', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame('oof', $results[3]['city'] ?? 'not found', $sql); - $this->assertSame('qux', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[4]['city'] ?? 'not found', $sql); $this->assertSame(5, count($results), $sql); } @@ -276,25 +276,25 @@ public function testAndChainTargetingASingleRelationColumnWithTheSameAffirmative . ' WHERE e1.id IS NOT NULL AND e2.id IS NOT NULL' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Huey'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); - $this->assertSame('oof', $offices[1]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); $this->assertSame(2, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::all( - Filter::equal('employee.name', 'foo'), - Filter::equal('employee.name', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::equal('employee.name', 'Huey') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('oof', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); $this->assertSame(2, count($results), $sql); } @@ -318,25 +318,25 @@ public function testAndChainTargetingASingleRelationColumnWithTheSameNegativeOpe . ' WHERE e1.id IS NULL AND e2.id IS NULL' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Huey'] )->fetchAll(); - $this->assertSame('baz', $offices[0]['city'] ?? 'not found'); - $this->assertSame('qux', $offices[1]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[1]['city'] ?? 'not found'); $this->assertSame(2, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::all( - Filter::unequal('employee.name', 'foo'), - Filter::unequal('employee.name', 'bar') + Filter::unequal('employee.name', 'Donald'), + Filter::unequal('employee.name', 'Huey') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('baz', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('qux', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[1]['city'] ?? 'not found', $sql); $this->assertSame(2, count($results), $sql); } @@ -357,23 +357,23 @@ public function testAndChainTargetingASingleRelationColumnWithDifferentOperators . ' WHERE e1.id IS NOT NULL AND e2.id IS NULL' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Huey'] )->fetchAll(); - $this->assertSame('bar', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); $this->assertSame(1, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::all( - Filter::equal('employee.name', 'foo'), - Filter::unequal('employee.name', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::unequal('employee.name', 'Huey') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('bar', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); $this->assertSame(1, count($results), $sql); } @@ -397,25 +397,25 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameAffirmative . ' WHERE NOT (e1.id IS NOT NULL OR e2.id IS NOT NULL)' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Huey'] )->fetchAll(); - $this->assertSame('baz', $offices[0]['city'] ?? 'not found'); - $this->assertSame('qux', $offices[1]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[1]['city'] ?? 'not found'); $this->assertSame(2, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::none( - Filter::equal('employee.name', 'foo'), - Filter::equal('employee.name', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::equal('employee.name', 'Huey') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('baz', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('qux', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[1]['city'] ?? 'not found', $sql); $this->assertSame(2, count($results), $sql); } @@ -439,25 +439,25 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameNegativeOpe . ' WHERE NOT (e1.id IS NULL OR e2.id IS NULL)' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Huey'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); - $this->assertSame('oof', $offices[1]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); $this->assertSame(2, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::none( - Filter::unequal('employee.name', 'foo'), - Filter::unequal('employee.name', 'bar') + Filter::unequal('employee.name', 'Donald'), + Filter::unequal('employee.name', 'Huey') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('oof', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); $this->assertSame(2, count($results), $sql); } @@ -478,23 +478,23 @@ public function testNotChainTargetingASingleRelationColumnWithDifferentOperators . ' WHERE NOT (e1.id IS NOT NULL OR e2.id IS NULL)' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Huey'] )->fetchAll(); - $this->assertSame('quux', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[0]['city'] ?? 'not found'); $this->assertSame(1, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::none( - Filter::equal('employee.name', 'foo'), - Filter::unequal('employee.name', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::unequal('employee.name', 'Huey') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('quux', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[0]['city'] ?? 'not found', $sql); $this->assertSame(1, count($results), $sql); } @@ -509,17 +509,17 @@ public function testNotChainTargetingASingleRelationColumnWithDifferentOperators */ public function testAndChainTargetingASingleRelationButDifferentColumnsWithDifferentOperators(Connection $db) { - $db->insert('department', ['id' => 1, 'name' => 'foo']); - $db->insert('employee', ['id' => 1, 'department_id' => 1, 'name' => 'foo', 'role' => 'bar']); - $db->insert('employee', ['id' => 2, 'department_id' => 1, 'name' => 'bar', 'role' => 'foo']); - $db->insert('department', ['id' => 2, 'name' => 'bar']); - $db->insert('employee', ['id' => 5, 'department_id' => 2, 'name' => 'foo', 'role' => 'oof']); - $db->insert('department', ['id' => 3, 'name' => 'baz']); - $db->insert('employee', ['id' => 6, 'department_id' => 3, 'name' => 'foo', 'role' => null]); - $db->insert('employee', ['id' => 7, 'department_id' => 3, 'name' => 'bar', 'role' => null]); - $db->insert('department', ['id' => 4, 'name' => 'qux']); - $db->insert('employee', ['id' => 8, 'department_id' => 4, 'name' => 'foo', 'role' => 'bar']); - $db->insert('employee', ['id' => 9, 'department_id' => 4, 'name' => 'foo', 'role' => 'baz']); + $db->insert('department', ['id' => 1, 'name' => 'Sales']); + $db->insert('employee', ['id' => 1, 'department_id' => 1, 'name' => 'Donald', 'role' => 'Accountant']); + $db->insert('employee', ['id' => 2, 'department_id' => 1, 'name' => 'Huey', 'role' => 'Manager']); + $db->insert('department', ['id' => 2, 'name' => 'Accounting']); + $db->insert('employee', ['id' => 5, 'department_id' => 2, 'name' => 'Donald', 'role' => 'Salesperson']); + $db->insert('department', ['id' => 3, 'name' => 'Kitchen']); + $db->insert('employee', ['id' => 6, 'department_id' => 3, 'name' => 'Donald', 'role' => null]); + $db->insert('employee', ['id' => 7, 'department_id' => 3, 'name' => 'Huey', 'role' => null]); + $db->insert('department', ['id' => 4, 'name' => 'QA']); + $db->insert('employee', ['id' => 8, 'department_id' => 4, 'name' => 'Donald', 'role' => 'Accountant']); + $db->insert('employee', ['id' => 9, 'department_id' => 4, 'name' => 'Donald', 'role' => 'Assistant']); // First a proof of concept by using a manually crafted SQL query $departments = $db->prepexec( @@ -528,12 +528,12 @@ public function testAndChainTargetingASingleRelationButDifferentColumnsWithDiffe . ' WHERE e.name = ? AND (e.role != ? OR e.role IS NULL)' . ' GROUP BY department.id' . ' ORDER BY department.id', - ['foo', 'bar'] + ['Donald', 'Accountant'] )->fetchAll(); - $this->assertSame('bar', $departments[0]['name'] ?? 'not found'); - $this->assertSame('baz', $departments[1]['name'] ?? 'not found'); - $this->assertSame('qux', $departments[2]['name'] ?? 'not found'); + $this->assertSame('Accounting', $departments[0]['name'] ?? 'not found'); + $this->assertSame('Kitchen', $departments[1]['name'] ?? 'not found'); + $this->assertSame('QA', $departments[2]['name'] ?? 'not found'); $this->assertSame(3, count($departments)); // Now let's do the same using the ORM @@ -541,27 +541,27 @@ public function testAndChainTargetingASingleRelationButDifferentColumnsWithDiffe ->columns(['department.name']) ->orderBy('department.id') ->filter(Filter::all( - Filter::equal('employee.name', 'foo'), - Filter::unequal('employee.role', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::unequal('employee.role', 'Accountant') )); $results = iterator_to_array($departments); $sql = $this->getSql($departments); - $this->assertSame('bar', $results[0]['name'] ?? 'not found', $sql); - $this->assertSame('baz', $results[1]['name'] ?? 'not found', $sql); - $this->assertSame('qux', $results[2]['name'] ?? 'not found', $sql); + $this->assertSame('Accounting', $results[0]['name'] ?? 'not found', $sql); + $this->assertSame('Kitchen', $results[1]['name'] ?? 'not found', $sql); + $this->assertSame('QA', $results[2]['name'] ?? 'not found', $sql); $this->assertSame(3, count($results), $sql); // The ORM may perform fine till now, but let's see what happens if we include some false positives - $db->insert('department', ['id' => 5, 'name' => 'quux']); - $db->insert('employee', ['id' => 10, 'department_id' => 5, 'name' => 'bar', 'role' => 'oof']); + $db->insert('department', ['id' => 5, 'name' => 'Admin']); + $db->insert('employee', ['id' => 10, 'department_id' => 5, 'name' => 'Huey', 'role' => 'Salesperson']); // This employee's role doesn't match but the name does neither, resulting in the department not showing up - $db->insert('employee', ['id' => 11, 'department_id' => 5, 'name' => 'oof', 'role' => 'foo']); + $db->insert('employee', ['id' => 11, 'department_id' => 5, 'name' => 'Dewey', 'role' => 'Manager']); // This department has no employees and as such none with the desired name, although the role, being not // set due to the left join, would match. It might also show up due to a NOT EXISTS/NOT IN. - $db->insert('department', ['id' => 6, 'name' => 'qa']); + $db->insert('department', ['id' => 6, 'name' => 'QA']); // Proof of concept first, again $departments = $db->prepexec( @@ -570,11 +570,11 @@ public function testAndChainTargetingASingleRelationButDifferentColumnsWithDiffe . ' WHERE e.name = ? AND (e.role != ? OR e.role IS NULL)' . ' GROUP BY department.id' . ' ORDER BY department.id', - ['bar', 'foo'] + ['Huey', 'Manager'] )->fetchAll(); - $this->assertSame('baz', $departments[0]['name'] ?? 'not found'); - $this->assertSame('quux', $departments[1]['name'] ?? 'not found'); + $this->assertSame('Kitchen', $departments[0]['name'] ?? 'not found'); + $this->assertSame('Admin', $departments[1]['name'] ?? 'not found'); $this->assertSame(2, count($departments)); // Now the ORM. Note that the result depends on how the subqueries are constructed to filter the results @@ -582,14 +582,14 @@ public function testAndChainTargetingASingleRelationButDifferentColumnsWithDiffe ->columns(['department.name']) ->orderBy('department.id') ->filter(Filter::all( - Filter::equal('employee.name', 'bar'), - Filter::unequal('employee.role', 'foo') + Filter::equal('employee.name', 'Huey'), + Filter::unequal('employee.role', 'Manager') )); $results = iterator_to_array($departments); $sql = $this->getSql($departments); - $this->assertSame('baz', $results[0]['name'] ?? 'not found', $sql); - $this->assertSame('quux', $results[1]['name'] ?? 'not found', $sql); + $this->assertSame('Kitchen', $results[0]['name'] ?? 'not found', $sql); + $this->assertSame('Admin', $results[1]['name'] ?? 'not found', $sql); $this->assertSame(2, count($results), $sql); } @@ -609,25 +609,25 @@ public function testAndChainTargetingASingleRelationButDifferentColumnsWithTheSa . ' WHERE e.name = ? AND e.role = ?' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Accountant'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); - $this->assertSame('oof', $offices[1]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); $this->assertSame(2, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::all( - Filter::equal('employee.name', 'foo'), - Filter::equal('employee.role', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::equal('employee.role', 'Accountant') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('oof', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); $this->assertSame(2, count($results), $sql); } @@ -647,33 +647,33 @@ public function testOrChainTargetingASingleRelationButDifferentColumnsWithDiffer . ' WHERE e.name = ? OR (e.role != ? OR e.role IS NULL)' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Accountant'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); - $this->assertSame('bar', $offices[1]['city'] ?? 'not found'); - $this->assertSame('baz', $offices[2]['city'] ?? 'not found'); - $this->assertSame('oof', $offices[3]['city'] ?? 'not found'); - $this->assertSame('qux', $offices[4]['city'] ?? 'not found'); - $this->assertSame('quux', $offices[5]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[4]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[5]['city'] ?? 'not found'); $this->assertSame(6, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::any( - Filter::equal('employee.name', 'foo'), - Filter::unequal('employee.role', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::unequal('employee.role', 'Accountant') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('bar', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('baz', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame('oof', $results[3]['city'] ?? 'not found', $sql); - $this->assertSame('qux', $results[4]['city'] ?? 'not found', $sql); - $this->assertSame('quux', $results[5]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[5]['city'] ?? 'not found', $sql); $this->assertSame(6, count($results), $sql); } @@ -693,27 +693,27 @@ public function testOrChainTargetingASingleRelationButDifferentColumnsWithTheSam . ' WHERE e.name = ? OR e.role = ?' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Accountant'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); - $this->assertSame('bar', $offices[1]['city'] ?? 'not found'); - $this->assertSame('oof', $offices[2]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found'); $this->assertSame(3, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::any( - Filter::equal('employee.name', 'foo'), - Filter::equal('employee.role', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::equal('employee.role', 'Accountant') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('bar', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('oof', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[2]['city'] ?? 'not found', $sql); $this->assertSame(3, count($results), $sql); } @@ -733,23 +733,23 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithDiffe . ' WHERE NOT (e.name = ? OR (e.role != ? OR e.role IS NULL))' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'qux'] + ['Donald', 'QA Lead'] )->fetchAll(); - $this->assertSame('baz', $offices[0]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[0]['city'] ?? 'not found'); $this->assertSame(1, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::none( - Filter::equal('employee.name', 'foo'), - Filter::unequal('employee.role', 'qux') + Filter::equal('employee.name', 'Donald'), + Filter::unequal('employee.role', 'QA Lead') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('baz', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[0]['city'] ?? 'not found', $sql); $this->assertSame(1, count($results), $sql); } @@ -769,31 +769,31 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithTheSa . ' WHERE NOT (e.name = ? OR e.role = ?) OR e.id IS NULL' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Accountant'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); - $this->assertSame('baz', $offices[1]['city'] ?? 'not found'); - $this->assertSame('oof', $offices[2]['city'] ?? 'not found'); - $this->assertSame('qux', $offices[3]['city'] ?? 'not found'); - $this->assertSame('quux', $offices[4]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[4]['city'] ?? 'not found'); $this->assertSame(5, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::none( - Filter::equal('employee.name', 'foo'), - Filter::equal('employee.role', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::equal('employee.role', 'Accountant') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('baz', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('oof', $offices[2]['city'] ?? 'not found', $sql); - $this->assertSame('qux', $results[3]['city'] ?? 'not found', $sql); - $this->assertSame('quux', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[4]['city'] ?? 'not found', $sql); $this->assertSame(5, count($results), $sql); } @@ -814,23 +814,23 @@ public function testAndChainTargetingMultipleRelationsWithTheSameAffirmativeOper . ' WHERE e.name = ? AND d.name = ?' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); $this->assertSame(1, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::all( - Filter::equal('employee.name', 'foo'), - Filter::equal('employee.department.name', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::equal('employee.department.name', 'Accounting') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); $this->assertSame(1, count($results), $sql); } @@ -851,25 +851,25 @@ public function testAndChainTargetingMultipleRelationsWithTheSameNegativeOperato . ' WHERE e.id IS NULL AND d.id IS NULL' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('bar', $offices[0]['city'] ?? 'not found'); - $this->assertSame('baz', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); $this->assertSame(2, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::all( - Filter::unequal('employee.name', 'foo'), - Filter::unequal('employee.department.name', 'bar') + Filter::unequal('employee.name', 'Donald'), + Filter::unequal('employee.department.name', 'Accounting') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('bar', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('baz', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); $this->assertSame(2, count($results), $sql); } @@ -890,23 +890,23 @@ public function testAndChainTargetingMultipleRelationsWithDifferentOperators(Con . ' WHERE e.name = ? AND d.name != ?' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('oof', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[0]['city'] ?? 'not found'); $this->assertSame(1, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::all( - Filter::equal('employee.name', 'foo'), - Filter::unequal('employee.department.name', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::unequal('employee.department.name', 'Accounting') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('oof', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[0]['city'] ?? 'not found', $sql); $this->assertSame(1, count($results), $sql); } @@ -927,27 +927,27 @@ public function testOrChainTargetingMultipleRelationsWithTheSameAffirmativeOpera . ' WHERE e.name = ? OR d.name = ?' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); - $this->assertSame('oof', $offices[1]['city'] ?? 'not found'); - $this->assertSame('baz', $offices[2]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); $this->assertSame(3, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::any( - Filter::equal('employee.name', 'foo'), - Filter::equal('employee.department.name', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::equal('employee.department.name', 'Accounting') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('oof', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('baz', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); $this->assertSame(3, count($results), $sql); } @@ -968,27 +968,27 @@ public function testOrChainTargetingMultipleRelationsWithTheSameNegativeOperator . ' WHERE e.id IS NULL OR d.id IS NULL' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('oof', $offices[0]['city'] ?? 'not found'); - $this->assertSame('bar', $offices[1]['city'] ?? 'not found'); - $this->assertSame('baz', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); $this->assertSame(3, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::any( - Filter::unequal('employee.name', 'foo'), - Filter::unequal('employee.department.name', 'bar') + Filter::unequal('employee.name', 'Donald'), + Filter::unequal('employee.department.name', 'Accounting') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('oof', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('bar', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('baz', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); $this->assertSame(3, count($results), $sql); } @@ -1009,27 +1009,27 @@ public function testOrChainTargetingMultipleRelationsWithDifferentOperators(Conn . ' WHERE e.name = ? OR d.name != ?' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); - $this->assertSame('oof', $offices[1]['city'] ?? 'not found'); - $this->assertSame('bar', $offices[2]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[2]['city'] ?? 'not found'); $this->assertSame(3, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::any( - Filter::equal('employee.name', 'foo'), - Filter::unequal('employee.department.name', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::unequal('employee.department.name', 'Accounting') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('oof', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('bar', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[2]['city'] ?? 'not found', $sql); $this->assertSame(3, count($results), $sql); } @@ -1050,25 +1050,25 @@ public function testNotChainTargetingMultipleRelationsWithTheSameAffirmativeOper . ' WHERE NOT (e.name = ? OR d.name = ?)' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); - $this->assertSame('bar', $offices[1]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); $this->assertSame(2, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::none( - Filter::equal('employee.name', 'foo'), - Filter::equal('employee.department.name', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::equal('employee.department.name', 'Accounting') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('bar', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); $this->assertSame(2, count($results), $sql); } @@ -1089,23 +1089,23 @@ public function testNotChainTargetingMultipleRelationsWithTheSameNegativeOperato . ' WHERE NOT (e.id IS NULL OR d.id IS NULL)' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); $this->assertSame(1, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::none( - Filter::unequal('employee.name', 'foo'), - Filter::unequal('employee.department.name', 'bar') + Filter::unequal('employee.name', 'Donald'), + Filter::unequal('employee.department.name', 'Accounting') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); $this->assertSame(1, count($results), $sql); } @@ -1126,25 +1126,25 @@ public function testNotChainTargetingMultipleRelationsWithDifferentOperators(Con . ' WHERE NOT (e.name = ? OR d.name != ?)' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['foo', 'bar'] + ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('oof', $offices[0]['city'] ?? 'not found'); - $this->assertSame('baz', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[0]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); $this->assertSame(2, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::none( - Filter::equal('employee.name', 'foo'), - Filter::unequal('employee.department.name', 'bar') + Filter::equal('employee.name', 'Donald'), + Filter::unequal('employee.department.name', 'Accounting') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('oof', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('baz', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); $this->assertSame(2, count($results), $sql); } @@ -1158,13 +1158,13 @@ public function testNotChainTargetingMultipleRelationsWithDifferentOperators(Con */ public function testUnequalTargetingAnOptionalToManyRelationIgnoresFalsePositives(Connection $db) { - $db->insert('office', ['id' => 1, 'city' => 'foo']); - $db->insert('department', ['id' => 1, 'name' => 'bar']); - $db->insert('department', ['id' => 2, 'name' => 'baz']); - $db->insert('employee', ['id' => 1, 'department_id' => 1, 'name' => 'qux', 'role' => 'quux']); // remote + $db->insert('office', ['id' => 1, 'city' => 'London']); + $db->insert('department', ['id' => 1, 'name' => 'Accounting']); + $db->insert('department', ['id' => 2, 'name' => 'Kitchen']); + $db->insert('employee', ['id' => 1, 'department_id' => 1, 'name' => 'Minnie', 'role' => 'CEO']); // remote $db->insert( 'employee', - ['id' => 2, 'department_id' => 2, 'office_id' => 1, 'name' => 'corge', 'role' => 'grault'] + ['id' => 2, 'department_id' => 2, 'office_id' => 1, 'name' => 'Goofy', 'role' => 'Developer'] ); // This POC uses inner joins to achieve the desired result @@ -1175,80 +1175,101 @@ public function testUnequalTargetingAnOptionalToManyRelationIgnoresFalsePositive . ' WHERE d.name != ?' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['bar'] + ['Accounting'] )->fetchAll(); - $this->assertSame('foo', $offices[0]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); // The ORM will use a NOT IN and needs to ignore false positives explicitly $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') - ->filter(Filter::unequal('employee.department.name', 'bar')); + ->filter(Filter::unequal('employee.department.name', 'Accounting')); $results = iterator_to_array($offices); - $this->assertSame('foo', $results[0]['city'] ?? 'not found', $this->getSql($offices)); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $this->getSql($offices)); } /** * Create a definite set of permutations with employees and offices * + * Employee | Role | City + * -------- | ------------ | --------- + * Donald | Accountant | London + * Huey | Manager | London + * Donald | Salesperson | Amsterdam + * Dewey | QA Lead | New York + * Donald | Accountant | Berlin + * Huey | Manager | Berlin + * Louie | Cook | Berlin + * Huey | Assistant | Sydney + * - | - | Cuxhaven + * * @param Connection $db */ protected function createOfficesAndEmployees(Connection $db) { - $db->insert('office', ['id' => 1, 'city' => 'foo']); // Two - $db->insert('employee', ['id' => 1, 'office_id' => 1, 'name' => 'foo', 'role' => 'bar']); - $db->insert('employee', ['id' => 2, 'office_id' => 1, 'name' => 'bar', 'role' => 'foo']); - $db->insert('office', ['id' => 2, 'city' => 'bar']); // One of the two - $db->insert('employee', ['id' => 3, 'office_id' => 2, 'name' => 'foo', 'role' => 'oof']); - $db->insert('office', ['id' => 3, 'city' => 'baz']); // None of the two - $db->insert('employee', ['id' => 4, 'office_id' => 3, 'name' => 'oof', 'role' => 'qux']); - $db->insert('office', ['id' => 4, 'city' => 'oof']); // The two plus another one - $db->insert('employee', ['id' => 5, 'office_id' => 4, 'name' => 'foo', 'role' => 'bar']); - $db->insert('employee', ['id' => 6, 'office_id' => 4, 'name' => 'bar', 'role' => 'foo']); - $db->insert('employee', ['id' => 7, 'office_id' => 4, 'name' => 'quux', 'role' => 'corge']); - $db->insert('office', ['id' => 5, 'city' => 'qux']); // None - $db->insert('office', ['id' => 6, 'city' => 'quux']); // The other one of the two - $db->insert('employee', ['id' => 8, 'office_id' => 6, 'name' => 'bar', 'role' => 'baz']); + $db->insert('office', ['id' => 1, 'city' => 'London']); // Two + $db->insert('employee', ['id' => 1, 'office_id' => 1, 'name' => 'Donald', 'role' => 'Accountant']); + $db->insert('employee', ['id' => 2, 'office_id' => 1, 'name' => 'Huey', 'role' => 'Manager']); + $db->insert('office', ['id' => 2, 'city' => 'Amsterdam']); // One of the two + $db->insert('employee', ['id' => 3, 'office_id' => 2, 'name' => 'Donald', 'role' => 'Salesperson']); + $db->insert('office', ['id' => 3, 'city' => 'New York']); // None of the two + $db->insert('employee', ['id' => 4, 'office_id' => 3, 'name' => 'Dewey', 'role' => 'QA Lead']); + $db->insert('office', ['id' => 4, 'city' => 'Berlin']); // The two plus another one + $db->insert('employee', ['id' => 5, 'office_id' => 4, 'name' => 'Donald', 'role' => 'Accountant']); + $db->insert('employee', ['id' => 6, 'office_id' => 4, 'name' => 'Huey', 'role' => 'Manager']); + $db->insert('employee', ['id' => 7, 'office_id' => 4, 'name' => 'Louie', 'role' => 'Cook']); + $db->insert('office', ['id' => 5, 'city' => 'Cuxhaven']); // None + $db->insert('office', ['id' => 6, 'city' => 'Sydney']); // The other one of the two + $db->insert('employee', ['id' => 8, 'office_id' => 6, 'name' => 'Huey', 'role' => 'Assistant']); } /** * Create a definite set of permutations with employees, offices and departments * + * Employee | Role | Department | City + * -------- | ------------ | ---------- | --------- + * Donald | Accountant | Accounting | London + * Dewey | Cook | Kitchen | London + * Donald | Salesperson | Kitchen | Berlin + * Huey | Assistant | Accounting | Berlin + * Huey | QA Lead | Kitchen | Amsterdam + * Mickey | Manager | Accounting | New York + * * @param Connection $db */ protected function createOfficesEmployeesAndDepartments(Connection $db) { - $db->insert('office', ['id' => 1, 'city' => 'foo']); - $db->insert('office', ['id' => 2, 'city' => 'oof']); - $db->insert('office', ['id' => 3, 'city' => 'bar']); - $db->insert('office', ['id' => 4, 'city' => 'baz']); - $db->insert('department', ['id' => 1, 'name' => 'bar']); - $db->insert('department', ['id' => 2, 'name' => 'baz']); + $db->insert('office', ['id' => 1, 'city' => 'London']); + $db->insert('office', ['id' => 2, 'city' => 'Berlin']); + $db->insert('office', ['id' => 3, 'city' => 'Amsterdam']); + $db->insert('office', ['id' => 4, 'city' => 'New York']); + $db->insert('department', ['id' => 1, 'name' => 'Accounting']); + $db->insert('department', ['id' => 2, 'name' => 'Kitchen']); $db->insert( 'employee', - ['id' => 1, 'office_id' => 1, 'department_id' => 1, 'name' => 'foo', 'role' => 'bar'] + ['id' => 1, 'office_id' => 1, 'department_id' => 1, 'name' => 'Donald', 'role' => 'Accountant'] ); $db->insert( 'employee', - ['id' => 2, 'office_id' => 1, 'department_id' => 2, 'name' => 'oof', 'role' => 'corge'] + ['id' => 2, 'office_id' => 1, 'department_id' => 2, 'name' => 'Dewey', 'role' => 'Cook'] ); $db->insert( 'employee', - ['id' => 3, 'office_id' => 2, 'department_id' => 2, 'name' => 'foo', 'role' => 'oof'] + ['id' => 3, 'office_id' => 2, 'department_id' => 2, 'name' => 'Donald', 'role' => 'Salesperson'] ); $db->insert( 'employee', - ['id' => 4, 'office_id' => 2, 'department_id' => 1, 'name' => 'bar', 'role' => 'baz'] + ['id' => 4, 'office_id' => 2, 'department_id' => 1, 'name' => 'Huey', 'role' => 'Assistant'] ); $db->insert( 'employee', - ['id' => 5, 'office_id' => 3, 'department_id' => 2, 'name' => 'bar', 'role' => 'qux'] + ['id' => 5, 'office_id' => 3, 'department_id' => 2, 'name' => 'Huey', 'role' => 'QA Lead'] ); $db->insert( 'employee', - ['id' => 6, 'office_id' => 4, 'department_id' => 1, 'name' => 'baz', 'role' => 'foo'] + ['id' => 6, 'office_id' => 4, 'department_id' => 1, 'name' => 'Mickey', 'role' => 'Manager'] ); } From ee90cd0c514d652f01a147b4e7ea65c142a6396b Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Thu, 29 Feb 2024 17:28:48 +0100 Subject: [PATCH 3/8] wip, some fixes, some experiments and comments --- tests/RelationFilterTest.php | 302 ++++++++++++++++++++++++----------- 1 file changed, 209 insertions(+), 93 deletions(-) diff --git a/tests/RelationFilterTest.php b/tests/RelationFilterTest.php index 415e8fd..053a6dd 100644 --- a/tests/RelationFilterTest.php +++ b/tests/RelationFilterTest.php @@ -15,7 +15,8 @@ * as follows: * * a) Number of conditions: single - * f) Comparisons: affirmation, negation + * b) Logical Operators: -, NOT + * f) Comparisons: affirmation, negation * a) Number of conditions: multiple * b) Logical Operators: AND, OR, NOT * c) Number of relations: single @@ -46,7 +47,7 @@ class RelationFilterTest extends TestCase use Databases; /** - * @equivalenceClass a:single, f:affirmation + * @equivalenceClass a:single, b:-, f:affirmation * @dataProvider databases * * @param Connection $db @@ -83,8 +84,9 @@ public function testSingleAffirmativeCondition(Connection $db) } /** - * @equivalenceClass a:single, f:negation + * @equivalenceClass a:single, b:-, f:negation * @dataProvider databases + * @todo the ORM fails because it's a (breaking) change in semantics of the filter * * @param Connection $db */ @@ -92,10 +94,58 @@ public function testSingleNegativeCondition(Connection $db) { $this->createOfficesAndEmployees($db); + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' WHERE e.name != ? OR e.id IS NULL' + . ' GROUP BY office.id' + . ' ORDER BY office.id', + ['Donald'] + )->fetchAll(); + + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[4]['city'] ?? 'not found'); + $this->assertSame(5, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::unequal('employee.name', 'Donald')); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + // I (nilmerg) don't like that Cuxhaven is part of the results here. That's an office + // without employees. The unequal filter though, to me, assumes one, just not one with + // the name of Donald. This is actually what I expect {@see testSingleNegativeConditionWithNotOperator} + // to return. But I feel like we cannot change this, as this has been introduced ages ago: + // https://github.com/Icinga/icingaweb2/issues/2583 + + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); + } + + /** + * @equivalenceClass a:single, b:NOT, f:affirmation + * @dataProvider databases + * @todo this is what {@see testSingleNegativeCondition} did before, thus the ORM cannot succeed + * + * @param Connection $db + */ + public function testSingleAffirmativeConditionWithNotOperator(Connection $db) + { + $this->createOfficesAndEmployees($db); + $offices = $db->prepexec( 'SELECT office.city FROM office' . ' LEFT JOIN employee e on e.office_id = office.id AND e.name = ?' - . ' WHERE e.id IS NULL' + . ' WHERE NOT (e.id IS NOT NULL)' . ' GROUP BY office.id' . ' ORDER BY office.id', ['Donald'] @@ -109,7 +159,9 @@ public function testSingleNegativeCondition(Connection $db) $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') - ->filter(Filter::unequal('employee.name', 'Donald')); + ->filter(Filter::none( + Filter::equal('employee.name', 'Donald') + )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); @@ -119,6 +171,48 @@ public function testSingleNegativeCondition(Connection $db) $this->assertSame(3, count($results), $sql); } + /** + * @equivalenceClass a:single, b:NOT, f:negation + * @dataProvider databases + * @todo This is new and the reason for the (breaking) change in {@see testSingleNegativeCondition} + * + * @param Connection $db + */ + public function testSingleNegativeConditionWithNotOperator(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' WHERE office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' WHERE e.name != ?' + . ' GROUP BY office.id' + . ' HAVING COUNT(e.id) > 0' + . ' )' + . ' ORDER BY office.id', + ['Donald'] + )->fetchAll(); + + $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[1]['city'] ?? 'not found'); + $this->assertSame(2, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::none( + Filter::unequal('employee.name', 'Donald') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame(2, count($results), $sql); + } + /** * Test whether multiple equal filters combined with OR on the same column of * the same to-many relation, include results that match any condition. @@ -171,6 +265,9 @@ public function testOrChainTargetingASingleRelationColumnWithTheSameAffirmativeO * * @equivalenceClass a:multiple, b:OR, c:single, d:same, e:same, f:negation * @dataProvider databases + * @todo the ORM fails because this test relies on the semantic change of {@see testSingleNegativeCondition}. + * {@see testNotChainTargetingASingleRelationColumnWithTheSameAffirmativeOperator} is the exact opposite + * and the expected results are what the ORM returns here. * * @param Connection $db */ @@ -182,7 +279,7 @@ public function testOrChainTargetingASingleRelationColumnWithTheSameNegativeOper 'SELECT office.city FROM office' . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' - . ' WHERE NOT (e1.id IS NOT NULL AND e2.id IS NOT NULL)' + . ' WHERE e1.id IS NULL OR e2.id IS NULL' . ' GROUP BY office.id' . ' ORDER BY office.id', ['Donald', 'Huey'] @@ -197,7 +294,7 @@ public function testOrChainTargetingASingleRelationColumnWithTheSameNegativeOper $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') - ->filter(Filter::any( + ->filter(Filter::any( // XOR, anything else wouldn't make sense: Huey != Donald || Donald != Huey Filter::unequal('employee.name', 'Donald'), Filter::unequal('employee.name', 'Huey') )); @@ -353,15 +450,16 @@ public function testAndChainTargetingASingleRelationColumnWithDifferentOperators $offices = $db->prepexec( 'SELECT office.city FROM office' . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' - . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' - . ' WHERE e1.id IS NOT NULL AND e2.id IS NULL' + . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name != ?' + . ' WHERE e1.id IS NOT NULL AND (e2.name != ? OR e2.id IS NULL)' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['Donald', 'Huey'] + ['Donald', 'Donald', 'Huey'] )->fetchAll(); $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); - $this->assertSame(1, count($offices)); + $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); + $this->assertSame(2, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -374,7 +472,8 @@ public function testAndChainTargetingASingleRelationColumnWithDifferentOperators $sql = $this->getSql($offices); $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame(1, count($results), $sql); + $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame(2, count($results), $sql); } /** @@ -434,17 +533,22 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameNegativeOpe $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' - . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' - . ' WHERE NOT (e1.id IS NULL OR e2.id IS NULL)' - . ' GROUP BY office.id' + . ' WHERE office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' WHERE e.name != ? AND e.name != ?' + . ' GROUP BY office.id' + . ' HAVING COUNT(e.id) > 0' + . ' )' . ' ORDER BY office.id', ['Donald', 'Huey'] )->fetchAll(); $this->assertSame('London', $offices[0]['city'] ?? 'not found'); - $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); - $this->assertSame(2, count($offices)); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[3]['city'] ?? 'not found'); + $this->assertSame(4, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -457,8 +561,10 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameNegativeOpe $sql = $this->getSql($offices); $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame(2, count($results), $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame(4, count($results), $sql); } /** @@ -504,6 +610,7 @@ public function testNotChainTargetingASingleRelationColumnWithDifferentOperators * * @equivalenceClass a:multiple, b:AND, c:single, d:different, e:different * @dataProvider databases + * @todo simplify, like the others * * @param Connection $db */ @@ -693,20 +800,21 @@ public function testOrChainTargetingASingleRelationButDifferentColumnsWithTheSam . ' WHERE e.name = ? OR e.role = ?' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['Donald', 'Accountant'] + ['Donald', 'Assistant'] )->fetchAll(); $this->assertSame('London', $offices[0]['city'] ?? 'not found'); $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found'); - $this->assertSame(3, count($offices)); + $this->assertSame('Sydney', $offices[3]['city'] ?? 'not found'); + $this->assertSame(4, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::any( Filter::equal('employee.name', 'Donald'), - Filter::equal('employee.role', 'Accountant') + Filter::equal('employee.role', 'Assistant') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); @@ -714,7 +822,8 @@ public function testOrChainTargetingASingleRelationButDifferentColumnsWithTheSam $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); $this->assertSame('Berlin', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame(3, count($results), $sql); + $this->assertSame('Sydney', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame(4, count($results), $sql); } /** @@ -733,7 +842,7 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithDiffe . ' WHERE NOT (e.name = ? OR (e.role != ? OR e.role IS NULL))' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['Donald', 'QA Lead'] + ['Huey', 'Manager'] )->fetchAll(); $this->assertSame('New York', $offices[0]['city'] ?? 'not found'); @@ -743,8 +852,8 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithDiffe ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::none( - Filter::equal('employee.name', 'Donald'), - Filter::unequal('employee.role', 'QA Lead') + Filter::equal('employee.name', 'Huey'), + Filter::unequal('employee.role', 'Manager') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); @@ -765,36 +874,34 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithTheSa $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' LEFT JOIN employee e ON e.office_id = office.id' - . ' WHERE NOT (e.name = ? OR e.role = ?) OR e.id IS NULL' - . ' GROUP BY office.id' + . ' WHERE office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e ON e.office_id = office.id' + . ' WHERE e.name = ? OR e.role = ?' + . ' GROUP BY office.id' + . ' HAVING COUNT(e.id) > 0' + . ' )' . ' ORDER BY office.id', - ['Donald', 'Accountant'] + ['Donald', 'Manager'] )->fetchAll(); - $this->assertSame('London', $offices[0]['city'] ?? 'not found'); - $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); - $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found'); - $this->assertSame('Cuxhaven', $offices[3]['city'] ?? 'not found'); - $this->assertSame('Sydney', $offices[4]['city'] ?? 'not found'); - $this->assertSame(5, count($offices)); + $this->assertSame('Cuxhaven', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[1]['city'] ?? 'not found'); + $this->assertSame(2, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::none( Filter::equal('employee.name', 'Donald'), - Filter::equal('employee.role', 'Accountant') + Filter::equal('employee.role', 'Manager') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found', $sql); - $this->assertSame('Cuxhaven', $results[3]['city'] ?? 'not found', $sql); - $this->assertSame('Sydney', $results[4]['city'] ?? 'not found', $sql); - $this->assertSame(5, count($results), $sql); + $this->assertSame('Cuxhaven', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame(2, count($results), $sql); } /** @@ -846,16 +953,16 @@ public function testAndChainTargetingMultipleRelationsWithTheSameNegativeOperato $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id AND e.name = ?' - . ' LEFT JOIN department d on e.department_id = d.id AND d.name = ?' - . ' WHERE e.id IS NULL AND d.id IS NULL' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE e.name != ? AND d.name != ?' . ' GROUP BY office.id' . ' ORDER BY office.id', ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); - $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); $this->assertSame(2, count($offices)); $offices = Office::on($db) @@ -868,8 +975,8 @@ public function testAndChainTargetingMultipleRelationsWithTheSameNegativeOperato $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); $this->assertSame(2, count($results), $sql); } @@ -963,32 +1070,32 @@ public function testOrChainTargetingMultipleRelationsWithTheSameNegativeOperator $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id AND e.name = ?' - . ' LEFT JOIN department d on e.department_id = d.id AND d.name = ?' - . ' WHERE e.id IS NULL OR d.id IS NULL' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE e.name != ? OR d.name != ?' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['Donald', 'Accounting'] + ['Mickey', 'Accounting'] )->fetchAll(); - $this->assertSame('Berlin', $offices[0]['city'] ?? 'not found'); - $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); - $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[2]['city'] ?? 'not found'); $this->assertSame(3, count($offices)); $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') ->filter(Filter::any( - Filter::unequal('employee.name', 'Donald'), + Filter::unequal('employee.name', 'Mickey'), Filter::unequal('employee.department.name', 'Accounting') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('Berlin', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[2]['city'] ?? 'not found', $sql); $this->assertSame(3, count($results), $sql); } @@ -1045,17 +1152,20 @@ public function testNotChainTargetingMultipleRelationsWithTheSameAffirmativeOper $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id' - . ' LEFT JOIN department d on e.department_id = d.id' - . ' WHERE NOT (e.name = ? OR d.name = ?)' - . ' GROUP BY office.id' + . ' WHERE office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE e.name = ? OR d.name = ?' + . ' GROUP BY office.id' + . ' HAVING COUNT(e.id) > 0' + . ' )' . ' ORDER BY office.id', ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('London', $offices[0]['city'] ?? 'not found'); - $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); - $this->assertSame(2, count($offices)); + $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); + $this->assertSame(1, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -1067,9 +1177,8 @@ public function testNotChainTargetingMultipleRelationsWithTheSameAffirmativeOper $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame(2, count($results), $sql); + $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame(1, count($results), $sql); } /** @@ -1084,16 +1193,20 @@ public function testNotChainTargetingMultipleRelationsWithTheSameNegativeOperato $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id AND e.name = ?' - . ' LEFT JOIN department d on e.department_id = d.id AND d.name = ?' - . ' WHERE NOT (e.id IS NULL OR d.id IS NULL)' - . ' GROUP BY office.id' + . ' WHERE office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE e.name != ? OR d.name != ?' + . ' GROUP BY office.id' + . ' HAVING COUNT(e.id) > 0' + . ' )' . ' ORDER BY office.id', ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('London', $offices[0]['city'] ?? 'not found'); - $this->assertSame(1, count($offices)); + // @todo this shouldn't be empty, add some matches to the test data + $this->assertEmpty($offices); $offices = Office::on($db) ->columns(['office.city']) @@ -1105,8 +1218,7 @@ public function testNotChainTargetingMultipleRelationsWithTheSameNegativeOperato $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame(1, count($results), $sql); + $this->assertEmpty($results, $sql); } /** @@ -1121,17 +1233,20 @@ public function testNotChainTargetingMultipleRelationsWithDifferentOperators(Con $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id' - . ' LEFT JOIN department d on e.department_id = d.id' - . ' WHERE NOT (e.name = ? OR d.name != ?)' - . ' GROUP BY office.id' + . ' WHERE office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE e.name = ? OR d.name != ?' + . ' GROUP BY office.id' + . ' HAVING COUNT(e.id) > 0' + . ' )' . ' ORDER BY office.id', ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('Berlin', $offices[0]['city'] ?? 'not found'); - $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); - $this->assertSame(2, count($offices)); + $this->assertSame('New York', $offices[0]['city'] ?? 'not found'); + $this->assertSame(1, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -1143,9 +1258,8 @@ public function testNotChainTargetingMultipleRelationsWithDifferentOperators(Con $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('Berlin', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame(2, count($results), $sql); + $this->assertSame('New York', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame(1, count($results), $sql); } /** @@ -1198,12 +1312,12 @@ public function testUnequalTargetingAnOptionalToManyRelationIgnoresFalsePositive * Donald | Accountant | London * Huey | Manager | London * Donald | Salesperson | Amsterdam - * Dewey | QA Lead | New York + * Dewey | Manager | New York * Donald | Accountant | Berlin * Huey | Manager | Berlin * Louie | Cook | Berlin - * Huey | Assistant | Sydney * - | - | Cuxhaven + * Huey | Assistant | Sydney * * @param Connection $db */ @@ -1215,7 +1329,7 @@ protected function createOfficesAndEmployees(Connection $db) $db->insert('office', ['id' => 2, 'city' => 'Amsterdam']); // One of the two $db->insert('employee', ['id' => 3, 'office_id' => 2, 'name' => 'Donald', 'role' => 'Salesperson']); $db->insert('office', ['id' => 3, 'city' => 'New York']); // None of the two - $db->insert('employee', ['id' => 4, 'office_id' => 3, 'name' => 'Dewey', 'role' => 'QA Lead']); + $db->insert('employee', ['id' => 4, 'office_id' => 3, 'name' => 'Dewey', 'role' => 'Manager']); $db->insert('office', ['id' => 4, 'city' => 'Berlin']); // The two plus another one $db->insert('employee', ['id' => 5, 'office_id' => 4, 'name' => 'Donald', 'role' => 'Accountant']); $db->insert('employee', ['id' => 6, 'office_id' => 4, 'name' => 'Huey', 'role' => 'Manager']); @@ -1237,6 +1351,8 @@ protected function createOfficesAndEmployees(Connection $db) * Huey | QA Lead | Kitchen | Amsterdam * Mickey | Manager | Accounting | New York * + * @todo Add some false positives + * * @param Connection $db */ protected function createOfficesEmployeesAndDepartments(Connection $db) From a9534490528ffc8f6f5b0a11699bb5ecf956ba3b Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 5 Mar 2024 17:15:34 +0100 Subject: [PATCH 4/8] remove experiments, add false positives, fixes --- tests/RelationFilterTest.php | 430 +++++++++++++++++++++-------------- 1 file changed, 262 insertions(+), 168 deletions(-) diff --git a/tests/RelationFilterTest.php b/tests/RelationFilterTest.php index 053a6dd..00e1503 100644 --- a/tests/RelationFilterTest.php +++ b/tests/RelationFilterTest.php @@ -68,7 +68,8 @@ public function testSingleAffirmativeCondition(Connection $db) $this->assertSame('London', $offices[0]['city'] ?? 'not found'); $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found'); - $this->assertSame(3, count($offices)); + $this->assertSame('Baghdad', $offices[3]['city'] ?? 'not found'); + $this->assertSame(4, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -80,13 +81,13 @@ public function testSingleAffirmativeCondition(Connection $db) $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); $this->assertSame('Berlin', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame(3, count($results), $sql); + $this->assertSame('Baghdad', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame(4, count($results), $sql); } /** * @equivalenceClass a:single, b:-, f:negation * @dataProvider databases - * @todo the ORM fails because it's a (breaking) change in semantics of the filter * * @param Connection $db */ @@ -96,19 +97,17 @@ public function testSingleNegativeCondition(Connection $db) $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id' - . ' WHERE e.name != ? OR e.id IS NULL' + . ' LEFT JOIN employee e on e.office_id = office.id AND e.name = ?' + . ' WHERE e.id IS NULL' . ' GROUP BY office.id' . ' ORDER BY office.id', ['Donald'] )->fetchAll(); - $this->assertSame('London', $offices[0]['city'] ?? 'not found'); - $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); - $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found'); - $this->assertSame('Cuxhaven', $offices[3]['city'] ?? 'not found'); - $this->assertSame('Sydney', $offices[4]['city'] ?? 'not found'); - $this->assertSame(5, count($offices)); + $this->assertSame('New York', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[2]['city'] ?? 'not found'); + $this->assertSame(3, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -117,24 +116,16 @@ public function testSingleNegativeCondition(Connection $db) $results = iterator_to_array($offices); $sql = $this->getSql($offices); - // I (nilmerg) don't like that Cuxhaven is part of the results here. That's an office - // without employees. The unequal filter though, to me, assumes one, just not one with - // the name of Donald. This is actually what I expect {@see testSingleNegativeConditionWithNotOperator} - // to return. But I feel like we cannot change this, as this has been introduced ages ago: - // https://github.com/Icinga/icingaweb2/issues/2583 - - $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('Berlin', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame('Cuxhaven', $results[3]['city'] ?? 'not found', $sql); - $this->assertSame('Sydney', $results[4]['city'] ?? 'not found', $sql); - $this->assertSame(5, count($results), $sql); + $this->assertSame('New York', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame(3, count($results), $sql); } /** * @equivalenceClass a:single, b:NOT, f:affirmation * @dataProvider databases - * @todo this is what {@see testSingleNegativeCondition} did before, thus the ORM cannot succeed + * @todo broken * * @param Connection $db */ @@ -144,17 +135,19 @@ public function testSingleAffirmativeConditionWithNotOperator(Connection $db) $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id AND e.name = ?' - . ' WHERE NOT (e.id IS NOT NULL)' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' WHERE NOT (e.name = ?)' . ' GROUP BY office.id' . ' ORDER BY office.id', ['Donald'] )->fetchAll(); - $this->assertSame('New York', $offices[0]['city'] ?? 'not found'); - $this->assertSame('Cuxhaven', $offices[1]['city'] ?? 'not found'); - $this->assertSame('Sydney', $offices[2]['city'] ?? 'not found'); - $this->assertSame(3, count($offices)); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Baghdad', $offices[4]['city'] ?? 'not found'); + $this->assertSame(5, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -165,16 +158,18 @@ public function testSingleAffirmativeConditionWithNotOperator(Connection $db) $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('New York', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('Cuxhaven', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('Sydney', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame(3, count($results), $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Baghdad', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); } /** * @equivalenceClass a:single, b:NOT, f:negation * @dataProvider databases - * @todo This is new and the reason for the (breaking) change in {@see testSingleNegativeCondition} + * @todo broken * * @param Connection $db */ @@ -184,20 +179,18 @@ public function testSingleNegativeConditionWithNotOperator(Connection $db) $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' WHERE office.id NOT IN (' - . ' SELECT office.id FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id' - . ' WHERE e.name != ?' - . ' GROUP BY office.id' - . ' HAVING COUNT(e.id) > 0' - . ' )' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' WHERE NOT (e.name != ? OR e.name IS NULL)' + . ' GROUP BY office.id' . ' ORDER BY office.id', ['Donald'] )->fetchAll(); - $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); - $this->assertSame('Cuxhaven', $offices[1]['city'] ?? 'not found'); - $this->assertSame(2, count($offices)); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Baghdad', $offices[3]['city'] ?? 'not found'); + $this->assertSame(4, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -208,9 +201,11 @@ public function testSingleNegativeConditionWithNotOperator(Connection $db) $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('Cuxhaven', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame(2, count($results), $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Baghdad', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame(4, count($results), $sql); } /** @@ -240,7 +235,8 @@ public function testOrChainTargetingASingleRelationColumnWithTheSameAffirmativeO $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found'); $this->assertSame('Sydney', $offices[3]['city'] ?? 'not found'); - $this->assertSame(4, count($offices)); + $this->assertSame('Baghdad', $offices[4]['city'] ?? 'not found'); + $this->assertSame(5, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -256,7 +252,8 @@ public function testOrChainTargetingASingleRelationColumnWithTheSameAffirmativeO $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); $this->assertSame('Berlin', $results[2]['city'] ?? 'not found', $sql); $this->assertSame('Sydney', $results[3]['city'] ?? 'not found', $sql); - $this->assertSame(4, count($results), $sql); + $this->assertSame('Baghdad', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); } /** @@ -265,9 +262,7 @@ public function testOrChainTargetingASingleRelationColumnWithTheSameAffirmativeO * * @equivalenceClass a:multiple, b:OR, c:single, d:same, e:same, f:negation * @dataProvider databases - * @todo the ORM fails because this test relies on the semantic change of {@see testSingleNegativeCondition}. - * {@see testNotChainTargetingASingleRelationColumnWithTheSameAffirmativeOperator} is the exact opposite - * and the expected results are what the ORM returns here. + * @todo different * * @param Connection $db */ @@ -294,18 +289,16 @@ public function testOrChainTargetingASingleRelationColumnWithTheSameNegativeOper $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') - ->filter(Filter::any( // XOR, anything else wouldn't make sense: Huey != Donald || Donald != Huey + ->filter(Filter::any( Filter::unequal('employee.name', 'Donald'), Filter::unequal('employee.name', 'Huey') )); $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('Cuxhaven', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame('Sydney', $results[3]['city'] ?? 'not found', $sql); - $this->assertSame(4, count($results), $sql); + $this->assertSame('New York', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame(2, count($results), $sql); } /** @@ -333,7 +326,8 @@ public function testOrChainTargetingASingleRelationColumnWithDifferentOperators( $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); $this->assertSame('Berlin', $offices[3]['city'] ?? 'not found'); $this->assertSame('Cuxhaven', $offices[4]['city'] ?? 'not found'); - $this->assertSame(5, count($offices)); + $this->assertSame('Baghdad', $offices[5]['city'] ?? 'not found'); + $this->assertSame(6, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -350,7 +344,8 @@ public function testOrChainTargetingASingleRelationColumnWithDifferentOperators( $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); $this->assertSame('Berlin', $results[3]['city'] ?? 'not found', $sql); $this->assertSame('Cuxhaven', $results[4]['city'] ?? 'not found', $sql); - $this->assertSame(5, count($results), $sql); + $this->assertSame('Baghdad', $results[5]['city'] ?? 'not found', $sql); + $this->assertSame(6, count($results), $sql); } /** @@ -378,7 +373,8 @@ public function testAndChainTargetingASingleRelationColumnWithTheSameAffirmative $this->assertSame('London', $offices[0]['city'] ?? 'not found'); $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); - $this->assertSame(2, count($offices)); + $this->assertSame('Baghdad', $offices[2]['city'] ?? 'not found'); + $this->assertSame(3, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -392,7 +388,8 @@ public function testAndChainTargetingASingleRelationColumnWithTheSameAffirmative $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame(2, count($results), $sql); + $this->assertSame('Baghdad', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame(3, count($results), $sql); } /** @@ -401,6 +398,7 @@ public function testAndChainTargetingASingleRelationColumnWithTheSameAffirmative * * @equivalenceClass a:multiple, b:AND, c:single, d:same, e:same, f:negation * @dataProvider databases + * @todo different * * @param Connection $db */ @@ -412,15 +410,17 @@ public function testAndChainTargetingASingleRelationColumnWithTheSameNegativeOpe 'SELECT office.city FROM office' . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' - . ' WHERE e1.id IS NULL AND e2.id IS NULL' + . ' WHERE e1.id IS NULL OR e2.id IS NULL' . ' GROUP BY office.id' . ' ORDER BY office.id', ['Donald', 'Huey'] )->fetchAll(); - $this->assertSame('New York', $offices[0]['city'] ?? 'not found'); - $this->assertSame('Cuxhaven', $offices[1]['city'] ?? 'not found'); - $this->assertSame(2, count($offices)); + $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[3]['city'] ?? 'not found'); + $this->assertSame(4, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -432,6 +432,8 @@ public function testAndChainTargetingASingleRelationColumnWithTheSameNegativeOpe $results = iterator_to_array($offices); $sql = $this->getSql($offices); + // TODO: Somewhat same as with IdoQuery, as it's the same result as in + // {@see testOrChainTargetingASingleRelationColumnWithTheSameNegativeOperator} $this->assertSame('New York', $results[0]['city'] ?? 'not found', $sql); $this->assertSame('Cuxhaven', $results[1]['city'] ?? 'not found', $sql); $this->assertSame(2, count($results), $sql); @@ -450,16 +452,15 @@ public function testAndChainTargetingASingleRelationColumnWithDifferentOperators $offices = $db->prepexec( 'SELECT office.city FROM office' . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' - . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name != ?' - . ' WHERE e1.id IS NOT NULL AND (e2.name != ? OR e2.id IS NULL)' + . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' + . ' WHERE e1.id IS NOT NULL AND e2.id IS NULL' . ' GROUP BY office.id' . ' ORDER BY office.id', - ['Donald', 'Donald', 'Huey'] + ['Donald', 'Huey'] )->fetchAll(); $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); - $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); - $this->assertSame(2, count($offices)); + $this->assertSame(1, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -472,8 +473,7 @@ public function testAndChainTargetingASingleRelationColumnWithDifferentOperators $sql = $this->getSql($offices); $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame(2, count($results), $sql); + $this->assertSame(1, count($results), $sql); } /** @@ -482,6 +482,7 @@ public function testAndChainTargetingASingleRelationColumnWithDifferentOperators * * @equivalenceClass a:multiple, b:NOT, c:single, d:same, e:same, f:affirmation * @dataProvider databases + * @todo broken * * @param Connection $db */ @@ -493,15 +494,17 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameAffirmative 'SELECT office.city FROM office' . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' - . ' WHERE NOT (e1.id IS NOT NULL OR e2.id IS NOT NULL)' + . ' WHERE NOT (e1.id IS NOT NULL AND e2.id IS NOT NULL)' . ' GROUP BY office.id' . ' ORDER BY office.id', ['Donald', 'Huey'] )->fetchAll(); - $this->assertSame('New York', $offices[0]['city'] ?? 'not found'); - $this->assertSame('Cuxhaven', $offices[1]['city'] ?? 'not found'); - $this->assertSame(2, count($offices)); + $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[3]['city'] ?? 'not found'); + $this->assertSame(4, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -513,9 +516,11 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameAffirmative $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('New York', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('Cuxhaven', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame(2, count($results), $sql); + $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame(4, count($results), $sql); } /** @@ -524,6 +529,7 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameAffirmative * * @equivalenceClass a:multiple, b:NOT, c:single, d:same, e:same, f:negation * @dataProvider databases + * @todo broken * * @param Connection $db */ @@ -533,22 +539,21 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameNegativeOpe $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' WHERE office.id NOT IN (' + . ' WHERE office.id IN (' . ' SELECT office.id FROM office' . ' LEFT JOIN employee e on e.office_id = office.id' - . ' WHERE e.name != ? AND e.name != ?' + . ' WHERE e.name = ? OR e.name = ?' . ' GROUP BY office.id' - . ' HAVING COUNT(e.id) > 0' + . ' HAVING COUNT(e.id) >= 2' . ' )' . ' ORDER BY office.id', ['Donald', 'Huey'] )->fetchAll(); $this->assertSame('London', $offices[0]['city'] ?? 'not found'); - $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); - $this->assertSame('Cuxhaven', $offices[2]['city'] ?? 'not found'); - $this->assertSame('Sydney', $offices[3]['city'] ?? 'not found'); - $this->assertSame(4, count($offices)); + $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Baghdad', $offices[2]['city'] ?? 'not found'); + $this->assertSame(3, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -561,15 +566,15 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameNegativeOpe $sql = $this->getSql($offices); $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('Cuxhaven', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame('Sydney', $results[3]['city'] ?? 'not found', $sql); - $this->assertSame(4, count($results), $sql); + $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Baghdad', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame(3, count($results), $sql); } /** * @equivalenceClass a:multiple, b:NOT, c:single, d:same, e:different * @dataProvider databases + * @todo broken * * @param Connection $db */ @@ -581,14 +586,19 @@ public function testNotChainTargetingASingleRelationColumnWithDifferentOperators 'SELECT office.city FROM office' . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' - . ' WHERE NOT (e1.id IS NOT NULL OR e2.id IS NULL)' + . ' WHERE NOT (e1.id IS NOT NULL AND e2.id IS NULL)' . ' GROUP BY office.id' . ' ORDER BY office.id', ['Donald', 'Huey'] )->fetchAll(); - $this->assertSame('Sydney', $offices[0]['city'] ?? 'not found'); - $this->assertSame(1, count($offices)); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[4]['city'] ?? 'not found'); + $this->assertSame('Baghdad', $offices[5]['city'] ?? 'not found'); + $this->assertSame(6, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -600,8 +610,13 @@ public function testNotChainTargetingASingleRelationColumnWithDifferentOperators $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('Sydney', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame(1, count($results), $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame('Baghdad', $results[5]['city'] ?? 'not found', $sql); + $this->assertSame(6, count($results), $sql); } /** @@ -763,7 +778,8 @@ public function testOrChainTargetingASingleRelationButDifferentColumnsWithDiffer $this->assertSame('Berlin', $offices[3]['city'] ?? 'not found'); $this->assertSame('Cuxhaven', $offices[4]['city'] ?? 'not found'); $this->assertSame('Sydney', $offices[5]['city'] ?? 'not found'); - $this->assertSame(6, count($offices)); + $this->assertSame('Baghdad', $offices[6]['city'] ?? 'not found'); + $this->assertSame(7, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -781,7 +797,8 @@ public function testOrChainTargetingASingleRelationButDifferentColumnsWithDiffer $this->assertSame('Berlin', $results[3]['city'] ?? 'not found', $sql); $this->assertSame('Cuxhaven', $results[4]['city'] ?? 'not found', $sql); $this->assertSame('Sydney', $results[5]['city'] ?? 'not found', $sql); - $this->assertSame(6, count($results), $sql); + $this->assertSame('Baghdad', $results[6]['city'] ?? 'not found', $sql); + $this->assertSame(7, count($results), $sql); } /** @@ -807,7 +824,8 @@ public function testOrChainTargetingASingleRelationButDifferentColumnsWithTheSam $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); $this->assertSame('Berlin', $offices[2]['city'] ?? 'not found'); $this->assertSame('Sydney', $offices[3]['city'] ?? 'not found'); - $this->assertSame(4, count($offices)); + $this->assertSame('Baghdad', $offices[4]['city'] ?? 'not found'); + $this->assertSame(5, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -823,12 +841,14 @@ public function testOrChainTargetingASingleRelationButDifferentColumnsWithTheSam $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); $this->assertSame('Berlin', $results[2]['city'] ?? 'not found', $sql); $this->assertSame('Sydney', $results[3]['city'] ?? 'not found', $sql); - $this->assertSame(4, count($results), $sql); + $this->assertSame('Baghdad', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); } /** * @equivalenceClass a:multiple, b:NOT, c:single, d:different, e:different * @dataProvider databases + * @todo broken * * @param Connection $db */ @@ -839,14 +859,18 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithDiffe $offices = $db->prepexec( 'SELECT office.city FROM office' . ' LEFT JOIN employee e ON e.office_id = office.id' - . ' WHERE NOT (e.name = ? OR (e.role != ? OR e.role IS NULL))' + . ' WHERE NOT (e.name = ? AND (e.role != ? OR e.role IS NULL))' . ' GROUP BY office.id' . ' ORDER BY office.id', ['Huey', 'Manager'] )->fetchAll(); - $this->assertSame('New York', $offices[0]['city'] ?? 'not found'); - $this->assertSame(1, count($offices)); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Baghdad', $offices[4]['city'] ?? 'not found'); + $this->assertSame(5, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -858,13 +882,18 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithDiffe $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('New York', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame(1, count($results), $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Baghdad', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); } /** * @equivalenceClass a:multiple, b:NOT, c:single, d:different, e:same * @dataProvider databases + * @todo broken * * @param Connection $db */ @@ -874,20 +903,20 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithTheSa $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' WHERE office.id NOT IN (' - . ' SELECT office.id FROM office' - . ' LEFT JOIN employee e ON e.office_id = office.id' - . ' WHERE e.name = ? OR e.role = ?' - . ' GROUP BY office.id' - . ' HAVING COUNT(e.id) > 0' - . ' )' + . ' LEFT JOIN employee e ON e.office_id = office.id' + . ' WHERE NOT (e.name = ? AND e.role = ?)' + . ' GROUP BY office.id' . ' ORDER BY office.id', ['Donald', 'Manager'] )->fetchAll(); - $this->assertSame('Cuxhaven', $offices[0]['city'] ?? 'not found'); - $this->assertSame('Sydney', $offices[1]['city'] ?? 'not found'); - $this->assertSame(2, count($offices)); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[4]['city'] ?? 'not found'); + $this->assertSame('Baghdad', $offices[5]['city'] ?? 'not found'); + $this->assertSame(6, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -899,14 +928,19 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithTheSa $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('Cuxhaven', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('Sydney', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame(2, count($results), $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame('Baghdad', $results[5]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); } /** * @equivalenceClass a:multiple, b:AND, c:multiple, e:same, f:affirmation * @dataProvider databases + * @todo broken * * @param Connection $db */ @@ -925,7 +959,8 @@ public function testAndChainTargetingMultipleRelationsWithTheSameAffirmativeOper )->fetchAll(); $this->assertSame('London', $offices[0]['city'] ?? 'not found'); - $this->assertSame(1, count($offices)); + $this->assertSame('Barcelona', $offices[1]['city'] ?? 'not found'); + $this->assertSame(2, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -938,7 +973,8 @@ public function testAndChainTargetingMultipleRelationsWithTheSameAffirmativeOper $sql = $this->getSql($offices); $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame(1, count($results), $sql); + $this->assertSame('Barcelona', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame(2, count($results), $sql); } /** @@ -953,17 +989,24 @@ public function testAndChainTargetingMultipleRelationsWithTheSameNegativeOperato $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id' - . ' LEFT JOIN department d on e.department_id = d.id' - . ' WHERE e.name != ? AND d.name != ?' - . ' GROUP BY office.id' + . ' WHERE office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' WHERE e.name = ?' + . ' GROUP BY office.id' + . ' ) AND office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE d.name = ?' + . ' GROUP BY office.id' + . ' )' . ' ORDER BY office.id', ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('London', $offices[0]['city'] ?? 'not found'); - $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); - $this->assertSame(2, count($offices)); + $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); + $this->assertSame(1, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -975,14 +1018,14 @@ public function testAndChainTargetingMultipleRelationsWithTheSameNegativeOperato $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame(2, count($results), $sql); + $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame(1, count($results), $sql); } /** * @equivalenceClass a:multiple, b:AND, c:multiple, e:different * @dataProvider databases + * @todo broken * * @param Connection $db */ @@ -1040,7 +1083,9 @@ public function testOrChainTargetingMultipleRelationsWithTheSameAffirmativeOpera $this->assertSame('London', $offices[0]['city'] ?? 'not found'); $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); - $this->assertSame(3, count($offices)); + $this->assertSame('Paris', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Barcelona', $offices[4]['city'] ?? 'not found'); + $this->assertSame(5, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -1055,12 +1100,15 @@ public function testOrChainTargetingMultipleRelationsWithTheSameAffirmativeOpera $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame(3, count($results), $sql); + $this->assertSame('Paris', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Barcelona', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); } /** * @equivalenceClass a:multiple, b:OR, c:multiple, e:same, f:negation * @dataProvider databases + * @todo broken * * @param Connection $db */ @@ -1081,7 +1129,9 @@ public function testOrChainTargetingMultipleRelationsWithTheSameNegativeOperator $this->assertSame('London', $offices[0]['city'] ?? 'not found'); $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); $this->assertSame('Amsterdam', $offices[2]['city'] ?? 'not found'); - $this->assertSame(3, count($offices)); + $this->assertSame('Paris', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Barcelona', $offices[4]['city'] ?? 'not found'); + $this->assertSame(5, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -1096,12 +1146,15 @@ public function testOrChainTargetingMultipleRelationsWithTheSameNegativeOperator $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); $this->assertSame('Amsterdam', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame(3, count($results), $sql); + $this->assertSame('Paris', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Barcelona', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); } /** * @equivalenceClass a:multiple, b:OR, c:multiple, e:different * @dataProvider databases + * @todo incorrect * * @param Connection $db */ @@ -1122,7 +1175,9 @@ public function testOrChainTargetingMultipleRelationsWithDifferentOperators(Conn $this->assertSame('London', $offices[0]['city'] ?? 'not found'); $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); $this->assertSame('Amsterdam', $offices[2]['city'] ?? 'not found'); - $this->assertSame(3, count($offices)); + $this->assertSame('Paris', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Barcelona', $offices[4]['city'] ?? 'not found'); + $this->assertSame(5, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -1137,12 +1192,15 @@ public function testOrChainTargetingMultipleRelationsWithDifferentOperators(Conn $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); $this->assertSame('Amsterdam', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame(3, count($results), $sql); + $this->assertSame('Paris', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Barcelona', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); } /** * @equivalenceClass a:multiple, b:NOT, c:multiple, e:same, f:affirmation * @dataProvider databases + * @todo broken * * @param Connection $db */ @@ -1152,20 +1210,20 @@ public function testNotChainTargetingMultipleRelationsWithTheSameAffirmativeOper $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' WHERE office.id NOT IN (' - . ' SELECT office.id FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id' - . ' LEFT JOIN department d on e.department_id = d.id' - . ' WHERE e.name = ? OR d.name = ?' - . ' GROUP BY office.id' - . ' HAVING COUNT(e.id) > 0' - . ' )' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE NOT (e.name = ? AND d.name = ?)' + . ' GROUP BY office.id' . ' ORDER BY office.id', ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); - $this->assertSame(1, count($offices)); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[2]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Paris', $offices[4]['city'] ?? 'not found'); + $this->assertSame(5, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -1177,13 +1235,18 @@ public function testNotChainTargetingMultipleRelationsWithTheSameAffirmativeOper $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame(1, count($results), $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Paris', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); } /** * @equivalenceClass a:multiple, b:NOT, c:multiple, e:same, f:negation * @dataProvider databases + * @todo broken * * @param Connection $db */ @@ -1193,20 +1256,20 @@ public function testNotChainTargetingMultipleRelationsWithTheSameNegativeOperato $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' WHERE office.id NOT IN (' - . ' SELECT office.id FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id' - . ' LEFT JOIN department d on e.department_id = d.id' - . ' WHERE e.name != ? OR d.name != ?' - . ' GROUP BY office.id' - . ' HAVING COUNT(e.id) > 0' - . ' )' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE NOT (e.name != ? AND d.name != ?)' + . ' GROUP BY office.id' . ' ORDER BY office.id', ['Donald', 'Accounting'] )->fetchAll(); - // @todo this shouldn't be empty, add some matches to the test data - $this->assertEmpty($offices); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Paris', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Barcelona', $offices[4]['city'] ?? 'not found'); + $this->assertSame(5, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -1218,12 +1281,18 @@ public function testNotChainTargetingMultipleRelationsWithTheSameNegativeOperato $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertEmpty($results, $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Paris', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Barcelona', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); } /** * @equivalenceClass a:multiple, b:NOT, c:multiple, e:different * @dataProvider databases + * @todo broken * * @param Connection $db */ @@ -1237,16 +1306,18 @@ public function testNotChainTargetingMultipleRelationsWithDifferentOperators(Con . ' SELECT office.id FROM office' . ' LEFT JOIN employee e on e.office_id = office.id' . ' LEFT JOIN department d on e.department_id = d.id' - . ' WHERE e.name = ? OR d.name != ?' + . ' WHERE e.name = ? AND d.name != ?' . ' GROUP BY office.id' - . ' HAVING COUNT(e.id) > 0' . ' )' . ' ORDER BY office.id', ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('New York', $offices[0]['city'] ?? 'not found'); - $this->assertSame(1, count($offices)); + $this->assertSame('London', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Paris', $offices[3]['city'] ?? 'not found'); + $this->assertSame(4, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -1258,8 +1329,11 @@ public function testNotChainTargetingMultipleRelationsWithDifferentOperators(Con $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('New York', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame(1, count($results), $sql); + $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Paris', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame(4, count($results), $sql); } /** @@ -1318,6 +1392,8 @@ public function testUnequalTargetingAnOptionalToManyRelationIgnoresFalsePositive * Louie | Cook | Berlin * - | - | Cuxhaven * Huey | Assistant | Sydney + * Donald | Manager | Baghdad + * Huey | Accountant | Baghdad * * @param Connection $db */ @@ -1337,6 +1413,9 @@ protected function createOfficesAndEmployees(Connection $db) $db->insert('office', ['id' => 5, 'city' => 'Cuxhaven']); // None $db->insert('office', ['id' => 6, 'city' => 'Sydney']); // The other one of the two $db->insert('employee', ['id' => 8, 'office_id' => 6, 'name' => 'Huey', 'role' => 'Assistant']); + $db->insert('office', ['id' => 7, 'city' => 'Baghdad']); // The two with switched roles + $db->insert('employee', ['id' => 9, 'office_id' => 7, 'name' => 'Donald', 'role' => 'Manager']); + $db->insert('employee', ['id' => 10, 'office_id' => 7, 'name' => 'Huey', 'role' => 'Accountant']); } /** @@ -1350,8 +1429,9 @@ protected function createOfficesAndEmployees(Connection $db) * Huey | Assistant | Accounting | Berlin * Huey | QA Lead | Kitchen | Amsterdam * Mickey | Manager | Accounting | New York - * - * @todo Add some false positives + * Mickey | Cook | Kitchen | Paris + * Louie | Accountant | Accounting | Paris + * Donald | Accountant | Accounting | Barcelona * * @param Connection $db */ @@ -1361,6 +1441,8 @@ protected function createOfficesEmployeesAndDepartments(Connection $db) $db->insert('office', ['id' => 2, 'city' => 'Berlin']); $db->insert('office', ['id' => 3, 'city' => 'Amsterdam']); $db->insert('office', ['id' => 4, 'city' => 'New York']); + $db->insert('office', ['id' => 5, 'city' => 'Paris']); + $db->insert('office', ['id' => 6, 'city' => 'Barcelona']); $db->insert('department', ['id' => 1, 'name' => 'Accounting']); $db->insert('department', ['id' => 2, 'name' => 'Kitchen']); $db->insert( @@ -1387,6 +1469,18 @@ protected function createOfficesEmployeesAndDepartments(Connection $db) 'employee', ['id' => 6, 'office_id' => 4, 'department_id' => 1, 'name' => 'Mickey', 'role' => 'Manager'] ); + $db->insert( + 'employee', + ['id' => 7, 'office_id' => 5, 'department_id' => 2, 'name' => 'Mickey', 'role' => 'Cook'] + ); + $db->insert( + 'employee', + ['id' => 8, 'office_id' => 5, 'department_id' => 1, 'name' => 'Louie', 'role' => 'Accountant'] + ); + $db->insert( + 'employee', + ['id' => 9, 'office_id' => 6, 'department_id' => 1, 'name' => 'Donald', 'role' => 'Accountant'] + ); } protected function createSchema(Connection $db, string $driver): void From 40559e2950500087fcc947ddf14a4951b60a62da Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 8 Mar 2024 14:16:20 +0100 Subject: [PATCH 5/8] docs, fixes --- tests/RelationFilterTest.php | 339 ++++++++++++++++------------------- 1 file changed, 158 insertions(+), 181 deletions(-) diff --git a/tests/RelationFilterTest.php b/tests/RelationFilterTest.php index 00e1503..af3f3c1 100644 --- a/tests/RelationFilterTest.php +++ b/tests/RelationFilterTest.php @@ -6,7 +6,6 @@ use ipl\Sql\Connection; use ipl\Sql\Test\Databases; use ipl\Stdlib\Filter; -use ipl\Tests\Orm\Lib\Model\Department; use ipl\Tests\Orm\Lib\Model\Office; use PHPUnit\Framework\TestCase; @@ -47,6 +46,8 @@ class RelationFilterTest extends TestCase use Databases; /** + * Search for offices where Donald works + * * @equivalenceClass a:single, b:-, f:affirmation * @dataProvider databases * @@ -86,6 +87,8 @@ public function testSingleAffirmativeCondition(Connection $db) } /** + * Search for offices where Donald doesn't work + * * @equivalenceClass a:single, b:-, f:negation * @dataProvider databases * @@ -123,9 +126,10 @@ public function testSingleNegativeCondition(Connection $db) } /** + * Search for offices where Donald doesn't work + * * @equivalenceClass a:single, b:NOT, f:affirmation * @dataProvider databases - * @todo broken * * @param Connection $db */ @@ -167,9 +171,10 @@ public function testSingleAffirmativeConditionWithNotOperator(Connection $db) } /** + * Search for offices where Donald works + * * @equivalenceClass a:single, b:NOT, f:negation * @dataProvider databases - * @todo broken * * @param Connection $db */ @@ -209,8 +214,7 @@ public function testSingleNegativeConditionWithNotOperator(Connection $db) } /** - * Test whether multiple equal filters combined with OR on the same column of - * the same to-many relation, include results that match any condition. + * Search for offices where either Donald or Huey works * * @equivalenceClass a:multiple, b:OR, c:single, d:same, e:same, f:affirmation * @dataProvider databases @@ -257,12 +261,13 @@ public function testOrChainTargetingASingleRelationColumnWithTheSameAffirmativeO } /** - * Test whether multiple unequal filters combined with OR on the same column - * of the same to-many relation, filter out results that match all conditions. + * Search for offices where either Donald or Huey doesn't work * * @equivalenceClass a:multiple, b:OR, c:single, d:same, e:same, f:negation * @dataProvider databases - * @todo different + * @todo Finds what {@see testAndChainTargetingASingleRelationColumnWithTheSameNegativeOperator} + * does. The exact opposite. Should they really be equal? They are in monitoring, but there + * the results are the other way round. (What ALL(!=) finds here, cannot be found there) * * @param Connection $db */ @@ -296,12 +301,16 @@ public function testOrChainTargetingASingleRelationColumnWithTheSameNegativeOper $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('New York', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('Cuxhaven', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame(2, count($results), $sql); + $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame(4, count($results), $sql); } /** + * Search for offices where Donald works or Huey doesn't + * * @equivalenceClass a:multiple, b:OR, c:single, d:same, e:different * @dataProvider databases * @@ -349,8 +358,7 @@ public function testOrChainTargetingASingleRelationColumnWithDifferentOperators( } /** - * Test whether multiple equal filters combined with AND on the same column of - * the same to-many relation, only include results that match all conditions. + * Search for offices where both Donald and Huey work * * @equivalenceClass a:multiple, b:AND, c:single, d:same, e:same, f:affirmation * @dataProvider databases @@ -393,12 +401,10 @@ public function testAndChainTargetingASingleRelationColumnWithTheSameAffirmative } /** - * Test whether multiple unequal filters combined with AND on the same column - * of the same to-many relation, filter out results that match any condition. + * Search for offices where neither Donald nor Huey work * * @equivalenceClass a:multiple, b:AND, c:single, d:same, e:same, f:negation * @dataProvider databases - * @todo different * * @param Connection $db */ @@ -410,17 +416,15 @@ public function testAndChainTargetingASingleRelationColumnWithTheSameNegativeOpe 'SELECT office.city FROM office' . ' LEFT JOIN employee e1 on e1.office_id = office.id AND e1.name = ?' . ' LEFT JOIN employee e2 on e2.office_id = office.id AND e2.name = ?' - . ' WHERE e1.id IS NULL OR e2.id IS NULL' + . ' WHERE e1.id IS NULL AND e2.id IS NULL' . ' GROUP BY office.id' . ' ORDER BY office.id', ['Donald', 'Huey'] )->fetchAll(); - $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); - $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); - $this->assertSame('Cuxhaven', $offices[2]['city'] ?? 'not found'); - $this->assertSame('Sydney', $offices[3]['city'] ?? 'not found'); - $this->assertSame(4, count($offices)); + $this->assertSame('New York', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[1]['city'] ?? 'not found'); + $this->assertSame(2, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -432,14 +436,14 @@ public function testAndChainTargetingASingleRelationColumnWithTheSameNegativeOpe $results = iterator_to_array($offices); $sql = $this->getSql($offices); - // TODO: Somewhat same as with IdoQuery, as it's the same result as in - // {@see testOrChainTargetingASingleRelationColumnWithTheSameNegativeOperator} $this->assertSame('New York', $results[0]['city'] ?? 'not found', $sql); $this->assertSame('Cuxhaven', $results[1]['city'] ?? 'not found', $sql); $this->assertSame(2, count($results), $sql); } /** + * Search for offices where Donald works and Huey doesn't + * * @equivalenceClass a:multiple, b:AND, c:single, d:same, e:different * @dataProvider databases * @@ -477,12 +481,10 @@ public function testAndChainTargetingASingleRelationColumnWithDifferentOperators } /** - * Test whether multiple equal filters combined with NOT on the same column of - * the same to-many relation, include results that match none of the conditions. + * Search for offices where either Donald or Huey doesn't work * * @equivalenceClass a:multiple, b:NOT, c:single, d:same, e:same, f:affirmation * @dataProvider databases - * @todo broken * * @param Connection $db */ @@ -524,12 +526,10 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameAffirmative } /** - * Test whether multiple unequal filters combined with NOT on the same column - * of the same to-many relation, only include results that match all conditions. + * Search for offices where Donald and Huey work * * @equivalenceClass a:multiple, b:NOT, c:single, d:same, e:same, f:negation * @dataProvider databases - * @todo broken * * @param Connection $db */ @@ -572,9 +572,10 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameNegativeOpe } /** + * Search for offices where either Donald doesn't work or Huey does + * * @equivalenceClass a:multiple, b:NOT, c:single, d:same, e:different * @dataProvider databases - * @todo broken * * @param Connection $db */ @@ -620,102 +621,35 @@ public function testNotChainTargetingASingleRelationColumnWithDifferentOperators } /** - * Test whether the ORM produces correct results if an unequal filter is combined - * with an equal filter on the same 1-n relation and both with different columns + * Search for offices where Huey works but not as manager * * @equivalenceClass a:multiple, b:AND, c:single, d:different, e:different * @dataProvider databases - * @todo simplify, like the others * * @param Connection $db */ public function testAndChainTargetingASingleRelationButDifferentColumnsWithDifferentOperators(Connection $db) { - $db->insert('department', ['id' => 1, 'name' => 'Sales']); - $db->insert('employee', ['id' => 1, 'department_id' => 1, 'name' => 'Donald', 'role' => 'Accountant']); - $db->insert('employee', ['id' => 2, 'department_id' => 1, 'name' => 'Huey', 'role' => 'Manager']); - $db->insert('department', ['id' => 2, 'name' => 'Accounting']); - $db->insert('employee', ['id' => 5, 'department_id' => 2, 'name' => 'Donald', 'role' => 'Salesperson']); - $db->insert('department', ['id' => 3, 'name' => 'Kitchen']); - $db->insert('employee', ['id' => 6, 'department_id' => 3, 'name' => 'Donald', 'role' => null]); - $db->insert('employee', ['id' => 7, 'department_id' => 3, 'name' => 'Huey', 'role' => null]); - $db->insert('department', ['id' => 4, 'name' => 'QA']); - $db->insert('employee', ['id' => 8, 'department_id' => 4, 'name' => 'Donald', 'role' => 'Accountant']); - $db->insert('employee', ['id' => 9, 'department_id' => 4, 'name' => 'Donald', 'role' => 'Assistant']); - - // First a proof of concept by using a manually crafted SQL query - $departments = $db->prepexec( - 'SELECT department.name FROM department' - . ' LEFT JOIN employee e on department.id = e.department_id' - . ' WHERE e.name = ? AND (e.role != ? OR e.role IS NULL)' - . ' GROUP BY department.id' - . ' ORDER BY department.id', - ['Donald', 'Accountant'] - )->fetchAll(); - - $this->assertSame('Accounting', $departments[0]['name'] ?? 'not found'); - $this->assertSame('Kitchen', $departments[1]['name'] ?? 'not found'); - $this->assertSame('QA', $departments[2]['name'] ?? 'not found'); - $this->assertSame(3, count($departments)); - - // Now let's do the same using the ORM - $departments = Department::on($db) - ->columns(['department.name']) - ->orderBy('department.id') - ->filter(Filter::all( - Filter::equal('employee.name', 'Donald'), - Filter::unequal('employee.role', 'Accountant') - )); - $results = iterator_to_array($departments); - $sql = $this->getSql($departments); - - $this->assertSame('Accounting', $results[0]['name'] ?? 'not found', $sql); - $this->assertSame('Kitchen', $results[1]['name'] ?? 'not found', $sql); - $this->assertSame('QA', $results[2]['name'] ?? 'not found', $sql); - $this->assertSame(3, count($results), $sql); - - // The ORM may perform fine till now, but let's see what happens if we include some false positives - $db->insert('department', ['id' => 5, 'name' => 'Admin']); - $db->insert('employee', ['id' => 10, 'department_id' => 5, 'name' => 'Huey', 'role' => 'Salesperson']); - - // This employee's role doesn't match but the name does neither, resulting in the department not showing up - $db->insert('employee', ['id' => 11, 'department_id' => 5, 'name' => 'Dewey', 'role' => 'Manager']); - - // This department has no employees and as such none with the desired name, although the role, being not - // set due to the left join, would match. It might also show up due to a NOT EXISTS/NOT IN. - $db->insert('department', ['id' => 6, 'name' => 'QA']); - - // Proof of concept first, again - $departments = $db->prepexec( - 'SELECT department.name FROM department' - . ' LEFT JOIN employee e on department.id = e.department_id' - . ' WHERE e.name = ? AND (e.role != ? OR e.role IS NULL)' - . ' GROUP BY department.id' - . ' ORDER BY department.id', - ['Huey', 'Manager'] - )->fetchAll(); - - $this->assertSame('Kitchen', $departments[0]['name'] ?? 'not found'); - $this->assertSame('Admin', $departments[1]['name'] ?? 'not found'); - $this->assertSame(2, count($departments)); + $this->createOfficesAndEmployees($db); - // Now the ORM. Note that the result depends on how the subqueries are constructed to filter the results - $departments = Department::on($db) - ->columns(['department.name']) - ->orderBy('department.id') + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') ->filter(Filter::all( Filter::equal('employee.name', 'Huey'), Filter::unequal('employee.role', 'Manager') )); - $results = iterator_to_array($departments); - $sql = $this->getSql($departments); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); - $this->assertSame('Kitchen', $results[0]['name'] ?? 'not found', $sql); - $this->assertSame('Admin', $results[1]['name'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Baghdad', $results[1]['city'] ?? 'not found', $sql); $this->assertSame(2, count($results), $sql); } /** + * Search for offices where Donald works as accountant + * * @equivalenceClass a:multiple, b:AND, c:single, d:different, e:same * @dataProvider databases * @@ -754,6 +688,8 @@ public function testAndChainTargetingASingleRelationButDifferentColumnsWithTheSa } /** + * Search for offices where Donald works or someone else not as accountant + * * @equivalenceClass a:multiple, b:OR, c:single, d:different, e:different * @dataProvider databases * @@ -802,6 +738,8 @@ public function testOrChainTargetingASingleRelationButDifferentColumnsWithDiffer } /** + * Search for offices where either Donald works or any other assistant + * * @equivalenceClass a:multiple, b:OR, c:single, d:different, e:same * @dataProvider databases * @@ -846,9 +784,10 @@ public function testOrChainTargetingASingleRelationButDifferentColumnsWithTheSam } /** + * Search for offices where not just Huey works or only as a manager + * * @equivalenceClass a:multiple, b:NOT, c:single, d:different, e:different * @dataProvider databases - * @todo broken * * @param Connection $db */ @@ -859,7 +798,7 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithDiffe $offices = $db->prepexec( 'SELECT office.city FROM office' . ' LEFT JOIN employee e ON e.office_id = office.id' - . ' WHERE NOT (e.name = ? AND (e.role != ? OR e.role IS NULL))' + . ' WHERE NOT (e.name = ? AND e.role != ?)' . ' GROUP BY office.id' . ' ORDER BY office.id', ['Huey', 'Manager'] @@ -891,9 +830,10 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithDiffe } /** + * Search for offices where not just Donald works or not as manager + * * @equivalenceClass a:multiple, b:NOT, c:single, d:different, e:same * @dataProvider databases - * @todo broken * * @param Connection $db */ @@ -934,13 +874,14 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithTheSa $this->assertSame('Berlin', $results[3]['city'] ?? 'not found', $sql); $this->assertSame('Sydney', $results[4]['city'] ?? 'not found', $sql); $this->assertSame('Baghdad', $results[5]['city'] ?? 'not found', $sql); - $this->assertSame(5, count($results), $sql); + $this->assertSame(6, count($results), $sql); } /** + * Search for offices where Donald works in the accounting department + * * @equivalenceClass a:multiple, b:AND, c:multiple, e:same, f:affirmation * @dataProvider databases - * @todo broken * * @param Connection $db */ @@ -978,6 +919,8 @@ public function testAndChainTargetingMultipleRelationsWithTheSameAffirmativeOper } /** + * Search for offices where Donald doesn't work in the accounting department + * * @equivalenceClass a:multiple, b:AND, c:multiple, e:same, f:negation * @dataProvider databases * @@ -992,21 +935,19 @@ public function testAndChainTargetingMultipleRelationsWithTheSameNegativeOperato . ' WHERE office.id NOT IN (' . ' SELECT office.id FROM office' . ' LEFT JOIN employee e on e.office_id = office.id' - . ' WHERE e.name = ?' - . ' GROUP BY office.id' - . ' ) AND office.id NOT IN (' - . ' SELECT office.id FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id' . ' LEFT JOIN department d on e.department_id = d.id' - . ' WHERE d.name = ?' + . ' WHERE e.name = ? AND d.name = ?' . ' GROUP BY office.id' . ' )' . ' ORDER BY office.id', ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); - $this->assertSame(1, count($offices)); + $this->assertSame('Berlin', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Paris', $offices[3]['city'] ?? 'not found'); + $this->assertSame(4, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -1018,14 +959,18 @@ public function testAndChainTargetingMultipleRelationsWithTheSameNegativeOperato $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame(1, count($results), $sql); + $this->assertSame('Berlin', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Paris', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame(4, count($results), $sql); } /** + * Search for offices where Donald works but not in the accounting department + * * @equivalenceClass a:multiple, b:AND, c:multiple, e:different * @dataProvider databases - * @todo broken * * @param Connection $db */ @@ -1061,6 +1006,8 @@ public function testAndChainTargetingMultipleRelationsWithDifferentOperators(Con } /** + * Search for offices where either Donald works or someone else in the accounting department + * * @equivalenceClass a:multiple, b:OR, c:multiple, e:same, f:affirmation * @dataProvider databases * @@ -1106,9 +1053,10 @@ public function testOrChainTargetingMultipleRelationsWithTheSameAffirmativeOpera } /** + * Search for offices where Mickey doesn't work or no-one in the accounting department + * * @equivalenceClass a:multiple, b:OR, c:multiple, e:same, f:negation * @dataProvider databases - * @todo broken * * @param Connection $db */ @@ -1118,10 +1066,19 @@ public function testOrChainTargetingMultipleRelationsWithTheSameNegativeOperator $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id' - . ' LEFT JOIN department d on e.department_id = d.id' - . ' WHERE e.name != ? OR d.name != ?' - . ' GROUP BY office.id' + . ' WHERE office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE e.name = ?' + . ' GROUP BY office.id' + . ' ) OR office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE d.name = ?' + . ' GROUP BY office.id' + . ' )' . ' ORDER BY office.id', ['Mickey', 'Accounting'] )->fetchAll(); @@ -1129,9 +1086,8 @@ public function testOrChainTargetingMultipleRelationsWithTheSameNegativeOperator $this->assertSame('London', $offices[0]['city'] ?? 'not found'); $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); $this->assertSame('Amsterdam', $offices[2]['city'] ?? 'not found'); - $this->assertSame('Paris', $offices[3]['city'] ?? 'not found'); - $this->assertSame('Barcelona', $offices[4]['city'] ?? 'not found'); - $this->assertSame(5, count($offices)); + $this->assertSame('Barcelona', $offices[3]['city'] ?? 'not found'); + $this->assertSame(4, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -1146,15 +1102,15 @@ public function testOrChainTargetingMultipleRelationsWithTheSameNegativeOperator $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); $this->assertSame('Amsterdam', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame('Paris', $results[3]['city'] ?? 'not found', $sql); - $this->assertSame('Barcelona', $results[4]['city'] ?? 'not found', $sql); - $this->assertSame(5, count($results), $sql); + $this->assertSame('Barcelona', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame(4, count($results), $sql); } /** + * Search for offices where either Donald works or no-one in the accounting department + * * @equivalenceClass a:multiple, b:OR, c:multiple, e:different * @dataProvider databases - * @todo incorrect * * @param Connection $db */ @@ -1164,10 +1120,18 @@ public function testOrChainTargetingMultipleRelationsWithDifferentOperators(Conn $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id' - . ' LEFT JOIN department d on e.department_id = d.id' - . ' WHERE e.name = ? OR d.name != ?' - . ' GROUP BY office.id' + . ' WHERE office.id IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' WHERE e.name = ?' + . ' GROUP BY office.id' + . ' ) OR office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE d.name = ?' + . ' GROUP BY office.id' + . ' )' . ' ORDER BY office.id', ['Donald', 'Accounting'] )->fetchAll(); @@ -1175,9 +1139,8 @@ public function testOrChainTargetingMultipleRelationsWithDifferentOperators(Conn $this->assertSame('London', $offices[0]['city'] ?? 'not found'); $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); $this->assertSame('Amsterdam', $offices[2]['city'] ?? 'not found'); - $this->assertSame('Paris', $offices[3]['city'] ?? 'not found'); - $this->assertSame('Barcelona', $offices[4]['city'] ?? 'not found'); - $this->assertSame(5, count($offices)); + $this->assertSame('Barcelona', $offices[3]['city'] ?? 'not found'); + $this->assertSame(4, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -1192,15 +1155,15 @@ public function testOrChainTargetingMultipleRelationsWithDifferentOperators(Conn $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); $this->assertSame('Amsterdam', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame('Paris', $results[3]['city'] ?? 'not found', $sql); - $this->assertSame('Barcelona', $results[4]['city'] ?? 'not found', $sql); - $this->assertSame(5, count($results), $sql); + $this->assertSame('Barcelona', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame(4, count($results), $sql); } /** + * Search for offices where Donald doesn't work in the accounting department + * * @equivalenceClass a:multiple, b:NOT, c:multiple, e:same, f:affirmation * @dataProvider databases - * @todo broken * * @param Connection $db */ @@ -1210,20 +1173,22 @@ public function testNotChainTargetingMultipleRelationsWithTheSameAffirmativeOper $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id' - . ' LEFT JOIN department d on e.department_id = d.id' - . ' WHERE NOT (e.name = ? AND d.name = ?)' - . ' GROUP BY office.id' + . ' WHERE office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE e.name = ? AND d.name = ?' + . ' GROUP BY office.id' + . ' )' . ' ORDER BY office.id', ['Donald', 'Accounting'] )->fetchAll(); - $this->assertSame('London', $offices[0]['city'] ?? 'not found'); - $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); - $this->assertSame('Amsterdam', $offices[2]['city'] ?? 'not found'); - $this->assertSame('New York', $offices[3]['city'] ?? 'not found'); - $this->assertSame('Paris', $offices[4]['city'] ?? 'not found'); - $this->assertSame(5, count($offices)); + $this->assertSame('Berlin', $offices[0]['city'] ?? 'not found'); + $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Paris', $offices[3]['city'] ?? 'not found'); + $this->assertSame(4, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -1235,18 +1200,23 @@ public function testNotChainTargetingMultipleRelationsWithTheSameAffirmativeOper $results = iterator_to_array($offices); $sql = $this->getSql($offices); - $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); - $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('Amsterdam', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame('New York', $results[3]['city'] ?? 'not found', $sql); - $this->assertSame('Paris', $results[4]['city'] ?? 'not found', $sql); - $this->assertSame(5, count($results), $sql); + $this->assertSame('Berlin', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Paris', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame(4, count($results), $sql); } /** + * Search for offices where no Donald doesn't work not in the accounting department + * + * Or for your convenience: Where Donald works + * * @equivalenceClass a:multiple, b:NOT, c:multiple, e:same, f:negation * @dataProvider databases - * @todo broken + * @todo Just take a look at the POC and wonder how that would ever be automatically generated. + * Especially if you consider adding another comparison into the mix. This is way too + * complex for the ORM to handle let alone for an ordinary user to understand. * * @param Connection $db */ @@ -1256,20 +1226,26 @@ public function testNotChainTargetingMultipleRelationsWithTheSameNegativeOperato $offices = $db->prepexec( 'SELECT office.city FROM office' - . ' LEFT JOIN employee e on e.office_id = office.id' - . ' LEFT JOIN department d on e.department_id = d.id' - . ' WHERE NOT (e.name != ? AND d.name != ?)' - . ' GROUP BY office.id' + . ' WHERE NOT (office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' WHERE e.name = ?' + . ' GROUP BY office.id' + . ' ) AND office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e on e.office_id = office.id' + . ' LEFT JOIN department d on e.department_id = d.id' + . ' WHERE e.name = ? AND d.name = ?' + . ' GROUP BY office.id' + . ' ))' . ' ORDER BY office.id', - ['Donald', 'Accounting'] + ['Donald', 'Donald', 'Accounting'] )->fetchAll(); $this->assertSame('London', $offices[0]['city'] ?? 'not found'); $this->assertSame('Berlin', $offices[1]['city'] ?? 'not found'); - $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); - $this->assertSame('Paris', $offices[3]['city'] ?? 'not found'); - $this->assertSame('Barcelona', $offices[4]['city'] ?? 'not found'); - $this->assertSame(5, count($offices)); + $this->assertSame('Barcelona', $offices[2]['city'] ?? 'not found'); + $this->assertSame(3, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -1283,16 +1259,15 @@ public function testNotChainTargetingMultipleRelationsWithTheSameNegativeOperato $this->assertSame('London', $results[0]['city'] ?? 'not found', $sql); $this->assertSame('Berlin', $results[1]['city'] ?? 'not found', $sql); - $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); - $this->assertSame('Paris', $results[3]['city'] ?? 'not found', $sql); - $this->assertSame('Barcelona', $results[4]['city'] ?? 'not found', $sql); - $this->assertSame(5, count($results), $sql); + $this->assertSame('Barcelona', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame(3, count($results), $sql); } /** + * Search for offices where Donald doesn't work or in the accounting department + * * @equivalenceClass a:multiple, b:NOT, c:multiple, e:different * @dataProvider databases - * @todo broken * * @param Connection $db */ @@ -1317,7 +1292,8 @@ public function testNotChainTargetingMultipleRelationsWithDifferentOperators(Con $this->assertSame('Amsterdam', $offices[1]['city'] ?? 'not found'); $this->assertSame('New York', $offices[2]['city'] ?? 'not found'); $this->assertSame('Paris', $offices[3]['city'] ?? 'not found'); - $this->assertSame(4, count($offices)); + $this->assertSame('Barcelona', $offices[4]['city'] ?? 'not found'); + $this->assertSame(5, count($offices)); $offices = Office::on($db) ->columns(['office.city']) @@ -1333,7 +1309,8 @@ public function testNotChainTargetingMultipleRelationsWithDifferentOperators(Con $this->assertSame('Amsterdam', $results[1]['city'] ?? 'not found', $sql); $this->assertSame('New York', $results[2]['city'] ?? 'not found', $sql); $this->assertSame('Paris', $results[3]['city'] ?? 'not found', $sql); - $this->assertSame(4, count($results), $sql); + $this->assertSame('Barcelona', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); } /** From 1854c13cb8e4670423ad5bdcb786fd02945b19c5 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Fri, 8 Mar 2024 15:37:25 +0100 Subject: [PATCH 6/8] Finito, for now --- tests/RelationFilterTest.php | 61 ++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/tests/RelationFilterTest.php b/tests/RelationFilterTest.php index af3f3c1..3193bf8 100644 --- a/tests/RelationFilterTest.php +++ b/tests/RelationFilterTest.php @@ -17,7 +17,7 @@ * b) Logical Operators: -, NOT * f) Comparisons: affirmation, negation * a) Number of conditions: multiple - * b) Logical Operators: AND, OR, NOT + * b) Logical Operators: AND, OR, NOT(AND), NOT(OR) * c) Number of relations: single * d) Columns: same * e) Operators: same @@ -40,6 +40,8 @@ * Every test contains at least one proof of concept using a manually crafted SQL query. (Not necessarily the same way * the ORM constructs it) Such must not fail. If they do, the dataset has changed. The ORM query must return the same * results. + * + * @todo NOT(OR) cases are missing */ class RelationFilterTest extends TestCase { @@ -128,7 +130,7 @@ public function testSingleNegativeCondition(Connection $db) /** * Search for offices where Donald doesn't work * - * @equivalenceClass a:single, b:NOT, f:affirmation + * @equivalenceClass a:single, b:NOT(AND), f:affirmation * @dataProvider databases * * @param Connection $db @@ -173,7 +175,7 @@ public function testSingleAffirmativeConditionWithNotOperator(Connection $db) /** * Search for offices where Donald works * - * @equivalenceClass a:single, b:NOT, f:negation + * @equivalenceClass a:single, b:NOT(AND), f:negation * @dataProvider databases * * @param Connection $db @@ -483,7 +485,7 @@ public function testAndChainTargetingASingleRelationColumnWithDifferentOperators /** * Search for offices where either Donald or Huey doesn't work * - * @equivalenceClass a:multiple, b:NOT, c:single, d:same, e:same, f:affirmation + * @equivalenceClass a:multiple, b:NOT(AND), c:single, d:same, e:same, f:affirmation * @dataProvider databases * * @param Connection $db @@ -511,10 +513,10 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameAffirmative $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') - ->filter(Filter::none( + ->filter(Filter::none(Filter::all( Filter::equal('employee.name', 'Donald'), Filter::equal('employee.name', 'Huey') - )); + ))); $results = iterator_to_array($offices); $sql = $this->getSql($offices); @@ -528,8 +530,10 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameAffirmative /** * Search for offices where Donald and Huey work * - * @equivalenceClass a:multiple, b:NOT, c:single, d:same, e:same, f:negation + * @equivalenceClass a:multiple, b:NOT(AND), c:single, d:same, e:same, f:negation * @dataProvider databases + * @todo Finds the exact opposite of {@see testAndChainTargetingASingleRelationColumnWithTheSameNegativeOperator}. + * No wonder, actually. But monitoring doesn't behave the same way :'( * * @param Connection $db */ @@ -558,10 +562,10 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameNegativeOpe $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') - ->filter(Filter::none( + ->filter(Filter::none(Filter::all( Filter::unequal('employee.name', 'Donald'), Filter::unequal('employee.name', 'Huey') - )); + ))); $results = iterator_to_array($offices); $sql = $this->getSql($offices); @@ -574,7 +578,7 @@ public function testNotChainTargetingASingleRelationColumnWithTheSameNegativeOpe /** * Search for offices where either Donald doesn't work or Huey does * - * @equivalenceClass a:multiple, b:NOT, c:single, d:same, e:different + * @equivalenceClass a:multiple, b:NOT(AND), c:single, d:same, e:different * @dataProvider databases * * @param Connection $db @@ -604,10 +608,10 @@ public function testNotChainTargetingASingleRelationColumnWithDifferentOperators $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') - ->filter(Filter::none( + ->filter(Filter::none(Filter::all( Filter::equal('employee.name', 'Donald'), Filter::unequal('employee.name', 'Huey') - )); + ))); $results = iterator_to_array($offices); $sql = $this->getSql($offices); @@ -786,8 +790,10 @@ public function testOrChainTargetingASingleRelationButDifferentColumnsWithTheSam /** * Search for offices where not just Huey works or only as a manager * - * @equivalenceClass a:multiple, b:NOT, c:single, d:different, e:different + * @equivalenceClass a:multiple, b:NOT(AND), c:single, d:different, e:different * @dataProvider databases + * @todo Finds the exact opposite of {@see testAndChainTargetingASingleRelationButDifferentColumnsWithDifferentOperators}. + * No wonder, actually. But monitoring doesn't behave the same way :'( * * @param Connection $db */ @@ -814,10 +820,10 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithDiffe $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') - ->filter(Filter::none( + ->filter(Filter::none(Filter::all( Filter::equal('employee.name', 'Huey'), Filter::unequal('employee.role', 'Manager') - )); + ))); $results = iterator_to_array($offices); $sql = $this->getSql($offices); @@ -832,8 +838,9 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithDiffe /** * Search for offices where not just Donald works or not as manager * - * @equivalenceClass a:multiple, b:NOT, c:single, d:different, e:same + * @equivalenceClass a:multiple, b:NOT(AND), c:single, d:different, e:same * @dataProvider databases + * @todo Finds Cuxhaven instead of Baghdad. Not wrong, actually. Why does monitoring behave differently? * * @param Connection $db */ @@ -861,10 +868,10 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithTheSa $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') - ->filter(Filter::none( + ->filter(Filter::none(Filter::all( Filter::equal('employee.name', 'Donald'), Filter::equal('employee.role', 'Manager') - )); + ))); $results = iterator_to_array($offices); $sql = $this->getSql($offices); @@ -1162,7 +1169,7 @@ public function testOrChainTargetingMultipleRelationsWithDifferentOperators(Conn /** * Search for offices where Donald doesn't work in the accounting department * - * @equivalenceClass a:multiple, b:NOT, c:multiple, e:same, f:affirmation + * @equivalenceClass a:multiple, b:NOT(AND), c:multiple, e:same, f:affirmation * @dataProvider databases * * @param Connection $db @@ -1193,10 +1200,10 @@ public function testNotChainTargetingMultipleRelationsWithTheSameAffirmativeOper $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') - ->filter(Filter::none( + ->filter(Filter::none(Filter::all( Filter::equal('employee.name', 'Donald'), Filter::equal('employee.department.name', 'Accounting') - )); + ))); $results = iterator_to_array($offices); $sql = $this->getSql($offices); @@ -1212,7 +1219,7 @@ public function testNotChainTargetingMultipleRelationsWithTheSameAffirmativeOper * * Or for your convenience: Where Donald works * - * @equivalenceClass a:multiple, b:NOT, c:multiple, e:same, f:negation + * @equivalenceClass a:multiple, b:NOT(AND), c:multiple, e:same, f:negation * @dataProvider databases * @todo Just take a look at the POC and wonder how that would ever be automatically generated. * Especially if you consider adding another comparison into the mix. This is way too @@ -1250,10 +1257,10 @@ public function testNotChainTargetingMultipleRelationsWithTheSameNegativeOperato $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') - ->filter(Filter::none( + ->filter(Filter::none(Filter::all( Filter::unequal('employee.name', 'Donald'), Filter::unequal('employee.department.name', 'Accounting') - )); + ))); $results = iterator_to_array($offices); $sql = $this->getSql($offices); @@ -1266,7 +1273,7 @@ public function testNotChainTargetingMultipleRelationsWithTheSameNegativeOperato /** * Search for offices where Donald doesn't work or in the accounting department * - * @equivalenceClass a:multiple, b:NOT, c:multiple, e:different + * @equivalenceClass a:multiple, b:NOT(AND), c:multiple, e:different * @dataProvider databases * * @param Connection $db @@ -1298,10 +1305,10 @@ public function testNotChainTargetingMultipleRelationsWithDifferentOperators(Con $offices = Office::on($db) ->columns(['office.city']) ->orderBy('office.id') - ->filter(Filter::none( + ->filter(Filter::none(Filter::all( Filter::equal('employee.name', 'Donald'), Filter::unequal('employee.department.name', 'Accounting') - )); + ))); $results = iterator_to_array($offices); $sql = $this->getSql($offices); From 142e67e3fba1de078ed395c991231f61f7329c66 Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 12 Mar 2024 16:39:25 +0100 Subject: [PATCH 7/8] cleanup --- tests/RelationFilterTest.php | 44 ------------------------------------ 1 file changed, 44 deletions(-) diff --git a/tests/RelationFilterTest.php b/tests/RelationFilterTest.php index 3193bf8..fc70d29 100644 --- a/tests/RelationFilterTest.php +++ b/tests/RelationFilterTest.php @@ -29,8 +29,6 @@ * e) Operators: same, different * f) Comparisons: affirmation, negation * - * If a test covers such a case, it is marked with the corresponding variables. - * * The tests rely on a few assumptions which are the same for all of them: * - All filters target a to-many relation * - The ORM only differs between negative and affirmative filters, hence why only equal and unequal filters are used @@ -1320,48 +1318,6 @@ public function testNotChainTargetingMultipleRelationsWithDifferentOperators(Con $this->assertSame(5, count($results), $sql); } - /** - * Test whether an unequal, that targets a to-many relation to which a link can only be established through an - * optional other relation, is built by the ORM in a way that coincidental matches are ignored - * - * This will fail if the ORM generates a NOT IN which uses a subquery that produces NULL values. - * - * @dataProvider databases - */ - public function testUnequalTargetingAnOptionalToManyRelationIgnoresFalsePositives(Connection $db) - { - $db->insert('office', ['id' => 1, 'city' => 'London']); - $db->insert('department', ['id' => 1, 'name' => 'Accounting']); - $db->insert('department', ['id' => 2, 'name' => 'Kitchen']); - $db->insert('employee', ['id' => 1, 'department_id' => 1, 'name' => 'Minnie', 'role' => 'CEO']); // remote - $db->insert( - 'employee', - ['id' => 2, 'department_id' => 2, 'office_id' => 1, 'name' => 'Goofy', 'role' => 'Developer'] - ); - - // This POC uses inner joins to achieve the desired result - $offices = $db->prepexec( - 'SELECT office.city FROM office' - . ' INNER JOIN employee e on e.office_id = office.id' - . ' INNER JOIN department d on e.department_id = d.id' - . ' WHERE d.name != ?' - . ' GROUP BY office.id' - . ' ORDER BY office.id', - ['Accounting'] - )->fetchAll(); - - $this->assertSame('London', $offices[0]['city'] ?? 'not found'); - - // The ORM will use a NOT IN and needs to ignore false positives explicitly - $offices = Office::on($db) - ->columns(['office.city']) - ->orderBy('office.id') - ->filter(Filter::unequal('employee.department.name', 'Accounting')); - $results = iterator_to_array($offices); - - $this->assertSame('London', $results[0]['city'] ?? 'not found', $this->getSql($offices)); - } - /** * Create a definite set of permutations with employees and offices * From 8ed9ed706a270ac28c039fd296a684a2548a81cd Mon Sep 17 00:00:00 2001 From: Johannes Meyer Date: Tue, 12 Mar 2024 16:42:28 +0100 Subject: [PATCH 8/8] add some missing cases --- tests/RelationFilterTest.php | 133 ++++++++++++++++++++++++++++++++--- 1 file changed, 123 insertions(+), 10 deletions(-) diff --git a/tests/RelationFilterTest.php b/tests/RelationFilterTest.php index fc70d29..6f0c73d 100644 --- a/tests/RelationFilterTest.php +++ b/tests/RelationFilterTest.php @@ -19,12 +19,10 @@ * a) Number of conditions: multiple * b) Logical Operators: AND, OR, NOT(AND), NOT(OR) * c) Number of relations: single - * d) Columns: same + * d) Columns: same, different * e) Operators: same * f) Comparisons: affirmation, negation * e) Operators: different - * d) Columns: different - * e) Operators: same, different * c) Number of relations: multiple * e) Operators: same, different * f) Comparisons: affirmation, negation @@ -652,12 +650,12 @@ public function testAndChainTargetingASingleRelationButDifferentColumnsWithDiffe /** * Search for offices where Donald works as accountant * - * @equivalenceClass a:multiple, b:AND, c:single, d:different, e:same + * @equivalenceClass a:multiple, b:AND, c:single, d:different, e:same, f:affirmation * @dataProvider databases * * @param Connection $db */ - public function testAndChainTargetingASingleRelationButDifferentColumnsWithTheSameOperator(Connection $db) + public function testAndChainTargetingASingleRelationButDifferentColumnsWithTheSameAffirmativeOperator(Connection $db) { $this->createOfficesAndEmployees($db); @@ -689,6 +687,55 @@ public function testAndChainTargetingASingleRelationButDifferentColumnsWithTheSa $this->assertSame(2, count($results), $sql); } + /** + * Search for offices where Donald doesn't work as accountant + * + * @equivalenceClass a:multiple, b:AND, c:single, d:different, e:same, f:negation + * @dataProvider databases + * + * @param Connection $db + */ + public function testAndChainTargetingASingleRelationButDifferentColumnsWithTheSameNegativeOperator(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' WHERE office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e ON e.office_id = office.id' + . ' WHERE e.name = ? AND e.role = ?' + . ' GROUP BY office.id' + . ' )' + . ' ORDER BY office.id', + ['Donald', 'Accountant'] + )->fetchAll(); + + $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[3]['city'] ?? 'not found'); + $this->assertSame('Baghdad', $offices[4]['city'] ?? 'not found'); + $this->assertSame(5, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::all( + Filter::unequal('employee.name', 'Donald'), + Filter::unequal('employee.role', 'Accountant') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame('Baghdad', $results[4]['city'] ?? 'not found', $sql); + $this->assertSame(5, count($results), $sql); + } + /** * Search for offices where Donald works or someone else not as accountant * @@ -742,12 +789,12 @@ public function testOrChainTargetingASingleRelationButDifferentColumnsWithDiffer /** * Search for offices where either Donald works or any other assistant * - * @equivalenceClass a:multiple, b:OR, c:single, d:different, e:same + * @equivalenceClass a:multiple, b:OR, c:single, d:different, e:same, f:affirmation * @dataProvider databases * * @param Connection $db */ - public function testOrChainTargetingASingleRelationButDifferentColumnsWithTheSameOperator(Connection $db) + public function testOrChainTargetingASingleRelationButDifferentColumnsWithTheSameAffirmativeOperator(Connection $db) { $this->createOfficesAndEmployees($db); @@ -785,6 +832,58 @@ public function testOrChainTargetingASingleRelationButDifferentColumnsWithTheSam $this->assertSame(5, count($results), $sql); } + /** + * Search for offices where either Donald doesn't work or no-one as manager + * + * @equivalenceClass a:multiple, b:OR, c:single, d:different, e:same, f:negation + * @dataProvider databases + * + * @param Connection $db + */ + public function testOrChainTargetingASingleRelationButDifferentColumnsWithTheSameNegativeOperator(Connection $db) + { + $this->createOfficesAndEmployees($db); + + $offices = $db->prepexec( + 'SELECT office.city FROM office' + . ' WHERE office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e ON e.office_id = office.id' + . ' WHERE e.name = ?' + . ' GROUP BY office.id' + . ' ) OR office.id NOT IN (' + . ' SELECT office.id FROM office' + . ' LEFT JOIN employee e ON e.office_id = office.id' + . ' WHERE e.role = ?' + . ' GROUP BY office.id' + . ' )' + . ' ORDER BY office.id', + ['Donald', 'Manager'] + )->fetchAll(); + + $this->assertSame('Amsterdam', $offices[0]['city'] ?? 'not found'); + $this->assertSame('New York', $offices[1]['city'] ?? 'not found'); + $this->assertSame('Cuxhaven', $offices[2]['city'] ?? 'not found'); + $this->assertSame('Sydney', $offices[3]['city'] ?? 'not found'); + $this->assertSame(4, count($offices)); + + $offices = Office::on($db) + ->columns(['office.city']) + ->orderBy('office.id') + ->filter(Filter::any( + Filter::unequal('employee.name', 'Donald'), + Filter::unequal('employee.role', 'Manager') + )); + $results = iterator_to_array($offices); + $sql = $this->getSql($offices); + + $this->assertSame('Amsterdam', $results[0]['city'] ?? 'not found', $sql); + $this->assertSame('New York', $results[1]['city'] ?? 'not found', $sql); + $this->assertSame('Cuxhaven', $results[2]['city'] ?? 'not found', $sql); + $this->assertSame('Sydney', $results[3]['city'] ?? 'not found', $sql); + $this->assertSame(4, count($results), $sql); + } + /** * Search for offices where not just Huey works or only as a manager * @@ -836,14 +935,15 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithDiffe /** * Search for offices where not just Donald works or not as manager * - * @equivalenceClass a:multiple, b:NOT(AND), c:single, d:different, e:same + * @equivalenceClass a:multiple, b:NOT(AND), c:single, d:different, e:same, f:affirmation * @dataProvider databases * @todo Finds Cuxhaven instead of Baghdad. Not wrong, actually. Why does monitoring behave differently? * * @param Connection $db */ - public function testNotChainTargetingASingleRelationButDifferentColumnsWithTheSameOperator(Connection $db) - { + public function testNotChainTargetingASingleRelationButDifferentColumnsWithTheSameAffirmativeOperator( + Connection $db + ) { $this->createOfficesAndEmployees($db); $offices = $db->prepexec( @@ -882,6 +982,19 @@ public function testNotChainTargetingASingleRelationButDifferentColumnsWithTheSa $this->assertSame(6, count($results), $sql); } + /** + * Search for offices where ... + * + * @equivalenceClass a:multiple, b:NOT(AND), c:single, d:different, e:same, f:negation + * @dataProvider databases + * + * @param Connection $db + */ + public function testNotChainTargetingASingleRelationButDifferentColumnsWithTheSameNegativeOperator(Connection $db) + { + $this->markTestIncomplete('This test has not been implemented yet.'); + } + /** * Search for offices where Donald works in the accounting department *