diff --git a/apps/dav/lib/Migration/Version1034Date20250605132605.php b/apps/dav/lib/Migration/Version1034Date20250605132605.php index 9bb7f4fe718..930968a39c7 100644 --- a/apps/dav/lib/Migration/Version1034Date20250605132605.php +++ b/apps/dav/lib/Migration/Version1034Date20250605132605.php @@ -60,6 +60,7 @@ class Version1034Date20250605132605 extends SimpleMigrationStep { 'notnull' => true, 'length' => 255, ]); + /** @psalm-suppress InvalidArgument legacy column */ $federatedCalendarsTable->addColumn('remote_Url', Types::STRING, [ 'notnull' => true, 'length' => 255, diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 8e2cdfc266d..f556057caaa 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -372,7 +372,9 @@ return array( 'OCP\\DB\\QueryBuilder\\IQueryFunction' => $baseDir . '/lib/public/DB/QueryBuilder/IQueryFunction.php', 'OCP\\DB\\QueryBuilder\\ITypedQueryBuilder' => $baseDir . '/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php', 'OCP\\DB\\QueryBuilder\\Sharded\\IShardMapper' => $baseDir . '/lib/public/DB/QueryBuilder/Sharded/IShardMapper.php', + 'OCP\\DB\\Schema\\ColumnType' => $baseDir . '/lib/public/DB/Schema/ColumnType.php', 'OCP\\DB\\Schema\\IColumn' => $baseDir . '/lib/public/DB/Schema/IColumn.php', + 'OCP\\DB\\Schema\\IForeignKeyConstraint' => $baseDir . '/lib/public/DB/Schema/IForeignKeyConstraint.php', 'OCP\\DB\\Schema\\IIndex' => $baseDir . '/lib/public/DB/Schema/IIndex.php', 'OCP\\DB\\Schema\\ITable' => $baseDir . '/lib/public/DB/Schema/ITable.php', 'OCP\\DB\\Schema\\IType' => $baseDir . '/lib/public/DB/Schema/IType.php', @@ -1785,6 +1787,7 @@ return array( 'OC\\DB\\SQLiteSessionInit' => $baseDir . '/lib/private/DB/SQLiteSessionInit.php', 'OC\\DB\\SchemaWrapper' => $baseDir . '/lib/private/DB/SchemaWrapper.php', 'OC\\DB\\Schema\\Column' => $baseDir . '/lib/private/DB/Schema/Column.php', + 'OC\\DB\\Schema\\ForeignKeyConstraint' => $baseDir . '/lib/private/DB/Schema/ForeignKeyConstraint.php', 'OC\\DB\\Schema\\Index' => $baseDir . '/lib/private/DB/Schema/Index.php', 'OC\\DB\\Schema\\Table' => $baseDir . '/lib/private/DB/Schema/Table.php', 'OC\\DB\\Schema\\Type' => $baseDir . '/lib/private/DB/Schema/Type.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 5ae915263d4..875ff36b584 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -413,7 +413,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\DB\\QueryBuilder\\IQueryFunction' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/IQueryFunction.php', 'OCP\\DB\\QueryBuilder\\ITypedQueryBuilder' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php', 'OCP\\DB\\QueryBuilder\\Sharded\\IShardMapper' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/Sharded/IShardMapper.php', + 'OCP\\DB\\Schema\\ColumnType' => __DIR__ . '/../../..' . '/lib/public/DB/Schema/ColumnType.php', 'OCP\\DB\\Schema\\IColumn' => __DIR__ . '/../../..' . '/lib/public/DB/Schema/IColumn.php', + 'OCP\\DB\\Schema\\IForeignKeyConstraint' => __DIR__ . '/../../..' . '/lib/public/DB/Schema/IForeignKeyConstraint.php', 'OCP\\DB\\Schema\\IIndex' => __DIR__ . '/../../..' . '/lib/public/DB/Schema/IIndex.php', 'OCP\\DB\\Schema\\ITable' => __DIR__ . '/../../..' . '/lib/public/DB/Schema/ITable.php', 'OCP\\DB\\Schema\\IType' => __DIR__ . '/../../..' . '/lib/public/DB/Schema/IType.php', @@ -1826,6 +1828,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\DB\\SQLiteSessionInit' => __DIR__ . '/../../..' . '/lib/private/DB/SQLiteSessionInit.php', 'OC\\DB\\SchemaWrapper' => __DIR__ . '/../../..' . '/lib/private/DB/SchemaWrapper.php', 'OC\\DB\\Schema\\Column' => __DIR__ . '/../../..' . '/lib/private/DB/Schema/Column.php', + 'OC\\DB\\Schema\\ForeignKeyConstraint' => __DIR__ . '/../../..' . '/lib/private/DB/Schema/ForeignKeyConstraint.php', 'OC\\DB\\Schema\\Index' => __DIR__ . '/../../..' . '/lib/private/DB/Schema/Index.php', 'OC\\DB\\Schema\\Table' => __DIR__ . '/../../..' . '/lib/private/DB/Schema/Table.php', 'OC\\DB\\Schema\\Type' => __DIR__ . '/../../..' . '/lib/private/DB/Schema/Type.php', diff --git a/lib/private/DB/Schema/Column.php b/lib/private/DB/Schema/Column.php index 1d9a1262406..daea23545da 100644 --- a/lib/private/DB/Schema/Column.php +++ b/lib/private/DB/Schema/Column.php @@ -12,6 +12,7 @@ namespace OC\DB\Schema; use Doctrine\DBAL\Schema\Column as DBALColumn; use Doctrine\DBAL\Schema\SchemaException as DBALSchemaException; use Doctrine\DBAL\Types\Type as DBALType; +use OCP\DB\Schema\ColumnType; use OCP\DB\Schema\IColumn; use OCP\DB\Schema\IType; use OCP\DB\Schema\SchemaException; @@ -33,11 +34,15 @@ class Column implements IColumn { } #[\Override] - public function setType(string|IType|DBALType $type): self { + public function setType(string|IType|DBALType|ColumnType $type): self { if ($type instanceof IType) { $type = $type->getName(); } + if ($type instanceof ColumnType) { + $type = $type->value; + } + $this->column->setType($type instanceof DBALType ? $type : DBALType::getType($type)); return $this; @@ -97,6 +102,11 @@ class Column implements IColumn { return new Type($this->column->getType()); } + #[\Override] + public function getColumnType(): ColumnType { + return ColumnType::from(DBALType::lookupName($this->column->getType())); + } + #[\Override] public function getLength(): ?int { return $this->column->getLength(); diff --git a/lib/private/DB/Schema/ForeignKeyConstraint.php b/lib/private/DB/Schema/ForeignKeyConstraint.php new file mode 100644 index 00000000000..1424c3fc4a7 --- /dev/null +++ b/lib/private/DB/Schema/ForeignKeyConstraint.php @@ -0,0 +1,30 @@ +keyConstraint->getName(); + return $value; + } +} diff --git a/lib/private/DB/Schema/Index.php b/lib/private/DB/Schema/Index.php index dc3ea40328e..37a613c0d0d 100644 --- a/lib/private/DB/Schema/Index.php +++ b/lib/private/DB/Schema/Index.php @@ -53,6 +53,11 @@ class Index implements IIndex { return $this->index->isSimpleIndex(); } + #[\Override] + public function hasColumnAtPosition(string $name, int $position = 0): bool { + return $this->index->hasColumnAtPosition($name, $position); + } + /** * Forwards any method not declared on IIndex to the wrapped Doctrine * DBAL index, e.g. mutators like `addFlag()` or `removeFlag()` that are diff --git a/lib/private/DB/Schema/Table.php b/lib/private/DB/Schema/Table.php index 25c3c7f6413..7f3e6db8015 100644 --- a/lib/private/DB/Schema/Table.php +++ b/lib/private/DB/Schema/Table.php @@ -10,10 +10,14 @@ declare(strict_types=1); namespace OC\DB\Schema; use Doctrine\DBAL\Schema\Column as DBALColumn; +use Doctrine\DBAL\Schema\ForeignKeyConstraint as DBALForeignKeyConstraint; use Doctrine\DBAL\Schema\Index as DBALIndex; use Doctrine\DBAL\Schema\SchemaException as DBALSchemaException; use Doctrine\DBAL\Schema\Table as DBALTable; +use Doctrine\DBAL\Types\Type as DBALType; +use OCP\DB\Schema\ColumnType; use OCP\DB\Schema\IColumn; +use OCP\DB\Schema\IForeignKeyConstraint; use OCP\DB\Schema\IIndex; use OCP\DB\Schema\ITable; use OCP\DB\Schema\SchemaException; @@ -36,7 +40,9 @@ class Table implements ITable { #[\Override] public function getName(): string { - return $this->table->getName(); + /** @var non-empty-lowercase-string $name */ + $name = $this->table->getName(); + return $name; } #[\Override] @@ -134,8 +140,11 @@ class Table implements ITable { } #[\Override] - public function addColumn(string $name, string $typeName, array $options = []): IColumn { + public function addColumn(string $name, string|ColumnType $typeName, array $options = []): IColumn { try { + if ($typeName instanceof ColumnType) { + $typeName = $typeName->value; + } return new Column($this->table->addColumn($name, $typeName, $options)); } catch (DBALSchemaException $e) { throw new SchemaException($e->getMessage(), $e->getCode(), $e); @@ -145,6 +154,14 @@ class Table implements ITable { #[\Override] public function modifyColumn(string $name, array $options): self { try { + if (isset($options['type'])) { + if ($options['type'] instanceof ColumnType) { + $options['type'] = $options['type']->value; + } + if (is_string($options['type'])) { + $options['type'] = DBALType::getType($options['type']); + } + } $this->table->modifyColumn($name, $options); } catch (DBALSchemaException $e) { throw new SchemaException($e->getMessage(), $e->getCode(), $e); @@ -210,8 +227,8 @@ class Table implements ITable { } #[\Override] - public function hasColumn(string $string): bool { - return $this->table->hasColumn($string); + public function hasColumn(string $name): bool { + return $this->table->hasColumn($name); } #[\Override] @@ -238,4 +255,12 @@ class Table implements ITable { $this->table->getIndexes(), )); } + + #[\Override] + public function getForeignKeys(): array { + return array_values(array_map( + static fn (DBALForeignKeyConstraint $keyConstraint): IForeignKeyConstraint => new ForeignKeyConstraint($keyConstraint), + $this->table->getForeignKeys(), + )); + } } diff --git a/lib/public/AppFramework/Db/Entity.php b/lib/public/AppFramework/Db/Entity.php index fe7c1414fe3..d49150ae2a4 100644 --- a/lib/public/AppFramework/Db/Entity.php +++ b/lib/public/AppFramework/Db/Entity.php @@ -8,6 +8,7 @@ namespace OCP\AppFramework\Db; +use OCP\DB\Schema\ColumnType; use OCP\DB\Types; use function lcfirst; use function substr; @@ -23,8 +24,8 @@ abstract class Entity { public $id; /** @var array $_updatedFields */ private array $_updatedFields = []; - /** @var array $_fieldTypes */ - protected array $_fieldTypes = ['id' => 'integer']; + /** @var array $_fieldTypes */ + protected array $_fieldTypes = ['id' => ColumnType::Integer]; /** * Simple alternative constructor for building entities from a request @@ -66,7 +67,7 @@ abstract class Entity { * @since 7.0.0 */ public function getFieldTypes(): array { - return $this->_fieldTypes; + return array_map(fn (ColumnType $type) => $type->value, $this->_fieldTypes); } /** @@ -98,47 +99,47 @@ abstract class Entity { // if type definition exists, cast to correct type if ($args[0] !== null && array_key_exists($name, $this->_fieldTypes)) { $type = $this->_fieldTypes[$name]; - if ($type === Types::BLOB) { + if ($type === ColumnType::Blob) { // (B)LOB is treated as string when we read from the DB if (is_resource($args[0])) { $args[0] = stream_get_contents($args[0]); } - $type = Types::STRING; + $type = ColumnType::String; } switch ($type) { - case Types::BIGINT: - case Types::SMALLINT: + case ColumnType::Bigint: + case ColumnType::Smallint: settype($args[0], Types::INTEGER); break; - case Types::BINARY: - case Types::DECIMAL: - case Types::TEXT: + case ColumnType::Binary: + case ColumnType::Decimal: + case ColumnType::Text: settype($args[0], Types::STRING); break; - case Types::TIME: - case Types::DATE: - case Types::DATETIME: - case Types::DATETIME_TZ: + case ColumnType::Time: + case ColumnType::Date: + case ColumnType::Datetime: + case ColumnType::DatetimeTz: if (!$args[0] instanceof \DateTime) { $args[0] = new \DateTime($args[0]); } break; - case Types::TIME_IMMUTABLE: - case Types::DATE_IMMUTABLE: - case Types::DATETIME_IMMUTABLE: - case Types::DATETIME_TZ_IMMUTABLE: + case ColumnType::TimeImmutable: + case ColumnType::DateImmutable: + case ColumnType::DatetimeImmutable: + case ColumnType::DatetimeTzImmutable: if (!$args[0] instanceof \DateTimeImmutable) { $args[0] = new \DateTimeImmutable($args[0]); } break; - case Types::JSON: + case ColumnType::Json: if (!is_array($args[0])) { $args[0] = json_decode($args[0], true); } break; default: - settype($args[0], $type); + settype($args[0], $type->value); } } $this->$name = $args[0]; @@ -187,7 +188,7 @@ abstract class Entity { protected function isGetterForBoolProperty(string $methodName): bool { if (str_starts_with($methodName, 'is')) { $fieldName = lcfirst(substr($methodName, 2)); - return isset($this->_fieldTypes[$fieldName]) && str_starts_with($this->_fieldTypes[$fieldName], 'bool'); + return isset($this->_fieldTypes[$fieldName]) && str_starts_with($this->_fieldTypes[$fieldName]->value, 'bool'); } return false; } @@ -258,23 +259,28 @@ abstract class Entity { * that value once its being returned from the database * * @param string $fieldName the name of the attribute - * @param Types::* $type the type which will be used to match a cast + * @param Types::*|ColumnType $type the type which will be used to match a cast * @since 31.0.0 Parameter $type is now restricted to {@see Types} constants. The formerly accidentally supported types 'int'|'bool'|'double' are mapped to Types::INTEGER|Types::BOOLEAN|Types::FLOAT accordingly. + * @since 35.0.0 Parameter $type now prefers using one of the {@see ColumnType} enum values. * @since 7.0.0 */ - protected function addType(string $fieldName, string $type): void { + protected function addType(string $fieldName, string|ColumnType $type): void { /** @psalm-suppress TypeDoesNotContainType */ if (in_array($type, ['bool', 'double', 'int', 'array', 'object'], true)) { // Mapping legacy strings to the actual types $type = match ($type) { - 'int' => Types::INTEGER, - 'bool' => Types::BOOLEAN, - 'double' => Types::FLOAT, + 'int' => ColumnType::Integer, + 'bool' => ColumnType::Boolean, + 'double' => ColumnType::Float, 'array', - 'object' => Types::STRING, + 'object' => ColumnType::String, }; } + if (is_string($type)) { + $type = ColumnType::from($type); + } + $this->_fieldTypes[$fieldName] = $type; } diff --git a/lib/public/DB/Schema/ColumnType.php b/lib/public/DB/Schema/ColumnType.php new file mode 100644 index 00000000000..f335c5e76c2 --- /dev/null +++ b/lib/public/DB/Schema/ColumnType.php @@ -0,0 +1,166 @@ + $columnNames + * @param list $columnNames * @param string|false $indexName * * @throws SchemaException @@ -37,9 +39,9 @@ interface ITable { public function setPrimaryKey(array $columnNames, string|false $indexName = false): self; /** - * @param list $columnNames - * @param list $flags - * @param array $options + * @param list $columnNames + * @param list $flags + * @param array $options * * @throws SchemaException * @since 35.0.0 @@ -77,7 +79,7 @@ interface ITable { /** * Drops an index from this table. * - * @param string $name The index name. + * @param non-empty-lowercase-string $name The index name. * * @throws SchemaException If the index does not exist. * @since 35.0.0 @@ -87,7 +89,7 @@ interface ITable { /** * Returns whether this table has an index with the given name. * - * @param string $name The index name. + * @param non-empty-lowercase-string $name The index name. * @since 35.0.0 */ public function hasIndex(string $name): bool; @@ -105,9 +107,9 @@ interface ITable { /** * Renames an index. * - * @param string $oldName The name of the index to rename from. - * @param string|null $newName The name of the index to rename to. - * If null is given, the index name will be auto-generated. + * @param non-empty-lowercase-string $oldName The name of the index to rename from. + * @param non-empty-lowercase-string|null $newName The name of the index to rename to. + * If null is given, the index name will be auto-generated. * * @return self This table instance. * @@ -118,8 +120,8 @@ interface ITable { public function renameIndex(string $oldName, ?string $newName = null): self; /** - * @param string $name - * @param string $typeName + * @param non-empty-lowercase-string $name + * @param Types::*|ColumnType $typeName * @param array{ * notnull?: bool, * length?: ?int, @@ -127,15 +129,17 @@ interface ITable { * unsigned?: bool, * autoincrement?: bool, * fixed?: bool, + * precision?: int, + * scale?: int, * } $options * * @throws SchemaException * @since 35.0.0 */ - public function addColumn(string $name, string $typeName, array $options = []): IColumn; + public function addColumn(string $name, string|ColumnType $typeName, array $options = []): IColumn; /** - * @param string $name + * @param non-empty-lowercase-string $name * @param array{ * notnull?: bool, * length?: ?int, @@ -143,6 +147,9 @@ interface ITable { * unsigned?: bool, * autoincrement?: bool, * fixed?: bool, + * precision?: int, + * scale?: int, + * type?: Types::*|ColumnType, * } $options * * @throws SchemaException @@ -152,6 +159,8 @@ interface ITable { /** * Drops a Column from the Table. + * + * @param non-empty-lowercase-string $name * @since 35.0.0 */ public function dropColumn(string $name): self; @@ -159,15 +168,15 @@ interface ITable { /** * Returns whether this table has a Column with the given name. * - * @param string $name The column name. + * @param non-empty-lowercase-string $name The column name. * @since 35.0.0 */ - public function hasColumn(string $string): bool; + public function hasColumn(string $name): bool; /** * Returns the Column with the given name. * - * @param string $name The column name. + * @param non-empty-lowercase-string $name The column name. * * @throws SchemaException If the column does not exist. * @since 35.0.0 @@ -195,9 +204,9 @@ interface ITable { * * Name is inferred from the local columns. * - * @param ITable|string $foreignTable Table schema instance or table name - * @param list $localColumnNames - * @param list $foreignColumnNames + * @param ITable|non-empty-lowercase-string $foreignTable Table schema instance or table name + * @param list $localColumnNames + * @param list $foreignColumnNames * @param array $options * * @throws SchemaException @@ -213,6 +222,8 @@ interface ITable { /** * Returns whether this table has a foreign key constraint with the given name. + * + * @param non-empty-string $name The foreign key name. * @since 35.0.0 */ public function hasForeignKey(string $name): bool; @@ -220,10 +231,16 @@ interface ITable { /** * Removes the foreign key constraint with the given name. * - * @param string $name The constraint name. + * @param non-empty-string $name The constraint name. * * @throws SchemaException * @since 35.0.0 */ public function removeForeignKey(string $name): void; + + /** + * @since 35.0.0 + * @return list + */ + public function getForeignKeys(): array; }