diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b691d74..7486b763 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,8 +33,15 @@ jobs: ports: - 5432:5432 env: + POSTGRES_USER: postgres POSTGRES_PASSWORD: pg-password + PGPASSWORD: pg-password POSTGRES_DB: cakephp_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - uses: actions/checkout@v4 @@ -49,9 +56,11 @@ jobs: - name: Setup Postgres if: matrix.db-type == 'pgsql' + env: + PGUSER: postgres + PGPASSWORD: pg-password run: | - export PGPASSWORD='pg-password' - psql -h 127.0.0.1 -U postgres -c 'CREATE DATABASE "cakephp_snapshot";' + psql -h 127.0.0.1 -c 'CREATE DATABASE "cakephp_snapshot";' - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/src/Command/BakeSeedCommand.php b/src/Command/BakeSeedCommand.php index 67ccd871..b3f64af2 100644 --- a/src/Command/BakeSeedCommand.php +++ b/src/Command/BakeSeedCommand.php @@ -150,9 +150,11 @@ public function templateData(Arguments $arguments): array */ public function bake(string $name, Arguments $args, ConsoleIo $io): void { + /** @var array $options */ + $options = array_merge($args->getOptions(), ['no-test' => true]); $newArgs = new Arguments( $args->getArguments(), - ['no-test' => true] + $args->getOptions(), + $options, ['name'] ); $this->_name = $name; diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php new file mode 100644 index 00000000..187f0b2f --- /dev/null +++ b/src/Db/Table/Column.php @@ -0,0 +1,803 @@ +null = FeatureFlags::$columnNullDefault; + } + + /** + * Sets the column name. + * + * @param string $name Name + * @return $this + */ + public function setName(string $name) + { + $this->name = $name; + + return $this; + } + + /** + * Gets the column name. + * + * @return string|null + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * Sets the column type. + * + * @param string|\Phinx\Util\Literal $type Column type + * @return $this + */ + public function setType(string|Literal $type) + { + $this->type = $type; + + return $this; + } + + /** + * Gets the column type. + * + * @return string|\Phinx\Util\Literal + */ + public function getType(): string|Literal + { + return $this->type; + } + + /** + * Sets the column limit. + * + * @param int|null $limit Limit + * @return $this + */ + public function setLimit(?int $limit) + { + $this->limit = $limit; + + return $this; + } + + /** + * Gets the column limit. + * + * @return int|null + */ + public function getLimit(): ?int + { + return $this->limit; + } + + /** + * Sets whether the column allows nulls. + * + * @param bool $null Null + * @return $this + */ + public function setNull(bool $null) + { + $this->null = $null; + + return $this; + } + + /** + * Gets whether the column allows nulls. + * + * @return bool + */ + public function getNull(): bool + { + return $this->null; + } + + /** + * Does the column allow nulls? + * + * @return bool + */ + public function isNull(): bool + { + return $this->getNull(); + } + + /** + * Sets the default column value. + * + * @param mixed $default Default + * @return $this + */ + public function setDefault(mixed $default) + { + $this->default = $default; + + return $this; + } + + /** + * Gets the default column value. + * + * @return mixed + */ + public function getDefault(): mixed + { + return $this->default; + } + + /** + * Sets generated option for identity columns. Ignored otherwise. + * + * @param string|null $generated Generated option + * @return $this + */ + public function setGenerated(?string $generated) + { + $this->generated = $generated; + + return $this; + } + + /** + * Gets generated option for identity columns. Null otherwise + * + * @return string|null + */ + public function getGenerated(): ?string + { + return $this->generated; + } + + /** + * Sets whether or not the column is an identity column. + * + * @param bool $identity Identity + * @return $this + */ + public function setIdentity(bool $identity) + { + $this->identity = $identity; + + return $this; + } + + /** + * Gets whether or not the column is an identity column. + * + * @return bool + */ + public function getIdentity(): bool + { + return $this->identity; + } + + /** + * Is the column an identity column? + * + * @return bool + */ + public function isIdentity(): bool + { + return $this->getIdentity(); + } + + /** + * Sets the name of the column to add this column after. + * + * @param string $after After + * @return $this + */ + public function setAfter(string $after) + { + $this->after = $after; + + return $this; + } + + /** + * Returns the name of the column to add this column after. + * + * @return string|null + */ + public function getAfter(): ?string + { + return $this->after; + } + + /** + * Sets the 'ON UPDATE' mysql column function. + * + * @param string $update On Update function + * @return $this + */ + public function setUpdate(string $update) + { + $this->update = $update; + + return $this; + } + + /** + * Returns the value of the ON UPDATE column function. + * + * @return string|null + */ + public function getUpdate(): ?string + { + return $this->update; + } + + /** + * Sets the number precision for decimal or float column. + * + * For example `DECIMAL(5,2)`, 5 is the precision and 2 is the scale, + * and the column could store value from -999.99 to 999.99. + * + * @param int|null $precision Number precision + * @return $this + */ + public function setPrecision(?int $precision) + { + $this->setLimit($precision); + + return $this; + } + + /** + * Gets the number precision for decimal or float column. + * + * For example `DECIMAL(5,2)`, 5 is the precision and 2 is the scale, + * and the column could store value from -999.99 to 999.99. + * + * @return int|null + */ + public function getPrecision(): ?int + { + return $this->limit; + } + + /** + * Sets the column identity increment. + * + * @param int $increment Number increment + * @return $this + */ + public function setIncrement(int $increment) + { + $this->increment = $increment; + + return $this; + } + + /** + * Gets the column identity increment. + * + * @return int|null + */ + public function getIncrement(): ?int + { + return $this->increment; + } + + /** + * Sets the column identity seed. + * + * @param int $seed Number seed + * @return $this + */ + public function setSeed(int $seed) + { + $this->seed = $seed; + + return $this; + } + + /** + * Gets the column identity seed. + * + * @return int + */ + public function getSeed(): ?int + { + return $this->seed; + } + + /** + * Sets the number scale for decimal or float column. + * + * For example `DECIMAL(5,2)`, 5 is the precision and 2 is the scale, + * and the column could store value from -999.99 to 999.99. + * + * @param int|null $scale Number scale + * @return $this + */ + public function setScale(?int $scale) + { + $this->scale = $scale; + + return $this; + } + + /** + * Gets the number scale for decimal or float column. + * + * For example `DECIMAL(5,2)`, 5 is the precision and 2 is the scale, + * and the column could store value from -999.99 to 999.99. + * + * @return int + */ + public function getScale(): ?int + { + return $this->scale; + } + + /** + * Sets the number precision and scale for decimal or float column. + * + * For example `DECIMAL(5,2)`, 5 is the precision and 2 is the scale, + * and the column could store value from -999.99 to 999.99. + * + * @param int $precision Number precision + * @param int $scale Number scale + * @return $this + */ + public function setPrecisionAndScale(int $precision, int $scale) + { + $this->setLimit($precision); + $this->scale = $scale; + + return $this; + } + + /** + * Sets the column comment. + * + * @param string|null $comment Comment + * @return $this + */ + public function setComment(?string $comment) + { + $this->comment = $comment; + + return $this; + } + + /** + * Gets the column comment. + * + * @return string + */ + public function getComment(): ?string + { + return $this->comment; + } + + /** + * Sets whether field should be signed. + * + * @param bool $signed Signed + * @return $this + */ + public function setSigned(bool $signed) + { + $this->signed = $signed; + + return $this; + } + + /** + * Gets whether field should be signed. + * + * @return bool + */ + public function getSigned(): bool + { + return $this->signed; + } + + /** + * Should the column be signed? + * + * @return bool + */ + public function isSigned(): bool + { + return $this->getSigned(); + } + + /** + * Sets whether the field should have a timezone identifier. + * Used for date/time columns only! + * + * @param bool $timezone Timezone + * @return $this + */ + public function setTimezone(bool $timezone) + { + $this->timezone = $timezone; + + return $this; + } + + /** + * Gets whether field has a timezone identifier. + * + * @return bool + */ + public function getTimezone(): bool + { + return $this->timezone; + } + + /** + * Should the column have a timezone? + * + * @return bool + */ + public function isTimezone(): bool + { + return $this->getTimezone(); + } + + /** + * Sets field properties. + * + * @param array $properties Properties + * @return $this + */ + public function setProperties(array $properties) + { + $this->properties = $properties; + + return $this; + } + + /** + * Gets field properties + * + * @return array + */ + public function getProperties(): array + { + return $this->properties; + } + + /** + * Sets field values. + * + * @param string[]|string $values Value(s) + * @return $this + */ + public function setValues(array|string $values) + { + if (!is_array($values)) { + $values = preg_split('/,\s*/', $values) ?: []; + } + $this->values = $values; + + return $this; + } + + /** + * Gets field values + * + * @return array|null + */ + public function getValues(): ?array + { + return $this->values; + } + + /** + * Sets the column collation. + * + * @param string $collation Collation + * @return $this + */ + public function setCollation(string $collation) + { + $this->collation = $collation; + + return $this; + } + + /** + * Gets the column collation. + * + * @return string|null + */ + public function getCollation(): ?string + { + return $this->collation; + } + + /** + * Sets the column character set. + * + * @param string $encoding Encoding + * @return $this + */ + public function setEncoding(string $encoding) + { + $this->encoding = $encoding; + + return $this; + } + + /** + * Gets the column character set. + * + * @return string|null + */ + public function getEncoding(): ?string + { + return $this->encoding; + } + + /** + * Sets the column SRID. + * + * @param int $srid SRID + * @return $this + */ + public function setSrid(int $srid) + { + $this->srid = $srid; + + return $this; + } + + /** + * Gets the column SRID. + * + * @return int|null + */ + public function getSrid(): ?int + { + return $this->srid; + } + + /** + * Gets all allowed options. Each option must have a corresponding `setFoo` method. + * + * @return array + */ + protected function getValidOptions(): array + { + return [ + 'limit', + 'default', + 'null', + 'identity', + 'scale', + 'after', + 'update', + 'comment', + 'signed', + 'timezone', + 'properties', + 'values', + 'collation', + 'encoding', + 'srid', + 'seed', + 'increment', + 'generated', + ]; + } + + /** + * Gets all aliased options. Each alias must reference a valid option. + * + * @return array + */ + protected function getAliasedOptions(): array + { + return [ + 'length' => 'limit', + 'precision' => 'limit', + ]; + } + + /** + * Utility method that maps an array of column options to this objects methods. + * + * @param array $options Options + * @throws \RuntimeException + * @return $this + */ + public function setOptions(array $options) + { + $validOptions = $this->getValidOptions(); + $aliasOptions = $this->getAliasedOptions(); + + if (isset($options['identity']) && $options['identity'] && !isset($options['null'])) { + $options['null'] = false; + } + + foreach ($options as $option => $value) { + if (isset($aliasOptions[$option])) { + // proxy alias -> option + $option = $aliasOptions[$option]; + } + + if (!in_array($option, $validOptions, true)) { + throw new RuntimeException(sprintf('"%s" is not a valid column option.', $option)); + } + + $method = 'set' . ucfirst($option); + $this->$method($value); + } + + return $this; + } +} diff --git a/src/Db/Table/ForeignKey.php b/src/Db/Table/ForeignKey.php new file mode 100644 index 00000000..b12fbfe2 --- /dev/null +++ b/src/Db/Table/ForeignKey.php @@ -0,0 +1,238 @@ + + */ + protected static array $validOptions = ['delete', 'update', 'constraint']; + + /** + * @var string[] + */ + protected array $columns = []; + + /** + * @var \Migrations\Db\Table\Table + */ + protected Table $referencedTable; + + /** + * @var string[] + */ + protected array $referencedColumns = []; + + /** + * @var string|null + */ + protected ?string $onDelete = null; + + /** + * @var string|null + */ + protected ?string $onUpdate = null; + + /** + * @var string|null + */ + protected ?string $constraint = null; + + /** + * Sets the foreign key columns. + * + * @param string[]|string $columns Columns + * @return $this + */ + public function setColumns(array|string $columns) + { + $this->columns = is_string($columns) ? [$columns] : $columns; + + return $this; + } + + /** + * Gets the foreign key columns. + * + * @return string[] + */ + public function getColumns(): array + { + return $this->columns; + } + + /** + * Sets the foreign key referenced table. + * + * @param \Migrations\Db\Table\Table $table The table this KEY is pointing to + * @return $this + */ + public function setReferencedTable(Table $table) + { + $this->referencedTable = $table; + + return $this; + } + + /** + * Gets the foreign key referenced table. + * + * @return \Migrations\Db\Table\Table + */ + public function getReferencedTable(): Table + { + return $this->referencedTable; + } + + /** + * Sets the foreign key referenced columns. + * + * @param string[] $referencedColumns Referenced columns + * @return $this + */ + public function setReferencedColumns(array $referencedColumns) + { + $this->referencedColumns = $referencedColumns; + + return $this; + } + + /** + * Gets the foreign key referenced columns. + * + * @return string[] + */ + public function getReferencedColumns(): array + { + return $this->referencedColumns; + } + + /** + * Sets ON DELETE action for the foreign key. + * + * @param string $onDelete On Delete + * @return $this + */ + public function setOnDelete(string $onDelete) + { + $this->onDelete = $this->normalizeAction($onDelete); + + return $this; + } + + /** + * Gets ON DELETE action for the foreign key. + * + * @return string|null + */ + public function getOnDelete(): ?string + { + return $this->onDelete; + } + + /** + * Gets ON UPDATE action for the foreign key. + * + * @return string|null + */ + public function getOnUpdate(): ?string + { + return $this->onUpdate; + } + + /** + * Sets ON UPDATE action for the foreign key. + * + * @param string $onUpdate On Update + * @return $this + */ + public function setOnUpdate(string $onUpdate) + { + $this->onUpdate = $this->normalizeAction($onUpdate); + + return $this; + } + + /** + * Sets constraint for the foreign key. + * + * @param string $constraint Constraint + * @return $this + */ + public function setConstraint(string $constraint) + { + $this->constraint = $constraint; + + return $this; + } + + /** + * Gets constraint name for the foreign key. + * + * @return string|null + */ + public function getConstraint(): ?string + { + return $this->constraint; + } + + /** + * Utility method that maps an array of index options to this objects methods. + * + * @param array $options Options + * @throws \RuntimeException + * @return $this + */ + public function setOptions(array $options) + { + foreach ($options as $option => $value) { + if (!in_array($option, static::$validOptions, true)) { + throw new RuntimeException(sprintf('"%s" is not a valid foreign key option.', $option)); + } + + // handle $options['delete'] as $options['update'] + if ($option === 'delete') { + $this->setOnDelete($value); + } elseif ($option === 'update') { + $this->setOnUpdate($value); + } else { + $method = 'set' . ucfirst($option); + $this->$method($value); + } + } + + return $this; + } + + /** + * From passed value checks if it's correct and fixes if needed + * + * @param string $action Action + * @throws \InvalidArgumentException + * @return string + */ + protected function normalizeAction(string $action): string + { + $constantName = 'static::' . str_replace(' ', '_', strtoupper(trim($action))); + if (!defined($constantName)) { + throw new InvalidArgumentException('Unknown action passed: ' . $action); + } + + return constant($constantName); + } +} diff --git a/src/Db/Table/Index.php b/src/Db/Table/Index.php new file mode 100644 index 00000000..2fb274ae --- /dev/null +++ b/src/Db/Table/Index.php @@ -0,0 +1,228 @@ +columns = is_string($columns) ? [$columns] : $columns; + + return $this; + } + + /** + * Gets the index columns. + * + * @return string[]|null + */ + public function getColumns(): ?array + { + return $this->columns; + } + + /** + * Sets the index type. + * + * @param string $type Type + * @return $this + */ + public function setType(string $type) + { + $this->type = $type; + + return $this; + } + + /** + * Gets the index type. + * + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * Sets the index name. + * + * @param string $name Name + * @return $this + */ + public function setName(string $name) + { + $this->name = $name; + + return $this; + } + + /** + * Gets the index name. + * + * @return string|null + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * Sets the index limit. + * + * @param int|array $limit limit value or array of limit value + * @return $this + */ + public function setLimit(int|array $limit) + { + $this->limit = $limit; + + return $this; + } + + /** + * Gets the index limit. + * + * @return int|array|null + */ + public function getLimit(): int|array|null + { + return $this->limit; + } + + /** + * Sets the index columns sort order. + * + * @param string[] $order column name sort order key value pair + * @return $this + */ + public function setOrder(array $order) + { + $this->order = $order; + + return $this; + } + + /** + * Gets the index columns sort order. + * + * @return string[]|null + */ + public function getOrder(): ?array + { + return $this->order; + } + + /** + * Sets the index included columns. + * + * @param string[] $includedColumns Columns + * @return $this + */ + public function setInclude(array $includedColumns) + { + $this->includedColumns = $includedColumns; + + return $this; + } + + /** + * Gets the index included columns. + * + * @return string[]|null + */ + public function getInclude(): ?array + { + return $this->includedColumns; + } + + /** + * Utility method that maps an array of index options to this objects methods. + * + * @param array $options Options + * @throws \RuntimeException + * @return $this + */ + public function setOptions(array $options) + { + // Valid Options + $validOptions = ['type', 'unique', 'name', 'limit', 'order', 'include']; + foreach ($options as $option => $value) { + if (!in_array($option, $validOptions, true)) { + throw new RuntimeException(sprintf('"%s" is not a valid index option.', $option)); + } + + // handle $options['unique'] + if (strcasecmp($option, self::UNIQUE) === 0) { + if ((bool)$value) { + $this->setType(self::UNIQUE); + } + continue; + } + + $method = 'set' . ucfirst($option); + $this->$method($value); + } + + return $this; + } +} diff --git a/src/Db/Table/Table.php b/src/Db/Table/Table.php new file mode 100644 index 00000000..ad826709 --- /dev/null +++ b/src/Db/Table/Table.php @@ -0,0 +1,85 @@ + + */ + protected array $options; + + /** + * @param string $name The table name + * @param array $options The creation options for this table + * @throws \InvalidArgumentException + */ + public function __construct(string $name, array $options = []) + { + if (empty($name)) { + throw new InvalidArgumentException('Cannot use an empty table name'); + } + + $this->name = $name; + $this->options = $options; + } + + /** + * Sets the table name. + * + * @param string $name The name of the table + * @return $this + */ + public function setName(string $name) + { + $this->name = $name; + + return $this; + } + + /** + * Gets the table name. + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Gets the table options + * + * @return array + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * Sets the table options + * + * @param array $options The options for the table creation + * @return $this + */ + public function setOptions(array $options) + { + $this->options = $options; + + return $this; + } +} diff --git a/tests/TestCase/Db/Table/ColumnTest.php b/tests/TestCase/Db/Table/ColumnTest.php new file mode 100644 index 00000000..6580636f --- /dev/null +++ b/tests/TestCase/Db/Table/ColumnTest.php @@ -0,0 +1,46 @@ +expectException(RuntimeException::class); + $this->expectExceptionMessage('"0" is not a valid column option.'); + + $column->setOptions(['identity']); + } + + public function testSetOptionsIdentity() + { + $column = new Column(); + $this->assertTrue($column->isNull()); + $this->assertFalse($column->isIdentity()); + + $column->setOptions(['identity' => true]); + $this->assertFalse($column->isNull()); + $this->assertTrue($column->isIdentity()); + } + + /** + * @runInSeparateProcess + */ + public function testColumnNullFeatureFlag() + { + $column = new Column(); + $this->assertTrue($column->isNull()); + + FeatureFlags::$columnNullDefault = false; + $column = new Column(); + $this->assertFalse($column->isNull()); + } +} diff --git a/tests/TestCase/Db/Table/ForeignKeyTest.php b/tests/TestCase/Db/Table/ForeignKeyTest.php new file mode 100644 index 00000000..73828038 --- /dev/null +++ b/tests/TestCase/Db/Table/ForeignKeyTest.php @@ -0,0 +1,99 @@ +fk = new ForeignKey(); + } + + public function testOnDeleteSetNullCanBeSetThroughOptions() + { + $this->assertEquals( + ForeignKey::SET_NULL, + $this->fk->setOptions(['delete' => ForeignKey::SET_NULL])->getOnDelete() + ); + } + + public function testInitiallyActionsEmpty() + { + $this->assertNull($this->fk->getOnDelete()); + $this->assertNull($this->fk->getOnUpdate()); + } + + /** + * @param string $dirtyValue + * @param string $valueOfConstant + * @dataProvider actionsProvider + */ + public function testBothActionsCanBeSetThroughSetters($dirtyValue, $valueOfConstant) + { + $this->fk->setOnDelete($dirtyValue)->setOnUpdate($dirtyValue); + $this->assertEquals($valueOfConstant, $this->fk->getOnDelete()); + $this->assertEquals($valueOfConstant, $this->fk->getOnUpdate()); + } + + /** + * @param string $dirtyValue + * @param string $valueOfConstant + * @dataProvider actionsProvider + */ + public function testBothActionsCanBeSetThroughOptions($dirtyValue, $valueOfConstant) + { + $this->fk->setOptions([ + 'delete' => $dirtyValue, + 'update' => $dirtyValue, + ]); + $this->assertEquals($valueOfConstant, $this->fk->getOnDelete()); + $this->assertEquals($valueOfConstant, $this->fk->getOnUpdate()); + } + + public function testUnknownActionsNotAllowedThroughSetter() + { + $this->expectException(InvalidArgumentException::class); + + $this->fk->setOnDelete('i m dump'); + } + + public function testUnknownActionsNotAllowedThroughOptions() + { + $this->expectException(InvalidArgumentException::class); + + $this->fk->setOptions(['update' => 'no yu a dumb']); + } + + public static function actionsProvider() + { + return [ + [ForeignKey::CASCADE, ForeignKey::CASCADE], + [ForeignKey::RESTRICT, ForeignKey::RESTRICT], + [ForeignKey::NO_ACTION, ForeignKey::NO_ACTION], + [ForeignKey::SET_NULL, ForeignKey::SET_NULL], + ['no Action ', ForeignKey::NO_ACTION], + ['Set nuLL', ForeignKey::SET_NULL], + ['no_Action', ForeignKey::NO_ACTION], + ['Set_nuLL', ForeignKey::SET_NULL], + ]; + } + + public function testSetOptionThrowsExceptionIfOptionIsNotString() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('"0" is not a valid foreign key option'); + + $this->fk->setOptions(['update']); + } +} diff --git a/tests/TestCase/Db/Table/IndexTest.php b/tests/TestCase/Db/Table/IndexTest.php new file mode 100644 index 00000000..6c9011c2 --- /dev/null +++ b/tests/TestCase/Db/Table/IndexTest.php @@ -0,0 +1,21 @@ +expectException(RuntimeException::class); + $this->expectExceptionMessage('"0" is not a valid index option.'); + + $column->setOptions(['type']); + } +} diff --git a/tests/TestCase/Db/Table/TableTest.php b/tests/TestCase/Db/Table/TableTest.php new file mode 100644 index 00000000..445bc8a0 --- /dev/null +++ b/tests/TestCase/Db/Table/TableTest.php @@ -0,0 +1,462 @@ +setType('badtype'); + $table = new Table('ntable', [], $adapter); + $table->addColumn($column, 'int'); + } catch (InvalidArgumentException $e) { + $this->assertInstanceOf( + 'InvalidArgumentException', + $e, + 'Expected exception of type InvalidArgumentException, got ' . get_class($e) + ); + $this->assertStringStartsWith('An invalid column type ', $e->getMessage()); + } + } + + public function testAddColumnWithColumnObject() + { + $adapter = new MysqlAdapter([]); + $column = new Column(); + $column->setName('email') + ->setType('integer'); + $table = new Table('ntable', [], $adapter); + $table->addColumn($column); + $actions = $this->getPendingActions($table); + $this->assertInstanceOf('Phinx\Db\Action\AddColumn', $actions[0]); + $this->assertSame($column, $actions[0]->getColumn()); + } + + public function testAddColumnWithNoAdapterSpecified() + { + try { + $table = new Table('ntable'); + $table->addColumn('realname', 'string'); + $this->fail('Expected the table object to throw an exception'); + } catch (RuntimeException $e) { + $this->assertInstanceOf( + 'RuntimeException', + $e, + 'Expected exception of type RuntimeException, got ' . get_class($e) + ); + } + } + + public function testAddComment() + { + $adapter = new MysqlAdapter([]); + $table = new Table('ntable', ['comment' => 'test comment'], $adapter); + $options = $table->getOptions(); + $this->assertEquals('test comment', $options['comment']); + } + + public function testAddIndexWithIndexObject() + { + $adapter = new MysqlAdapter([]); + $index = new Index(); + $index->setType(Index::INDEX) + ->setColumns(['email']); + $table = new Table('ntable', [], $adapter); + $table->addIndex($index); + $actions = $this->getPendingActions($table); + $this->assertInstanceOf('Phinx\Db\Action\AddIndex', $actions[0]); + $this->assertSame($index, $actions[0]->getIndex()); + } + + /** + * @dataProvider provideTimestampColumnNames + * @param AdapterInterface $adapter + * @param string|null $createdAtColumnName * @param string|null $updatedAtColumnName * @param string $expectedCreatedAtColumnName * @param string $expectedUpdatedAtColumnName * @param bool $withTimezone + */ + public function testAddTimestamps(AdapterInterface $adapter, $createdAtColumnName, $updatedAtColumnName, $expectedCreatedAtColumnName, $expectedUpdatedAtColumnName, $withTimezone) + { + $table = new Table('ntable', [], $adapter); + $table->addTimestamps($createdAtColumnName, $updatedAtColumnName, $withTimezone); + $actions = $this->getPendingActions($table); + + $columns = []; + + foreach ($actions as $action) { + $columns[] = $action->getColumn(); + } + + $this->assertEquals($expectedCreatedAtColumnName, $columns[0]->getName()); + $this->assertEquals('timestamp', $columns[0]->getType()); + $this->assertEquals('CURRENT_TIMESTAMP', $columns[0]->getDefault()); + $this->assertEquals($withTimezone, $columns[0]->getTimezone()); + $this->assertEquals('', $columns[0]->getUpdate()); + + $this->assertEquals($expectedUpdatedAtColumnName, $columns[1]->getName()); + $this->assertEquals('timestamp', $columns[1]->getType()); + $this->assertEquals($withTimezone, $columns[1]->getTimezone()); + $this->assertEquals('CURRENT_TIMESTAMP', $columns[1]->getUpdate()); + $this->assertTrue($columns[1]->isNull()); + $this->assertNull($columns[1]->getDefault()); + } + + /** + * @dataProvider provideAdapters + * @param AdapterInterface $adapter + */ + public function testAddTimestampsNoUpdated(AdapterInterface $adapter) + { + $table = new Table('ntable', [], $adapter); + $table->addTimestamps(null, false); + $actions = $this->getPendingActions($table); + + $columns = []; + + foreach ($actions as $action) { + $columns[] = $action->getColumn(); + } + + $this->assertCount(1, $columns); + + $this->assertSame('created_at', $columns[0]->getName()); + $this->assertSame('timestamp', $columns[0]->getType()); + $this->assertSame('CURRENT_TIMESTAMP', $columns[0]->getDefault()); + $this->assertFalse($columns[0]->getTimezone()); + $this->assertSame('', $columns[0]->getUpdate()); + } + + /** + * @dataProvider provideAdapters + * @param AdapterInterface $adapter + */ + public function testAddTimestampsNoCreated(AdapterInterface $adapter) + { + $table = new Table('ntable', [], $adapter); + $table->addTimestamps(false, null); + $actions = $this->getPendingActions($table); + + $columns = []; + + foreach ($actions as $action) { + $columns[] = $action->getColumn(); + } + + $this->assertCount(1, $columns); + + $this->assertSame('updated_at', $columns[0]->getName()); + $this->assertSame('timestamp', $columns[0]->getType()); + $this->assertFalse($columns[0]->getTimezone()); + $this->assertSame('CURRENT_TIMESTAMP', $columns[0]->getUpdate()); + $this->assertTrue($columns[0]->isNull()); + $this->assertNull($columns[0]->getDefault()); + } + + /** + * @dataProvider provideAdapters + * @param AdapterInterface $adapter + */ + public function testAddTimestampsThrowsOnBothFalse(AdapterInterface $adapter) + { + $table = new Table('ntable', [], $adapter); + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Cannot set both created_at and updated_at columns to false'); + $table->addTimestamps(false, false); + } + + /** + * @dataProvider provideTimestampColumnNames + * @param AdapterInterface $adapter + * @param string|null $createdAtColumnName + * @param string|null $updatedAtColumnName + * @param string $expectedCreatedAtColumnName + * @param string $expectedUpdatedAtColumnName + * @param bool $withTimezone + */ + public function testAddTimestampsWithTimezone(AdapterInterface $adapter, $createdAtColumnName, $updatedAtColumnName, $expectedCreatedAtColumnName, $expectedUpdatedAtColumnName, $withTimezone) + { + $table = new Table('ntable', [], $adapter); + $table->addTimestampsWithTimezone($createdAtColumnName, $updatedAtColumnName); + $actions = $this->getPendingActions($table); + + $columns = []; + + foreach ($actions as $action) { + $columns[] = $action->getColumn(); + } + + $this->assertEquals($expectedCreatedAtColumnName, $columns[0]->getName()); + $this->assertEquals('timestamp', $columns[0]->getType()); + $this->assertEquals('CURRENT_TIMESTAMP', $columns[0]->getDefault()); + $this->assertTrue($columns[0]->getTimezone()); + $this->assertEquals('', $columns[0]->getUpdate()); + + $this->assertEquals($expectedUpdatedAtColumnName, $columns[1]->getName()); + $this->assertEquals('timestamp', $columns[1]->getType()); + $this->assertTrue($columns[1]->getTimezone()); + $this->assertEquals('CURRENT_TIMESTAMP', $columns[1]->getUpdate()); + $this->assertTrue($columns[1]->isNull()); + $this->assertNull($columns[1]->getDefault()); + } + + public function testInsert() + { + $adapterStub = $this->getMockBuilder('\Phinx\Db\Adapter\MysqlAdapter') + ->setConstructorArgs([[]]) + ->getMock(); + $table = new Table('ntable', [], $adapterStub); + $data = [ + 'column1' => 'value1', + 'column2' => 'value2', + ]; + $table->insert($data); + $expectedData = [ + $data, + ]; + $this->assertEquals($expectedData, $table->getData()); + } + + public function testInsertMultipleRowsWithoutZeroKey() + { + $adapterStub = $this->getMockBuilder('\Phinx\Db\Adapter\MysqlAdapter') + ->setConstructorArgs([[]]) + ->getMock(); + $table = new Table('ntable', [], $adapterStub); + $data = [ + 1 => [ + 'column1' => 'value1', + 'column2' => 'value2', + ], + 2 => [ + 'column1' => 'value1', + 'column2' => 'value2', + ], + ]; + $table->insert($data); + $expectedData = array_values($data); + $this->assertEquals($expectedData, $table->getData()); + } + + public function testInsertSaveEmptyData() + { + $adapterStub = $this->getMockBuilder('\Phinx\Db\Adapter\MysqlAdapter') + ->setConstructorArgs([[]]) + ->getMock(); + $table = new Table('ntable', [], $adapterStub); + + $adapterStub->expects($this->never())->method('bulkinsert'); + + $table->insert([])->save(); + } + + public function testInsertSaveData() + { + $adapterStub = $this->getMockBuilder('\Phinx\Db\Adapter\MysqlAdapter') + ->setConstructorArgs([[]]) + ->getMock(); + $table = new Table('ntable', [], $adapterStub); + $data = [ + [ + 'column1' => 'value1', + ], + [ + 'column1' => 'value2', + ], + ]; + + $moreData = [ + [ + 'column1' => 'value3', + ], + [ + 'column1' => 'value4', + ], + ]; + + $adapterStub->expects($this->exactly(1)) + ->method('bulkinsert') + ->with($table->getTable(), [$data[0], $data[1], $moreData[0], $moreData[1]]); + + $table->insert($data) + ->insert($moreData) + ->save(); + } + + public function testSaveAfterSaveData() + { + $adapterStub = $this->getMockBuilder('\Phinx\Db\Adapter\MysqlAdapter') + ->setConstructorArgs([[]]) + ->getMock(); + $table = new Table('ntable', [], $adapterStub); + $data = [ + [ + 'column1' => 'value1', + ], + [ + 'column1' => 'value2', + ], + ]; + + $adapterStub->expects($this->any()) + ->method('isValidColumnType') + ->willReturn(true); + $adapterStub->expects($this->exactly(1)) + ->method('bulkinsert') + ->with($table->getTable(), [$data[0], $data[1]]); + + $table + ->addColumn('column1', 'string', ['null' => true]) + ->save(); + $table + ->insert($data) + ->saveData(); + $table + ->changeColumn('column1', 'string', ['null' => false]) + ->save(); + } + + public function testResetAfterAddingData() + { + $adapterStub = $this->getMockBuilder('\Phinx\Db\Adapter\MysqlAdapter') + ->setConstructorArgs([[]]) + ->getMock(); + $table = new Table('ntable', [], $adapterStub); + $columns = ['column1']; + $data = [['value1']]; + $table->insert($columns, $data)->save(); + $this->assertEquals([], $table->getData()); + } + + public function testPendingAfterAddingData() + { + $adapterStub = $this->getMockBuilder('\Phinx\Db\Adapter\MysqlAdapter') + ->setConstructorArgs([[]]) + ->getMock(); + $table = new Table('ntable', [], $adapterStub); + $columns = ['column1']; + $data = [['value1']]; + $table->insert($columns, $data); + $this->assertTrue($table->hasPendingActions()); + } + + public function testPendingAfterAddingColumn() + { + $adapterStub = $this->getMockBuilder('\Phinx\Db\Adapter\MysqlAdapter') + ->setConstructorArgs([[]]) + ->getMock(); + $adapterStub->expects($this->any()) + ->method('isValidColumnType') + ->willReturn(true); + $table = new Table('ntable', [], $adapterStub); + $table->addColumn('column1', 'integer', ['null' => true]); + $this->assertTrue($table->hasPendingActions()); + } + + public function testGetColumn() + { + $adapterStub = $this->getMockBuilder('\Phinx\Db\Adapter\MysqlAdapter') + ->setConstructorArgs([[]]) + ->getMock(); + + $column1 = (new Column())->setName('column1'); + + $adapterStub->expects($this->exactly(2)) + ->method('getColumns') + ->willReturn([ + $column1, + ]); + + $table = new Table('ntable', [], $adapterStub); + + $this->assertEquals($column1, $table->getColumn('column1')); + $this->assertNull($table->getColumn('column2')); + } + + /** + * @dataProvider removeIndexDataprovider + * @param string $indexIdentifier + * @param Index $index + */ + public function testRemoveIndex($indexIdentifier, Index $index) + { + $adapterStub = $this->getMockBuilder('\Phinx\Db\Adapter\MysqlAdapter') + ->setConstructorArgs([[]]) + ->getMock(); + + $table = new Table('table', [], $adapterStub); + $table->removeIndex($indexIdentifier); + + $indexes = array_map(function (DropIndex $action) { + return $action->getIndex(); + }, $this->getPendingActions($table)); + + $this->assertEquals([$index], $indexes); + } + + public static function removeIndexDataprovider() + { + return [ + [ + 'indexA', + (new Index())->setColumns(['indexA']), + ], + [ + ['indexB', 'indexC'], + (new Index())->setColumns(['indexB', 'indexC']), + ], + [ + ['indexD'], + (new Index())->setColumns(['indexD']), + ], + ]; + } + + protected function getPendingActions($table) + { + $prop = new ReflectionProperty(get_class($table), 'actions'); + $prop->setAccessible(true); + + return $prop->getValue($table)->getActions(); + } +}