From b1d34fca1c0ea1aeb30c7dbf9fa0a934acd916e8 Mon Sep 17 00:00:00 2001 From: romanb Date: Fri, 21 Aug 2009 18:13:22 +0000 Subject: [PATCH] [2.0] Work on SchemaTool and DBAL. --- doctrine-mapping.xsd | 70 +++-- .../DBAL/Platforms/AbstractPlatform.php | 48 +--- lib/Doctrine/DBAL/Platforms/MySqlPlatform.php | 75 +----- .../DBAL/Platforms/OraclePlatform.php | 6 +- .../DBAL/Platforms/PostgreSqlPlatform.php | 5 +- .../DBAL/Schema/AbstractSchemaManager.php | 6 +- lib/Doctrine/ORM/Mapping/ClassMetadata.php | 7 +- .../ORM/Mapping/Driver/AnnotationDriver.php | 16 +- .../Mapping/Driver/DoctrineAnnotations.php | 12 +- lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 43 ++- lib/Doctrine/ORM/Tools/SchemaTool.php | 251 ++++++++++++++++-- lib/Doctrine/ORM/UnitOfWork.php | 6 +- .../DBAL/Platforms/MySqlPlatformTest.php | 2 +- tests/Doctrine/Tests/ORM/AllTests.php | 1 - .../Tests/ORM/Functional/AllTests.php | 1 + .../ORM/Functional/SchemaTool/AllTests.php | 30 +++ .../SchemaTool/MySqlSchemaToolTest.php | 98 +++++++ .../SchemaTool/PostgreSqlSchemaToolTest.php | 1 + .../Doctrine.Tests.ORM.Mapping.User.dcm.xml | 12 +- tests/Doctrine/Tests/ORM/Tools/AllTests.php | 30 --- .../Tests/ORM/Tools/SchemaToolTest.php | 29 -- 21 files changed, 510 insertions(+), 239 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/SchemaTool/AllTests.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SchemaTool/MySqlSchemaToolTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SchemaTool/PostgreSqlSchemaToolTest.php delete mode 100644 tests/Doctrine/Tests/ORM/Tools/AllTests.php delete mode 100644 tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index 3665c3193..359acd128 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -8,15 +8,15 @@ - - + + @@ -52,27 +52,28 @@ - + + + - + - - - - + + + + - @@ -122,6 +123,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -138,13 +172,13 @@ - + - + @@ -153,7 +187,7 @@ - + @@ -171,7 +205,7 @@ - + @@ -179,8 +213,8 @@ - - + + @@ -193,8 +227,8 @@ - - + + diff --git a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php index 2a2d22d48..7c10d8ed7 100644 --- a/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php @@ -362,14 +362,6 @@ abstract class AbstractPlatform * must contain a logical expression or an array with logical expressions. * These expressions will be matched against the first parameter. * - * Example: - * - * $q = new Doctrine_Query(); - * $q->select('u.*') - * ->from('User u') - * ->where($q->expr->in( 'id', array(1,2,3))); - * - * * @param string $column the value that should be matched against * @param string|array(string) values that will be matched against $column * @return string logical expression @@ -390,14 +382,6 @@ abstract class AbstractPlatform /** * Returns SQL that checks if a expression is null. * - * Example: - * - * $q = new Doctrine_Query(); - * $q->select('u.*') - * ->from('User u') - * ->where($q->expr->isNull('id')); - * - * * @param string $expression the expression that should be compared to null * @return string logical expression */ @@ -409,14 +393,6 @@ abstract class AbstractPlatform /** * Returns SQL that checks if a expression is not null. * - * Example: - * - * $q = new Doctrine_Query(); - * $q->select('u.*') - * ->from('User u') - * ->where($q->expr->isNotNull('id')); - * - * * @param string $expression the expression that should be compared to null * @return string logical expression */ @@ -435,14 +411,6 @@ abstract class AbstractPlatform * http://www.w3schools.com/sql/sql_between.asp. If you want complete database * independence you should avoid using between(). * - * Example: - * - * $q = new Doctrine_Query(); - * $q->select('u.*') - * ->from('User u') - * ->where($q->expr->between('id', 1, 5)); - * - * * @param string $expression the value to compare to * @param string $value1 the lower value to compare with * @param string $value2 the higher value to compare with @@ -504,18 +472,24 @@ abstract class AbstractPlatform } /** - * Gets the SQL statement(s) to create a table with the specified name, columns and options + * Gets the SQL statement(s) to create a table with the specified name, columns and constraints * on this platform. * - * @param string $table - * @param array $columns - * @param array $options - * @return array + * @param string $table The name of the table. + * @param array $columns The column definitions for the table. + * @param array $options The table constraints. + * @return array The sequence of SQL statements. */ public function getCreateTableSql($table, array $columns, array $options = array()) { $columnListSql = $this->getColumnDeclarationListSql($columns); + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $uniqueConstraint) { + $columnListSql .= ', UNIQUE(' . implode(', ', array_values($uniqueConstraint)) . ')'; + } + } + if (isset($options['primary']) && ! empty($options['primary'])) { $columnListSql .= ', PRIMARY KEY(' . implode(', ', array_unique(array_values($options['primary']))) . ')'; } diff --git a/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php b/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php index f8fda0e49..a793bc152 100644 --- a/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php @@ -227,7 +227,7 @@ class MySqlPlatform extends AbstractPlatform $fixed = (isset($field['fixed'])) ? $field['fixed'] : false; return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)') - : ($length ? 'VARCHAR(' . $length . ')' : 'TEXT'); + : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'); } public function getClobDeclarationSql(array $field) @@ -408,7 +408,7 @@ class MySqlPlatform extends AbstractPlatform $queryFields = $this->getColumnDeclarationListSql($fields); // build indexes for all foreign key fields (needed in MySQL!!) - if (isset($options['foreignKeys'])) { + /*if (isset($options['foreignKeys'])) { foreach ($options['foreignKeys'] as $fk) { $local = $fk['local']; $found = false; @@ -433,6 +433,12 @@ class MySqlPlatform extends AbstractPlatform $options['indexes'][$local] = array('fields' => array($local => array())); } } + }*/ + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $uniqueConstraint) { + $queryFields .= ', UNIQUE(' . implode(', ', array_values($uniqueConstraint)) . ')'; + } } // add all indexes @@ -471,9 +477,7 @@ class MySqlPlatform extends AbstractPlatform // get the type of the table if (isset($options['type'])) { $type = $options['type']; - }/* else { - $type = $this->getAttribute(Doctrine::ATTR_DEFAULT_TABLE_TYPE); - }*/ + } if ($type) { $optionStrings[] = 'ENGINE = ' . $type; @@ -589,6 +593,7 @@ class MySqlPlatform extends AbstractPlatform if ( ! $name) { throw DoctrineException::updateMe('no valid table name specified'); } + foreach ($changes as $changeName => $change) { switch ($changeName) { case 'add': @@ -670,61 +675,6 @@ class MySqlPlatform extends AbstractPlatform return 'ALTER TABLE ' . $name . ' ' . $query; } - /** - * Get the stucture of a field into an array - * - * @author Leoncx - * @param string $table name of the table on which the index is to be created - * @param string $name name of the index to be created - * @param array $definition associative array that defines properties of the index to be created. - * Currently, only one property named FIELDS is supported. This property - * is also an associative with the names of the index fields as array - * indexes. Each entry of this array is set to another type of associative - * array that specifies properties of the index that are specific to - * each field. - * - * Currently, only the sorting property is supported. It should be used - * to define the sorting direction of the index. It may be set to either - * ascending or descending. - * - * Not all DBMS support index sorting direction configuration. The DBMS - * drivers of those that do not support it ignore this property. Use the - * function supports() to determine whether the DBMS driver can manage indexes. - * - * Example - * array( - * 'fields' => array( - * 'user_name' => array( - * 'sorting' => 'ASC' - * 'length' => 10 - * ), - * 'last_login' => array() - * ) - * ) - * @throws PDOException - * @return void - * @override - */ - public function getCreateIndexSql($table, $name, array $definition) - { - $table = $table; - $type = ''; - if (isset($definition['type'])) { - switch (strtolower($definition['type'])) { - case 'fulltext': - case 'unique': - $type = strtoupper($definition['type']) . ' '; - break; - default: - throw DoctrineException::updateMe('Unknown index type ' . $definition['type']); - } - } - $query = 'CREATE ' . $type . 'INDEX ' . $name . ' ON ' . $table; - $query .= ' (' . $this->getIndexFieldDeclarationListSql($definition['fields']) . ')'; - - return $query; - } - /** * Obtain DBMS specific SQL code portion needed to declare an integer type * field to be used in statements like CREATE TABLE. @@ -791,7 +741,7 @@ class MySqlPlatform extends AbstractPlatform */ public function getIndexDeclarationSql($name, array $definition) { - $type = ''; + $type = ''; if (isset($definition['type'])) { switch (strtolower($definition['type'])) { case 'fulltext': @@ -818,7 +768,6 @@ class MySqlPlatform extends AbstractPlatform } /** - * getIndexFieldDeclarationList * Obtain DBMS specific SQL code portion needed to set an index * declaration to be used in statements like CREATE TABLE. * @@ -908,7 +857,7 @@ class MySqlPlatform extends AbstractPlatform } /** - * Get the platform name for this instance + * Get the platform name for this instance. * * @return string */ diff --git a/lib/Doctrine/DBAL/Platforms/OraclePlatform.php b/lib/Doctrine/DBAL/Platforms/OraclePlatform.php index 31406f707..f74c38976 100644 --- a/lib/Doctrine/DBAL/Platforms/OraclePlatform.php +++ b/lib/Doctrine/DBAL/Platforms/OraclePlatform.php @@ -241,7 +241,7 @@ class OraclePlatform extends AbstractPlatform public function getCreateTableSql($table, array $columns, array $options = array()) { - $indexes = isset($options['indexes']) ? $options['indexes']:array(); + $indexes = isset($options['indexes']) ? $options['indexes'] : array(); $options['indexes'] = array(); $sql = parent::getCreateTableSql($table, $columns, $options); @@ -258,9 +258,9 @@ class OraclePlatform extends AbstractPlatform if (isset($indexes) && ! empty($indexes)) { foreach ($indexes as $indexName => $definition) { - // create nonunique indexes, as they are a part od CREATE TABLE DDL + // create nonunique indexes, as they are a part of CREATE TABLE DDL if ( ! isset($definition['type']) || - (isset($definition['type']) && strtolower($definition['type']) != 'unique')) { + (isset($definition['type']) && strtolower($definition['type']) != 'unique')) { $sql[] = $this->getCreateIndexSql($table, $indexName, $definition); } } diff --git a/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php b/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php index bc97dc9a9..860f949e1 100644 --- a/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php +++ b/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php @@ -616,11 +616,8 @@ class PostgreSqlPlatform extends AbstractPlatform } if (isset($options['foreignKeys'])) { - foreach ((array) $options['foreignKeys'] as $k => $definition) { - if (is_array($definition)) { - $sql[] = $this->getCreateForeignKeySql($name, $definition); - } + $sql[] = $this->getCreateForeignKeySql($name, $definition); } } diff --git a/lib/Doctrine/DBAL/Schema/AbstractSchemaManager.php b/lib/Doctrine/DBAL/Schema/AbstractSchemaManager.php index 618337248..6b5998c1d 100644 --- a/lib/Doctrine/DBAL/Schema/AbstractSchemaManager.php +++ b/lib/Doctrine/DBAL/Schema/AbstractSchemaManager.php @@ -166,10 +166,10 @@ abstract class AbstractSchemaManager } /** - * List the columns for a given table + * List the columns for a given table. * - * @param string $table The name of the table - * @return array $tableColumns + * @param string $table The name of the table. + * @return array $tableColumns The column descriptions. */ public function listTableColumns($table) { diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadata.php b/lib/Doctrine/ORM/Mapping/ClassMetadata.php index 38102e475..cbc8bf629 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadata.php @@ -233,11 +233,6 @@ final class ClassMetadata * The value specifies the name of the index. To create a multi-column index, * just use the same name for several mappings. * - * - unique (string, optional, schema-only) - * Whether a unique constraint should be generated for the column. - * The value specifies the name of the unique constraint. To create a multi-column - * unique constraint, just use the same name for several mappings. - * * - foreignKey (string, optional, schema-only) * * @var array @@ -316,6 +311,8 @@ final class ClassMetadata * * name => * schema => + * indexes => array + * uniqueConstraints => array * * @var array */ diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 682d1b4f0..d962bd495 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -73,10 +73,21 @@ class AnnotationDriver implements Driver // Evaluate DoctrineTable annotation if (isset($classAnnotations['Doctrine\ORM\Mapping\Table'])) { $tableAnnot = $classAnnotations['Doctrine\ORM\Mapping\Table']; - $metadata->setPrimaryTable(array( + $primaryTable = array( 'name' => $tableAnnot->name, 'schema' => $tableAnnot->schema - )); + ); + if ($tableAnnot->indexes !== null) { + foreach ($tableAnnot->indexes as $indexAnnot) { + $primaryTable['indexes'][$indexAnnot->name] = array('fields' => $indexAnnot->columns); + } + } + if ($tableAnnot->uniqueConstraints !== null) { + foreach ($tableAnnot->uniqueConstraints as $uniqueConstraint) { + $primaryTable['uniqueConstraints'][] = $uniqueConstraint->columns; + } + } + $metadata->setPrimaryTable($primaryTable); } // Evaluate InheritanceType annotation @@ -135,7 +146,6 @@ class AnnotationDriver implements Driver ); } else if ($joinColumnsAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinColumns')) { foreach ($joinColumnsAnnot->value as $joinColumn) { - //$joinColumns = $joinColumnsAnnot->value; $joinColumns[] = array( 'name' => $joinColumn->name, 'referencedColumnName' => $joinColumn->referencedColumnName, diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index 499a9cc23..c6c838bcb 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -21,7 +21,7 @@ namespace Doctrine\ORM\Mapping; -use \Doctrine\Common\Annotations\Annotation; +use Doctrine\Common\Annotations\Annotation; /* Annotations */ @@ -94,6 +94,16 @@ final class ElementCollection extends Annotation { final class Table extends Annotation { public $name; public $schema; + public $indexes; + public $uniqueConstraints; +} +final class UniqueConstraint extends Annotation { + public $name; + public $columns; +} +final class Index extends Annotation { + public $name; + public $columns; } final class JoinTable extends Annotation { public $name; diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index ae78c814a..6e26b8ae7 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -55,6 +55,21 @@ class XmlDriver extends AbstractFileDriver if (isset($xmlRoot['inheritance-type'])) { $metadata->setInheritanceType((string)$xmlRoot['inheritance-type']); } + + // Evaluate + if (isset($xmlRoot->indexes)) { + foreach ($xmlRoot->indexes->index as $index) { + $metadata->primaryTable['indexes'][$index['name']] = array('fields' => + explode(',', $index['columns'])); + } + } + + // Evaluate + if (isset($xmlRoot->{'unique-constraints'})) { + foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} as $unique) { + $metadata->primaryTable['uniqueConstraints'][] = explode(',', $index['columns']); + } + } // Evaluate mappings if (isset($xmlRoot->field)) { @@ -69,6 +84,12 @@ class XmlDriver extends AbstractFileDriver if (isset($fieldMapping['length'])) { $mapping['length'] = (int)$fieldMapping['length']; } + if (isset($fieldMapping['precision'])) { + $mapping['precision'] = (int)$fieldMapping['precision']; + } + if (isset($fieldMapping['scale'])) { + $mapping['scale'] = (int)$fieldMapping['scale']; + } $metadata->mapField($mapping); } } @@ -96,10 +117,10 @@ class XmlDriver extends AbstractFileDriver foreach ($xmlRoot->{'one-to-one'} as $oneToOneElement) { $mapping = array( 'fieldName' => (string)$oneToOneElement['field'], - 'targetEntity' => (string)$oneToOneElement['targetEntity'] + 'targetEntity' => (string)$oneToOneElement['target-entity'] ); - if (isset($oneToOneElement['mappedBy'])) { - $mapping['mappedBy'] = (string)$oneToOneElement['mappedBy']; + if (isset($oneToOneElement['mapped-by'])) { + $mapping['mappedBy'] = (string)$oneToOneElement['mapped-by']; } else { $joinColumns = array(); if (isset($oneToOneElement->{'join-column'})) { @@ -127,8 +148,8 @@ class XmlDriver extends AbstractFileDriver foreach ($xmlRoot->{'one-to-many'} as $oneToManyElement) { $mapping = array( 'fieldName' => (string)$oneToManyElement['field'], - 'targetEntity' => (string)$oneToManyElement['targetEntity'], - 'mappedBy' => (string)$oneToManyElement['mappedBy'] + 'targetEntity' => (string)$oneToManyElement['target-entity'], + 'mappedBy' => (string)$oneToManyElement['mapped-by'] ); if (isset($oneToManyElement->cascade)) { $mapping['cascade'] = $this->_getCascadeMappings($oneToManyElement->cascade); @@ -142,7 +163,7 @@ class XmlDriver extends AbstractFileDriver foreach ($xmlRoot->{'many-to-one'} as $manyToOneElement) { $mapping = array( 'fieldName' => (string)$manyToOneElement['field'], - 'targetEntity' => (string)$manyToOneElement['targetEntity'] + 'targetEntity' => (string)$manyToOneElement['target-entity'] ); $joinColumns = array(); if (isset($manyToOneElement->{'join-column'})) { @@ -167,11 +188,11 @@ class XmlDriver extends AbstractFileDriver foreach ($xmlRoot->{'many-to-many'} as $manyToManyElement) { $mapping = array( 'fieldName' => (string)$manyToManyElement['field'], - 'targetEntity' => (string)$manyToManyElement['targetEntity'] + 'targetEntity' => (string)$manyToManyElement['target-entity'] ); if (isset($manyToManyElement['mappedBy'])) { - $mapping['mappedBy'] = (string)$manyToManyElement['mappedBy']; + $mapping['mappedBy'] = (string)$manyToManyElement['mapped-by']; } else if (isset($manyToManyElement->{'join-table'})) { $joinTableElement = $manyToManyElement->{'join-table'}; $joinTable = array( @@ -242,7 +263,7 @@ class XmlDriver extends AbstractFileDriver { $joinColumn = array( 'name' => (string)$joinColumnElement['name'], - 'referencedColumnName' => (string)$joinColumnElement['referencedColumnName'] + 'referencedColumnName' => (string)$joinColumnElement['referenced-column-name'] ); if (isset($joinColumnElement['unique'])) { $joinColumn['unique'] = (bool)$joinColumnElement['unique']; @@ -251,10 +272,10 @@ class XmlDriver extends AbstractFileDriver $joinColumn['nullable'] = (bool)$joinColumnElement['nullable']; } if (isset($joinColumnElement['onDelete'])) { - $joinColumn['onDelete'] = (string)$joinColumnElement['onDelete']; + $joinColumn['onDelete'] = (string)$joinColumnElement['on-delete']; } if (isset($joinColumnElement['onUpdate'])) { - $joinColumn['onUpdate'] = (string)$joinColumnElement['onUpdate']; + $joinColumn['onUpdate'] = (string)$joinColumnElement['on-update']; } return $joinColumn; diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index d6554554a..ebe49f48b 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -28,8 +28,6 @@ use Doctrine\DBAL\Types\Type, * The SchemaTool is a tool to create and/or drop database schemas based on * ClassMetadata class descriptors. * - * @author Konsta Vesterinen - * @author Lukas Smith (PEAR MDB2 library) * @author Roman Borschel * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @link www.doctrine-project.org @@ -70,10 +68,11 @@ class SchemaTool } /** - * Gets an array of DDL statements for the specified array of ClassMetadata instances. + * Gets the list of DDL statements that are required to create the database schema for + * the given list of ClassMetadata instances. * * @param array $classes - * @return array $sql + * @return array $sql The SQL statements needed to create the schema for the classes. */ public function getCreateSchemaSql(array $classes) { @@ -140,11 +139,18 @@ class SchemaTool $foreignKeyConstraints[] = $constraint; } } else if ($class->isInheritanceTypeTablePerClass()) { - //TODO + throw DoctrineException::notSupported(); } else { $columns = $this->_gatherColumns($class, $options); $this->_gatherRelationsSql($class, $sql, $columns, $foreignKeyConstraints); } + + if (isset($class->primaryTable['indexes'])) { + $options['indexes'] = $class->primaryTable['indexes']; + } + if (isset($class->primaryTable['uniqueConstraints'])) { + $options['uniqueConstraints'] = $class->primaryTable['uniqueConstraints']; + } $sql = array_merge($sql, $this->_platform->getCreateTableSql( $class->getQuotedTableName($this->_platform), $columns, $options)); @@ -175,6 +181,14 @@ class SchemaTool return $sql; } + /** + * Gets a portable column definition as required by the DBAL for the discriminator + * column of a class. + * + * @param ClassMetadata $class + * @return array The portable column definition of the discriminator column as required by + * the DBAL. + */ private function _getDiscriminatorColumnDefinition($class) { $discrColumn = $class->discriminatorColumn; @@ -187,11 +201,13 @@ class SchemaTool } /** - * Gathers the column definitions of all field mappings found in the given class. + * Gathers the column definitions as required by the DBAL of all field mappings + * found in the given class. * * @param ClassMetadata $class - * @param array $options - * @return array + * @param array $options The table options/constraints where any additional options/constraints + * that are required by columns should be appended. + * @return array The list of portable column definitions as required by the DBAL. */ private function _gatherColumns($class, array &$options) { @@ -203,14 +219,23 @@ class SchemaTool return $columns; } - + + /** + * Creates a column definition as required by the DBAL from an ORM field mapping definition. + * + * @param ClassMetadata $class The class that owns the field mapping. + * @param array $mapping The field mapping. + * @param array $options The table options/constraints where any additional options/constraints + * required by the column should be appended. + * @return array The portable column definition as required by the DBAL. + */ private function _gatherColumn($class, array $mapping, array &$options) { $column = array(); $column['name'] = $class->getQuotedColumnName($mapping['fieldName'], $this->_platform); $column['type'] = Type::getType($mapping['type']); - $column['length'] = $mapping['length']; - $column['notnull'] = ! $mapping['nullable']; + $column['length'] = isset($mapping['length']) ? $mapping['length'] : null; + $column['notnull'] = isset($mapping['nullable']) ? ! $mapping['nullable'] : false; if (isset($mapping['default'])) { $column['default'] = $mapping['default']; } @@ -225,6 +250,18 @@ class SchemaTool return $column; } + /** + * Gathers the SQL for properly setting up the relations of the given class. + * This includes the SQL for foreign key constraints and join tables. + * + * @param ClassMetadata $class + * @param array $sql The sequence of SQL statements where any new statements should be appended. + * @param array $columns The list of columns in the class's primary table where any additional + * columns required by relations should be appended. + * @param array $constraints The constraints of the table where any additional constraints + * required by relations should be appended. + * @return void + */ private function _gatherRelationsSql($class, array &$sql, array &$columns, array &$constraints) { foreach ($class->associationMappings as $fieldName => $mapping) { @@ -246,16 +283,24 @@ class SchemaTool $columns[$column['name']] = $column; $constraint['local'][] = $column['name']; $constraint['foreign'][] = $joinColumn['referencedColumnName']; + if (isset($joinColumn['onUpdate'])) { + $constraint['onUpdate'] = $joinColumn['onUpdate']; + } + if (isset($joinColumn['onDelete'])) { + $constraint['onDelete'] = $joinColumn['onDelete']; + } } $constraints[] = $constraint; } else if ($mapping->isOneToMany() && $mapping->isOwningSide) { //... create join table, one-many through join table supported later - throw DoctrineException::updateMe("Not yet implemented."); + throw DoctrineException::notSupported(); } else if ($mapping->isManyToMany() && $mapping->isOwningSide) { // create join table $joinTableColumns = array(); $joinTableOptions = array(); $joinTable = $mapping->getJoinTable(); + + // Build first FK constraint (relation table => source table) $constraint1 = array( 'tableName' => $mapping->getQuotedJoinTableName($this->_platform), 'foreignTable' => $class->getQuotedTableName($this->_platform), @@ -271,9 +316,16 @@ class SchemaTool $joinTableColumns[$column['name']] = $column; $constraint1['local'][] = $column['name']; $constraint1['foreign'][] = $joinColumn['referencedColumnName']; + if (isset($joinColumn['onUpdate'])) { + $constraint1['onUpdate'] = $joinColumn['onUpdate']; + } + if (isset($joinColumn['onDelete'])) { + $constraint1['onDelete'] = $joinColumn['onDelete']; + } } $constraints[] = $constraint1; - + + // Build second FK constraint (relation table => target table) $constraint2 = array(); $constraint2['tableName'] = $mapping->getQuotedJoinTableName($this->_platform); $constraint2['foreignTable'] = $foreignClass->getQuotedTableName($this->_platform); @@ -289,33 +341,192 @@ class SchemaTool $joinTableColumns[$inverseJoinColumn['name']] = $column; $constraint2['local'][] = $inverseJoinColumn['name']; $constraint2['foreign'][] = $inverseJoinColumn['referencedColumnName']; + if (isset($inverseJoinColumn['onUpdate'])) { + $constraint2['onUpdate'] = $inverseJoinColumn['onUpdate']; + } + if (isset($joinColumn['onDelete'])) { + $constraint2['onDelete'] = $inverseJoinColumn['onDelete']; + } } $constraints[] = $constraint2; - + + // Get the SQL for creating the join table and merge it with the others $sql = array_merge($sql, $this->_platform->getCreateTableSql( $mapping->getQuotedJoinTableName($this->_platform), $joinTableColumns, $joinTableOptions) ); } } } - + + /** + * Drops the database schema for the given classes. + * + * @param array $classes + * @return void + */ public function dropSchema(array $classes) { - //TODO + $dropSchemaSql = $this->getDropSchemaSql($classes); + $conn = $this->_em->getConnection(); + foreach ($dropSchemaSql as $sql) { + $conn->execute($sql); + } } - + + /** + * Gets the SQL needed to drop the database schema for the given classes. + * + * @param array $classes + * @return array + */ public function getDropSchemaSql(array $classes) { - //TODO + $sql = array(); + $commitOrder = $classes; //FIXME: get real commit order!! + + // Drop tables in reverse commit order + for ($i = count($commitOrder) - 1; $i >= 0; --$i) { + $class = $commitOrder[$i]; + if ($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName) { + continue; + } + $sql[] = $this->_platform->getDropTableSql($class->getTableName()); + } + + return $sql; } + /** + * Updates the database schema of the given classes by comparing the ClassMetadata + * instances to the current database schema that is inspected. + * + * @param array $classes + * @return void + */ public function updateSchema(array $classes) { - //TODO + $updateSchemaSql = $this->getUpdateSchemaSql($classes); + $conn = $this->_em->getConnection(); + foreach ($updateSchemaSql as $sql) { + $conn->execute($sql); + } } + /** + * Gets the sequence of SQL statements that need to be performed in order + * to bring the given class mappings in-synch with the relational schema. + * + * @param array $classes The classes to consider. + * @return array The sequence of SQL statements. + */ public function getUpdateSchemaSql(array $classes) { - //TODO + $sql = array(); + $conn = $this->_em->getConnection(); + $sm = $conn->getSchemaManager(); + + $tables = $sm->listTables(); + $newClasses = array(); + + foreach ($classes as $class) { + $tableName = $class->getTableName(); + $tableExists = false; + foreach ($tables as $index => $table) { + if ($tableName == $table) { + $tableExists = true; + unset($tables[$index]); + break; + } + } + if ( ! $tableExists) { + $newClasses[] = $class; + } else { + $newFields = array(); + $newJoinColumns = array(); + $currentColumns = $sm->listTableColumns($tableName); + + foreach ($class->fieldMappings as $fieldMapping) { + $exists = false; + foreach ($currentColumns as $index => $column) { + if ($column['name'] == $fieldMapping['columnName']) { + // Column exists, check for changes + + // 1. check for nullability change + // 2. check for uniqueness change + // 3. check for length change if type string + // 4. check for type change + + unset($currentColumns[$index]); + $exists = true; + break; + } + } + if ( ! $exists) { + $newFields[] = $fieldMapping; + } + } + + foreach ($class->associationMappings as $assoc) { + if ($assoc->isOwningSide && $assoc->isOneToOne()) { + foreach ($assoc->targetToSourceKeyColumns as $targetColumn => $sourceColumn) { + $exists = false; + foreach ($currentColumns as $index => $column) { + if ($column['name'] == $sourceColumn) { + // Column exists, check for changes + + // 1. check for nullability change + + unset($currentColumns[$index]); + $exists = true; + break; + } + } + if ( ! $exists) { + $newJoinColumns[$sourceColumn] = array( + 'name' => $sourceColumn, + 'type' => 'integer' //FIXME!!! + ); + } + } + } + } + + if ($newFields || $newJoinColumns) { + $changes = array(); + foreach ($newFields as $newField) { + $options = array(); + $changes['add'][$newField['columnName']] = $this->_gatherColumn($class, $newField, $options); + } + foreach ($newJoinColumns as $name => $joinColumn) { + $changes['add'][$name] = $joinColumn; + } + $sql[] = $this->_platform->getAlterTableSql($tableName, $changes); + } + + // Drop any remaining columns + if ($currentColumns) { + $changes = array(); + foreach ($currentColumns as $column) { + $options = array(); + $changes['remove'][$column['name']] = $column; + } + $sql[] = $this->_platform->getAlterTableSql($tableName, $changes); + } + } + } + + if ($newClasses) { + $sql = array_merge($this->getCreateSchemaSql($newClasses), $sql); + } + + // Drop any remaining tables (Probably not a good idea, because the given class list + // may not be complete!) + /*if ($tables) { + foreach ($tables as $table) { + $sql[] = $this->_platform->getDropTableSql($table); + } + }*/ + + return $sql; } } diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 5fd7c9ca4..94549cc9c 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -25,9 +25,7 @@ use Doctrine\Common\Collections\ArrayCollection, Doctrine\Common\Collections\Collection, Doctrine\Common\DoctrineException, Doctrine\Common\PropertyChangedListener, - Doctrine\ORM\Event\LifecycleEventArgs, - Doctrine\ORM\Internal\CommitOrderCalculator, - Doctrine\ORM\Internal\CommitOrderNode; + Doctrine\ORM\Event\LifecycleEventArgs; /** * The UnitOfWork is responsible for tracking changes to objects during an @@ -233,7 +231,7 @@ class UnitOfWork implements PropertyChangedListener { $this->_em = $em; $this->_evm = $em->getEventManager(); - $this->_commitOrderCalculator = new CommitOrderCalculator(); + $this->_commitOrderCalculator = new Internal\CommitOrderCalculator(); $this->_useCExtension = $this->_em->getConfiguration()->getUseCExtension(); } diff --git a/tests/Doctrine/Tests/DBAL/Platforms/MySqlPlatformTest.php b/tests/Doctrine/Tests/DBAL/Platforms/MySqlPlatformTest.php index b8a0db2d1..43a3a0eb8 100644 --- a/tests/Doctrine/Tests/DBAL/Platforms/MySqlPlatformTest.php +++ b/tests/Doctrine/Tests/DBAL/Platforms/MySqlPlatformTest.php @@ -126,7 +126,7 @@ class MySqlPlatformTest extends \Doctrine\Tests\DbalTestCase 'Variable string declaration is not correct' ); $this->assertEquals( - 'TEXT', + 'VARCHAR(255)', $this->_platform->getVarcharTypeDeclarationSql(array()), 'Long string declaration is not correct' ); diff --git a/tests/Doctrine/Tests/ORM/AllTests.php b/tests/Doctrine/Tests/ORM/AllTests.php index e6458cfd3..2deb14bb4 100644 --- a/tests/Doctrine/Tests/ORM/AllTests.php +++ b/tests/Doctrine/Tests/ORM/AllTests.php @@ -26,7 +26,6 @@ class AllTests $suite->addTest(Query\AllTests::suite()); $suite->addTest(Hydration\AllTests::suite()); $suite->addTest(Entity\AllTests::suite()); - $suite->addTest(Tools\AllTests::suite()); $suite->addTest(Associations\AllTests::suite()); $suite->addTest(Mapping\AllTests::suite()); $suite->addTest(Functional\AllTests::suite()); diff --git a/tests/Doctrine/Tests/ORM/Functional/AllTests.php b/tests/Doctrine/Tests/ORM/Functional/AllTests.php index 22a8129d1..7adcf7bc1 100644 --- a/tests/Doctrine/Tests/ORM/Functional/AllTests.php +++ b/tests/Doctrine/Tests/ORM/Functional/AllTests.php @@ -42,6 +42,7 @@ class AllTests $suite->addTestSuite('Doctrine\Tests\ORM\Functional\EntityRepositoryTest'); $suite->addTest(Locking\AllTests::suite()); + $suite->addTest(SchemaTool\AllTests::suite()); return $suite; } diff --git a/tests/Doctrine/Tests/ORM/Functional/SchemaTool/AllTests.php b/tests/Doctrine/Tests/ORM/Functional/SchemaTool/AllTests.php new file mode 100644 index 000000000..e2ff24c91 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SchemaTool/AllTests.php @@ -0,0 +1,30 @@ +addTestSuite('Doctrine\Tests\ORM\Functional\SchemaTool\MySqlSchemaToolTest'); + + return $suite; + } +} + +if (PHPUnit_MAIN_METHOD == 'Orm_Functional_Tools_AllTests::main') { + AllTests::main(); +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SchemaTool/MySqlSchemaToolTest.php b/tests/Doctrine/Tests/ORM/Functional/SchemaTool/MySqlSchemaToolTest.php new file mode 100644 index 000000000..d9c341ca6 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SchemaTool/MySqlSchemaToolTest.php @@ -0,0 +1,98 @@ +_em->getConnection()->getDatabasePlatform()->getName() !== 'mysql') { + $this->markTestSkipped('The ' . __CLASS__ .' requires the use of mysql.'); + } + } + + public function testGetCreateSchemaSql() + { + $classes = array( + $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress'), + $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'), + $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsPhonenumber'), + ); + + $tool = new SchemaTool($this->_em); + $sql = $tool->getCreateSchemaSql($classes); + $this->assertEquals(count($sql), 8); + $this->assertEquals("CREATE TABLE cms_addresses (id INT AUTO_INCREMENT NOT NULL, country VARCHAR(50) NOT NULL, zip VARCHAR(50) NOT NULL, city VARCHAR(50) NOT NULL, user_id INT DEFAULT NULL, PRIMARY KEY(id))", $sql[0]); + $this->assertEquals("CREATE TABLE cms_users_groups (user_id INT DEFAULT NULL, group_id INT DEFAULT NULL, PRIMARY KEY(user_id, group_id))", $sql[1]); + $this->assertEquals("CREATE TABLE cms_users (id INT AUTO_INCREMENT NOT NULL, status VARCHAR(50) NOT NULL, username VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))", $sql[2]); + $this->assertEquals("CREATE TABLE cms_phonenumbers (phonenumber VARCHAR(50) NOT NULL, user_id INT DEFAULT NULL, PRIMARY KEY(phonenumber))", $sql[3]); + $this->assertEquals("ALTER TABLE cms_addresses ADD FOREIGN KEY (user_id) REFERENCES cms_users(id)", $sql[4]); + $this->assertEquals("ALTER TABLE cms_users_groups ADD FOREIGN KEY (user_id) REFERENCES cms_users(id)", $sql[5]); + $this->assertEquals("ALTER TABLE cms_users_groups ADD FOREIGN KEY (group_id) REFERENCES cms_groups(id)", $sql[6]); + $this->assertEquals("ALTER TABLE cms_phonenumbers ADD FOREIGN KEY (user_id) REFERENCES cms_users(id)", $sql[7]); + } + + public function testGetUpdateSchemaSql() + { + $classes = array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\SchemaToolEntityA') + ); + + $tool = new SchemaTool($this->_em); + + $tool->createSchema($classes); + + // Add field to SchemaToolEntityA + $classA = $classes[0]; + $classA->mapField(array( + 'fieldName' => 'newField', + 'columnName' => 'new_field', + 'type' => 'string', + 'length' => 50, + 'nullable' => false + )); + + // Introduce SchemaToolEntityB + $classB = new ClassMetadata(__NAMESPACE__ . '\SchemaToolEntityB'); + $classB->setTableName('schematool_entity_b'); + $classB->mapField(array( + 'fieldName' => 'id', + 'columnName' => 'id', + 'type' => 'integer', + 'nullable' => false, + 'id' => true + )); + $classB->mapField(array( + 'fieldName' => 'field', + 'columnName' => 'field', + 'type' => 'string', + 'nullable' => false + )); + $classes[] = $classB; + + $sql = $tool->getUpdateSchemaSql($classes); + + $this->assertEquals(2, count($sql)); + $this->assertEquals("CREATE TABLE schematool_entity_b (id INT NOT NULL, field VARCHAR(255) NOT NULL, PRIMARY KEY(id))", $sql[0]); + $this->assertEquals("ALTER TABLE schematool_entity_a ADD new_field VARCHAR(50) NOT NULL", $sql[1]); + + } +} + +/** @Entity @Table(name="schematool_entity_a") */ +class SchemaToolEntityA { + /** @Id @Column(type="integer") */ + private $id; + private $newField; +} + +class SchemaToolEntityB { + private $id; + private $field; +} + diff --git a/tests/Doctrine/Tests/ORM/Functional/SchemaTool/PostgreSqlSchemaToolTest.php b/tests/Doctrine/Tests/ORM/Functional/SchemaTool/PostgreSqlSchemaToolTest.php new file mode 100644 index 000000000..b3d9bbc7f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SchemaTool/PostgreSqlSchemaToolTest.php @@ -0,0 +1 @@ + - - + + - + - + - + - + diff --git a/tests/Doctrine/Tests/ORM/Tools/AllTests.php b/tests/Doctrine/Tests/ORM/Tools/AllTests.php deleted file mode 100644 index 3e83ef963..000000000 --- a/tests/Doctrine/Tests/ORM/Tools/AllTests.php +++ /dev/null @@ -1,30 +0,0 @@ -addTestSuite('Doctrine\Tests\ORM\Tools\SchemaToolTest'); - - return $suite; - } -} - -if (PHPUnit_MAIN_METHOD == 'Orm_Tools_AllTests::main') { - AllTests::main(); -} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php b/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php deleted file mode 100644 index 9c939823b..000000000 --- a/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php +++ /dev/null @@ -1,29 +0,0 @@ -setDatabasePlatform(new \Doctrine\DBAL\Platforms\MySqlPlatform()); - - $em = $this->_getTestEntityManager($conn); - - $classes = array( - $em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress'), - $em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'), - $em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsPhonenumber'), - ); - - $exporter = new SchemaTool($em); - $sql = $exporter->getCreateSchemaSql($classes); - $this->assertEquals(count($sql), 8); - } -} \ No newline at end of file