diff --git a/CHANGELOG.md b/CHANGELOG.md index 48a7be35c0..866562f23e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,58 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 2.9.2 - TBD + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 2.9.1 - 2017-12-07 + +### Added + +- Nothing. + +### Changed + +- [#289](https://github.com/zendframework/zend-db/pull/289) reverts a change + introduced in 2.9.0 and modifies the behavior of the PDO adapter slightly + to remove a regression. In 2.9.0, when binding parameters with names that + contained characters not supported by PDO, we would pass the parameter names + to `md5()`; this caused a regression, as the SQL string containing the + parameter name was not also updated. + + This patch modifies the behavior during a bind-operation to instead raise an + exception if a parameter name contains characters not supported by PDO. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + ## 2.9.0 - 2017-12-06 ### Added diff --git a/src/Adapter/Driver/Pdo/Pdo.php b/src/Adapter/Driver/Pdo/Pdo.php index ddc9ad67ec..9c57701f57 100644 --- a/src/Adapter/Driver/Pdo/Pdo.php +++ b/src/Adapter/Driver/Pdo/Pdo.php @@ -304,8 +304,16 @@ public function getPrepareType() public function formatParameterName($name, $type = null) { if ($type === null && ! is_numeric($name) || $type == self::PARAMETERIZATION_NAMED) { - // using MD5 because of the PDO restriction [A-Za-z0-9_] for bindParam name - return ':' . md5($name); + // @see https://bugs.php.net/bug.php?id=43130 + if (preg_match('/[^a-zA-Z0-9_]/', $name)) { + throw new Exception\RuntimeException(sprintf( + 'The PDO param %s contains invalid characters.' + . ' Only alphabetic characters, digits, and underscores (_)' + . ' are allowed.', + $name + )); + } + return ':' . $name; } return '?'; diff --git a/src/Sql/Ddl/Index/Index.php b/src/Sql/Ddl/Index/Index.php index a0239089a4..704fab0a14 100644 --- a/src/Sql/Ddl/Index/Index.php +++ b/src/Sql/Ddl/Index/Index.php @@ -26,7 +26,7 @@ class Index extends AbstractIndex * @param null|string $name * @param array $lengths */ - public function __construct($columns, $name = null, array $lengths = []) + public function __construct($columns = null, $name = null, array $lengths = []) { $this->setColumns($columns); diff --git a/src/Sql/Platform/Platform.php b/src/Sql/Platform/Platform.php index e702caac46..0aaab84907 100644 --- a/src/Sql/Platform/Platform.php +++ b/src/Sql/Platform/Platform.php @@ -32,17 +32,19 @@ public function __construct(AdapterInterface $adapter) { $this->defaultPlatform = $adapter->getPlatform(); - $mySqlPlatform = new Mysql\Mysql(); - $sqlServerPlatform = new SqlServer\SqlServer(); - $oraclePlatform = new Oracle\Oracle(); - $ibmDb2Platform = new IbmDb2\IbmDb2(); - $sqlitePlatform = new Sqlite\Sqlite(); - - $this->decorators['mysql'] = $mySqlPlatform->getDecorators(); - $this->decorators['sqlserver'] = $sqlServerPlatform->getDecorators(); - $this->decorators['oracle'] = $oraclePlatform->getDecorators(); - $this->decorators['ibmdb2'] = $ibmDb2Platform->getDecorators(); - $this->decorators['sqlite'] = $sqlitePlatform->getDecorators(); + $mySqlPlatform = new Mysql\Mysql(); + $sqlServerPlatform = new SqlServer\SqlServer(); + $oraclePlatform = new Oracle\Oracle(); + $ibmDb2Platform = new IbmDb2\IbmDb2(); + $sqlitePlatform = new Sqlite\Sqlite(); + $postgresqlPlatform = new Postgresql\Postgresql(); + + $this->decorators['mysql'] = $mySqlPlatform->getDecorators(); + $this->decorators['sqlserver'] = $sqlServerPlatform->getDecorators(); + $this->decorators['oracle'] = $oraclePlatform->getDecorators(); + $this->decorators['ibmdb2'] = $ibmDb2Platform->getDecorators(); + $this->decorators['sqlite'] = $sqlitePlatform->getDecorators(); + $this->decorators['postgresql'] = $postgresqlPlatform->getDecorators(); } /** diff --git a/src/Sql/Platform/Postgresql/Ddl/AlterTableDecorator.php b/src/Sql/Platform/Postgresql/Ddl/AlterTableDecorator.php new file mode 100644 index 0000000000..c02a2c1967 --- /dev/null +++ b/src/Sql/Platform/Postgresql/Ddl/AlterTableDecorator.php @@ -0,0 +1,195 @@ + [ + [1 => "DROP CONSTRAINT IF EXISTS %1\$s,\n", 'combinedby' => ""], + ] + ]; + + protected $indexSpecification = [ + 'statementEnd' => '%1$s', + self::ADD_INDEXES => [ + "%1\$s" => [ + [1 => '%1$s;', 'combinedby' => "\n"] + ] + ], + self::DROP_INDEXES => [ + "%1\$s" => [ + [1 => 'DROP INDEX IF EXISTS %1$s;', 'combinedby' => "\n"] + ] + ] + ]; + + /** + * @inheritDoc + */ + public function setSubject($subject) + { + $this->subject = $subject; + + $this->specifications[self::DROP_CONSTRAINTS] = $this->dropConstraintSpecification; + $this->subject->specifications = $this->specifications; + + return $this; + } + + /** + * @inheritDoc + */ + protected function buildSqlString( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + $this->separateIndexesFromConstraints(); + $this->duplicateDropConstraintToDropIndex(); + $this->deleteUnneededSpecification(); + + // unlike CreateTableDecorator where CREATE TABLE is always present for new tables, regardless of Incex creation + // PostgreSQL does not use ALTER TABLE to add/drop indexes to existing tables. + // Therefore, if the only change is index related, DDL would have dangling ALTER TABLE. + // Consequently, table alterations outside of ALTER TABLE syntax get processed as whole different specification chunk + $alterTable = ''; + if ($this->hasCreateTable) { + $alterTable = parent::buildSqlString($platform, $driver, $parameterContainer); + $this->subject->specifications = $this->specifications = $this->indexSpecification; + } + + $indexes = parent::buildSqlString($platform, $driver, $parameterContainer); + return $alterTable.$indexes; + } + + private function separateIndexesFromConstraints() + { + // take advantage of PHP's ability to access protected properties of different instances created from same class + $this->addIndexes = array_filter($this->subject->addConstraints, function ($constraint) { + return $constraint instanceof Index; + }); + + $filteredConstraints = array_filter($this->subject->addConstraints, function ($constraint) { + return !($constraint instanceof Index); + }); + + $this->subject->addConstraints = $filteredConstraints; + + array_walk($this->addIndexes, function (&$index, $key) { + $indexDecorator = new IndexDecorator(); + $indexDecorator->setSubject($index); + $indexDecorator->setTable($this->subject->table); + $index = $indexDecorator; + }); + } + + /** + * DROP CONSTRAINT always with DROP INDEX to compensate for dropConstraint() interface + * only accepting strings, not inspectable objects. + * @TODO if new signature removeConstraint(string|AbstractConstraint) gets approved, delete this method + */ + private function duplicateDropConstraintToDropIndex() + { + $this->dropIndexes = $this->subject->dropConstraints; + } + + /** + * @param PlatformInterface|null $adapterPlatform + * @return array|void + */ + protected function processAddIndexes(PlatformInterface $adapterPlatform = null) + { + if (!$this->addIndexes) { + return; + } + + $sqls = []; + + foreach ($this->addIndexes as $index) { + $sqls[] = $this->processExpression($index, $adapterPlatform); + } + + return [$sqls]; + } + + protected function processDropIndexes(PlatformInterface $adapterPlatform = null) + { + if (!$this->dropIndexes) { + return; + } + + $sqls = []; + + foreach ($this->dropIndexes as $index) { + $sqls[] = $adapterPlatform->quoteIdentifier($index); + } + + return [$sqls]; + } + + /** + * @param PlatformInterface|null $adapterPlatform + * @return array|void + */ + protected function processStatementEnd(PlatformInterface $adapterPlatform = null) + { + return [";\n"]; + } + + private function deleteUnneededSpecification() + { + $subject = $this->subject; + if (!($subject->addColumns || $subject->changeColumns || $subject->dropColumns + || $subject->addConstraints || $subject->dropConstraints)) { + $this->hasCreateTable = false; + + unset($this->indexSpecification['statementEnd']); + $this->subject->specifications = $this->specifications = $this->indexSpecification; + } + } +} diff --git a/src/Sql/Platform/Postgresql/Ddl/CreateTableDecorator.php b/src/Sql/Platform/Postgresql/Ddl/CreateTableDecorator.php new file mode 100644 index 0000000000..b626c42148 --- /dev/null +++ b/src/Sql/Platform/Postgresql/Ddl/CreateTableDecorator.php @@ -0,0 +1,120 @@ + [ + "\n%1\$s" => [ + [1 => '%1$s;', 'combinedby' => "\n"] + ] + ] + ]; + + /** + * @param $subject + * @return mixed + */ + public function setSubject($subject) + { + $this->subject = $subject; + + $this->specifications = array_merge($this->specifications, $this->indexSpecification); + $this->subject->specifications = $this->specifications; + + return $this; + } + + /** + * @inheritDoc + */ + protected function buildSqlString( + PlatformInterface $platform, + DriverInterface $driver = null, + ParameterContainer $parameterContainer = null + ) { + $this->separateIndexesFromConstraints(); + + return parent::buildSqlString($platform, $driver, $parameterContainer); + } + + private function separateIndexesFromConstraints() + { + // take advantage of PHP's ability to access protected properties of different instances created from same class + $this->indexes = array_filter($this->subject->constraints, function ($constraint) { + return $constraint instanceof Index; + }); + + $filteredConstraints = array_filter($this->subject->constraints, function ($constraint) { + return !($constraint instanceof Index); + }); + + $this->subject->constraints = $filteredConstraints; + + array_walk($this->indexes, function (&$index, $key) { + $indexDecorator = new IndexDecorator(); + $indexDecorator->setSubject($index); + $indexDecorator->setTable($this->subject->table); + $index = $indexDecorator; + }); + } + + /** + * @param PlatformInterface|null $adapterPlatform + * @return array|void + */ + protected function processIndexes(PlatformInterface $adapterPlatform = null) + { + if (!$this->indexes) { + return; + } + + $sqls = []; + + foreach ($this->indexes as $index) { + $sqls[] = $this->processExpression($index, $adapterPlatform); + } + + return [$sqls]; + } + + /** + * @param PlatformInterface|null $adapterPlatform + * @return array|void + */ + protected function processStatementEnd(PlatformInterface $adapterPlatform = null) + { + return ["\n);"]; + } +} diff --git a/src/Sql/Platform/Postgresql/Ddl/Index/IndexDecorator.php b/src/Sql/Platform/Postgresql/Ddl/Index/IndexDecorator.php new file mode 100644 index 0000000000..ec0e0428aa --- /dev/null +++ b/src/Sql/Platform/Postgresql/Ddl/Index/IndexDecorator.php @@ -0,0 +1,61 @@ +subject = $subject; + $this->subject->specification = $this->specification; + } + + public function setTable($table) + { + $this->table = $table; + } + + /** + * @inheritDoc + */ + public function getExpressionData() + { + if (!$this->table) { + throw new InvalidQueryException('PostgreSQL Index needs table name specified.'); + } + + $expressionData = $this->subject->getExpressionData(); + + // [0] => specification + // [1] => values + // [2] => types + array_splice($expressionData[0][1], 1, 0, $this->table); + array_splice($expressionData[0][2], 1, 0, self::TYPE_IDENTIFIER); + + return $expressionData; + } +} diff --git a/src/Sql/Platform/Postgresql/Postgresql.php b/src/Sql/Platform/Postgresql/Postgresql.php new file mode 100644 index 0000000000..83b114a496 --- /dev/null +++ b/src/Sql/Platform/Postgresql/Postgresql.php @@ -0,0 +1,21 @@ +setTypeDecorator('Zend\Db\Sql\Ddl\CreateTable', new Ddl\CreateTableDecorator()); + $this->setTypeDecorator('Zend\Db\Sql\Ddl\AlterTable', new Ddl\AlterTableDecorator()); + } +} diff --git a/test/Adapter/Driver/Pdo/PdoTest.php b/test/Adapter/Driver/Pdo/PdoTest.php index 6ac716e143..ae6b58bcc6 100644 --- a/test/Adapter/Driver/Pdo/PdoTest.php +++ b/test/Adapter/Driver/Pdo/PdoTest.php @@ -43,16 +43,16 @@ public function testGetDatabasePlatformName() public function getParamsAndType() { return [ - [ 'foo', null, ':' . md5('foo')], - [ 'foo-', null, ':' . md5('foo-')], - [ 'foo$', null, ':' . md5('foo$')], + [ 'foo', null, ':foo' ], + [ 'foo_bar', null, ':foo_bar' ], + [ '123foo', null, ':123foo' ], [ 1, null, '?' ], - [ '1', null, '?'], - [ 'foo', Pdo::PARAMETERIZATION_NAMED, ':' . md5('foo')], - [ 'foo-', Pdo::PARAMETERIZATION_NAMED, ':' . md5('foo-')], - [ 'foo$', Pdo::PARAMETERIZATION_NAMED, ':' . md5('foo$')], - [ 1, Pdo::PARAMETERIZATION_NAMED, ':' . md5('1')], - [ '1', Pdo::PARAMETERIZATION_NAMED, ':' . md5('1')], + [ '1', null, '?' ], + [ 'foo', Pdo::PARAMETERIZATION_NAMED, ':foo' ], + [ 'foo_bar', Pdo::PARAMETERIZATION_NAMED, ':foo_bar' ], + [ '123foo', Pdo::PARAMETERIZATION_NAMED, ':123foo' ], + [ 1, Pdo::PARAMETERIZATION_NAMED, ':1' ], + [ '1', Pdo::PARAMETERIZATION_NAMED, ':1' ], ]; } @@ -64,4 +64,23 @@ public function testFormatParameterName($name, $type, $expected) $result = $this->pdo->formatParameterName($name, $type); $this->assertEquals($expected, $result); } + + public function getInvalidParamName() + { + return [ + [ 'foo%' ], + [ 'foo-' ], + [ 'foo$' ], + [ 'foo0!' ] + ]; + } + + /** + * @dataProvider getInvalidParamName + * @expectedException Zend\Db\Exception\RuntimeException + */ + public function testFormatParameterNameWithInvalidCharacters($name) + { + $this->pdo->formatParameterName($name); + } } diff --git a/test/Adapter/Driver/Pdo/StatementIntegrationTest.php b/test/Adapter/Driver/Pdo/StatementIntegrationTest.php index 871f43b96e..a25f12867b 100644 --- a/test/Adapter/Driver/Pdo/StatementIntegrationTest.php +++ b/test/Adapter/Driver/Pdo/StatementIntegrationTest.php @@ -55,7 +55,7 @@ protected function tearDown() public function testStatementExecuteWillConvertPhpBoolToPdoBoolWhenBinding() { $this->pdoStatementMock->expects($this->any())->method('bindParam')->with( - $this->equalTo(':' . md5('foo')), + $this->equalTo(':foo'), $this->equalTo(false), $this->equalTo(\PDO::PARAM_BOOL) ); @@ -65,7 +65,7 @@ public function testStatementExecuteWillConvertPhpBoolToPdoBoolWhenBinding() public function testStatementExecuteWillUsePdoStrByDefaultWhenBinding() { $this->pdoStatementMock->expects($this->any())->method('bindParam')->with( - $this->equalTo(':' . md5('foo')), + $this->equalTo(':foo'), $this->equalTo('bar'), $this->equalTo(\PDO::PARAM_STR) ); diff --git a/test/Adapter/Driver/Pdo/StatementTest.php b/test/Adapter/Driver/Pdo/StatementTest.php index b36294bff7..d103b494cd 100644 --- a/test/Adapter/Driver/Pdo/StatementTest.php +++ b/test/Adapter/Driver/Pdo/StatementTest.php @@ -128,28 +128,4 @@ public function testExecute() $this->statement->prepare('SELECT 1'); self::assertInstanceOf('Zend\Db\Adapter\Driver\Pdo\Result', $this->statement->execute()); } - - /** - * @see https://github.com/zendframework/zend-db/pull/224 - */ - public function testExecuteWithSpecialCharInBindParam() - { - $testSqlite = new TestAsset\SqliteMemoryPdo('CREATE TABLE test (text_ TEXT, text$ TEXT);'); - $this->statement->setDriver(new Pdo(new Connection($testSqlite))); - $this->statement->initialize($testSqlite); - - $this->statement->prepare(sprintf( - 'INSERT INTO test (text_, text$) VALUES (:%s, :%s)', - md5('text_'), - md5('text$') - )); - $result = $this->statement->execute([ 'text_' => 'foo', 'text$' => 'bar']); - $this->assertInstanceOf(Result::class, $result); - $this->assertTrue($result->valid()); - - $result = $testSqlite->query('SELECT * FROM test'); - $values = $result->fetch(); - $this->assertEquals('foo', $values['text_']); - $this->assertEquals('bar', $values['text$']); - } } diff --git a/test/Sql/Platform/PlatformTest.php b/test/Sql/Platform/PlatformTest.php index 574918ee0b..9259425d21 100644 --- a/test/Sql/Platform/PlatformTest.php +++ b/test/Sql/Platform/PlatformTest.php @@ -13,8 +13,9 @@ use ReflectionMethod; use Zend\Db\Adapter\Adapter; use Zend\Db\Adapter\StatementContainer; -use Zend\Db\Sql\Platform\Platform; +use Zend\Db\Sql\Platform\Postgresql\Postgresql; use ZendTest\Db\TestAsset; +use Zend\Db\Sql\Platform\Platform; class PlatformTest extends TestCase { @@ -38,13 +39,26 @@ public function testResolvePlatformName() $reflectionMethod->setAccessible(true); - self::assertEquals('mysql', $reflectionMethod->invoke($platform, new TestAsset\TrustingMysqlPlatform())); + self::assertEquals('mysql', $reflectionMethod->invoke( + $platform, + new TestAsset\TrustingMysqlPlatform() + )); self::assertEquals('sqlserver', $reflectionMethod->invoke( $platform, new TestAsset\TrustingSqlServerPlatform() )); - self::assertEquals('oracle', $reflectionMethod->invoke($platform, new TestAsset\TrustingOraclePlatform())); - self::assertEquals('sql92', $reflectionMethod->invoke($platform, new TestAsset\TrustingSql92Platform())); + self::assertEquals('oracle', $reflectionMethod->invoke( + $platform, + new TestAsset\TrustingOraclePlatform() + )); + self::assertEquals('sql92', $reflectionMethod->invoke( + $platform, + new TestAsset\TrustingSql92Platform() + )); + $this->assertEquals('postgresql', $reflectionMethod->invoke( + $platform, + new TestAsset\TrustingPostgresqlPlatform() + )); } /** @@ -89,6 +103,17 @@ public function testAbstractPlatformCrashesGracefullyOnMissingDefaultPlatformWit $platform->getDecorators(); } + /** + * @dataProvider availablePlatformDecorators + */ + public function testDecoratorsRegistered($adapter, $decorators) + { + $platform = new Platform($adapter); + $registeredDecorators = $platform->getDecorators(); + + $this->assertEquals($registeredDecorators, $decorators); + } + /** * @param string $platformName * @@ -111,6 +136,8 @@ protected function resolveAdapter($platformName) case 'SqlServer': $platform = new TestAsset\TrustingSqlServerPlatform(); break; + case 'PostgreSQL' : + $platform = new TestAsset\TrustingPostgresqlPlatform(); } /* @var $mockDriver \Zend\Db\Adapter\Driver\DriverInterface|\PHPUnit_Framework_MockObject_MockObject */ @@ -123,4 +150,12 @@ protected function resolveAdapter($platformName) return new Adapter($mockDriver, $platform); } + + public function availablePlatformDecorators() + { + return [ + //@TODO add all supported platforms + [$this->resolveAdapter('PostgreSQL'), (new Postgresql())->getDecorators()], + ]; + } } diff --git a/test/Sql/Platform/Postgresql/Ddl/AlterTableDecoratorTest.php b/test/Sql/Platform/Postgresql/Ddl/AlterTableDecoratorTest.php new file mode 100644 index 0000000000..c42fa852a1 --- /dev/null +++ b/test/Sql/Platform/Postgresql/Ddl/AlterTableDecoratorTest.php @@ -0,0 +1,81 @@ +setSubject($createTable); + + $adapterPlatform = new Postgresql(); + $createTableSql = $this->trimExtraIndents($alterTableDecorator->getSqlString($adapterPlatform)); + $this->assertEquals($expectedSql, $createTableSql); + } + + public function tableAlterationsProvider() + { + $newIdx = new Ddl\Index\Index('field_1', 'new_idx'); + $newField_2 = new Ddl\Column\Varchar('field_2'); + $newField_2->setLength(1024); + + // AlterTable on its own + $noIndex = new Ddl\AlterTable('no_index'); + $noIndex->addColumn($newField_2); + + $expectedNewFieldNoIndex = 'ALTER TABLE "no_index"' . "\n" + . 'ADD COLUMN "field_2" VARCHAR(1024) NOT NULL;'; + + // AlterTable on its own + $onlyIndex = new Ddl\AlterTable('only_index'); + $onlyIndex->addConstraint($newIdx); + + $expectedOnlyIndex = 'CREATE INDEX "new_idx" ON "only_index"("field_1");'; + + // AlterTable with Create Index + $mixedAddIndex = new Ddl\AlterTable('mixed_index'); + $mixedAddIndex->addColumn($newField_2); + $mixedAddIndex->addConstraint($newIdx); + + $expectedMixedAddIndex = 'ALTER TABLE "mixed_index"' . "\n" + . 'ADD COLUMN "field_2" VARCHAR(1024) NOT NULL;' . "\n" + . 'CREATE INDEX "new_idx" ON "mixed_index"("field_1");'; + + // Drop Index + // DROP CONSTRAINT always with DROP INDEX to compensate for dropConstraint() + // interface only accepting strings, not inspectable object. + $dropIndex = new Ddl\AlterTable('drop_index'); + $dropIndex->dropConstraint('new_idx'); + + $expectedOnlyDropIndex = 'ALTER TABLE "drop_index"' . "\n" + . 'DROP CONSTRAINT IF EXISTS "new_idx";' . "\n" + . 'DROP INDEX IF EXISTS "new_idx";'; + + return [ + [$noIndex, $expectedNewFieldNoIndex], + [$onlyIndex, $expectedOnlyIndex], + [$mixedAddIndex, $expectedMixedAddIndex], + [$dropIndex, $expectedOnlyDropIndex] + ]; + } + + private function trimExtraIndents($sqlString) + { + return implode("\n", array_map("trim", explode("\n", $sqlString))); + } +} diff --git a/test/Sql/Platform/Postgresql/Ddl/CreateTableDecoratorTest.php b/test/Sql/Platform/Postgresql/Ddl/CreateTableDecoratorTest.php new file mode 100644 index 0000000000..695e599d12 --- /dev/null +++ b/test/Sql/Platform/Postgresql/Ddl/CreateTableDecoratorTest.php @@ -0,0 +1,90 @@ +setSubject($createTable); + + $createTableSql = $createTableDecorator->getSqlString(new Postgresql()); + $this->assertEquals($expectedSql, $createTableSql); + } + + public function tableDefinitionsProvider() + { + $id = new Ddl\Column\Integer('id', false, null); + $name = new Ddl\Column\Varchar('username', false, null); + $name->setLength(1024); + $nameUnique = new Ddl\Constraint\UniqueKey('username'); + + $idIndex = new Ddl\Index\Index('id', 'id_idx'); + $nameIndex = new Ddl\Index\Index('username', 'username_index'); + + // CREATE TABLE only. + $columnsOnly = new CreateTable('columns_only'); + $columnsOnly->addColumn($id); + $columnsOnly->addColumn($name); + + $expectedColumnsOnly = 'CREATE TABLE "columns_only" ( ' . "\n" + . ' "id" INTEGER NOT NULL,' . "\n" + . ' "username" VARCHAR(1024) NOT NULL ' . "\n" + . ');'; + + // CreateTable with Create Index + $withSingleIndex = new CreateTable('with_single_index'); + $withSingleIndex->addColumn($id); + $withSingleIndex->addColumn($name); + $withSingleIndex->addConstraint($idIndex); + + $expectedWithSingleInstance = 'CREATE TABLE "with_single_index" ( ' . "\n" + . ' "id" INTEGER NOT NULL,' . "\n" + . ' "username" VARCHAR(1024) NOT NULL ' . "\n" + . '); ' . "\n" + . 'CREATE INDEX "id_idx" ON "with_single_index"("id");'; + + + // Mixed handling of index separation from constraints. + $mixed = new CreateTable('mixed'); + $mixed->addColumn($id); + $mixed->addColumn($name); + $mixed->addConstraint($nameUnique); + $mixed->addConstraint($idIndex); + $mixed->addConstraint($nameIndex); + + $expectedMixed = 'CREATE TABLE "mixed" ( ' . "\n" + . ' "id" INTEGER NOT NULL,' . "\n" + . ' "username" VARCHAR(1024) NOT NULL , ' . "\n" + . ' UNIQUE ("username") ' . "\n" + . '); ' . "\n" + . 'CREATE INDEX "id_idx" ON "mixed"("id");' . "\n" + . 'CREATE INDEX "username_index" ON "mixed"("username");'; + + return [ + [$columnsOnly, $expectedColumnsOnly], + [$withSingleIndex, $expectedWithSingleInstance], + [$mixed, $expectedMixed], + ]; + } +} diff --git a/test/Sql/Platform/Postgresql/Ddl/Index/IndexDecoratorTest.php b/test/Sql/Platform/Postgresql/Ddl/Index/IndexDecoratorTest.php new file mode 100644 index 0000000000..fc156298f1 --- /dev/null +++ b/test/Sql/Platform/Postgresql/Ddl/Index/IndexDecoratorTest.php @@ -0,0 +1,52 @@ +setName('test_index'); + $index->setColumns(['test_column_one', 'test_column_two']); + + $postgresIndex = new IndexDecorator(); + $postgresIndex->setSubject($index); + $postgresIndex->setTable('test_table'); // PostgreSQL must have table name to operate on, unlike other engines + + $expressionData = $postgresIndex->getExpressionData()[0]; + + // [0] => specification + // [1] => values + // [2] => types + $this->assertEquals('CREATE INDEX %s ON %s(%s, %s)', $expressionData[0]); + $this->assertEquals(['test_index', 'test_table', 'test_column_one', 'test_column_two'], $expressionData[1]); + $this->assertEquals( + [Index::TYPE_IDENTIFIER, Index::TYPE_IDENTIFIER, Index::TYPE_IDENTIFIER, Index::TYPE_IDENTIFIER], + $expressionData[2] + ); + } + + public function testExceptionThrownIfNoTableSpecified() + { + $postgresIndex = new IndexDecorator(); + + $this->setExpectedException(InvalidQueryException::class); + $postgresIndex->getExpressionData(); + } +} diff --git a/test/Sql/Platform/Postgresql/PostgresqlTest.php b/test/Sql/Platform/Postgresql/PostgresqlTest.php new file mode 100644 index 0000000000..ac81400ae7 --- /dev/null +++ b/test/Sql/Platform/Postgresql/PostgresqlTest.php @@ -0,0 +1,31 @@ +getDecorators(); + + $this->assertArrayHasKey('Zend\Db\Sql\Ddl\CreateTable', $decorators); + $this->assertInstanceOf(Ddl\CreateTableDecorator::class, $decorators['Zend\Db\Sql\Ddl\CreateTable']); + $this->assertArrayHasKey('Zend\Db\Sql\Ddl\AlterTable', $decorators); + $this->assertInstanceOf(Ddl\AlterTableDecorator::class, $decorators['Zend\Db\Sql\Ddl\AlterTable']); + } +} diff --git a/test/TestAsset/TrustingPostgresqlPlatform.php b/test/TestAsset/TrustingPostgresqlPlatform.php new file mode 100644 index 0000000000..ba452da2f6 --- /dev/null +++ b/test/TestAsset/TrustingPostgresqlPlatform.php @@ -0,0 +1,20 @@ +quoteTrustedValue($value); + } +} \ No newline at end of file