From 6ab7bbaa5b873dd92c50d1871fcd4a5ba2ac126b Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 30 Nov 2023 23:59:42 -0500 Subject: [PATCH 1/5] Import Db/Table package and get tests passing Import the Db/Table package from phinx and get the tests passing. My plan here is to import all of the 'data transfer' objects from phinx and get them referencing each other. Once that is complete, we'll need to import the Plan and Migration wrappers. This will form the core of the API compatibility with phinx. The adapter layer can be replaced by a mix of Cake's Database package and some more dialect style platform wrappers. Finally we'll need to provide the same CLI interface that migrations has always given. --- src/Db/Table/Column.php | 802 +++++++++++++++++++++ src/Db/Table/ForeignKey.php | 238 ++++++ src/Db/Table/Index.php | 227 ++++++ src/Db/Table/Table.php | 84 +++ tests/TestCase/Db/Table/ColumnTest.php | 46 ++ tests/TestCase/Db/Table/ForeignKeyTest.php | 99 +++ tests/TestCase/Db/Table/IndexTest.php | 21 + tests/TestCase/Db/Table/TableTest.php | 462 ++++++++++++ 8 files changed, 1979 insertions(+) create mode 100644 src/Db/Table/Column.php create mode 100644 src/Db/Table/ForeignKey.php create mode 100644 src/Db/Table/Index.php create mode 100644 src/Db/Table/Table.php create mode 100644 tests/TestCase/Db/Table/ColumnTest.php create mode 100644 tests/TestCase/Db/Table/ForeignKeyTest.php create mode 100644 tests/TestCase/Db/Table/IndexTest.php create mode 100644 tests/TestCase/Db/Table/TableTest.php diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php new file mode 100644 index 00000000..a4f0fb47 --- /dev/null +++ b/src/Db/Table/Column.php @@ -0,0 +1,802 @@ +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($type) + { + $this->type = $type; + + return $this; + } + + /** + * Gets the column type. + * + * @return string|\Phinx\Util\Literal + */ + public function getType() + { + 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 = (bool)$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($default) + { + $this->default = $default; + + return $this; + } + + /** + * Gets the default column value. + * + * @return mixed + */ + public function getDefault() + { + 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 = (bool)$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 = (bool)$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($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..70e752bf --- /dev/null +++ b/src/Db/Table/ForeignKey.php @@ -0,0 +1,238 @@ + + */ + protected static $validOptions = ['delete', 'update', 'constraint']; + + /** + * @var string[] + */ + protected $columns = []; + + /** + * @var \Phinx\Db\Table\Table + */ + protected $referencedTable; + + /** + * @var string[] + */ + protected $referencedColumns = []; + + /** + * @var string|null + */ + protected $onDelete; + + /** + * @var string|null + */ + protected $onUpdate; + + /** + * @var string|null + */ + protected $constraint; + + /** + * Sets the foreign key columns. + * + * @param string[]|string $columns Columns + * @return $this + */ + public function setColumns($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 \Phinx\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 \Phinx\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..90203997 --- /dev/null +++ b/src/Db/Table/Index.php @@ -0,0 +1,227 @@ +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($limit) + { + $this->limit = $limit; + + return $this; + } + + /** + * Gets the index limit. + * + * @return int|array|null + */ + public function getLimit() + { + 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..6744ea98 --- /dev/null +++ b/src/Db/Table/Table.php @@ -0,0 +1,84 @@ + + */ + protected $options; + + /** + * @param string $name The table name + * @param array $options The creation options for this table + * @throws \InvalidArgumentException + */ + public function __construct($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..79727b12 --- /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(); + } +} From b633cd70778bfb9326ff23d91491b4f1bc6ad3a5 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 1 Dec 2023 00:06:19 -0500 Subject: [PATCH 2/5] Fix phpcs --- src/Db/Table/Column.php | 51 +++++++++++----------- src/Db/Table/ForeignKey.php | 18 ++++---- src/Db/Table/Index.php | 19 ++++---- src/Db/Table/Table.php | 7 +-- tests/TestCase/Db/Table/ForeignKeyTest.php | 2 +- 5 files changed, 50 insertions(+), 47 deletions(-) diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index a4f0fb47..adcdb038 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -11,6 +11,7 @@ use Phinx\Config\FeatureFlags; use Phinx\Db\Adapter\AdapterInterface; use Phinx\Db\Adapter\PostgresAdapter; +use Phinx\Util\Literal; use RuntimeException; /** @@ -61,104 +62,104 @@ class Column /** * @var string */ - protected $name; + protected string $name; /** * @var string|\Phinx\Util\Literal */ - protected $type; + protected string|Literal $type; /** * @var int|null */ - protected $limit; + protected ?int $limit = null; /** * @var bool */ - protected $null = true; + protected bool $null = true; /** * @var mixed */ - protected $default; + protected mixed $default; /** * @var bool */ - protected $identity = false; + protected bool $identity = false; /** * Postgres-only column option for identity (always|default) * * @var ?string */ - protected $generated = PostgresAdapter::GENERATED_ALWAYS; + protected ?string $generated = PostgresAdapter::GENERATED_ALWAYS; /** * @var int|null */ - protected $seed; + protected ?int $seed = null; /** * @var int|null */ - protected $increment; + protected ?int $increment = null; /** * @var int|null */ - protected $scale; + protected ?int $scale = null; /** * @var string|null */ - protected $after; + protected ?string $after = null; /** * @var string|null */ - protected $update; + protected ?string $update = null; /** * @var string|null */ - protected $comment; + protected ?string $comment = null; /** * @var bool */ - protected $signed = true; + protected bool $signed = true; /** * @var bool */ - protected $timezone = false; + protected bool $timezone = false; /** * @var array */ - protected $properties = []; + protected array $properties = []; /** * @var string|null */ - protected $collation; + protected ?string $collation = null; /** * @var string|null */ - protected $encoding; + protected ?string $encoding = null; /** * @var int|null */ - protected $srid; + protected ?int $srid = null; /** * @var array|null */ - protected $values; + protected ?array $values = null; /** * Column constructor @@ -197,7 +198,7 @@ public function getName(): ?string * @param string|\Phinx\Util\Literal $type Column type * @return $this */ - public function setType($type) + public function setType(string|Literal $type) { $this->type = $type; @@ -209,7 +210,7 @@ public function setType($type) * * @return string|\Phinx\Util\Literal */ - public function getType() + public function getType(): string|Literal { return $this->type; } @@ -276,7 +277,7 @@ public function isNull(): bool * @param mixed $default Default * @return $this */ - public function setDefault($default) + public function setDefault(mixed $default) { $this->default = $default; @@ -288,7 +289,7 @@ public function setDefault($default) * * @return mixed */ - public function getDefault() + public function getDefault(): mixed { return $this->default; } @@ -636,7 +637,7 @@ public function getProperties(): array * @param string[]|string $values Value(s) * @return $this */ - public function setValues($values) + public function setValues(array|string $values) { if (!is_array($values)) { $values = preg_split('/,\s*/', $values) ?: []; diff --git a/src/Db/Table/ForeignKey.php b/src/Db/Table/ForeignKey.php index 70e752bf..6790f8f1 100644 --- a/src/Db/Table/ForeignKey.php +++ b/src/Db/Table/ForeignKey.php @@ -21,37 +21,37 @@ class ForeignKey /** * @var array */ - protected static $validOptions = ['delete', 'update', 'constraint']; + protected static array $validOptions = ['delete', 'update', 'constraint']; /** * @var string[] */ - protected $columns = []; + protected array $columns = []; /** - * @var \Phinx\Db\Table\Table + * @var \Migrations\Db\Table\Table */ - protected $referencedTable; + protected Table $referencedTable; /** * @var string[] */ - protected $referencedColumns = []; + protected array $referencedColumns = []; /** * @var string|null */ - protected $onDelete; + protected ?string $onDelete = null; /** * @var string|null */ - protected $onUpdate; + protected ?string $onUpdate = null; /** * @var string|null */ - protected $constraint; + protected ?string $constraint = null; /** * Sets the foreign key columns. @@ -59,7 +59,7 @@ class ForeignKey * @param string[]|string $columns Columns * @return $this */ - public function setColumns($columns) + public function setColumns(array|string $columns) { $this->columns = is_string($columns) ? [$columns] : $columns; diff --git a/src/Db/Table/Index.php b/src/Db/Table/Index.php index 90203997..2fb274ae 100644 --- a/src/Db/Table/Index.php +++ b/src/Db/Table/Index.php @@ -1,4 +1,5 @@ columns = is_string($columns) ? [$columns] : $columns; @@ -131,7 +132,7 @@ public function getName(): ?string * @param int|array $limit limit value or array of limit value * @return $this */ - public function setLimit($limit) + public function setLimit(int|array $limit) { $this->limit = $limit; @@ -143,7 +144,7 @@ public function setLimit($limit) * * @return int|array|null */ - public function getLimit() + public function getLimit(): int|array|null { return $this->limit; } diff --git a/src/Db/Table/Table.php b/src/Db/Table/Table.php index 6744ea98..ad826709 100644 --- a/src/Db/Table/Table.php +++ b/src/Db/Table/Table.php @@ -1,4 +1,5 @@ */ - protected $options; + protected array $options; /** * @param string $name The table name * @param array $options The creation options for this table * @throws \InvalidArgumentException */ - public function __construct($name, array $options = []) + public function __construct(string $name, array $options = []) { if (empty($name)) { throw new InvalidArgumentException('Cannot use an empty table name'); diff --git a/tests/TestCase/Db/Table/ForeignKeyTest.php b/tests/TestCase/Db/Table/ForeignKeyTest.php index 79727b12..73828038 100644 --- a/tests/TestCase/Db/Table/ForeignKeyTest.php +++ b/tests/TestCase/Db/Table/ForeignKeyTest.php @@ -3,8 +3,8 @@ namespace Migrations\Test\TestCase\Phinx\Db\Table; -use Migrations\Db\Table\ForeignKey; use InvalidArgumentException; +use Migrations\Db\Table\ForeignKey; use PHPUnit\Framework\TestCase; use RuntimeException; From fb05f0c06cf186206aaf04fd8e849fe6287b5f62 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 1 Dec 2023 18:21:36 -0500 Subject: [PATCH 3/5] Appease psalm --- src/Command/BakeSeedCommand.php | 4 +++- src/Db/Table/ForeignKey.php | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) 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/ForeignKey.php b/src/Db/Table/ForeignKey.php index 6790f8f1..b12fbfe2 100644 --- a/src/Db/Table/ForeignKey.php +++ b/src/Db/Table/ForeignKey.php @@ -79,7 +79,7 @@ public function getColumns(): array /** * Sets the foreign key referenced table. * - * @param \Phinx\Db\Table\Table $table The table this KEY is pointing to + * @param \Migrations\Db\Table\Table $table The table this KEY is pointing to * @return $this */ public function setReferencedTable(Table $table) @@ -92,7 +92,7 @@ public function setReferencedTable(Table $table) /** * Gets the foreign key referenced table. * - * @return \Phinx\Db\Table\Table + * @return \Migrations\Db\Table\Table */ public function getReferencedTable(): Table { From 8afd804a42dfa3f57d6dfd7a300ce5289e9f8855 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 1 Dec 2023 22:26:53 -0500 Subject: [PATCH 4/5] Fix more psalm errors. --- src/Db/Table/Column.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index adcdb038..187f0b2f 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -246,7 +246,7 @@ public function getLimit(): ?int */ public function setNull(bool $null) { - $this->null = (bool)$null; + $this->null = $null; return $this; } @@ -549,7 +549,7 @@ public function getComment(): ?string */ public function setSigned(bool $signed) { - $this->signed = (bool)$signed; + $this->signed = $signed; return $this; } @@ -583,7 +583,7 @@ public function isSigned(): bool */ public function setTimezone(bool $timezone) { - $this->timezone = (bool)$timezone; + $this->timezone = $timezone; return $this; } From 09b32a457b90f15370cf0a7308dbece48979b438 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 1 Dec 2023 22:39:53 -0500 Subject: [PATCH 5/5] Fix CI configuration --- .github/workflows/ci.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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