diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 21643be96..1f54a095b 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -20,6 +20,7 @@ namespace Doctrine\ORM\Mapping; use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\DBAL\Types\Type; use ReflectionClass; /** @@ -746,6 +747,14 @@ class ClassMetadataInfo implements ClassMetadata $this->isIdentifierComposite = true; } } + + if (Type::hasType($mapping['type']) && Type::getType($mapping['type'])->canRequireSQLConversion()) { + if (isset($mapping['id']) && $mapping['id'] === true) { + throw MappingException::sqlConversionNotAllowedForIdentifiers($this->name, $mapping['fieldName'], $mapping['type']); + } + + $mapping['requireSQLConversion'] = true; + } } /** diff --git a/lib/Doctrine/ORM/Mapping/MappingException.php b/lib/Doctrine/ORM/Mapping/MappingException.php index e89347d42..014714bf8 100644 --- a/lib/Doctrine/ORM/Mapping/MappingException.php +++ b/lib/Doctrine/ORM/Mapping/MappingException.php @@ -226,6 +226,11 @@ class MappingException extends \Doctrine\ORM\ORMException return new self("Setting Id field '$fieldName' as versionale in entity class '$className' is not supported."); } + public static function sqlConversionNotAllowedForIdentifiers($className, $fieldName, $type) + { + return new self("It is not possible to set id field '$fieldName' to type '$type' in entity class '$className'. The type '$type' requires conversion SQL which is not allowed for identifiers."); + } + /** * @param string $className * @param string $columnName diff --git a/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php b/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php index 84540a337..e3bb9a943 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php @@ -65,6 +65,11 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister $columnAlias = $this->getSQLColumnAlias($columnName); $this->_rsm->addFieldResult($alias, $columnAlias, $field, $class->name); + if (isset($class->fieldMappings[$field]['requireSQLConversion'])) { + $type = Type::getType($class->getTypeOfField($field)); + $sql = $type->convertToPHPValueSQL($sql, $this->_platform); + } + return $sql . ' AS ' . $columnAlias; } diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 6b883ac13..92ce37ac6 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -338,10 +338,19 @@ class BasicEntityPersister $set = $params = $types = array(); foreach ($updateData as $columnName => $value) { - $set[] = (isset($this->_class->fieldNames[$columnName])) - ? $this->_class->getQuotedColumnName($this->_class->fieldNames[$columnName], $this->_platform) . ' = ?' - : $columnName . ' = ?'; + $column = $columnName; + $placeholder = '?'; + + if (isset($this->_class->fieldNames[$columnName])) { + $column = $this->_class->getQuotedColumnName($this->_class->fieldNames[$columnName], $this->_platform); + if (isset($this->_class->fieldMappings[$this->_class->fieldNames[$columnName]]['requireSQLConversion'])) { + $type = Type::getType($this->_columnTypes[$columnName]); + $placeholder = $type->convertToDatabaseValueSQL('?', $this->_platform); + } + } + + $set[] = $column . ' = ' . $placeholder; $params[] = $value; $types[] = $this->_columnTypes[$columnName]; } @@ -1121,7 +1130,19 @@ class BasicEntityPersister ); } else { $columns = array_unique($columns); - $values = array_fill(0, count($columns), '?'); + + $values = array(); + foreach ($columns AS $column) { + $placeholder = '?'; + + if (isset($this->_columnTypes[$column]) && + isset($this->_class->fieldMappings[$this->_class->fieldNames[$column]]['requireSQLConversion'])) { + $type = Type::getType($this->_columnTypes[$column]); + $placeholder = $type->convertToDatabaseValueSQL('?', $this->_platform); + } + + $values[] = $placeholder; + } $insertSql = 'INSERT INTO ' . $this->_class->getQuotedTableName($this->_platform) . ' (' . implode(', ', $columns) . ') VALUES (' . implode(', ', $values) . ')'; @@ -1160,6 +1181,7 @@ class BasicEntityPersister } } else if ($this->_class->generatorType != ClassMetadata::GENERATOR_TYPE_IDENTITY || $this->_class->identifier[0] != $name) { $columns[] = $this->_class->getQuotedColumnName($name, $this->_platform); + $this->_columnTypes[$name] = $this->_class->fieldMappings[$name]['type']; } } @@ -1182,6 +1204,11 @@ class BasicEntityPersister $this->_rsm->addFieldResult($alias, $columnAlias, $field); + if (isset($class->fieldMappings[$field]['requireSQLConversion'])) { + $type = Type::getType($class->getTypeOfField($field)); + $sql = $type->convertToPHPValueSQL($sql, $this->_platform); + } + return $sql . ' AS ' . $columnAlias; } @@ -1263,6 +1290,8 @@ class BasicEntityPersister foreach ($criteria as $field => $value) { $conditionSql .= $conditionSql ? ' AND ' : ''; + + $placeholder = '?'; if (isset($this->_class->columnNames[$field])) { $className = (isset($this->_class->fieldMappings[$field]['inherited'])) @@ -1270,6 +1299,11 @@ class BasicEntityPersister : $this->_class->name; $conditionSql .= $this->_getSQLTableAlias($className) . '.' . $this->_class->getQuotedColumnName($field, $this->_platform); + + if (isset($this->_class->fieldMappings[$field]['requireSQLConversion'])) { + $type = Type::getType($this->_class->getTypeOfField($field)); + $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->_platform); + } } else if (isset($this->_class->associationMappings[$field])) { if ( ! $this->_class->associationMappings[$field]['isOwningSide']) { throw ORMException::invalidFindByInverseAssociation($this->_class->name, $field); @@ -1290,7 +1324,7 @@ class BasicEntityPersister throw ORMException::unrecognizedField($field); } - $conditionSql .= (is_array($value)) ? ' IN (?)' : (($value === null) ? ' IS NULL' : ' = ?'); + $conditionSql .= (is_array($value)) ? ' IN (?)' : (($value === null) ? ' IS NULL' : ' = ' . $placeholder); } return $conditionSql; } diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 624f14b89..3b58dfba2 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -20,6 +20,7 @@ namespace Doctrine\ORM\Query; use Doctrine\DBAL\LockMode, + Doctrine\DBAL\Types\Type, Doctrine\ORM\Mapping\ClassMetadata, Doctrine\ORM\Query, Doctrine\ORM\Query\QueryException, @@ -1002,9 +1003,16 @@ class SqlWalker implements TreeWalker $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); $columnName = $class->getQuotedColumnName($fieldName, $this->_platform); - $columnAlias = $this->getSQLColumnAlias($columnName); + $columnAlias = $this->getSQLColumnAlias($columnName); - $sql .= $sqlTableAlias . '.' . $columnName . ' AS ' . $columnAlias; + $col = $sqlTableAlias . '.' . $columnName; + + if (isset($class->fieldMappings[$fieldName]['requireSQLConversion'])) { + $type = Type::getType($class->getTypeOfField($fieldName)); + $col = $type->convertToPHPValueSQL($col, $this->_conn->getDatabasePlatform()); + } + + $sql .= $col . ' AS ' . $columnAlias; if ( ! $hidden) { $this->_rsm->addScalarResult($columnAlias, $resultAlias); @@ -1086,7 +1094,14 @@ class SqlWalker implements TreeWalker $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); $quotedColumnName = $class->getQuotedColumnName($fieldName, $this->_platform); - $sqlParts[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS '. $columnAlias; + $col = $sqlTableAlias . '.' . $quotedColumnName; + + if (isset($class->fieldMappings[$fieldName]['requireSQLConversion'])) { + $type = Type::getType($class->getTypeOfField($fieldName)); + $col = $type->convertToPHPValueSQL($col, $this->_platform); + } + + $sqlParts[] = $col . ' AS '. $columnAlias; $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); } @@ -1108,7 +1123,14 @@ class SqlWalker implements TreeWalker $columnAlias = $this->getSQLColumnAlias($mapping['columnName']); $quotedColumnName = $subClass->getQuotedColumnName($fieldName, $this->_platform); - $sqlParts[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; + $col = $sqlTableAlias . '.' . $quotedColumnName; + + if (isset($subClass->fieldMappings[$fieldName]['requireSQLConversion'])) { + $type = Type::getType($subClass->getTypeOfField($fieldName)); + $col = $type->convertToPHPValueSQL($col, $this->_platform); + } + + $sqlParts[] = $col . ' AS ' . $columnAlias; $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); } diff --git a/tests/Doctrine/Tests/DbalTypes/NegativeToPositiveType.php b/tests/Doctrine/Tests/DbalTypes/NegativeToPositiveType.php new file mode 100644 index 000000000..ae704f8bd --- /dev/null +++ b/tests/Doctrine/Tests/DbalTypes/NegativeToPositiveType.php @@ -0,0 +1,34 @@ +getIntegerTypeDeclarationSQL($fieldDeclaration); + } + + public function canRequireSQLConversion() + { + return true; + } + + public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform) + { + return 'ABS(' . $sqlExpr . ')'; + } + + public function convertToPHPValueSQL($sqlExpr, $platform) + { + return '-(' . $sqlExpr . ')'; + } +} diff --git a/tests/Doctrine/Tests/DbalTypes/UpperCaseStringType.php b/tests/Doctrine/Tests/DbalTypes/UpperCaseStringType.php new file mode 100644 index 000000000..47e8c790d --- /dev/null +++ b/tests/Doctrine/Tests/DbalTypes/UpperCaseStringType.php @@ -0,0 +1,29 @@ +_inserts[$tableName][] = $data; } + /** + * @override + */ + public function executeUpdate($query, array $params = array(), array $types = array()) + { + $this->_executeUpdates[] = array('query' => $query, 'params' => $params, 'types' => $types); + } + /** * @override */ @@ -84,6 +93,11 @@ class ConnectionMock extends \Doctrine\DBAL\Connection return $this->_inserts; } + public function getExecuteUpdates() + { + return $this->_executeUpdates; + } + public function reset() { $this->_inserts = array(); diff --git a/tests/Doctrine/Tests/Models/CustomType/CustomTypeChild.php b/tests/Doctrine/Tests/Models/CustomType/CustomTypeChild.php new file mode 100644 index 000000000..e178ab51c --- /dev/null +++ b/tests/Doctrine/Tests/Models/CustomType/CustomTypeChild.php @@ -0,0 +1,21 @@ +friendsWithMe = new \Doctrine\Common\Collections\ArrayCollection(); + $this->myFriends = new \Doctrine\Common\Collections\ArrayCollection(); + } + + public function addMyFriend(CustomTypeParent $friend) + { + $this->getMyFriends()->add($friend); + $friend->addFriendWithMe($this); + } + + public function getMyFriends() + { + return $this->myFriends; + } + + public function addFriendWithMe(CustomTypeParent $friend) + { + $this->getFriendsWithMe()->add($friend); + } + + public function getFriendsWithMe() + { + return $this->friendsWithMe; + } +} diff --git a/tests/Doctrine/Tests/Models/CustomType/CustomTypeUpperCase.php b/tests/Doctrine/Tests/Models/CustomType/CustomTypeUpperCase.php new file mode 100644 index 000000000..26e0ec115 --- /dev/null +++ b/tests/Doctrine/Tests/Models/CustomType/CustomTypeUpperCase.php @@ -0,0 +1,21 @@ +useModelSet('customtype'); + parent::setUp(); + } + + public function testUpperCaseStringType() + { + $entity = new CustomTypeUpperCase(); + $entity->lowerCaseString = 'foo'; + + $this->_em->persist($entity); + $this->_em->flush(); + + $id = $entity->id; + + $this->_em->clear(); + + $entity = $this->_em->find('\Doctrine\Tests\Models\CustomType\CustomTypeUpperCase', $id); + + $this->assertEquals('foo', $entity->lowerCaseString, 'Entity holds lowercase string'); + $this->assertEquals('FOO', $this->_em->getConnection()->fetchColumn("select lowerCaseString from customtype_uppercases where id=".$entity->id.""), 'Database holds uppercase string'); + } + + public function testTypeValueSqlWithAssociations() + { + $parent = new CustomTypeParent(); + $parent->customInteger = -1; + $parent->child = new CustomTypeChild(); + + $friend1 = new CustomTypeParent(); + $friend2 = new CustomTypeParent(); + + $parent->addMyFriend($friend1); + $parent->addMyFriend($friend2); + + $this->_em->persist($parent); + $this->_em->persist($friend1); + $this->_em->persist($friend2); + $this->_em->flush(); + + $parentId = $parent->id; + + $this->_em->clear(); + + $entity = $this->_em->find('Doctrine\Tests\Models\CustomType\CustomTypeParent', $parentId); + + $this->assertTrue($entity->customInteger < 0, 'Fetched customInteger negative'); + $this->assertEquals(1, $this->_em->getConnection()->fetchColumn("select customInteger from customtype_parents where id=".$entity->id.""), 'Database has stored customInteger positive'); + + $this->assertNotNull($parent->child, 'Child attached'); + $this->assertCount(2, $entity->getMyFriends(), '2 friends attached'); + } + + public function testSelectDQL() + { + $parent = new CustomTypeParent(); + $parent->customInteger = -1; + $parent->child = new CustomTypeChild(); + + $this->_em->persist($parent); + $this->_em->flush(); + + $parentId = $parent->id; + + $this->_em->clear(); + + $query = $this->_em->createQuery("SELECT p, p.customInteger, c from Doctrine\Tests\Models\CustomType\CustomTypeParent p JOIN p.child c where p.id = " . $parentId); + + $result = $query->getResult(); + + $this->assertEquals(1, count($result)); + $this->assertInstanceOf('Doctrine\Tests\Models\CustomType\CustomTypeParent', $result[0][0]); + $this->assertEquals(-1, $result[0][0]->customInteger); + + $this->assertEquals(-1, $result[0]['customInteger']); + + $this->assertEquals('foo', $result[0][0]->child->lowerCaseString); + } +} diff --git a/tests/Doctrine/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php b/tests/Doctrine/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php new file mode 100644 index 000000000..04c47893d --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php @@ -0,0 +1,79 @@ +_em = $this->_getTestEntityManager(); + + $this->_persister = new BasicEntityPersister($this->_em, $this->_em->getClassMetadata("Doctrine\Tests\Models\CustomType\CustomTypeParent")); + } + + public function testGetInsertSQLUsesTypeValuesSQL() + { + $method = new \ReflectionMethod($this->_persister, '_getInsertSQL'); + $method->setAccessible(true); + + $sql = $method->invoke($this->_persister); + + $this->assertEquals('INSERT INTO customtype_parents (customInteger, child_id) VALUES (ABS(?), ?)', $sql); + } + + public function testUpdateUsesTypeValuesSQL() + { + $child = new CustomTypeChild(); + + $parent = new CustomTypeParent(); + $parent->customInteger = 1; + $parent->child = $child; + + $this->_em->getUnitOfWork()->registerManaged($parent, array('id' => 1), array('customInteger' => 0, 'child' => null)); + $this->_em->getUnitOfWork()->registerManaged($child, array('id' => 1), array()); + + $this->_em->getUnitOfWork()->propertyChanged($parent, 'customInteger', 0, 1); + $this->_em->getUnitOfWork()->propertyChanged($parent, 'child', null, $child); + + $this->_persister->update($parent); + + $executeUpdates = $this->_em->getConnection()->getExecuteUpdates(); + + $this->assertEquals('UPDATE customtype_parents SET customInteger = ABS(?), child_id = ? WHERE id = ?', $executeUpdates[0]['query']); + } + + public function testGetSelectConditionSQLUsesTypeValuesSQL() + { + $method = new \ReflectionMethod($this->_persister, '_getSelectConditionSQL'); + $method->setAccessible(true); + + $sql = $method->invoke($this->_persister, array('customInteger' => 1, 'child' => 1)); + + $this->assertEquals('t0.customInteger = ABS(?) AND t0.child_id = ?', $sql); + } +} diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index 7ddfe77b8..ceb1a3729 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -2,6 +2,7 @@ namespace Doctrine\Tests\ORM\Query; +use Doctrine\DBAL\Types\Type as DBALType; use Doctrine\ORM\Query; require_once __DIR__ . '/../../TestInit.php'; @@ -1333,6 +1334,62 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase 'SELECT c0_.id AS id0, c0_.name AS name1 FROM cms_employees c0_ GROUP BY c0_.id, c0_.name, c0_.spouse_id' ); } + + public function testCustomTypeValueSql() + { + if (DBALType::hasType('negative_to_positive')) { + DBALType::overrideType('negative_to_positive', 'Doctrine\Tests\DbalTypes\NegativeToPositiveType'); + } else { + DBALType::addType('negative_to_positive', 'Doctrine\Tests\DbalTypes\NegativeToPositiveType'); + } + + $this->assertSqlGeneration( + 'SELECT p.customInteger FROM Doctrine\Tests\Models\CustomType\CustomTypeParent p WHERE p.id = 1', + 'SELECT -(c0_.customInteger) AS customInteger0 FROM customtype_parents c0_ WHERE c0_.id = 1' + ); + } + + public function testCustomTypeValueSqlIgnoresIdentifierColumn() + { + if (DBALType::hasType('negative_to_positive')) { + DBALType::overrideType('negative_to_positive', 'Doctrine\Tests\DbalTypes\NegativeToPositiveType'); + } else { + DBALType::addType('negative_to_positive', 'Doctrine\Tests\DbalTypes\NegativeToPositiveType'); + } + + $this->assertSqlGeneration( + 'SELECT p.id FROM Doctrine\Tests\Models\CustomType\CustomTypeParent p WHERE p.id = 1', + 'SELECT c0_.id AS id0 FROM customtype_parents c0_ WHERE c0_.id = 1' + ); + } + + public function testCustomTypeValueSqlForAllFields() + { + if (DBALType::hasType('negative_to_positive')) { + DBALType::overrideType('negative_to_positive', 'Doctrine\Tests\DbalTypes\NegativeToPositiveType'); + } else { + DBALType::addType('negative_to_positive', 'Doctrine\Tests\DbalTypes\NegativeToPositiveType'); + } + + $this->assertSqlGeneration( + 'SELECT p FROM Doctrine\Tests\Models\CustomType\CustomTypeParent p', + 'SELECT c0_.id AS id0, -(c0_.customInteger) AS customInteger1 FROM customtype_parents c0_' + ); + } + + public function testCustomTypeValueSqlForPartialObject() + { + if (DBALType::hasType('negative_to_positive')) { + DBALType::overrideType('negative_to_positive', 'Doctrine\Tests\DbalTypes\NegativeToPositiveType'); + } else { + DBALType::addType('negative_to_positive', 'Doctrine\Tests\DbalTypes\NegativeToPositiveType'); + } + + $this->assertSqlGeneration( + 'SELECT partial p.{id, customInteger} FROM Doctrine\Tests\Models\CustomType\CustomTypeParent p', + 'SELECT c0_.id AS id0, -(c0_.customInteger) AS customInteger1 FROM customtype_parents c0_' + ); + } } diff --git a/tests/Doctrine/Tests/ORM/Query/UpdateSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/UpdateSqlGenerationTest.php index a8a59ff63..a65efe079 100644 --- a/tests/Doctrine/Tests/ORM/Query/UpdateSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/UpdateSqlGenerationTest.php @@ -21,6 +21,8 @@ namespace Doctrine\Tests\ORM\Query; +use Doctrine\DBAL\Types\Type as DBALType; + require_once __DIR__ . '/../../TestInit.php'; /** @@ -42,6 +44,12 @@ class UpdateSqlGenerationTest extends \Doctrine\Tests\OrmTestCase private $_em; protected function setUp() { + if (DBALType::hasType('negative_to_positive')) { + DBALType::overrideType('negative_to_positive', 'Doctrine\Tests\DbalTypes\NegativeToPositiveType'); + } else { + DBALType::addType('negative_to_positive', 'Doctrine\Tests\DbalTypes\NegativeToPositiveType'); + } + $this->_em = $this->_getTestEntityManager(); } @@ -186,4 +194,12 @@ class UpdateSqlGenerationTest extends \Doctrine\Tests\OrmTestCase "UPDATE cms_users SET status = 'inactive' WHERE (SELECT COUNT(*) FROM cms_users_groups c0_ WHERE c0_.user_id = cms_users.id) = 10" ); } + + public function testCustomTypeValueSqlCompletelyIgnoredInUpdateStatements() + { + $this->assertSqlGeneration( + 'UPDATE Doctrine\Tests\Models\CustomType\CustomTypeParent p SET p.customInteger = 1 WHERE p.id = 1', + 'UPDATE customtype_parents SET customInteger = 1 WHERE id = 1' + ); + } } diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index 0900e4e99..e0b9d7bee 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -112,6 +112,11 @@ abstract class OrmFunctionalTestCase extends OrmTestCase 'Doctrine\Tests\Models\Legacy\LegacyArticle', 'Doctrine\Tests\Models\Legacy\LegacyCar', ), + 'customtype' => array( + 'Doctrine\Tests\Models\CustomType\CustomTypeChild', + 'Doctrine\Tests\Models\CustomType\CustomTypeParent', + 'Doctrine\Tests\Models\CustomType\CustomTypeUpperCase', + ), ); protected function useModelSet($setName) @@ -219,6 +224,13 @@ abstract class OrmFunctionalTestCase extends OrmTestCase $conn->executeUpdate('DELETE FROM legacy_users'); } + if (isset($this->_usedModelSets['customtype'])) { + $conn->executeUpdate('DELETE FROM customtype_parent_friends'); + $conn->executeUpdate('DELETE FROM customtype_parents'); + $conn->executeUpdate('DELETE FROM customtype_children'); + $conn->executeUpdate('DELETE FROM customtype_uppercases'); + } + $this->_em->clear(); }